diff --git a/src/services/feature-api.js b/src/services/feature-api.js index 82ee94bcbec1005f3ef14a067e3bbddc74d1bd8a..bfabc583d85386be69d43928e1e319e16315b3d6 100644 --- a/src/services/feature-api.js +++ b/src/services/feature-api.js @@ -1,10 +1,10 @@ import axios from '@/axios-client.js'; import store from '../store'; -const baseUrl = store.state.configuration.VUE_APP_DJANGO_API_BASE; const featureAPI = { async getFeaturesBbox(project_slug, queryParams) { + const baseUrl = store.state.configuration.VUE_APP_DJANGO_API_BASE; const response = await axios.get( `${baseUrl}projects/${project_slug}/feature-bbox/${queryParams ? '?' + queryParams : ''}` ); @@ -22,6 +22,21 @@ const featureAPI = { } }, + async getProjectFeature(project_slug, feature_id) { + const baseUrl = store.state.configuration.VUE_APP_DJANGO_API_BASE; + const response = await axios.get( + `${baseUrl}v2/features/${feature_id}/?project__slug=${project_slug}` + ); + if ( + response.status === 200 && + response.data + ) { + return response.data; + } else { + return null; + } + }, + async getPaginatedFeatures(url) { const response = await axios.get(url); if ( @@ -35,6 +50,7 @@ const featureAPI = { }, async getFeatureEvents(featureId) { + const baseUrl = store.state.configuration.VUE_APP_DJANGO_API_BASE; const response = await axios.get( `${baseUrl}features/${featureId}/events/` ); @@ -49,6 +65,7 @@ const featureAPI = { }, async getFeatureAttachments(featureId) { + const baseUrl = store.state.configuration.VUE_APP_DJANGO_API_BASE; const response = await axios.get( `${baseUrl}features/${featureId}/attachments/` ); @@ -63,6 +80,7 @@ const featureAPI = { }, async getFeatureLinks(featureId) { + const baseUrl = store.state.configuration.VUE_APP_DJANGO_API_BASE; const response = await axios.get( `${baseUrl}features/${featureId}/feature-links/` ); @@ -92,6 +110,7 @@ const featureAPI = { async postOrPutFeature({ method, feature_id, feature_type__slug, project__slug, data }) { + const baseUrl = store.state.configuration.VUE_APP_DJANGO_API_BASE; let url = `${baseUrl}v2/features/`; if (method === 'PUT') { url += `${feature_id}/? @@ -112,6 +131,7 @@ const featureAPI = { }, async updateFeature({ feature_id, feature_type__slug, project__slug, newStatus }) { + const baseUrl = store.state.configuration.VUE_APP_DJANGO_API_BASE; const url = `${baseUrl}features/${feature_id}/?feature_type__slug=${feature_type__slug}&project__slug=${project__slug}`; const response = await axios({ @@ -127,6 +147,7 @@ const featureAPI = { }, async postComment({ featureId, comment }) { + const baseUrl = store.state.configuration.VUE_APP_DJANGO_API_BASE; const response = await axios.post( `${baseUrl}features/${featureId}/comments/`, { comment } ); @@ -141,6 +162,7 @@ const featureAPI = { }, async postCommentAttachment({ featureId, file, fileName, commentId, title }) { + const baseUrl = store.state.configuration.VUE_APP_DJANGO_API_BASE; const formdata = new FormData(); formdata.append('file', file, fileName); formdata.append('title', title); diff --git a/src/services/map-service.js b/src/services/map-service.js index 128ba6d6c1944b3293d81519f995020af4afcf18..fa21a885caa783817e4ed5f22bea243e5113a398 100644 --- a/src/services/map-service.js +++ b/src/services/map-service.js @@ -24,7 +24,7 @@ import Point from 'ol/geom/Point.js'; import axios from '@/axios-client.js'; import router from '@/router'; import store from '@/store'; -import { statusChoices } from '@/utils'; +import { retrieveFeatureProperties } from '@/utils'; let dictLayersToMap = {}; @@ -157,11 +157,6 @@ const mapService = { console.error(error.message); }); - /* const accuracyFeature = new Feature(); - this.geolocation.on('change:accuracyGeometry', () => { - accuracyFeature.setGeometry(this.geolocation.getAccuracyGeometry()); - }); */ - const positionFeature = new Feature(); positionFeature.setStyle( geolocationStyle ); @@ -179,7 +174,6 @@ const mapService = { this.geolocationSource = new VectorSource({ features: [positionFeature], - //features: [accuracyFeature, positionFeature], }); new VectorLayer({ map: this.map, @@ -257,7 +251,7 @@ const mapService = { document.getElementById('goToFeatureTypeDetail').onclick = goToFeatureTypeDetail; }, - onMapClick(event) { + async onMapClick (event) { //* retrieve features under pointer const features = this.map.getFeaturesAtPixel(event.pixel, { layerFilter: (l) => l === this.mvtLayer || this.olLayer @@ -268,7 +262,7 @@ const mapService = { const isEdited = router.history.current.name === 'editer-signalement' && router.history.current.params.slug_signal === featureId; //* avoid opening popup on feature currently edited if (featureId && !isEdited) { - const popupContent = this._createContentPopup(features[0]); + const popupContent = await this._createContentPopup(features[0]); this.content.innerHTML = popupContent.html; this.overlay.setPosition(event.coordinate); this.addRouterToPopup({ @@ -488,7 +482,8 @@ const mapService = { return { color, opacity }; }, - addVectorTileLayer: function ({ url, projectId, featureTypes, formFilters = {}, queryParams = {} }) { + addVectorTileLayer: function ({ url, project_slug, featureTypes, formFilters = {}, queryParams = {} }) { + const projectId = project_slug.split('-')[0]; const format_cfg = {/*featureClass: Feature*/ }; const mvt = new MVT(format_cfg); const options = { @@ -506,6 +501,7 @@ const mapService = { source: layerSource }); this.featureTypes = featureTypes; // store featureTypes for popups + this.projectSlug = project_slug; // store projectSlug for popups this.queryParams = queryParams; // store queryParams for popups this.mvtLayer.setZIndex(30); @@ -592,7 +588,7 @@ const mapService = { } }, - addFeatures: function ({ features, filter = {}, featureTypes, addToMap = true, queryParams = {} }) { + addFeatures: function ({ features, filter = {}, featureTypes, addToMap = true, project_slug, queryParams = {} }) { console.log('addToMap', addToMap); const drawSource = new VectorSource(); let retour; @@ -619,6 +615,7 @@ const mapService = { this.olLayer = olLayer; this.drawSource = drawSource; this.featureTypes = featureTypes; // store featureTypes for popups + this.projectSlug = project_slug; // store projectSlug for popups this.queryParams = queryParams; // store queryParams for popup routes return drawSource; }, @@ -633,18 +630,18 @@ const mapService = { createCustomFiedsContent(featureType, feature) { const { customfield_set } = featureType; - if (!customfield_set) return ''; + // create an html row for a customField const rowTemplate = (customfield) => { const { label, name } = customfield; const value = feature.getProperties()[name]; - return label && value !== undefined ? `<div class="customField-row">${label}: ${value}</div>` : ''; + return label && value !== undefined ? `<div class="customField-row">${label} : ${value}</div>` : ''; }; - + // generate html for each customField set to be displayed from featureType display config let rows = ''; for (const customField of customfield_set) { - rows += rowTemplate(customField); + rows += featureType.displayed_fields.includes(customField.name) ? rowTemplate(customField) : ''; } - + // wrap all rows into customFields container return rows.length > 0 ? `<div id="customFields"> <div class="ui divider"></div> @@ -653,69 +650,30 @@ const mapService = { </div>` : ''; }, - _createContentPopup: function (feature) { - const formatDate = (current_datetime) => { - let formatted_date = current_datetime.getFullYear() + '-' + ('0' + (current_datetime.getMonth() + 1)).slice(-2) + '-' + ('0' + current_datetime.getDate()).slice(-2) + ' ' + - ('0' + current_datetime.getHours()).slice(-2) + ':' + ('0' + current_datetime.getMinutes()).slice(-2); - return formatted_date; - }; - let featureType, status, updated_on, creator, index; // index is used to retrieve feature by query when browsing features - - if (feature.getProperties) { - const properties = feature.getProperties(); - // ! "creator" doesn't exist in geojson, only creator_id is found, but we cannot retrieve the name in frontend's available data - ({ status, updated_on, creator, index } = properties); // using parenthesis to allow destructuring object without declaration - if (this.featureTypes) { - featureType = feature.getProperties().feature_type || - this.featureTypes.find((x) => x.slug.split('-')[0] === '' + feature.getProperties().feature_type_id); - } - } else { //? TPD: I couldn't find when this code is used, is this still in use ? - status = feature.status; - if (status) status = status.name; - updated_on = feature.updated_on; - creator = feature.creator; - if (this.featureTypes) { - featureType = this.featureTypes.find((x) => x.slug === feature.feature_type.slug); - } - } - if (updated_on && !isNaN(new Date(updated_on))) { //* check if it is already formatted - updated_on = formatDate(new Date(updated_on)); - } - if (status) { - if (status.label) { //* when the label is already in the feature - status = status.label; - } else if (this.featureTypes) { //* if not, retrieve the name/label from the list - status = statusChoices.find((x) => x.value === status).name; - } - } - - let author = ''; - if (creator) { - author = creator.full_name - ? `<div> - Auteur : ${creator.first_name} ${creator.last_name} - </div>` - : creator.username ? `<div>Auteur: ${creator.username}</div>` : ''; - } - - const title = feature.getProperties ? feature.getProperties().title : feature.title; + _createContentPopup: async function (feature) { + const props = await retrieveFeatureProperties(feature, this.featureTypes, this.projectSlug); + const { feature_type, index } = props; // index is used to retrieve feature by query when browsing features + // generate html for each native fields + const statusHtml = `<div>Statut : ${props.status}</div>`; + const featureTypeHtml = `<div>Type de signalement : ${feature_type ? '<a id="goToFeatureTypeDetail" class="pointer">' + feature_type.title + '</a>' : 'Type de signalement inconnu'}</div>`; + const updatedOnHtml = `<div>Dernière mise à jour : ${props.updated_on}</div>`; + const createdOnHtml = `<div>Date de création : ${props.created_on}</div>`; + const creatorHtml = props.creator ? `<div>Auteur : ${props.creator}</div>` : ''; + const lastEditorHtml = props.display_last_editor ? `<div>Dernier éditeur : ${props.display_last_editor}</div>` : ''; + // wrapping up finale html to fill popup, filtering native fields to display and adding filtered customFields const html = `<h4> - <a id="goToFeatureDetail" class="pointer">${title}</a> + <a id="goToFeatureDetail" class="pointer">${feature.getProperties ? feature.getProperties().title : feature.title}</a> </h4> <div class="fields"> - <div> - Statut : ${status} - </div> - <div> - Type : ${featureType ? '<a id="goToFeatureTypeDetail" class="pointer">' + featureType.title + '</a>' : 'Type de signalement inconnu'} - </div> - <div> - Dernière mise à jour : ${updated_on} - </div> - ${author} - ${this.createCustomFiedsContent(featureType, feature)} + ${feature_type.displayed_fields.includes('status') ? statusHtml : ''} + ${feature_type.displayed_fields.includes('feature_type') ? featureTypeHtml : ''} + ${feature_type.displayed_fields.includes('updated_on') ? updatedOnHtml : ''} + ${feature_type.displayed_fields.includes('created_on') ? createdOnHtml : ''} + ${feature_type.displayed_fields.includes('display_creator') ? creatorHtml : ''} + ${feature_type.displayed_fields.includes('display_last_editor') ? lastEditorHtml : ''} + ${this.createCustomFiedsContent(feature_type, feature)} </div>`; - return { html, featureType, index }; + return { html, feature_type, index }; }, zoom(zoomlevel) { diff --git a/src/utils/index.js b/src/utils/index.js index 53bf9cebd18015f128973c3010e6aefa483ca36e..d8ffcf81d53eac14fe6c731b027e6254b7d1f108 100644 --- a/src/utils/index.js +++ b/src/utils/index.js @@ -1,3 +1,5 @@ +import featureAPI from '@/services/feature-api'; + export function formatStringDate(stringDate) { const date = new Date(stringDate); if (date instanceof Date && !isNaN(date.valueOf())) { @@ -139,6 +141,50 @@ export const featureNativeFields = [ { name: 'display_last_editor', label: 'Dernier éditeur', field_type: 'Champ GéoContrib' }, ]; +export const formatDate = (current_datetime) => { + let formatted_date = current_datetime.getFullYear() + '-' + ('0' + (current_datetime.getMonth() + 1)).slice(-2) + '-' + ('0' + current_datetime.getDate()).slice(-2) + ' ' + + ('0' + current_datetime.getHours()).slice(-2) + ':' + ('0' + current_datetime.getMinutes()).slice(-2); + return formatted_date; +}; + +export const retrieveFeatureProperties = async (feature, featureTypes, projectSlug) => { + const properties = feature.getProperties(); + let { feature_type, status, updated_on, created_on, creator, display_last_editor, index } = properties; + + if (creator) { + creator = creator.full_name ? `${creator.first_name} ${creator.last_name}` : creator.username; + } else if (properties.feature_id) { + //* if *** MVT *** feature, retrieve display_creator and display_last_editor by fetching the feature details from API + const fetchedFeature = await featureAPI.getProjectFeature(projectSlug, properties.feature_id); + if (fetchedFeature) { + creator = fetchedFeature.properties.display_creator; + display_last_editor = fetchedFeature.properties.display_last_editor; + feature_type = fetchedFeature.properties.feature_type; + } + } + + if (featureTypes && feature_type) { + feature_type = featureTypes.find((el) => el.slug === feature_type.slug || feature_type); + } + + if (updated_on && !isNaN(new Date(updated_on))) { //* check if date is already formatted + updated_on = formatDate(new Date(updated_on)); + } + if (created_on && !isNaN(new Date(created_on))) { //* check if date is already formatted + created_on = formatDate(new Date(created_on)); + } + + if (status) { + if (status.label) { //* when the label is already in the feature + status = status.label; + } else if (featureTypes) { //* if not, retrieve the name/label from the list + status = statusChoices.find((el) => el.value === status).name; + } + } + + return { feature_type, status, updated_on, created_on, creator, display_last_editor, index }; +}; + export function findXformValue(feature, customField) { if (!feature) return null; if (feature.properties) { diff --git a/src/views/Feature/FeatureDetail.vue b/src/views/Feature/FeatureDetail.vue index 991bc5e11c69cbfc82cf830f1848ee455bcd71d8..7a43a9f5aee1762887b6108844f42886df6e8b11 100644 --- a/src/views/Feature/FeatureDetail.vue +++ b/src/views/Feature/FeatureDetail.vue @@ -237,6 +237,7 @@ export default { isLeaving: false, isSavingChanges: false, map: null, + slug: this.$route.params.slug, slugSignal: '', }; }, @@ -293,11 +294,11 @@ export default { }, isModerator() { - return this.USER_LEVEL_PROJECTS && this.USER_LEVEL_PROJECTS[this.$route.params.slug] === 'Modérateur'; + return this.USER_LEVEL_PROJECTS && this.USER_LEVEL_PROJECTS[this.slug] === 'Modérateur'; }, isAdministrator() { - return this.USER_LEVEL_PROJECTS && this.USER_LEVEL_PROJECTS[this.$route.params.slug] === 'Administrateur projet'; + return this.USER_LEVEL_PROJECTS && this.USER_LEVEL_PROJECTS[this.slug] === 'Administrateur projet'; }, canEditFeature() { @@ -368,8 +369,8 @@ export default { //* Chargements des features et infos projet en cas d'arrivée directe sur la page ou de refresh if (!this.project) { promises.push( - this.GET_PROJECT(this.$route.params.slug), - this.GET_PROJECT_INFO(this.$route.params.slug), + this.GET_PROJECT(this.slug), + this.GET_PROJECT_INFO(this.slug), ); } //* changement de requête selon s'il y a un id ou un offset(dans le cas du parcours des signalements filtrés) @@ -378,7 +379,7 @@ export default { } else if (!this.currentFeature || this.currentFeature.id !== this.slugSignal) { promises.push( this.GET_PROJECT_FEATURE({ - project_slug: this.$route.params.slug, + project_slug: this.slug, feature_id: this.slugSignal, }) ); @@ -428,7 +429,7 @@ export default { this.$router.push({ name: 'project_detail', params: { - slug: this.$route.params.slug, + slug: this.slug, message, }, }); @@ -451,7 +452,7 @@ export default { fetchFilteredFeature() { // TODO : if no query for sort, use project default ones const queryString = new URLSearchParams({ ...this.$route.query }); - const url = `${this.$store.state.configuration.VUE_APP_DJANGO_API_BASE}projects/${this.$route.params.slug}/feature-paginated/?limit=1&${queryString}&output=geojson`; + const url = `${this.$store.state.configuration.VUE_APP_DJANGO_API_BASE}projects/${this.slug}/feature-paginated/?limit=1&${queryString}&output=geojson`; return featureAPI.getPaginatedFeatures(url) .then((data) => { if (data && data.results && data.results.features && data.results.features[0]) { @@ -528,8 +529,10 @@ export default { addFeatureToMap() { const featureGroup = mapService.addFeatures({ + project_slug: this.slug, features: [this.currentFeature], featureTypes: this.feature_types, + addToMap: true, }); mapService.fitExtent(buffer(featureGroup.getExtent(),200)); diff --git a/src/views/Feature/FeatureEdit.vue b/src/views/Feature/FeatureEdit.vue index 92b6004a2ae1bdd0f694c28041505002b4b70a39..cc33d39c4e5732c20e75c9c1fafb925200091bed 100644 --- a/src/views/Feature/FeatureEdit.vue +++ b/src/views/Feature/FeatureEdit.vue @@ -984,9 +984,10 @@ export default { (feat) => feat.id !== currentFeatureId ); mapService.addFeatures({ + addToMap: true, + project_slug: this.project.slug, features: allFeaturesExceptCurrent, featureTypes: this.feature_types, - addToMap: true }); if (this.currentRouteName === 'editer-signalement') { editionService.setFeatureToEdit(this.currentFeature); diff --git a/src/views/Project/FeaturesListAndMap.vue b/src/views/Project/FeaturesListAndMap.vue index 8bf9e85e87657ace651ff0952826c55b9cd4d0d0..0240d950a5acd6bdd065b3994e2b59b6eeb5c5cd 100644 --- a/src/views/Project/FeaturesListAndMap.vue +++ b/src/views/Project/FeaturesListAndMap.vue @@ -347,11 +347,11 @@ export default { // --------- End sidebar events ---------- this.$nextTick(() => { - const project_id = this.projectSlug.split('-')[0]; + const mvtUrl = `${this.API_BASE_URL}features.mvt`; mapService.addVectorTileLayer({ url: mvtUrl, - projectId: project_id, + project_slug: this.projectSlug, featureTypes: this.feature_types, formFilters: this.form, queryParams: this.queryparams, diff --git a/src/views/Project/ProjectDetail.vue b/src/views/Project/ProjectDetail.vue index ccbb87732b5e42f2e4799def93f2c1f67106a205..e30ba3e1bf23a605d7bd1f8cc9057157db769680 100644 --- a/src/views/Project/ProjectDetail.vue +++ b/src/views/Project/ProjectDetail.vue @@ -407,11 +407,10 @@ export default { } await this.INITIATE_MAP({ el: this.$refs.map, layersToLoad }); this.checkForOfflineFeature(); - const project_id = this.$route.params.slug.split('-')[0]; const mvtUrl = `${this.API_BASE_URL}features.mvt`; mapService.addVectorTileLayer({ url: mvtUrl, - projectId: project_id, + project_slug: this.slug, featureTypes: this.feature_types, queryParams: { ordering: this.project.feature_browsing_default_sort, @@ -430,13 +429,14 @@ export default { .then(() => { this.featuresLoading = false; mapService.addFeatures({ + addToMap: true, + project_slug: this.slug, features: [...this.features, ...featuresOffline], featureTypes: this.feature_types, - addToMap: true, queryParams: { ordering: this.project.feature_browsing_default_sort, filter: this.project.feature_browsing_default_filter, - } + }, }); }) .catch((err) => {