Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • geocontrib/geocontrib-frontend
  • ext_matthieu/geocontrib-frontend
  • fnecas/geocontrib-frontend
  • MatthieuE/geocontrib-frontend
4 results
Show changes
...@@ -6,15 +6,13 @@ ...@@ -6,15 +6,13 @@
<form <form
id="form-layers" id="form-layers"
action="."
method="post"
enctype="multipart/form-data"
class="ui form" class="ui form"
> >
<!-- {{ formset.management_form }} --> <!-- {{ formset.management_form }} -->
<div class="ui buttons"> <div class="ui buttons">
<a <button
class="ui compact small icon left floated button green" class="ui compact small icon left floated button green"
type="button"
data-variation="mini" data-variation="mini"
@click="addBasemap" @click="addBasemap"
> >
...@@ -23,12 +21,12 @@ ...@@ -23,12 +21,12 @@
aria-hidden="true" aria-hidden="true"
/> />
<span>&nbsp;Créer un fond cartographique</span> <span>&nbsp;Créer un fond cartographique</span>
</a> </button>
</div> </div>
<div <div
v-if="basemaps" v-if="basemaps"
class="ui" class="ui margin-bottom margin-top"
> >
<BasemapListItem <BasemapListItem
v-for="basemap in basemaps" v-for="basemap in basemaps"
...@@ -36,19 +34,19 @@ ...@@ -36,19 +34,19 @@
:basemap="basemap" :basemap="basemap"
/> />
</div> </div>
<div class="margin-top">
<button <button
type="button" v-if="basemaps && basemaps[0] && basemaps[0].title && basemaps[0].layers.length > 0"
class="ui teal icon floated button" type="button"
@click="saveChanges" class="ui teal icon floated button"
> @click="saveChanges"
<i >
class="white save icon" <i
aria-hidden="true" class="white save icon"
/> aria-hidden="true"
Enregistrer les changements />
</button> Enregistrer les changements
</div> </button>
</form> </form>
</div> </div>
</template> </template>
...@@ -116,7 +114,7 @@ export default { ...@@ -116,7 +114,7 @@ export default {
.then((response) => { .then((response) => {
const errors = response.filter( const errors = response.filter(
(res) => (res) =>
res.status === 200 && res.status === 201 && res.status === 204 res.status !== 200 && res.status !== 201 && res.status !== 204
); );
if (errors.length === 0) { if (errors.length === 0) {
this.DISPLAY_MESSAGE({ this.DISPLAY_MESSAGE({
...@@ -138,12 +136,4 @@ export default { ...@@ -138,12 +136,4 @@ export default {
}, },
}, },
}; };
</script> </script>
\ No newline at end of file
<style lang="less" scoped>
#project-basemaps {
min-width: 300px;
}
</style>
\ No newline at end of file
...@@ -4,24 +4,6 @@ ...@@ -4,24 +4,6 @@
v-if="permissions && permissions.can_view_project && project" v-if="permissions && permissions.can_view_project && project"
id="project-detail" id="project-detail"
> >
<div
id="message"
class="fullwidth"
>
<div
v-if="tempMessage"
class="ui positive message"
>
<p>
<i
class="check icon"
aria-hidden="true"
/>
{{ tempMessage }}
</p>
</div>
</div>
<ProjectHeader <ProjectHeader
:arrays-offline="arraysOffline" :arrays-offline="arraysOffline"
@retrieveInfo="retrieveProjectInfo" @retrieveInfo="retrieveProjectInfo"
...@@ -32,27 +14,47 @@ ...@@ -32,27 +14,47 @@
<div class="row"> <div class="row">
<div class="eight wide column"> <div class="eight wide column">
<ProjectFeatureTypes <ProjectFeatureTypes
:loading="projectInfoLoading" :loading="featureTypesLoading"
:project="project" :project="project"
@delete="toggleDeleteFeatureTypeModal" @delete="toggleDeleteFeatureTypeModal"
@update="updateAfterImport"
/> />
</div> </div>
<div class="eight wide column map-container"> <div class="eight wide column block-map">
<div <div class="map-container">
:class="{ active: mapLoading }" <div
class="ui inverted dimmer" id="map"
> ref="map"
<div class="ui text loader"> />
Chargement de la carte... <div
:class="{ active: mapLoading }"
class="ui inverted dimmer"
>
<div class="ui text loader">
Chargement de la carte...
</div>
</div>
<SidebarLayers
v-if="basemaps && map && !projectInfoLoading"
ref="sidebar"
/>
<Geolocation />
<div
id="popup"
class="ol-popup"
>
<a
id="popup-closer"
href="#"
class="ol-popup-closer"
/>
<div
id="popup-content"
/>
</div> </div>
</div> </div>
<div
id="map"
ref="map"
/>
<router-link <router-link
id="features-list" id="features-list"
:to="{ :to="{
...@@ -68,20 +70,6 @@ ...@@ -68,20 +70,6 @@
Voir tous les signalements Voir tous les signalements
</div> </div>
</router-link> </router-link>
<div
id="popup"
class="ol-popup"
>
<a
id="popup-closer"
href="#"
class="ol-popup-closer"
/>
<div
id="popup-content"
/>
</div>
</div> </div>
</div> </div>
...@@ -89,7 +77,7 @@ ...@@ -89,7 +77,7 @@
<div class="sixteen wide column"> <div class="sixteen wide column">
<div class="ui two stackable cards"> <div class="ui two stackable cards">
<ProjectLastFeatures <ProjectLastFeatures
:loading="featuresLoading" ref="lastFeatures"
/> />
<ProjectLastComments <ProjectLastComments
:loading="projectInfoLoading" :loading="projectInfoLoading"
...@@ -139,6 +127,8 @@ import ProjectLastFeatures from '@/components/Project/Detail/ProjectLastFeatures ...@@ -139,6 +127,8 @@ import ProjectLastFeatures from '@/components/Project/Detail/ProjectLastFeatures
import ProjectLastComments from '@/components/Project/Detail/ProjectLastComments'; import ProjectLastComments from '@/components/Project/Detail/ProjectLastComments';
import ProjectParameters from '@/components/Project/Detail/ProjectParameters'; import ProjectParameters from '@/components/Project/Detail/ProjectParameters';
import ProjectModal from '@/components/Project/Detail/ProjectModal'; import ProjectModal from '@/components/Project/Detail/ProjectModal';
import SidebarLayers from '@/components/Map/SidebarLayers';
import Geolocation from '@/components/Map/Geolocation';
export default { export default {
name: 'ProjectDetail', name: 'ProjectDetail',
...@@ -149,7 +139,9 @@ export default { ...@@ -149,7 +139,9 @@ export default {
ProjectLastFeatures, ProjectLastFeatures,
ProjectLastComments, ProjectLastComments,
ProjectParameters, ProjectParameters,
ProjectModal ProjectModal,
SidebarLayers,
Geolocation,
}, },
filters: { filters: {
...@@ -166,8 +158,8 @@ export default { ...@@ -166,8 +158,8 @@ export default {
props: { props: {
message: { message: {
type: String, type: Object,
default: '' default: () => {}
} }
}, },
...@@ -181,8 +173,8 @@ export default { ...@@ -181,8 +173,8 @@ export default {
is_suscriber: false, is_suscriber: false,
tempMessage: null, tempMessage: null,
projectInfoLoading: true, projectInfoLoading: true,
featureTypesLoading: false,
featureTypeToDelete: null, featureTypeToDelete: null,
featuresLoading: true,
mapLoading: true, mapLoading: true,
}; };
}, },
...@@ -204,20 +196,18 @@ export default { ...@@ -204,20 +196,18 @@ export default {
'feature_types' 'feature_types'
]), ]),
...mapState([ ...mapState([
'last_comments',
'user', 'user',
'user_permissions', 'user_permissions',
'reloadIntervalId', 'reloadIntervalId',
]), ]),
...mapState('map', [ ...mapState('map', [
'map' 'map',
'basemaps',
'availableLayers',
]), ]),
API_BASE_URL() { API_BASE_URL() {
return this.configuration.VUE_APP_DJANGO_API_BASE; return this.configuration.VUE_APP_DJANGO_API_BASE;
}, },
isSharedProject() {
return this.$route.path.includes('projet-partage');
},
}, },
created() { created() {
...@@ -229,23 +219,17 @@ export default { ...@@ -229,23 +219,17 @@ export default {
}) })
.then((data) => (this.is_suscriber = data.is_suscriber)); .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() { mounted() {
this.retrieveProjectInfo(); this.retrieveProjectInfo();
if (this.message) { if (this.message) {
this.tempMessage = this.message; this.DISPLAY_MESSAGE(this.message);
document
.getElementById('message')
.scrollIntoView({ block: 'end', inline: 'nearest' });
setTimeout(() => (this.tempMessage = null), 5000); //* hide message after 5 seconds
} }
}, },
destroyed() { beforeDestroy() {
this.$store.dispatch('CANCEL_CURRENT_SEARCH_REQUEST');
this.CLEAR_RELOAD_INTERVAL_ID(); this.CLEAR_RELOAD_INTERVAL_ID();
this.CLOSE_PROJECT_MODAL(); this.CLOSE_PROJECT_MODAL();
}, },
...@@ -254,8 +238,6 @@ export default { ...@@ -254,8 +238,6 @@ export default {
...mapMutations([ ...mapMutations([
'CLEAR_RELOAD_INTERVAL_ID', 'CLEAR_RELOAD_INTERVAL_ID',
'DISPLAY_MESSAGE', 'DISPLAY_MESSAGE',
'DISPLAY_LOADER',
'DISCARD_LOADER',
]), ]),
...mapMutations('modals', [ ...mapMutations('modals', [
'OPEN_PROJECT_MODAL', 'OPEN_PROJECT_MODAL',
...@@ -272,33 +254,25 @@ export default { ...@@ -272,33 +254,25 @@ export default {
'GET_PROJECT_FEATURES' 'GET_PROJECT_FEATURES'
]), ]),
...mapActions('feature-type', [ ...mapActions('feature-type', [
'GET_IMPORTS' 'GET_PROJECT_FEATURE_TYPES',
]), ]),
getRouteUrl(url) {
if (this.isSharedProject) {
url = url.replace('projet', 'projet-partage');
}
return url.replace(this.$store.state.configuration.BASE_URL, ''); //* remove duplicate /geocontrib
},
retrieveProjectInfo() { retrieveProjectInfo() {
this.DISPLAY_LOADER('Projet en cours de chargement.');
Promise.all([ Promise.all([
this.GET_PROJECT(this.slug), this.GET_PROJECT(this.slug),
this.GET_PROJECT_INFO(this.slug) this.GET_PROJECT_INFO(this.slug)
]) ])
.then(() => { .then(() => {
this.DISCARD_LOADER(); this.$nextTick(() => {
this.projectInfoLoading = false; const map = mapService.getMap();
setTimeout(() => {
let map = mapService.getMap();
if (map) mapService.destroyMap(); if (map) mapService.destroyMap();
this.initMap(); this.initMap();
}, 1000); });
}) })
.catch((err) => { .catch((err) => {
console.error(err); console.error(err);
this.DISCARD_LOADER(); })
.finally(() => {
this.projectInfoLoading = false; this.projectInfoLoading = false;
}); });
}, },
...@@ -415,66 +389,96 @@ export default { ...@@ -415,66 +389,96 @@ export default {
this.featureTypeToDelete = featureType; this.featureTypeToDelete = featureType;
this.OPEN_PROJECT_MODAL('deleteFeatureType'); this.OPEN_PROJECT_MODAL('deleteFeatureType');
}, },
/**
* Initializes the map if the project is accessible and the user has view permissions.
* This method sets up the map, loads vector tile layers, and handles offline features.
*/
async initMap() { async initMap() {
// Check if the project is accessible and the user has view permissions
if (this.project && this.permissions.can_view_project) { if (this.project && this.permissions.can_view_project) {
await this.INITIATE_MAP(this.$refs.map); // Initialize the map using the provided element reference
await this.INITIATE_MAP({ el: this.$refs.map });
// Check for offline features
this.checkForOfflineFeature(); this.checkForOfflineFeature();
const project_id = this.$route.params.slug.split('-')[0]; // Define the URL for vector tile layers
const mvtUrl = `${this.API_BASE_URL}features.mvt`; const mvtUrl = `${this.API_BASE_URL}features.mvt`;
mapService.addVectorTileLayer( // Define parameters for loading layers
mvtUrl, const params = {
project_id, project_slug: this.slug,
this.feature_types featureTypes: this.feature_types,
); queryParams: {
this.mapLoading = false; ordering: this.project.feature_browsing_default_sort,
filter: this.project.feature_browsing_default_filter,
},
};
// Add vector tile layers to the map
mapService.addVectorTileLayer({
url: mvtUrl,
...params
});
// Modify offline feature properties (setting color to 'red')
this.arraysOffline.forEach((x) => (x.geojson.properties.color = 'red')); this.arraysOffline.forEach((x) => (x.geojson.properties.color = 'red'));
// Extract offline features from arraysOffline
const featuresOffline = this.arraysOffline.map((x) => x.geojson); const featuresOffline = this.arraysOffline.map((x) => x.geojson);
// Add offline features to the map if available
this.GET_PROJECT_FEATURES({ if (featuresOffline && featuresOffline.length > 0) {
project_slug: this.slug, mapService.addFeatures({
ordering: '-created_on', addToMap: true,
limit: null, features: featuresOffline,
geojson: true, ...params
})
.then(() => {
this.featuresLoading = false;
mapService.addFeatures(
[...this.features, ...featuresOffline],
{},
this.feature_types,
true
);
})
.catch((err) => {
console.error(err);
this.featuresLoading = false;
}); });
}
// Get the bounding box of features and fit the map to it
featureAPI.getFeaturesBbox(this.slug).then((bbox) => { featureAPI.getFeaturesBbox(this.slug).then((bbox) => {
if (bbox) { if (bbox) {
mapService.fitBounds(bbox); mapService.fitBounds(bbox);
} }
this.mapLoading = false; // Mark map loading as complete
}); });
} }
}, },
updateAfterImport() {
// reload feature types
this.featureTypesLoading = true;
this.GET_PROJECT_FEATURE_TYPES(this.slug)
.then(() => {
this.featureTypesLoading = false;
});
// reload last features
this.$refs.lastFeatures.fetchLastFeatures();
// reload map
const map = mapService.getMap();
if (map) mapService.destroyMap();
this.mapLoading = true;
this.initMap();
},
}, },
}; };
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
.fullwidth { .fullwidth {
width: 100%; width: 100%;
} }
.map-container { .block-map {
display: flex !important; display: flex !important;
flex-direction: column; flex-direction: column;
.map-container {
position: relative;
height: 100%;
#map {
border: 1px solid grey;
}
}
.button { .button {
margin-top: 0.5em; margin-top: 0.5em;
} }
} }
div.geolocation-container {
/* each button have .5em space between, zoom buttons are 60px high and full screen button is 34px high */
top: calc(1.1em + 60px);
}
</style> </style>
...@@ -46,7 +46,7 @@ ...@@ -46,7 +46,7 @@
</li> </li>
</ul> </ul>
</div> </div>
<div class="field"> <div class="field file-logo">
<label>Illustration du projet</label> <label>Illustration du projet</label>
<img <img
v-if="thumbnailFileSrc.length || form.thumbnail.length" v-if="thumbnailFileSrc.length || form.thumbnail.length"
...@@ -150,7 +150,7 @@ ...@@ -150,7 +150,7 @@
Visibilité des signalements archivés Visibilité des signalements archivés
</label> </label>
<Dropdown <Dropdown
:options="levelPermissions" :options="levelPermissionsArc"
:selected="form.access_level_arch_feature.name" :selected="form.access_level_arch_feature.name"
:selection.sync="form.access_level_arch_feature" :selection.sync="form.access_level_arch_feature"
/> />
...@@ -169,7 +169,7 @@ ...@@ -169,7 +169,7 @@
</div> </div>
<div class="two fields"> <div class="two fields">
<div class="fields grouped"> <div class="fields grouped checkboxes">
<div class="field"> <div class="field">
<div class="ui checkbox"> <div class="ui checkbox">
<input <input
...@@ -219,26 +219,116 @@ ...@@ -219,26 +219,116 @@
name="fast_edition_mode" name="fast_edition_mode"
> >
<label for="fast_edition_mode">Mode d'édition rapide de signalements</label> <label for="fast_edition_mode">Mode d'édition rapide de signalements</label>
<div
class="
ui
small
button
circular
compact
absolute-right
icon
teal
"
data-tooltip="Consulter la documentation"
data-position="right center"
data-variation="mini"
@click="goToDocumentationFeature"
>
<i class="question icon" />
</div>
</div>
</div>
<div class="field">
<div class="ui checkbox">
<input
id="feature_assignement"
v-model="form.feature_assignement"
class="hidden"
type="checkbox"
name="feature_assignement"
>
<label for="feature_assignement">Activation de l'assignation de signalements aux membres du projet</label>
</div>
</div>
<div class="fields grouped">
<div class="field">
<label for="feature_browsing">Configuration du parcours de signalement</label>
</div>
<div
id="feature_browsing_filter"
class="field inline"
>
<label for="feature_browsing_default_filter">Filtrer sur</label>
<Dropdown
:options="featureBrowsingOptions.filter"
:selected="form.feature_browsing_default_filter.name"
:selection.sync="form.feature_browsing_default_filter"
/>
</div>
<div
id="feature_browsing_sort"
class="field inline"
>
<label for="feature_browsing_default_sort">Trier par</label>
<Dropdown
:options="featureBrowsingOptions.sort"
:selected="form.feature_browsing_default_sort.name"
:selection.sync="form.feature_browsing_default_sort"
/>
</div> </div>
</div> </div>
</div> </div>
<div class="field required"> <div class="field">
<label>Niveau de zoom maximum de la carte</label> <label>Niveau de zoom maximum de la carte</label>
<div class="range-container"> <div class="map-maxzoom-selector">
<input <div class="range-container">
v-model="form.map_max_zoom_level" <input
type="range" v-model="form.map_max_zoom_level"
min="0" type="range"
max="22" min="0"
step="1" max="22"
><output class="range-output-bubble">{{ step="1"
form.map_max_zoom_level @input="zoomMap"
}}</output> ><output class="range-output-bubble">{{
scalesTable[form.map_max_zoom_level]
}}</output>
</div>
<div class="map-preview">
<label>Aperçu :</label>
<div
id="map"
ref="map"
/>
<div class="no-preview">
pas de fond&nbsp;de&nbsp;carte disponible à&nbsp;cette&nbsp;échelle
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
<div
v-if="filteredAttributes.length > 0"
class="ui horizontal divider"
>
ATTRIBUTS
</div>
<div class="fields grouped">
<ProjectAttributeForm
v-for="(attribute, index) in filteredAttributes"
:key="index"
:attribute="attribute"
:form-project-attributes="form.project_attributes"
@update:project_attributes="updateProjectAttributes($event)"
/>
</div>
<div class="ui divider" /> <div class="ui divider" />
<button <button
...@@ -258,17 +348,20 @@ ...@@ -258,17 +348,20 @@
<script> <script>
import axios from '@/axios-client.js'; import axios from '@/axios-client.js';
import Dropdown from '@/components/Dropdown.vue'; import Dropdown from '@/components/Dropdown';
import ProjectAttributeForm from '@/components/Project/Edition/ProjectAttributeForm';
import mapService from '@/services/map-service';
import TextareaMarkdown from 'textarea-markdown'; import TextareaMarkdown from 'textarea-markdown';
import { mapState } from 'vuex'; import { mapActions, mapState } from 'vuex';
export default { export default {
name: 'ProjectEdit', name: 'ProjectEdit',
components: { components: {
Dropdown, Dropdown,
ProjectAttributeForm
}, },
data() { data() {
...@@ -279,13 +372,30 @@ export default { ...@@ -279,13 +372,30 @@ export default {
name: 'Sélectionner une image ...', name: 'Sélectionner une image ...',
size: 0, size: 0,
}, },
errors_archive_feature: [],
errors: { errors: {
title: [], title: [],
access_level_pub_feature: [], access_level_pub_feature: [],
access_level_arch_feature: [], access_level_arch_feature: [],
}, },
errorThumbnail: [], errorThumbnail: [],
featureBrowsingOptions: {
filter: [{
name: 'Désactivé',
value: ''
},
{
name: 'Type de signalement',
value: 'feature_type_slug',
}],
sort: [{
name: 'Date de création',
value: '-created_on',
},
{
name: 'Date de modification',
value: '-updated_on'
}],
},
form: { form: {
title: '', title: '',
slug: '', slug: '',
...@@ -298,8 +408,6 @@ export default { ...@@ -298,8 +408,6 @@ export default {
creator: null, creator: null,
access_level_pub_feature: { name: '', value: '' }, access_level_pub_feature: { name: '', value: '' },
access_level_arch_feature: { name: '', value: '' }, access_level_arch_feature: { name: '', value: '' },
archive_feature: 0,
delete_feature: 0,
map_max_zoom_level: 22, map_max_zoom_level: 22,
nb_features: 0, nb_features: 0,
nb_published_features: 0, nb_published_features: 0,
...@@ -308,21 +416,52 @@ export default { ...@@ -308,21 +416,52 @@ export default {
nb_contributors: 0, nb_contributors: 0,
is_project_type: false, is_project_type: false,
generate_share_link: false, generate_share_link: false,
feature_assignement: false,
fast_edition_mode: false, fast_edition_mode: false,
feature_browsing_default_filter: '',
feature_browsing_default_sort: '-created_on',
project_attributes: [],
}, },
thumbnailFileSrc: '', thumbnailFileSrc: '',
scalesTable: [
'1:500 000 000',
'1:250 000 000',
'1:150 000 000',
'1:70 000 000',
'1:35 000 000',
'1:15 000 000',
'1:10 000 000',
'1:4 000 000',
'1:2 000 000',
'1:1 000 000',
'1:500 000',
'1:250 000',
'1:150 000',
'1:70 000',
'1:35 000',
'1:15 000',
'1:8 000',
'1:4 000',
'1:2 000',
'1:1 000',
'1:500',
'1:250',
'1:150',
]
}; };
}, },
computed: { computed: {
...mapState([ ...mapState([
'levelsPermissions', 'levelsPermissions',
'projectAttributes'
]), ]),
...mapState('projects', ['project']), ...mapState('projects', ['project']),
DJANGO_BASE_URL: function () { DJANGO_BASE_URL: function () {
return this.$store.state.configuration.VUE_APP_DJANGO_BASE; return this.$store.state.configuration.VUE_APP_DJANGO_BASE;
}, },
levelPermissions(){ levelPermissionsArc(){
const levels = new Array(); const levels = new Array();
if(this.levelsPermissions) { if(this.levelsPermissions) {
this.levelsPermissions.forEach((item) => { this.levelsPermissions.forEach((item) => {
...@@ -332,7 +471,7 @@ export default { ...@@ -332,7 +471,7 @@ export default {
value: item.user_type_id, value: item.user_type_id,
}); });
} }
if (!this.form.moderation && item.user_type_id == 'moderator') { if (!this.form.moderation && item.user_type_id === 'moderator') {
levels.pop(); levels.pop();
} }
}); });
...@@ -356,7 +495,14 @@ export default { ...@@ -356,7 +495,14 @@ export default {
}); });
} }
return levels; return levels;
} },
/**
* Filter out attribute of field type list without option
*/
filteredAttributes() {
return this.projectAttributes.filter(attr => attr.field_type === 'boolean' || attr.options);
},
}, },
watch: { watch: {
...@@ -366,11 +512,12 @@ export default { ...@@ -366,11 +512,12 @@ export default {
} }
} }
}, },
created() { mounted() {
this.definePageType(); this.definePageType();
if (this.action === 'create') { if (this.action === 'create') {
this.thumbnailFileSrc = require('@/assets/img/default.png'); this.thumbnailFileSrc = require('@/assets/img/default.png');
this.initPreviewMap();
} else if (this.action === 'edit' || this.action === 'create_from') { } else if (this.action === 'edit' || this.action === 'create_from') {
if (!this.project) { if (!this.project) {
this.$store.dispatch('projects/GET_PROJECT', this.$route.params.slug) this.$store.dispatch('projects/GET_PROJECT', this.$route.params.slug)
...@@ -383,14 +530,14 @@ export default { ...@@ -383,14 +530,14 @@ export default {
this.fillProjectForm(); this.fillProjectForm();
} }
} }
},
mounted() {
let textarea = document.querySelector('textarea'); let textarea = document.querySelector('textarea');
new TextareaMarkdown(textarea); new TextareaMarkdown(textarea);
}, },
methods: { methods: {
...mapActions('map', [
'INITIATE_MAP'
]),
definePageType() { definePageType() {
if (this.$router.history.current.name === 'project_create') { if (this.$router.history.current.name === 'project_create') {
this.action = 'create'; this.action = 'create';
...@@ -467,20 +614,11 @@ export default { ...@@ -467,20 +614,11 @@ export default {
} }
}, },
checkEmpty() {
//* forbid empty fields
if (!this.form.archive_feature) {
this.form.archive_feature = 0;
}
if (!this.form.delete_feature) {
this.form.delete_feature = 0;
}
},
goBackNrefresh(slug) { goBackNrefresh(slug) {
Promise.all([ Promise.all([
this.$store.dispatch('GET_USER_LEVEL_PROJECTS'), //* refresh projects user levels this.$store.dispatch('GET_USER_LEVEL_PROJECTS'), //* refresh projects user levels
this.$store.dispatch('GET_USER_LEVEL_PERMISSIONS'), //* refresh projects permissions this.$store.dispatch('GET_USER_LEVEL_PERMISSIONS'), //* refresh projects permissions
this.$store.dispatch('projects/GET_PROJECT', slug), //* refresh current project
]).then(() => ]).then(() =>
// * go back to project list // * go back to project list
this.$router.push({ this.$router.push({
...@@ -524,12 +662,6 @@ export default { ...@@ -524,12 +662,6 @@ export default {
}, },
checkForm() { checkForm() {
if (this.form.archive_feature > this.form.delete_feature) {
this.errors_archive_feature.push(
"Le délais de suppression doit être supérieur au délais d'archivage."
);
return false;
}
for (const key in this.errors) { for (const key in this.errors) {
if ((key === 'title' && this.form[key]) || this.form[key].value) { if ((key === 'title' && this.form[key]) || this.form[key].value) {
this.errors[key] = []; this.errors[key] = [];
...@@ -553,23 +685,16 @@ export default { ...@@ -553,23 +685,16 @@ export default {
return; return;
} }
const projectData = { const projectData = {
title: this.form.title, ...this.form,
description: this.form.description,
access_level_arch_feature: this.form.access_level_arch_feature.value, access_level_arch_feature: this.form.access_level_arch_feature.value,
access_level_pub_feature: this.form.access_level_pub_feature.value, access_level_pub_feature: this.form.access_level_pub_feature.value,
archive_feature: this.form.archive_feature, feature_browsing_default_sort: this.form.feature_browsing_default_sort.value,
delete_feature: this.form.delete_feature, feature_browsing_default_filter: this.form.feature_browsing_default_filter.value,
map_max_zoom_level: this.form.map_max_zoom_level,
is_project_type: this.form.is_project_type,
generate_share_link: this.form.generate_share_link,
fast_edition_mode: this.form.fast_edition_mode,
moderation: this.form.moderation,
}; };
let url = `${this.$store.state.configuration.VUE_APP_DJANGO_API_BASE}projects/`;
if (this.action === 'edit') { if (this.action === 'edit') {
await axios await axios
.put((url += `${this.project.slug}/`), projectData) .put((`${this.$store.state.configuration.VUE_APP_DJANGO_API_BASE}v2/projects/${this.project.slug}/`), projectData)
.then((response) => { .then((response) => {
if (response && response.status === 200) { if (response && response.status === 200) {
//* send thumbnail after feature_type was updated //* send thumbnail after feature_type was updated
...@@ -587,8 +712,9 @@ export default { ...@@ -587,8 +712,9 @@ export default {
throw error; throw error;
}); });
} else { } else {
let url = `${this.$store.state.configuration.VUE_APP_DJANGO_API_BASE}v2/projects/`;
if (this.action === 'create_from') { if (this.action === 'create_from') {
url += `${this.project.slug}/duplicate/`; url = `${this.$store.state.configuration.VUE_APP_DJANGO_API_BASE}projects/${this.project.slug}/duplicate/`;
} }
this.loading = true; this.loading = true;
await axios await axios
...@@ -613,9 +739,12 @@ export default { ...@@ -613,9 +739,12 @@ export default {
}); });
} }
}, },
fillProjectForm() { fillProjectForm() {
this.form = { ...this.project }; //* create a new object to avoid modifying original one //* create a new object to avoid modifying original one
if (this.action === 'create_from') { //* if duplication of project, generate new name this.form = { ...this.project };
//* if duplication of project, generate new name
if (this.action === 'create_from') {
this.form.title = this.form.title =
this.project.title + this.project.title +
` (Copie-${new Date() ` (Copie-${new Date()
...@@ -624,40 +753,128 @@ export default { ...@@ -624,40 +753,128 @@ export default {
.replace(',', '')})`; .replace(',', '')})`;
this.form.is_project_type = false; this.form.is_project_type = false;
} }
//* transform string values to objects for dropdowns display (could be in a computed) //* transform string values to objects used with dropdowns
// fill dropdown current selection for archived feature viewing permission
if (this.levelPermissionsArc) {
const accessLevelArc = this.levelPermissionsArc.find(
(el) => el.name === this.project.access_level_arch_feature
);
if (accessLevelArc) {
this.form.access_level_arch_feature = {
name: this.project.access_level_arch_feature,
value: accessLevelArc.value ,
};
}
}
// fill dropdown current selection for published feature viewing permission
if (this.levelPermissionsPub) { if (this.levelPermissionsPub) {
const value = this.levelPermissionsPub.find( const accessLevelPub = this.levelPermissionsPub.find(
(el) => el.name === this.project.access_level_pub_feature (el) => el.name === this.project.access_level_pub_feature
); );
if(value){ if (accessLevelPub) {
this.form.access_level_pub_feature = { this.form.access_level_pub_feature = {
name: this.project.access_level_pub_feature, name: this.project.access_level_pub_feature,
value: value.value , value: accessLevelPub.value ,
}; };
} }
} }
if (this.levelPermissions) { // fill dropdown current selection for feature browsing default filtering
const value = this.levelPermissions.find( const default_filter = this.featureBrowsingOptions.filter.find(
(el) => el.name === this.project.access_level_arch_feature (el) => el.value === this.project.feature_browsing_default_filter
); );
if(value){ if (default_filter) {
this.form.access_level_arch_feature = { this.form.feature_browsing_default_filter = default_filter;
name: this.project.access_level_arch_feature, }
value: value.value , // fill dropdown current selection for feature browsing default sorting
}; const default_sort = this.featureBrowsingOptions.sort.find(
} (el) => el.value === this.project.feature_browsing_default_sort
);
if (default_sort) {
this.form.feature_browsing_default_sort = default_sort;
}
this.initPreviewMap();
},
initPreviewMap () {
const map = mapService.getMap();
if (map) mapService.destroyMap();
//On récupère le zoom maximum autorisé par la couche
const maxZoomLayer = this.$store.state.configuration.DEFAULT_BASE_MAP_OPTIONS.maxZoom;
let activeZoom = maxZoomLayer;
if(this.project && this.project.map_max_zoom_level < maxZoomLayer){
activeZoom = this.project.map_max_zoom_level;
}
this.INITIATE_MAP({
el: this.$refs.map,
zoom: activeZoom,
center: this.$store.state.configuration.MAP_PREVIEW_CENTER,
maxZoom: 22,
controls: [],
zoomControl: false,
//On désactive le zoom et le pan => gérer par le composant zoom max
interactions: { dragPan: false, mouseWheelZoom: false }
});
// add default basemap (in other maps the component SidebarLayer handles layers)
mapService.addLayers(
null,
this.$store.state.configuration.DEFAULT_BASE_MAP_SERVICE,
this.$store.state.configuration.DEFAULT_BASE_MAP_OPTIONS,
this.$store.state.configuration.DEFAULT_BASE_MAP_SCHEMA_TYPE
);
//La tuile au dessus du zoom maximum n'existe pas
//On attend un peu qu'elle se charge et on zoom si besoin
setTimeout(() => {
mapService.zoom(this.project ? this.project.map_max_zoom_level : 22);
}, 500);
},
zoomMap() {
mapService.zoom(this.form.map_max_zoom_level);
},
/**
* Updates the value of a project attribute or adds a new attribute if it does not exist.
*
* This function looks for an attribute by its ID. If the attribute exists, its value is updated.
* If the attribute does not exist, a new attribute object is added to the `project_attributes` array.
*
* @param {String} value - The new value to be assigned to the project attribute.
* @param {Number} attributeId - The ID of the attribute to be updated or added.
*/
updateProjectAttributes({ value, attributeId }) {
// Find the index of the attribute in the project_attributes array.
const attributeIndex = this.form.project_attributes.findIndex(el => el.attribute_id === attributeId);
if (attributeIndex !== -1) {
// Directly update the attribute's value if it exists.
this.form.project_attributes[attributeIndex].value = value;
} else {
// Add a new attribute object if it does not exist.
this.form.project_attributes.push({ attribute_id: attributeId, value });
} }
}, },
goToDocumentationFeature() {
window.open(this.$store.state.configuration.VUE_APP_URL_DOCUMENTATION_FEATURE);
}
}, },
}; };
</script> </script>
<style media="screen"> <style media="screen" lang="less">
#form-input-file-logo { #form-input-file-logo {
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
} }
.file-logo {
min-height: calc(150px + 2.4285em);
display: flex;
flex-direction: column;
justify-content: space-between;
}
.close.icon:hover { .close.icon:hover {
cursor: pointer; cursor: pointer;
} }
...@@ -672,4 +889,79 @@ textarea { ...@@ -672,4 +889,79 @@ textarea {
border: 1px solid rgba(34, 36, 38, .15); border: 1px solid rgba(34, 36, 38, .15);
padding: .78571429em 1em; padding: .78571429em 1em;
} }
</style>
\ No newline at end of file .checkboxes {
padding-left: .5em;
.absolute-right.ui.compact.icon.button {
position: absolute;
right: -2.75em;
top: calc(50% - 1em);
padding: .4em;
}
}
.map-maxzoom-selector {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
input, output {
height: fit-content;
}
output {
white-space: nowrap;
min-width: auto;
}
.range-container {
margin-bottom: 2rem;
}
.map-preview {
margin-top: -1rem;
display: flex;
position: relative;
label {
white-space: nowrap;
font-size: .95em;
margin-right: 1rem;
}
#map {
min-height: 80px;
height: 80px;
width: 150px;
max-width: 150px;
z-index: 1;
}
.no-preview {
position: absolute;
top: 25%;
left: 25%;
text-align: center;
font-size: .75em;
color: #656565;
}
}
}
label[for=feature_browsing] {
padding-left: 2em;
}
label[for=feature_browsing_default_filter],
label[for=feature_browsing_default_sort] {
min-width: 4em;
}
#feature_browsing_filter,
#feature_browsing_sort {
margin-left: 2.5rem;
}
@media only screen and (min-width: 1100px) {
#feature_browsing_filter {
margin-top: -2.25em;
}
#feature_browsing_filter,
#feature_browsing_sort {
float: right;
}
}
</style>
...@@ -143,9 +143,10 @@ ...@@ -143,9 +143,10 @@
<script> <script>
import axios from '@/axios-client.js'; import axios from '@/axios-client.js';
import { mapState } from 'vuex'; import { mapMutations, mapState } from 'vuex';
import Dropdown from '@/components/Dropdown.vue'; import Dropdown from '@/components/Dropdown.vue';
import { formatUserOption } from '@/utils';
export default { export default {
name: 'ProjectMembers', name: 'ProjectMembers',
...@@ -188,16 +189,7 @@ export default { ...@@ -188,16 +189,7 @@ export default {
userOptions: function () { userOptions: function () {
return this.projectUsers return this.projectUsers
.filter((el) => el.userLevel.value === 'logged_user') .filter((el) => el.userLevel.value === 'logged_user')
.map((el) => { .map((el) => formatUserOption(el.user)); // Format user data to fit dropdown option structure
let name = el.user.first_name || '';
if (el.user.last_name) {
name = name + ' ' + el.user.last_name;
}
return {
name: [name, el.user.username],
value: el.user.id,
};
});
}, },
levelOptions: function () { levelOptions: function () {
...@@ -247,10 +239,16 @@ export default { ...@@ -247,10 +239,16 @@ export default {
destroyed() { destroyed() {
//* allow user to change page if ever stuck on loader //* allow user to change page if ever stuck on loader
this.$store.commit('DISCARD_LOADER'); this.DISCARD_LOADER();
}, },
methods: { methods: {
...mapMutations([
'DISPLAY_MESSAGE',
'DISPLAY_LOADER',
'DISCARD_LOADER'
]),
validateNewMember() { validateNewMember() {
this.newMember.errors = []; this.newMember.errors = [];
if (!this.newMember.user.value) { if (!this.newMember.user.value) {
...@@ -300,67 +298,69 @@ export default { ...@@ -300,67 +298,69 @@ export default {
}); });
}, },
/**
* Saves the updated members and their roles for a project.
* Displays a loader while the update is in progress and provides feedback upon completion or error.
*/
saveMembers() { saveMembers() {
this.$store.commit( // Display a loader to indicate that the update process is ongoing
'DISPLAY_LOADER', this.DISPLAY_LOADER('Mise à jour des membres du projet en cours ...');
'Mise à jour des membres du projet en cours ...'
);
// Prepare the data to be sent in the API request
const data = this.projectUsers.map((member) => { const data = this.projectUsers.map((member) => {
return { return {
user: member.user, user: member.user,
level: { level: {
display: member.userLevel.name, display: member.userLevel.name, // Display name of the user level
codename: member.userLevel.value, codename: member.userLevel.value, // Codename of the user level
}, },
}; };
}); });
// Make an API request to update the project members
axios axios
.put( .put(
`${this.$store.state.configuration.VUE_APP_DJANGO_API_BASE}projects/${this.project.slug}/utilisateurs/`, `${this.$store.state.configuration.VUE_APP_DJANGO_API_BASE}projects/${this.project.slug}/utilisateurs/`,
data data
) )
.then((response) => { .then((response) => {
// Check if the response status is 200 (OK)
if (response.status === 200) { if (response.status === 200) {
this.$store.dispatch('GET_USER_LEVEL_PROJECTS'); //* update user status in top right menu // Dispatch an action to update the user status in the top right menu
this.$store.commit('DISPLAY_MESSAGE', { comment: 'Permissions mises à jour', level: 'positive' }); this.$store.dispatch('GET_USER_LEVEL_PROJECTS');
// Display a positive message indicating success
this.DISPLAY_MESSAGE({ comment: 'Permissions mises à jour avec succès', level: 'positive' });
} else { } else {
this.$store.commit( // Display a generic error message if the response status is not 200
'DISPLAY_MESSAGE', this.DISPLAY_MESSAGE({
{ comment: "Une erreur s'est produite pendant la mises à jour des permissions",
comment : "Une erreur s'est produite pendant la mises à jour des permissions", level: 'negative'
level: 'negative' });
}
);
} }
this.$store.commit('DISCARD_LOADER'); // Hide the loader regardless of the request result
this.DISCARD_LOADER();
}) })
.catch((error) => { .catch((error) => {
this.$store.commit('DISCARD_LOADER'); // Hide the loader if an error occurs
throw error; this.DISCARD_LOADER();
}); // Determine the error message to display
}, const errorMessage = error.response && error.response.data && error.response.data.error
? error.response.data.error
fetchMembers() { : "Une erreur s'est produite pendant la mises à jour des permissions";
// todo: move function to a service // Display the error message
return axios this.DISPLAY_MESSAGE({
.get( comment: errorMessage,
`${this.$store.state.configuration.VUE_APP_DJANGO_API_BASE}projects/${this.$route.params.slug}/utilisateurs/` level: 'negative'
) });
.then((response) => response.data) // Log the error to the console for debugging
.catch((error) => { console.error(error);
throw error;
}); });
}, },
populateMembers() { populateMembers() {
this.$store.commit( this.DISPLAY_LOADER('Récupération des membres en cours...');
'DISPLAY_LOADER', this.$store.dispatch('projects/GET_PROJECT_USERS', this.$route.params.slug).then((members) => {
'Récupération des membres en cours...' this.DISCARD_LOADER();
);
this.fetchMembers().then((members) => {
this.$store.commit('DISCARD_LOADER');
this.projectUsers = members.map((el) => { this.projectUsers = members.map((el) => {
return { return {
userLevel: { name: el.level.display, value: el.level.codename }, userLevel: { name: el.level.display, value: el.level.codename },
......
...@@ -9,6 +9,7 @@ ...@@ -9,6 +9,7 @@
v-if="user && user.can_create_project && isOnline" v-if="user && user.can_create_project && isOnline"
:to="{ name: 'project_create', params: { action: 'create' } }" :to="{ name: 'project_create', params: { action: 'create' } }"
class="ui green basic button" class="ui green basic button"
data-test="create-project"
> >
<i <i
class="plus icon" class="plus icon"
...@@ -22,6 +23,7 @@ ...@@ -22,6 +23,7 @@
name: 'project_type_list', name: 'project_type_list',
}" }"
class="ui blue basic button" class="ui blue basic button"
data-test="to-project-models"
> >
<i <i
class="copy icon" class="copy icon"
...@@ -33,18 +35,21 @@ ...@@ -33,18 +35,21 @@
<!-- FILTRES DES PROJETS --> <!-- FILTRES DES PROJETS -->
<ProjectsMenu <ProjectsMenu
:loading="loading"
@filter="setProjectsFilters" @filter="setProjectsFilters"
@getData="getData"
@loading="setLoader" @loading="setLoader"
/> />
<div <div
v-if="configuration.DISPLAY_FORBIDDEN_PROJECTS" v-if="configuration.DISPLAY_FORBIDDEN_PROJECTS"
id="forbidden-projects" id="forbidden-projects"
class="ui toggle checkbox" class="ui toggle checkbox margin-top"
> >
<input <input
v-model="displayForbiddenProjects" :checked="displayForbiddenProjects"
type="checkbox" type="checkbox"
@input="toggleForbiddenProjects"
> >
<label> <label>
N'afficher que les projets disponibles à la consultation N'afficher que les projets disponibles à la consultation
...@@ -55,14 +60,12 @@ ...@@ -55,14 +60,12 @@
<div <div
v-if="projects" v-if="projects"
class="ui divided items dimmable dimmed" class="ui divided items dimmable dimmed"
data-test="project-list"
> >
<div <div :class="['ui inverted dimmer', { active: loading }]">
:class="{ active: loading }"
class="ui inverted dimmer"
>
<div class="ui loader" /> <div class="ui loader" />
</div> </div>
<ProjectsListItem <ProjectsListItem
v-for="project in projects" v-for="project in projects"
:key="project.slug" :key="project.slug"
...@@ -78,9 +81,8 @@ ...@@ -78,9 +81,8 @@
<!-- PAGINATION --> <!-- PAGINATION -->
<Pagination <Pagination
v-if="count" v-if="count"
:nb-pages="Math.ceil(count/10)" :nb-pages="nbPages"
:on-page-change="SET_CURRENT_PAGE" @page-update="changePage"
@change-page="changePage"
/> />
</div> </div>
</div> </div>
...@@ -123,49 +125,30 @@ export default { ...@@ -123,49 +125,30 @@ export default {
DJANGO_BASE_URL() { DJANGO_BASE_URL() {
return this.$store.state.configuration.VUE_APP_DJANGO_BASE; return this.$store.state.configuration.VUE_APP_DJANGO_BASE;
}, },
}, nbPages() {
return Math.ceil(this.count / 10);
watch: {
filters: {
deep: true,
handler(newValue) {
if (newValue) {
this.getData();
}
}
},
displayForbiddenProjects(newValue) {
if (newValue) {
this.SET_PROJECTS_FILTER({
filter: 'accessible',
value: 'true'
});
} else {
this.SET_PROJECTS_FILTER({
filter: 'accessible',
value: null
});
}
this.getData();
} }
}, },
created() { created() {
this.SET_CURRENT_PAGE(1);
// Empty stored text to search
this.SET_PROJECTS_SEARCH_STATE({ text: null });
// Empty stored project list
this.$store.commit('projects/SET_PROJECT', null); this.$store.commit('projects/SET_PROJECT', null);
this.SET_PROJECTS_FILTER({ // Init display of restricted access projects
filter: 'accessible',
value: 'true'
});
this.displayForbiddenProjects = this.configuration.DISPLAY_FORBIDDEN_PROJECTS_DEFAULT; this.displayForbiddenProjects = this.configuration.DISPLAY_FORBIDDEN_PROJECTS_DEFAULT;
this.setForbiddenProjectsFilter(true);
}, },
methods: { methods: {
...mapMutations('projects', [ ...mapMutations('projects', [
'SET_CURRENT_PAGE', 'SET_CURRENT_PAGE',
'SET_PROJECTS_FILTER' 'SET_PROJECTS_FILTER',
'SET_PROJECTS_SEARCH_STATE',
]), ]),
...mapActions('projects', [ ...mapActions('projects', [
'GET_PROJECTS' 'GET_PROJECTS',
]), ]),
getData(page) { getData(page) {
...@@ -187,8 +170,26 @@ export default { ...@@ -187,8 +170,26 @@ export default {
this.getData(e); this.getData(e);
}, },
setProjectsFilters(e) { setProjectsFilters(e, noUpdate) {
this.SET_PROJECTS_FILTER(e); this.SET_PROJECTS_FILTER(e);
// Reset the page number at filter change
this.SET_CURRENT_PAGE(1);
// Wait that all filters are set in store to fetch data when component is created
if (!noUpdate) {
this.getData();
}
},
toggleForbiddenProjects(e) {
this.displayForbiddenProjects = e.target.checked;
this.setForbiddenProjectsFilter();
},
setForbiddenProjectsFilter(noUpdate) {
this.setProjectsFilters({
filter: 'accessible',
value: this.displayForbiddenProjects ? 'true' : null
}, noUpdate);
}, },
} }
}; };
...@@ -227,10 +228,10 @@ export default { ...@@ -227,10 +228,10 @@ export default {
color: rgb(94, 94, 94); color: rgb(94, 94, 94);
} }
input:checked ~ label::before { input:checked ~ label::before {
background-color: teal !important; background-color: var(--primary-color, #008c86) !important;
} }
input:checked ~ label { input:checked ~ label {
color: teal !important; color: var(--primary-color, #008c86) !important;
} }
} }
......
...@@ -2,6 +2,7 @@ const webpack = require('webpack'); ...@@ -2,6 +2,7 @@ const webpack = require('webpack');
const fs = require('fs'); const fs = require('fs');
const packageJson = fs.readFileSync('./package.json'); const packageJson = fs.readFileSync('./package.json');
const version = JSON.parse(packageJson).version || 0; const version = JSON.parse(packageJson).version || 0;
module.exports = { module.exports = {
publicPath: '/geocontrib/', publicPath: '/geocontrib/',
devServer: { devServer: {
...@@ -34,6 +35,7 @@ module.exports = { ...@@ -34,6 +35,7 @@ module.exports = {
themeColor: '#1da025' themeColor: '#1da025'
}, },
configureWebpack: { configureWebpack: {
devtool: 'source-map',
plugins: [ plugins: [
new webpack.DefinePlugin({ new webpack.DefinePlugin({
'process.env': { 'process.env': {
...@@ -42,5 +44,14 @@ module.exports = { ...@@ -42,5 +44,14 @@ module.exports = {
}) })
] ]
}, },
// the rest of your original module.exports code goes here transpileDependencies: [
// Add dependencies that use modern JavaScript syntax, based on encountered errors
'ol',
'color-rgba',
'color-parse',
'@sentry/browser',
'@sentry/core',
'@sentry/vue',
'@sentry-internal'
]
}; };
module.exports = {
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader'
}
},
{
test: /\.vue$/,
loader: 'vue-loader'
}
]
}
};
\ No newline at end of file