<template> <div v-frag> <div v-if="permissions && permissions.can_view_project && project" v-frag > <div id="message" class="fullwidth" > <div v-if="tempMessage" class="ui positive message" > <p><i class="check icon" /> {{ tempMessage }}</p> </div> </div> <div id="message_info" class="fullwidth" > <div v-if="infoMessage" class="ui info message" style="text-align: left" > <div class="header"> <i class="info circle icon" /> Informations </div> <ul class="list"> {{ infoMessage }} </ul> </div> </div> <div class="row"> <div class="four wide middle aligned column"> <img class="ui small spaced image" :src=" project.thumbnail.includes('default') ? require('@/assets/img/default.png') : DJANGO_BASE_URL + project.thumbnail + refreshId() " > <div class="ui hidden divider" /> <div class="ui basic teal label" data-tooltip="Membres" > <i class="user icon" />{{ project.nb_contributors }} </div> <div class="ui basic teal label" data-tooltip="Signalements publiés" > <i class="map marker icon" />{{ project.nb_published_features }} </div> <div class="ui basic teal label" data-tooltip="Commentaires" > <i class="comment icon" />{{ project.nb_published_features_comments }} </div> </div> <div class="ten wide column important-flex space-between"> <div> <h1 class="ui header"> {{ project.title }} </h1> <div class="ui hidden divider" /> <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" /> </a> <router-link v-if=" permissions && permissions.can_update_project && isOffline() !== true " :to="{ name: 'project_edit', params: { 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" /> </router-link> <a v-if="isProjectAdmin && 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" /> </a> </div> <button v-if="isProjectAdmin && !isSharedProject && project.generate_share_link" class="ui teal left labeled icon button" @click="copyLink" > <i class="left icon share square" /> 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> </div> </div> </div> </div> <div v-if="arraysOffline.length > 0"> {{ arraysOffline.length }} modification<span v-if="arraysOffline.length>1">s</span> en attente <button :disabled="isOffline()" class="ui fluid labeled teal icon button" @click="sendOfflineFeatures()" > <i class="upload icon" /> Envoyer au serveur </button> </div> </div> <div class="row"> <div class="seven wide column"> <h3 class="ui header"> Types de signalements </h3> <div class="ui middle aligned divided list"> <div :class="{ active: projectInfoLoading }" class="ui inverted dimmer" > <div class="ui text loader"> Récupération des types de signalements en cours... </div> </div> <div :class="{ active: featureTypeImporting }" class="ui inverted dimmer" > <div class="ui text loader"> Traitement du fichier en cours ... </div> </div> <div v-for="(type, index) in feature_types" :key="type.title + '-' + index" class="item" > <div class="feature-type-container"> <router-link :to="{ name: 'details-type-signalement', params: { feature_type_slug: type.slug }, }" class="feature-type-title" > <img v-if="type.geom_type === 'point'" class="list-image-type" src="@/assets/img/marker.png" > <img v-if="type.geom_type === 'linestring'" class="list-image-type" src="@/assets/img/line.png" > <img v-if="type.geom_type === 'polygon'" class="list-image-type" src="@/assets/img/polygon.png" > {{ type.title }} </router-link> <div class="middle aligned content"> <router-link v-if=" project && permissions && permissions.can_create_feature " :to="{ name: 'ajouter-signalement', params: { slug_type_signal: type.slug }, }" class=" ui compact small icon right floated button button-hover-green " data-tooltip="Ajouter un signalement" data-position="top right" data-variation="mini" > <i class="ui plus icon" /> </router-link> <router-link v-if=" project && permissions && permissions.can_create_feature_type && isOffline() !== true " :to="{ name: 'dupliquer-type-signalement', params: { slug_type_signal: type.slug }, }" class=" ui compact small icon right floated button button-hover-green " data-tooltip="Dupliquer un type de signalement" data-position="top right" data-variation="mini" > <i class="inverted grey copy alternate icon" /> </router-link> <div v-if="isImporting(type)" class="import-message" > <i class="info circle icon" /> Import en cours </div> <div v-else v-frag > <a v-if="isProjectAdmin && isOffline() !== true" 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" @click="toggleDeleteFeatureType(type)" > <i class="inverted grey trash alternate icon" /> </a> <router-link v-if=" project && permissions && permissions.can_create_feature_type && isOffline() !== true " :to="{ name: 'editer-symbologie-signalement', params: { slug_type_signal: type.slug }, }" 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" /> </router-link> <router-link v-if=" project && type.is_editable && permissions && permissions.can_create_feature_type && isOffline() !== true " :to="{ name: 'editer-type-signalement', params: { slug_type_signal: type.slug }, }" class=" ui compact small icon right floated button button-hover-orange " data-tooltip="Éditer le type de signalement" data-position="top center" data-variation="mini" > <i class="inverted grey pencil alternate icon" /> </router-link> </div> </div> </div> </div> <div v-if="feature_types.length === 0"> <i> Le projet ne contient pas encore de type de signalements. </i> </div> </div> <div id="nouveau-type-signalement"> <router-link v-if=" permissions && permissions.can_update_project && isOffline() !== true " :to="{ name: 'ajouter-type-signalement', params: { slug }, }" class="ui compact basic button" > <i class="ui plus icon" />Créer un nouveau type de signalement </router-link> </div> <div class="nouveau-type-signalement"> <div v-if=" permissions && permissions.can_update_project && isOffline() !== true " class=" ui compact basic button button-align-left " > <i class="ui plus icon" /> <label class="ui" for="json_file" > <span class="label" >Créer un nouveau type de signalement à partir d'un GeoJSON</span> </label> <input id="json_file" type="file" accept="application/json, .json, .geojson" style="display: none" name="json_file" @change="onFileChange" > </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, feature_type_slug: 'create' }, }" class="ui compact basic button button-align-left" > <i class="ui plus icon" /> Créer un nouveau type de signalement à partir du catalogue {{ CATALOG_NAME|| 'IDGO' }} </router-link> </div> <div v-if="fileToImport.size > 0" id="button-import" > <button :disabled="fileToImport.size === 0" class="ui fluid teal icon button" @click="toNewFeatureType" > <i class="upload icon" /> Lancer l'import avec le fichier {{ fileToImport.name }} </button> </div> </div> <div class="seven wide column"> <div :class="{ active: mapLoading }" class="ui inverted dimmer" > <div class="ui text loader"> Chargement de la carte... </div> </div> <div id="map" ref="map" /> </div> </div> <div class="row"> <div class="fourteen wide column"> <div class="ui two stackable cards"> <div class="red card"> <div class="content"> <div class="center aligned header"> Derniers signalements </div> <div class="center aligned description"> <div :class="{ active: featuresLoading }" class="ui inverted dimmer" > <div class="ui text loader"> Récupération des signalements en cours... </div> </div> <div class="ui relaxed list"> <div v-for="(item, index) in features.slice(-5)" :key="item.properties.title + index" class="item" > <div class="content"> <div> <router-link :to="{ name: 'details-signalement', params: { slug, slug_type_signal: item.properties.feature_type.slug, slug_signal: item.id, }, }" > {{ item.properties.title || item.id }} </router-link> </div> <div class="description"> <i> [{{ item.properties.created_on }} <span v-if="user && item.properties.creator"> , par {{ item.properties.creator.full_name ? item.properties.creator.full_name : item.properties.creator.username }} </span> ] </i> </div> </div> </div> <i v-if="features.length === 0" >Aucun signalement pour le moment.</i> </div> </div> </div> </div> <div class="orange card"> <div class="content"> <div class="center aligned header"> Derniers commentaires </div> <div class="center aligned description"> <div :class="{ active: projectInfoLoading }" class="ui inverted dimmer" > <div class="ui text loader"> Récupération des commentaires en cours... </div> </div> <div class="ui relaxed list"> <div v-for="(item, index) in last_comments" :key="'comment ' + index" class="item" > <div class="content"> <div> <router-link :to="getRouteUrl(item.related_feature.feature_url)" > "{{ item.comment }}" </router-link> </div> <div class="description"> <i>[ {{ item.created_on }}<span v-if="user && item.display_author" >, par {{ item.display_author }} </span> ]</i> </div> </div> </div> <i v-if="!last_comments || last_comments.length === 0" >Aucun commentaire pour le moment.</i> </div> </div> </div> </div> </div> </div> </div> <div class="row"> <div class="fourteen wide column"> <div class="ui grey segment"> <h3 class="ui header"> Paramètres du projet </h3> <div class="ui five stackable cards"> <div class="card"> <div class="center aligned content"> <h4 class="ui center aligned icon header"> <i class="disabled grey archive icon" /> <div class="content"> Délai avant archivage automatique </div> </h4> </div> <div class="center aligned extra content"> {{ project.archive_feature }} jours </div> </div> <div class="card"> <div class="content"> <h4 class="ui center aligned icon header"> <i class="disabled grey trash alternate icon" /> <div class="content"> Délai avant suppression automatique </div> </h4> </div> <div class="center aligned extra content"> {{ project.delete_feature }} jours </div> </div> <div class="card"> <div class="content"> <h4 class="ui center aligned icon header"> <i class="disabled grey eye icon" /> <div class="content"> Visibilité des signalements publiés </div> </h4> </div> <div class="center aligned extra content"> {{ project.access_level_pub_feature }} </div> </div> <div class="card"> <div class="content"> <h4 class="ui center aligned icon header"> <i class="disabled grey eye icon" /> <div class="content"> Visibilité des signalements archivés </div> </h4> </div> <div class="center aligned extra content"> {{ project.access_level_arch_feature }} </div> </div> <div class="card"> <div class="content"> <h4 class="ui center aligned icon header"> <i class="disabled grey cogs icon" /> <div class="content"> Modération </div> </h4> </div> <div class="center aligned extra content"> {{ project.moderation ? "Oui" : "Non" }} </div> </div> </div> </div> </div> </div> </div> <span v-else> <i class="icon exclamation triangle" /> <span>Vous ne disposez pas des droits nécessaires pour consulter ce projet.</span> </span> <div v-if="modalType" class="ui dimmer modals page transition visible active" style="display: flex !important" > <div :class="[ 'ui mini modal subscription', { 'transition visible active': modalType }, ]" > <i class="close icon" @click="modalType = false" /> <div class="ui icon header"> <i :class="[modalType === 'subscribe' ? 'envelope' : 'trash', 'icon']" /> {{ modalType === 'subscribe' ? 'Notifications' : 'Suppression' }} du {{ modalType === 'deleteFeatureType' ? 'type de signalement ' + featureTypeToDelete.title : 'projet' }} </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 :class="['ui compact fluid button', modalType === 'subscribe' && !is_suscriber ? 'green' : 'red']" @click="handleModalClick" > <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> </div> <div :class="isFileSizeModalOpen ? 'active' : ''" class="ui dimmer" > <div :class="isFileSizeModalOpen ? 'active' : ''" class="ui modal tiny" style="top: 20%" > <div class="header"> Fichier trop grand! </div> <div class="content"> <p> Impossible de créer un type de signalement à partir d'un fichier GeoJSON de plus de 10Mo (celui importé fait {{ fileSize }} Mo). </p> </div> <div class="actions"> <div class="ui button teal" @click="closeFileSizeModal" > Fermer </div> </div> </div> </div> </div> </template> <script> 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'; import axios from '@/axios-client.js'; import { fileConvertSizeToMo } from '@/assets/js/utils'; export default { name: 'ProjectDetails', directives: { frag, }, filters: { setDate(value) { const date = new Date(value); const d = date.toLocaleDateString('fr', { year: '2-digit', month: 'numeric', day: 'numeric', }); return d; }, }, props: { message: { type: String, default: '' } }, data() { return { infoMessage: '', importMessage: null, arraysOffline: [], arraysOfflineErrors: [], confirmMsg: false, geojsonImport: [], fileToImport: { name: '', size: 0 }, slug: this.$route.params.slug, modalType: false, is_suscriber: false, tempMessage: null, projectInfoLoading: true, featureTypeImporting: false, featureTypeToDelete: null, featuresLoading: true, isFileSizeModalOpen: false, mapLoading: true, }; }, computed: { ...mapGetters([ 'permissions' ]), ...mapState('projects', [ 'project' ]), ...mapState([ 'configuration', ]), ...mapState('feature_type', [ 'feature_types', 'importFeatureTypeData' ]), ...mapState('feature', [ 'features' ]), ...mapState([ 'last_comments', 'user', 'user_permissions', 'reloadIntervalId', ]), ...mapState('map', [ 'map' ]), DJANGO_BASE_URL() { return this.configuration.VUE_APP_DJANGO_BASE; }, API_BASE_URL() { 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); }, isSharedProject() { return this.$route.path.includes('projet-partage'); }, isProjectAdmin() { return this.user_permissions && this.user_permissions[this.slug] && this.user_permissions[this.slug].is_project_administrator; }, }, watch: { feature_types: { deep: true, handler(newValue, oldValue) { if (newValue && newValue !== oldValue) { this.GET_IMPORTS({ project_slug: this.$route.params.slug, }); } }, }, importFeatureTypeData: { deep: true, handler(newValue) { if ( newValue && newValue.some((el) => el.status === 'pending') && !this.reloadIntervalId ) { this.SET_RELOAD_INTERVAL_ID( setInterval(() => { this.GET_IMPORTS({ project_slug: this.$route.params.slug, }); }, this.$store.state.configuration.VUE_APP_RELOAD_INTERVAL) ); } else if ( newValue && !newValue.some((el) => el.status === 'pending') && this.reloadIntervalId ) { this.GET_PROJECT_FEATURE_TYPES(this.slug); this.CLEAR_RELOAD_INTERVAL_ID(); } }, }, }, created() { if (this.user) { projectAPI .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() { this.retrieveProjectInfo(); if (this.message) { this.tempMessage = this.message; document .getElementById('message') .scrollIntoView({ block: 'end', inline: 'nearest' }); setTimeout(() => (this.tempMessage = null), 5000); //* hide message after 5 seconds } }, destroyed() { this.CLEAR_RELOAD_INTERVAL_ID(); }, methods: { ...mapMutations([ 'SET_RELOAD_INTERVAL_ID', 'CLEAR_RELOAD_INTERVAL_ID', 'DISPLAY_MESSAGE', ]), ...mapActions('projects', [ 'GET_PROJECT_INFO', 'GET_PROJECT', ]), ...mapActions('map', [ 'INITIATE_MAP' ]), ...mapActions('feature_type', [ 'GET_IMPORTS' ]), ...mapActions('feature', [ 'GET_PROJECT_FEATURES' ]), ...mapActions('feature_type', [ 'GET_PROJECT_FEATURE_TYPES' ]), refreshId() { return '?ver=' + Math.random(); }, getRouteUrl(url) { if (this.isSharedProject) { url = url.replace('projet', 'projet-partage'); } return url.replace(this.$store.state.configuration.BASE_URL, ''); //* remove duplicate /geocontrib }, isOffline() { return navigator.onLine === false; }, isImporting(type) { if (this.importFeatureTypeData) { const singleImportData = this.importFeatureTypeData.find( (el) => el.feature_type_title === type.slug ); return singleImportData && singleImportData.status === 'pending'; } 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.$store.commit('DISPLAY_LOADER', 'Projet en cours de chargement.'); Promise.all([ this.GET_PROJECT(this.slug), this.GET_PROJECT_INFO(this.slug) ]) .then(() => { this.$store.commit('DISCARD_LOADER'); this.projectInfoLoading = false; setTimeout(() => { let map = mapUtil.getMap(); if (map) map.remove(); this.initMap(); }, 1000); }) .catch((err) => { console.error(err); this.$store.commit('DISCARD_LOADER'); this.projectInfoLoading = false; }); }, checkForOfflineFeature() { let arraysOffline = []; let localStorageArray = localStorage.getItem('geocontrib_offline'); if (localStorageArray) { arraysOffline = JSON.parse(localStorageArray); this.arraysOffline = arraysOffline.filter( (x) => x.project === this.slug ); } }, sendOfflineFeatures() { var promises = []; let self = this; this.arraysOfflineErrors = []; this.arraysOffline.forEach((feature) => { if (feature.type === 'post') { promises.push( axios .post(`${this.API_BASE_URL}features/`, feature.geojson) .then((response) => { if (response.status === 201 && response.data) { return 'OK'; } else { self.arraysOfflineErrors.push(feature); } }) .catch((error) => { console.error(error); self.arraysOfflineErrors.push(feature); }) ); } else if (feature.type === 'put') { promises.push( axios .put( `${this.API_BASE_URL}features/${feature.featureId}`, feature.geojson ) .then((response) => { if (response.status === 200 && response.data) { return 'OK'; } else { self.arraysOfflineErrors.push(feature); } }) .catch((error) => { console.error(error); self.arraysOfflineErrors.push(feature); }) ); } }); 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.slug ); this.arraysOffline = []; arraysOffline = arraysOfflineOtherProject.concat( this.arraysOfflineErrors ); localStorage.setItem('geocontrib_offline', JSON.stringify(arraysOffline)); }, toNewFeatureType() { this.featureTypeImporting = true; this.$router.push({ name: 'ajouter-type-signalement', params: { geojson: this.geojsonImport, fileToImport: this.fileToImport, }, }); this.featureTypeImporting = false; }, onFileChange(e) { this.featureTypeImporting = true; var files = e.target.files || e.dataTransfer.files; if (!files.length) return; this.fileToImport = files[0]; // TODO : VALIDATION IF FILE IS JSON if (parseFloat(fileConvertSizeToMo(this.fileToImport.size)) > 10) { this.isFileSizeModalOpen = true; } else if (this.fileToImport.size > 0) { const fr = new FileReader(); try { fr.onload = (e) => { this.geojsonImport = JSON.parse(e.target.result); this.featureTypeImporting = false; }; fr.readAsText(this.fileToImport); //* stock filename to import features afterward this.$store.commit( 'feature_type/SET_FILE_TO_IMPORT', this.fileToImport ); } catch (err) { console.error(err); this.featureTypeImporting = false; } } else { this.featureTypeImporting = false; } }, closeFileSizeModal() { this.fileToImport = { name: '', size: 0 }; this.featureTypeImporting = false; this.isFileSizeModalOpen = false; }, 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.modalType = false; if (this.is_suscriber) this.infoMessage = 'Vous êtes maintenant abonné aux notifications de ce projet.'; else this.infoMessage = 'Vous ne recevrez plus les notifications de ce projet.'; setTimeout(() => (this.infoMessage = ''), 3000); }); }, deleteProject() { projectAPI.deleteProject(this.API_BASE_URL, this.slug) .then((response) => { if (response === 'success') { this.$router.push('/'); this.DISPLAY_MESSAGE({ comment: `Le projet ${this.project.title} a bien été supprimé.`, level: 'positive' }); } else { this.DISPLAY_MESSAGE({ comment: `Une erreur est survenu lors de la suppression du projet ${this.project.title}.`, level: 'negative' }); } }); }, deleteFeatureType() { featureTypeAPI.deleteFeatureType(this.featureTypeToDelete.slug) .then((response) => { this.modalType = false; if (response === 'success') { this.retrieveProjectInfo(); this.DISPLAY_MESSAGE({ comment: `Le type de signalement ${this.featureTypeToDelete.title} a bien été supprimé.`, level: 'positive', }); } else { this.DISPLAY_MESSAGE({ comment: `Une erreur est survenu lors de la suppression du type de signalement ${this.featureTypeToDelete.title}.`, level: 'negative', }); } 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); this.checkForOfflineFeature(); let project_id = this.$route.params.slug.split('-')[0]; const mvtUrl = `${this.API_BASE_URL}features.mvt/?tile={z}/{x}/{y}&project_id=${project_id}`; mapUtil.addVectorTileLayer( mvtUrl, this.$route.params.slug, this.$store.state.feature_type.feature_types ); this.mapLoading = false; this.arraysOffline.forEach((x) => (x.geojson.properties.color = 'red')); const featuresOffline = this.arraysOffline.map((x) => x.geojson); this.GET_PROJECT_FEATURES({ project_slug: this.slug, ordering: '-created_on', limit: null, geojson: true, }) .then(() => { this.featuresLoading = false; mapUtil.addFeatures( [...this.features, ...featuresOffline], {}, true, this.$store.state.feature_type.feature_types ); }) .catch((err) => { console.error(err); this.featuresLoading = false; }); featureAPI.getFeaturesBbox(this.slug).then((bbox) => { if (bbox) { mapUtil.getMap().fitBounds(bbox, { padding: [25, 25] }); } }); } }, }, }; </script> <style> #map { width: 100%; height: 100%; min-height: 250px; } /* // ! missing style in semantic.min.css, je ne comprends pas comment... */ .ui.right.floated.button { float: right; } </style> <style scoped> .list-image-type { margin-right: 5px; height: 25px; vertical-align: bottom; } .feature-type-container { display: flex; justify-content: space-between; align-items: center; } .feature-type-container > .middle.aligned.content { width: 50%; } .feature-type-title { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; line-height: 1.5em; } .nouveau-type-signalement { margin-top: 1em; } .nouveau-type-signalement .label{ cursor: pointer; } #button-import { margin-top: 0.5em; } .fullwidth { width: 100%; } .button-align-left { display: flex; align-items: center; text-align: left; width: fit-content; } .space-between { justify-content: space-between; } .flex-column-right { flex-direction: column !important; align-items: flex-end; } .import-message { width: fit-content; line-height: 2em; 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>