Newer
Older
Sébastien DA ROCHA
committed
<template>

Timothee P
committed
<div id="project-edit">
<div
:class="{ active: loading }"
class="ui inverted dimmer"
>
<div class="ui text loader">
Projet en cours de création. Vous allez être redirigé.
</div>
</div>
<form
id="form-project-edit"
class="ui form"
>
Sébastien DA ROCHA
committed
<h1>
<span
v-if="action === 'edit'"
>Édition du projet "{{ form.title }}"</span>
Sébastien DA ROCHA
committed
<span v-else-if="action === 'create'">Création d'un projet</span>
</h1>
<div class="ui horizontal divider">
INFORMATIONS
</div>
Sébastien DA ROCHA
committed
<div class="two fields">
<div class="required field">
<label for="title">Titre</label>
<input
Sébastien DA ROCHA
committed
type="text"
required
maxlength="128"
name="title"
>
<ul
id="errorlist-title"
class="errorlist"
>
<li
v-for="error in errors.title"
:key="error"
>
Sébastien DA ROCHA
committed
</div>
<div class="field file-logo">
Sébastien DA ROCHA
committed
<label>Illustration du projet</label>
<img
v-if="thumbnailFileSrc.length || form.thumbnail.length"
Sébastien DA ROCHA
committed
id="form-input-file-logo"
Sébastien DA ROCHA
committed
:src="
thumbnailFileSrc
? thumbnailFileSrc
: DJANGO_BASE_URL + form.thumbnail
"
>
<label
class="ui icon button"
for="thumbnail"
>
Sébastien DA ROCHA
committed
<span class="label">{{
form.thumbnail_name ? form.thumbnail_name : fileToImport.name
}}</span>
</label>
<input
Sébastien DA ROCHA
committed
class="file-selection"
type="file"
accept="image/jpeg, image/png"
style="display: none"
name="thumbnail"
<ul
v-if="errorThumbnail.length"
id="errorlist-thumbnail"
class="errorlist"
>
Sébastien DA ROCHA
committed
</div>
</div>
<div class="two fields">
<div class="field">
<label for="description">Description</label>
<textarea
id="editor"
v-model="form.description"
data-preview="#preview"
name="description"
rows="5"
/>
<!-- {{ form.description.errors }} -->
</div>
<div class="field">
<label for="preview">Aperçu</label>
<div
id="preview"
class="description preview"
name="preview"
/>
</div>
Sébastien DA ROCHA
committed
</div>
<div class="ui horizontal divider">
PARAMÈTRES
</div>
Sébastien DA ROCHA
committed
<div class="two fields">
<div
id="published-visibility"
class="required field"
>
<label
for="access_level_pub_feature"
>Visibilité des signalements publiés</label>
Sébastien DA ROCHA
committed
<Dropdown
Sébastien DA ROCHA
committed
:selected="form.access_level_pub_feature.name"
:selection.sync="form.access_level_pub_feature"
/>
<ul
id="errorlist-access_level_pub_feature"
class="errorlist"
>
<li
v-for="error in errors.access_level_pub_feature"
:key="error"
>
Sébastien DA ROCHA
committed
</div>
<div
id="archived-visibility"
class="required field"
>
Sébastien DA ROCHA
committed
<label for="access_level_arch_feature">
Visibilité des signalements archivés
</label>
<Dropdown
:options="levelPermissionsArc"
Sébastien DA ROCHA
committed
:selected="form.access_level_arch_feature.name"
:selection.sync="form.access_level_arch_feature"
/>
<ul
id="errorlist-access_level_arch_feature"
class="errorlist"
>
<li
v-for="error in errors.access_level_arch_feature"
:key="error"
>
Sébastien DA ROCHA
committed
</div>
</div>
<div class="fields grouped checkboxes">
<div class="field">
<div class="ui checkbox">
<input
id="moderation"
v-model="form.moderation"
class="hidden"
type="checkbox"
name="moderation"
>
<label for="moderation">Modération</label>
</div>
</div>
Sébastien DA ROCHA
committed
<div class="field">
<div class="ui checkbox">
<input
id="is_project_type"
v-model="form.is_project_type"
class="hidden"
type="checkbox"
name="is_project_type"
>
<label for="is_project_type">Est un projet type</label>
</div>
</div>
<div class="field">
<div class="ui checkbox">
<input
id="generate_share_link"
v-model="form.generate_share_link"
class="hidden"
type="checkbox"
name="generate_share_link"
>
<label for="generate_share_link">Génération d'un lien de partage externe</label>
</div>
</div>
<div class="field">
<div class="ui checkbox">
<input
id="fast_edition_mode"
v-model="form.fast_edition_mode"
class="hidden"
type="checkbox"
name="fast_edition_mode"
>
<label for="fast_edition_mode">Mode d'édition rapide de signalements</label>
<a
class="
ui
small
button
circular
compact
absolute-right
icon
teal
"
data-tooltip="Consulter la documentation"
data-position="right center"
data-variation="mini"
href="https://geocontrib.readthedocs.io/fr/latest/documentation_fonctionnelle/feature_editing/"
target="_blank"
rel="noopener"
>
<i class="question icon" />
</a>
<div class="fields grouped">
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
<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>
Sébastien DA ROCHA
committed
<label>Niveau de zoom maximum de la carte</label>
<div class="map-maxzoom-selector">
<div class="range-container">
<input
v-model="form.map_max_zoom_level"
type="range"
min="0"
max="22"
step="1"
@input="zoomMap"
><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 de carte disponible à cette échelle
</div>
</div>

Timothee P
committed
</div>
</div>
<div class="ui horizontal divider">
ATTRIBUTS
</div>
metourneau
committed
<div class="fields grouped">
<ProjectAttributeForm
v-for="(attribute, index) in projectAttributes"
:attribute="attribute"
:form-project-attributes="form.project_attributes"
@update:project_attributes="updateProjectAttributes($event)"
Sébastien DA ROCHA
committed
id="send-project"
type="button"
class="ui teal icon button"
@click="postForm"
>
<i
class="white save icon"
aria-hidden="true"
/> Enregistrer les changements
Sébastien DA ROCHA
committed
</button>
</form>
</div>
</template>
<script>
import Dropdown from '@/components/Dropdown';
import ProjectAttributeForm from '@/components/Project/Edition/ProjectAttributeForm';
import mapService from '@/services/map-service';
Sébastien DA ROCHA
committed
import TextareaMarkdown from 'textarea-markdown';
import { mapActions, mapState } from 'vuex';
Sébastien DA ROCHA
committed
export default {
Sébastien DA ROCHA
committed
components: {
Dropdown,
ProjectAttributeForm
Sébastien DA ROCHA
committed
},
data() {
return {
Sébastien DA ROCHA
committed
fileToImport: {
Sébastien DA ROCHA
committed
size: 0,
},
errors: {
title: [],
access_level_pub_feature: [],
access_level_arch_feature: [],
},
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'
Sébastien DA ROCHA
committed
form: {
title: '',
slug: '',
created_on: '',
updated_on: '',
description: '',
Sébastien DA ROCHA
committed
moderation: false,
thumbnail: '', // todo : utiliser l'image par défaut
thumbnail_name: '', // todo: delete after getting image in jpg or png instead of data64 (require post to django)
Sébastien DA ROCHA
committed
creator: null,
access_level_pub_feature: { name: '', value: '' },
access_level_arch_feature: { name: '', value: '' },
Sébastien DA ROCHA
committed
archive_feature: 0,
delete_feature: 0,
Sébastien DA ROCHA
committed
nb_features: 0,
nb_published_features: 0,
nb_comments: 0,
nb_published_features_comments: 0,
nb_contributors: 0,
is_project_type: false,
generate_share_link: false,

Timothee P
committed
fast_edition_mode: false,
feature_browsing_default_filter: '',
feature_browsing_default_sort: '-created_on',
project_attributes: [],
Sébastien DA ROCHA
committed
},
'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',
]
Sébastien DA ROCHA
committed
};
},
computed: {
...mapState('projects', ['project']),
Sébastien DA ROCHA
committed
return this.$store.state.configuration.VUE_APP_DJANGO_BASE;
},
const levels = new Array();
if(this.levelsPermissions) {
this.levelsPermissions.forEach((item) => {
name: this.translateRoleToFrench(item.user_type_id),
if (!this.form.moderation && item.user_type_id === 'moderator') {
const levels = new Array();
if (this.levelsPermissions) {
this.levelsPermissions.forEach((item) => {
if (
item.user_type_id !== 'super_contributor' &&
item.user_type_id !== 'admin' &&
item.user_type_id !== 'moderator'
) {
name: this.translateRoleToFrench(item.user_type_id),
Sébastien DA ROCHA
committed
},
watch: {
'form.moderation': function (newValue){
if(!newValue && this.form.access_level_arch_feature.value === 'moderator') {
this.form.access_level_arch_feature = { name: '', value: '' };
}
}
},
this.definePageType();
if (this.action === 'create') {
this.thumbnailFileSrc = require('@/assets/img/default.png');
} else if (this.action === 'edit' || this.action === 'create_from') {
if (!this.project) {
this.$store.dispatch('projects/GET_PROJECT', this.$route.params.slug)
.then((projet) => {
});
} else {
this.fillProjectForm();
}
let textarea = document.querySelector('textarea');
new TextareaMarkdown(textarea);
},
Sébastien DA ROCHA
committed
methods: {
...mapActions('map', [
'INITIATE_MAP'
]),
Sébastien DA ROCHA
committed
definePageType() {
if (this.$router.history.current.name === 'project_create') {
this.action = 'create';
} else if (this.$router.history.current.name === 'project_edit') {
this.action = 'edit';
} else if (this.$router.history.current.name === 'project_create_from') {
this.action = 'create_from';
Sébastien DA ROCHA
committed
}
},
translateRoleToFrench(role){
switch (role) {
case 'admin':
return 'Administrateur projet';
case 'moderator':
return 'Modérateur';
case 'contributor':
return 'Contributeur';
case 'logged_user':
return 'Utilisateur connecté';
case 'anonymous':
return 'Utilisateur anonyme';
}
Sébastien DA ROCHA
committed
truncate(n, len) {
const ext = n.substring(n.lastIndexOf('.') + 1, n.length).toLowerCase();
Sébastien DA ROCHA
committed
if (filename.length <= len) {
return n;
}
filename = filename.substr(0, len) + (n.length > len ? '[...]' : '');
return filename + '.' + ext;
Sébastien DA ROCHA
committed
},
const url = window.URL || window.webkitURL;
const image = new Image();
image.onload = function () {
handleFile(true);
};
image.onerror = function () {
handleFile(false);
};
image.src = url.createObjectURL(files);
},
Sébastien DA ROCHA
committed
onFileChange(e) {
// * read image file
const files = e.target.files || e.dataTransfer.files;
const _this = this; //* 'this' is different in onload function
function handleFile(isValid) {
if (isValid) {
_this.fileToImport = files[0]; //* store the file to post later
const reader = new FileReader(); //* read the file to display in the page
reader.onload = function (e) {
_this.thumbnailFileSrc = e.target.result;
};
reader.readAsDataURL(_this.fileToImport);
_this.errorThumbnail = [];
} else {
_this.errorThumbnail.push(
"Transférez une image valide. Le fichier que vous avez transféré n'est pas une image, ou il est corrompu."
);
}
}
if (files.length) {
//* check if file is an image and pass callback to handle file
this.validateImgFile(files[0], handleFile);
}
Sébastien DA ROCHA
committed
},
if (!this.form.archive_feature) {
this.form.archive_feature = 0;
}
if (!this.form.delete_feature) {
this.form.delete_feature = 0;
}
Sébastien DA ROCHA
committed
goBackNrefresh(slug) {
this.$store.dispatch('GET_USER_LEVEL_PROJECTS'), //* refresh projects user levels
this.$store.dispatch('GET_USER_LEVEL_PERMISSIONS'), //* refresh projects permissions

Florent Lavelle
committed
this.$store.dispatch('projects/GET_PROJECT', slug), //* refresh current project
]).then(() =>
// * go back to project list
this.$router.push({
Sébastien DA ROCHA
committed
params: { slug },
Sébastien DA ROCHA
committed
);
},
postProjectThumbnail(projectSlug) {
//* send img to the backend when feature_type is created
if (this.fileToImport) {
formData.append('file', this.fileToImport);
const url =
this.$store.state.configuration.VUE_APP_DJANGO_API_BASE +
return axios
.put(url, formData, {
headers: {
},
})
.then((response) => {
if (response && response.status === 200) {
this.goBackNrefresh(projectSlug);
}
})
.catch((error) => {
let err_msg =
"Transférez une image valide. Le fichier que vous avez transféré n'est pas une image, ou il est corrompu.";
if (error.response.data[0]) {
err_msg = error.response.data[0];
}
this.errorThumbnail.push(err_msg);
throw error;
});
}
Sébastien DA ROCHA
committed
},
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."
);
if ((key === 'title' && this.form[key]) || this.form[key].value) {
this.errors[key] = [];
} else if (!this.errors[key].length) {
this.errors[key].push(
key === 'title'
? 'Veuillez compléter ce champ.'
: 'Sélectionnez un choix valide. Ce choix ne fait pas partie de ceux disponibles.'
);
document
.getElementById(`errorlist-${key}`)
.scrollIntoView({ block: 'end', inline: 'nearest' });
return false;
}
}
return true;
},
Sébastien DA ROCHA
committed
async postForm() {
Sébastien DA ROCHA
committed
const projectData = {
Sébastien DA ROCHA
committed
access_level_arch_feature: this.form.access_level_arch_feature.value,
access_level_pub_feature: this.form.access_level_pub_feature.value,
feature_browsing_default_sort: this.form.feature_browsing_default_sort.value,
feature_browsing_default_filter: this.form.feature_browsing_default_filter.value,
Sébastien DA ROCHA
committed
};
Sébastien DA ROCHA
committed
await axios
.put((`${this.$store.state.configuration.VUE_APP_DJANGO_API_BASE}v2/projects/${this.project.slug}/`), projectData)
Sébastien DA ROCHA
committed
.then((response) => {
if (response && response.status === 200) {
//* send thumbnail after feature_type was updated
Sébastien DA ROCHA
committed
}
})
.catch((error) => {
if (error.response && error.response.data.title[0]) {
this.errors.title.push(error.response.data.title[0]);
}
Sébastien DA ROCHA
committed
throw error;
});
let url = `${this.$store.state.configuration.VUE_APP_DJANGO_API_BASE}v2/projects/`;
url = `${this.$store.state.configuration.VUE_APP_DJANGO_API_BASE}projects/${this.project.slug}/duplicate/`;
Sébastien DA ROCHA
committed
await axios
Sébastien DA ROCHA
committed
.then((response) => {
if (response && response.status === 201 && response.data) {
//* send thumbnail after feature_type was created
Sébastien DA ROCHA
committed
}
Sébastien DA ROCHA
committed
})
.catch((error) => {
if (error.response && error.response.data.title[0]) {
this.errors.title.push(error.response.data.title[0]);
}
Sébastien DA ROCHA
committed
throw error;
});
}
},
//* create a new object to avoid modifying original one
this.form = { ...this.project };
//* if duplication of project, generate new name
if (this.action === 'create_from') {

Timothee P
committed
this.form.title =
this.project.title +
` (Copie-${new Date()
.toLocaleString()
.slice(0, -3)

Timothee P
committed
this.form.is_project_type = false;
}
//* 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) {
const accessLevelPub = this.levelPermissionsPub.find(
(el) => el.name === this.project.access_level_pub_feature
if (accessLevelPub) {
this.form.access_level_pub_feature = {
name: this.project.access_level_pub_feature,
value: accessLevelPub.value ,
// fill dropdown current selection for feature browsing default filtering
const default_filter = this.featureBrowsingOptions.filter.find(
(el) => el.value === this.project.feature_browsing_default_filter
);
if (default_filter) {
this.form.feature_browsing_default_filter = default_filter;
}
// 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();
metourneau
committed
//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,
metourneau
committed
zoom: activeZoom,
center: this.$store.state.configuration.MAP_PREVIEW_CENTER,
maxZoom: 22,
controls: [],
zoomControl: false,
metourneau
committed
//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
);
metourneau
committed
//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 });
}
}
Sébastien DA ROCHA
committed
};
</script>
<style media="screen" lang="less">
Sébastien DA ROCHA
committed
#form-input-file-logo {
margin-left: auto;
margin-right: auto;
}
.file-logo {
min-height: calc(150px + 2.4285em);
display: flex;
flex-direction: column;
justify-content: space-between;
}
Sébastien DA ROCHA
committed
.close.icon:hover {
cursor: pointer;
}
textarea {
height: 10em;
}
.description.preview {
height: 10em;
overflow: scroll;
border: 1px solid rgba(34, 36, 38, .15);
padding: .78571429em 1em;
}
.checkboxes {
padding-left: .5em;
.absolute-right.ui.compact.icon.button {
position: absolute;
right: -2.75em;
top: calc(50% - 1em);
padding: .4em;
}
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
}
.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;
}