diff --git a/src/components/Feature/Detail/FeatureTable.vue b/src/components/Feature/Detail/FeatureTable.vue index ba6b36833117d222f154f8b8c4535298c77e23da..66164bea05d85659a651b5d1c31f1dbcaea41116 100644 --- a/src/components/Feature/Detail/FeatureTable.vue +++ b/src/components/Feature/Detail/FeatureTable.vue @@ -237,20 +237,7 @@ export default { featureFields() { return this.fastEditionMode ? this.extra_forms : this.featureData; } - }, - - methods: { - toFeature(link) { - this.$emit('tofeature', { - name: 'details-signalement', - params: { - slug_type_signal: link.feature_to.feature_type_slug, - slug_signal: link.feature_to.feature_id, - }, - }); - }, } - }; </script> diff --git a/src/store/modules/feature-type.store.js b/src/store/modules/feature-type.store.js index 7838123006da49c9fc0922f04dcb6df8701ad0a6..b822ac4c8fc2f61e54b82445d224d750b6f0309a 100644 --- a/src/store/modules/feature-type.store.js +++ b/src/store/modules/feature-type.store.js @@ -9,14 +9,14 @@ const getColorsStyles = (customForms) => customForms }); const pending2draftFeatures = (features) => { - const result = []; for (const el of features) { - if (el.properties.status === 'pending') { + if (el.properties && el.properties.status === 'pending') { el.properties.status = 'draft'; + } else if (el.status === 'pending') { + el.status = 'draft'; } - result.push(el); } - return result; + return features; }; const feature_type = { @@ -199,16 +199,15 @@ const feature_type = { if (!name && state.fileToImport) { name = state.fileToImport.name; } - if (rootState.projects.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(); geojson = JSON.parse(textFile); } - const unmoderatedFeatures = pending2draftFeatures(geojson.features); - geojson= { + const unmoderatedFeatures = pending2draftFeatures(geojson.features || geojson); + geojson = geojson.features ? { type: 'FeatureCollection', features: unmoderatedFeatures - }; + } : unmoderatedFeatures; } const fileToImport = new File([JSON.stringify(geojson)], name, { type }); diff --git a/src/store/modules/feature.store.js b/src/store/modules/feature.store.js index 39d27f776f83d6b0713361a8185e30309ec37bc1..910ed900bc70754fff43b3f40b7164566dfac5d0 100644 --- a/src/store/modules/feature.store.js +++ b/src/store/modules/feature.store.js @@ -175,7 +175,7 @@ const feature = { }); }, - SEND_FEATURE({ state, rootState, commit, dispatch }, { routeName, query, extraForms }) { + SEND_FEATURE({ state, rootState, rootGetters, commit, dispatch }, { routeName, query, extraForms }) { function redirect(featureName, response) { // when modifying more than 2 features, exit this function (to avoid conflict with next feature call to GET_PROJECT_FEATURE) if (routeName === 'editer-attribut-signalement') return response; @@ -240,11 +240,9 @@ const feature = { extraFormObject[field.name] = field.value; } } - return { + let geojson = { id: state.form.feature_id || state.currentFeature.id, type: 'Feature', - geometry: state.form.geometry || state.form.geom || - state.currentFeature.geometry || state.currentFeature.properties.geom, properties: { title: state.form.title, description: state.form.description.value, @@ -254,6 +252,12 @@ const feature = { ...extraFormObject } }; + // if not in the case of a non geographical feature type, add geometry to geojson, else send without geometry + if (rootGetters['feature-type/feature_type'].geom_type !== 'none') { + geojson['geometry'] = state.form.geometry || state.form.geom || + state.currentFeature.geometry || state.currentFeature.properties.geom; + } + return geojson; } const geojson = createGeojson(); diff --git a/src/utils/index.js b/src/utils/index.js index 12a54ae61668fc197b898c81a34c822cb12c3c9a..66fa35387fe58fdd0dca73ff49f0b23a0b964c35 100644 --- a/src/utils/index.js +++ b/src/utils/index.js @@ -1,5 +1,6 @@ import featureAPI from '@/services/feature-api'; import { isNil } from 'lodash'; + export function formatStringDate(stringDate) { const date = new Date(stringDate); if (date instanceof Date && !isNaN(date.valueOf())) { diff --git a/src/views/Feature/FeatureDetail.vue b/src/views/Feature/FeatureDetail.vue index 3f82ce106030a0a639de1c8261314b075497c595..9068857a9ba1f8542588e13f2ecb36970d7da9da 100644 --- a/src/views/Feature/FeatureDetail.vue +++ b/src/views/Feature/FeatureDetail.vue @@ -29,10 +29,12 @@ :feature-type="feature_type" :fast-edition-mode="project.fast_edition_mode" :can-edit-feature="canEditFeature" - @tofeature="pushNgo" /> </div> - <div class="eight wide column"> + <div + v-if="feature_type && feature_type.geom_type !== 'none'" + class="eight wide column" + > <div class="map-container"> <div id="map" @@ -320,9 +322,14 @@ export default { }, watch: { + /** + * To navigate back or forward to the previous or next URL, the query params in url are updated + * since the route doesn't change, mounted is not called, then the page isn't updated + * To reload page infos we need to call initPage() when query changes + */ '$route.query'(newValue, oldValue) { - if (newValue !== oldValue) { //* Navigate back or forward to the previous or next URL - this.initPage(); //* doesn't update the page at query changes, thus it is done manually here + if (newValue !== oldValue) { + this.initPage(); } }, }, @@ -358,7 +365,12 @@ export default { async initPage() { await this.getPageInfo(); - if (this.currentFeature) this.initMap(); + if(this.feature_type && this.feature_type.geom_type === 'none') { + // setting map to null to ensure map would be created when navigating next to a geographical feature + this.map = null; + } else if (this.currentFeature) { + this.initMap(); + } }, async getPageInfo() { @@ -416,18 +428,11 @@ export default { this.next(); }, - async reloadPage() { - await this.getPageInfo(); - mapService.removeFeatures(); - this.addFeatureToMap(); - }, - pushNgo(newEntry) { - this.$router.push(newEntry) //* update the params or queries in the route/url - .then(() => { - this.reloadPage(); - }) - .catch(() => true); //* catch error if navigation get aborted (in beforeRouteUpdate) + //* update the params or queries in the route/url + this.$router.push(newEntry) + //* catch error if navigation get aborted (in beforeRouteUpdate) + .catch(() => true); }, goBackToProject(message) { @@ -554,7 +559,9 @@ export default { } ).then((response) => { if (response === 'reloadPage') { - this.reloadPage(); + // when query doesn't change we need to reload the page infos with initPage(), + // since it would not be called from the watcher'$route.query' when the query does change + this.initPage(); } }); } diff --git a/src/views/Feature/FeatureEdit.vue b/src/views/Feature/FeatureEdit.vue index 4d96589faa8036b190cfaa32762dda5cb63c71a1..3a6d9c71e279e8449fa81b330f915cf5e0974f7b 100644 --- a/src/views/Feature/FeatureEdit.vue +++ b/src/views/Feature/FeatureEdit.vue @@ -86,7 +86,8 @@ <!-- Geom Field --> <div - v-if="currentRouteName !== 'editer-attribut-signalement'" + v-if="currentRouteName !== 'editer-attribut-signalement' + && feature_type && feature_type.geom_type !== 'none'" class="field" > <label :for="form.geom.id_for_label">{{ form.geom.label }}</label> @@ -579,8 +580,11 @@ export default { Promise.all(promises).then(() => { if (this.currentRouteName !== 'editer-attribut-signalement') { this.initForm(); - this.initMap(); - this.onFeatureTypeLoaded(); // init map tools + // if not in the case of a non geographical feature type, init map + if (this.feature_type.geom_type !== 'none') { + this.initMap(); + this.initMapTools(); + } } this.$store.dispatch('feature/INIT_EXTRA_FORMS'); }); @@ -753,7 +757,7 @@ export default { }, updateStore() { - this.$store.commit('feature/UPDATE_FORM', { + return this.$store.commit('feature/UPDATE_FORM', { title: this.form.title.value, status: this.form.status.value, description: this.form.description, @@ -828,9 +832,13 @@ export default { async postForm(extraForms) { let response; - let is_valid = this.checkFormGeom() && this.checkAddedForm(); + let is_valid = this.checkAddedForm(); + // if not in the case of a non geographical feature type, check geometry's validity + if (this.feature_type && this.feature_type.geom_type !== 'none') { + is_valid = is_valid && this.checkFormGeom(); + } if (!this.feature_type.title_optional) { - is_valid = this.checkFormTitle() && is_valid; + is_valid = is_valid && this.checkFormTitle(); } if (is_valid) { @@ -907,7 +915,7 @@ export default { //* ************* MAP *************** *// - onFeatureTypeLoaded() { + initMapTools() { const geomType = this.feature_type.geom_type; editionService.addEditionControls(geomType, this.isEditable); editionService.draw.on('drawend', (evt) => { diff --git a/src/views/FeatureType/FeatureTypeDetail.vue b/src/views/FeatureType/FeatureTypeDetail.vue index 8d780b126a938e1078c348210bb7b5fa0b17046c..b140f7ca28493f537ce032fe888e41347f164348 100644 --- a/src/views/FeatureType/FeatureTypeDetail.vue +++ b/src/views/FeatureType/FeatureTypeDetail.vue @@ -1,6 +1,6 @@ <template> <div - v-if="structure" + v-if="feature_type" id="feature-type-detail" > <div class="ui stackable grid"> @@ -8,42 +8,42 @@ <div class="ui attached secondary segment"> <h1 class="ui center aligned header ellipsis"> <img - v-if="structure.geom_type === 'point'" + v-if="feature_type.geom_type === 'point'" class="ui medium image" alt="Géométrie point" src="@/assets/img/marker.png" > <img - v-if="structure.geom_type === 'linestring'" + v-if="feature_type.geom_type === 'linestring'" class="ui medium image" alt="Géométrie ligne" src="@/assets/img/line.png" > <img - v-if="structure.geom_type === 'polygon'" + v-if="feature_type.geom_type === 'polygon'" class="ui medium image" alt="Géométrie polygone" src="@/assets/img/polygon.png" > <img - v-if="structure.geom_type === 'multipoint'" + v-if="feature_type.geom_type === 'multipoint'" class="ui medium image" alt="Géométrie point" src="@/assets/img/multimarker.png" > <img - v-if="structure.geom_type === 'multilinestring'" + v-if="feature_type.geom_type === 'multilinestring'" class="ui medium image" alt="Géométrie ligne" src="@/assets/img/multiline.png" > <img - v-if="structure.geom_type === 'multipolygon'" + v-if="feature_type.geom_type === 'multipolygon'" class="ui medium image" alt="Géométrie polygone" src="@/assets/img/multipolygon.png" > - {{ structure.title }} + {{ feature_type.title }} </h1> </div> <div class="ui attached segment"> @@ -123,8 +123,9 @@ @change="onGeojsonFileChange" > </div> + <div - v-if="structure.geom_type === 'point'" + v-if="feature_type.geom_type === 'point' || feature_type.geom_type === 'none'" class="field" > <label @@ -218,10 +219,10 @@ style="margin-bottom: 1em;" > <option value="GeoJSON"> - GeoJSON + {{ feature_type.geom_type === 'none' ? 'JSON' : 'GeoJSON' }} </option> <option - v-if="structure.geom_type === 'point'" + v-if="feature_type.geom_type === 'point' || feature_type.geom_type === 'none'" value="CSV" > CSV @@ -354,10 +355,10 @@ Voir tous les signalements </router-link> <router-link - v-if="permissions.can_create_feature && structure.geom_type && !structure.geom_type.includes('multi')" + v-if="permissions.can_create_feature && feature_type.geom_type && !feature_type.geom_type.includes('multi')" :to="{ name: 'ajouter-signalement', - params: { slug_type_signal: structure.slug }, + params: { slug_type_signal: feature_type.slug }, }" class="ui icon button button-hover-green margin-25" > @@ -441,7 +442,7 @@ export default { exportFormat: 'GeoJSON', exportLoading: false, lastFeatures: [], - featuresCount: 0 + featuresCount: 0, }; }, @@ -452,6 +453,9 @@ export default { ...mapGetters('projects', [ 'project' ]), + ...mapGetters('feature-type', [ + 'feature_type' + ]), ...mapState([ 'reloadIntervalId', 'configuration', @@ -477,20 +481,9 @@ export default { IDGO() { return this.$store.state.configuration.VUE_APP_IDGO; }, - structure: function () { - if (Object.keys(this.feature_types).length) { - const st = this.feature_types.find( - (el) => el.slug === this.featureTypeSlug - ); - if (st) { - return st; - } - } - return {}; - }, orderedCustomFields() { - if (Object.keys(this.structure).length) { - return [...this.structure.customfield_set].sort( + if (this.feature_type) { + return [...this.feature_type.customfield_set].sort( (a, b) => a.position - b.position ); } @@ -498,6 +491,12 @@ export default { }, }, + watch: { + feature_type(newValue) { + this.toggleJsonUploadOption(newValue); + } + }, + created() { if (!this.project) { this.$store.dispatch('projects/GET_PROJECT', this.slug); @@ -508,7 +507,7 @@ export default { ); this.$store.dispatch('feature-type/GET_IMPORTS', { project_slug: this.$route.params.slug, - feature_type: this.$route.params.feature_type_slug + feature_type: this.featureTypeSlug }); this.getLastFeatures(); if (this.$route.params.type === 'external-geojson') { @@ -516,6 +515,8 @@ export default { } // empty prerecorded lists in case the list has been previously loaded with a limit in other component like FeatureExtraForm this.SET_PRERECORDED_LISTS([]); + // This function is also called by watcher at this stage, but to be safe in edge case + this.toggleJsonUploadOption(this.feature_type); }, @@ -558,6 +559,20 @@ export default { this.showImport = !this.showImport; }, + /** + * In the case of a non geographical feature type, replace geoJSON by JSON in file to upload options + * + * @param {Object} featureType - The current featureType. + */ + toggleJsonUploadOption(featureType) { + if (featureType && featureType.geom_type === 'none') { + this.geojsonFileToImport = { + name: 'Sélectionner un fichier JSON ...', + size: 0, + }; + } + }, + async checkPreRecordedValue(fieldValue, listName) { const fieldLabel = fieldValue.label || fieldValue; // encode special characters like apostrophe or white space @@ -568,9 +583,9 @@ export default { return this.selectedPrerecordedListValues[listName].some((el) => el.label === fieldLabel); }, - async isValidTypes(data) { + async isValidTypes(features) { this.importError = ''; - const fields = this.structure.customfield_set.map((el) => { + const fields = this.feature_type.customfield_set.map((el) => { return { name: el.name, field_type: el.field_type, @@ -578,7 +593,7 @@ export default { }; }); - for (const feature of data.features) { + for (const feature of features) { for (const { name, field_type, options } of fields) { const properties = feature.properties || feature; @@ -678,12 +693,22 @@ export default { // Parse the CSV string into rows const rows = parseCSV(csvString, delimiter); - // Extract headers and check for required fields 'lat' and 'lon' + // Extract headers const headers = rows.shift(); - if (!headers.includes('lat') || !headers.includes('lon')) { - this.importError = 'Les champs obligatoires "lat" et "lon" sont absents.'; - return false; - } + if (this.feature_type.geom_type !== 'none') { + // Check for required fields 'lat' and 'lon' in headers + if (!headers.includes('lat') || !headers.includes('lon')) { + this.importError = 'Les champs obligatoires "lat" et "lon" sont absents des headers.'; + return false; + } + // Verify the presence and validity of coordinate values + const hasCoordValues = checkLonLatValues(headers, rows); + if (!hasCoordValues) { + this.importError = 'Les valeurs de "lon" et "lat" ne sont pas valides ou absentes.'; + return false; + } + + } // Ensure there are data rows after the headers if (rows.length === 0) { @@ -697,18 +722,11 @@ export default { return false; } - // Verify the presence and validity of coordinate values - const hasCoordValues = checkLonLatValues(headers, rows); - if (!hasCoordValues) { - this.importError = 'Les valeurs de "lon" et "lat" ne sont pas valides.'; - return false; - } - // Convert the CSV string to a JSON object for further processing const jsonFromCsv = await csv({ delimiter }).fromString(csvString); // Validate the types of values in the JSON object - const validity = await this.isValidTypes({ features: jsonFromCsv }); + const validity = await this.isValidTypes(jsonFromCsv); return validity; }, @@ -751,7 +769,7 @@ export default { // If the file is smaller than 10 Mo, check its validity try { const json = JSON.parse(fileContent); - jsonValidity = await this.isValidTypes(json); + jsonValidity = await this.isValidTypes(json.features || json); } catch (error) { this.DISPLAY_MESSAGE({ comment: error, level: 'negative' }); jsonValidity = false; @@ -761,13 +779,14 @@ export default { jsonValidity = true; } - // If the GeoJSON is valid, update the component state with the file + // If the GeoJSON is valid, update the component state with the file and set the file in store if (jsonValidity) { - this.geojsonFileToImport = files[0]; // TODO: Remove this value from state as it is stored (first attempt didn't work) + this.geojsonFileToImport = files[0]; this.SET_FILE_TO_IMPORT(this.geojsonFileToImport); } else { // Clear any previously selected geojson file to disable import button this.geojsonFileToImport = geojsonFileToImport; + this.toggleJsonUploadOption(this.feature_type); } // Stop the loading process @@ -793,6 +812,7 @@ export default { // Clear any previously selected geojson file to avoid confusion this.geojsonFileToImport = geojsonFileToImport; + this.toggleJsonUploadOption(this.feature_type); // Retrieve the files from the event const files = e.target.files || e.dataTransfer.files; @@ -889,15 +909,16 @@ export default { exportFeatures() { this.exportLoading = true; + let exportFormat = this.feature_type.geom_type === 'none' && this.exportFormat === 'GeoJSON' ? 'json' : this.exportFormat.toLowerCase(); const url = ` - ${this.$store.state.configuration.VUE_APP_DJANGO_API_BASE}projects/${this.slug}/feature-type/${this.featureTypeSlug}/export/?format_export=${this.exportFormat.toLowerCase()} + ${this.$store.state.configuration.VUE_APP_DJANGO_API_BASE}projects/${this.slug}/feature-type/${this.featureTypeSlug}/export/?format_export=${exportFormat} `; featureAPI.getFeaturesBlob(url) .then((blob) => { if (blob) { const link = document.createElement('a'); link.href = URL.createObjectURL(blob); - link.download = `${this.project.title}-${this.structure.title}.${this.exportFormat === 'GeoJSON' ? 'json' : 'csv'}`; + link.download = `${this.project.title}-${this.feature_type.title}.${exportFormat}`; link.click(); setTimeout(function(){ URL.revokeObjectURL(link.href); diff --git a/src/views/FeatureType/FeatureTypeEdit.vue b/src/views/FeatureType/FeatureTypeEdit.vue index 9871f0e109af5ded8bba445e0103f9571ac11f2d..95551cd8509b85f8156877f9b10c73216a41954a 100644 --- a/src/views/FeatureType/FeatureTypeEdit.vue +++ b/src/views/FeatureType/FeatureTypeEdit.vue @@ -170,6 +170,7 @@ export default { { value: 'linestring', name: 'Ligne' }, { value: 'point', name: 'Point' }, { value: 'polygon', name: 'Polygone' }, + { value: 'none', name: 'Aucune' }, ], form: { colors_style: { @@ -551,6 +552,7 @@ export default { this.loading = false; }); }, + postCSVFeatures(feature_type_slug) { this.$store .dispatch('feature-type/SEND_FEATURES_FROM_CSV', {