Something went wrong on our end
-
Timothee P authoredTimothee P authored
ProjectEdit.vue 24.95 KiB
<template>
<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"
>
<h1>
<span
v-if="action === 'edit'"
>Édition du projet "{{ form.title }}"</span>
<span v-else-if="action === 'create'">Création d'un projet</span>
</h1>
<div class="ui horizontal divider">
INFORMATIONS
</div>
<div class="two fields">
<div class="required field">
<label for="title">Titre</label>
<input
id="title"
v-model="form.title"
type="text"
required
maxlength="128"
name="title"
>
<ul
id="errorlist-title"
class="errorlist"
>
<li
v-for="error in errors.title"
:key="error"
>
{{ error }}
</li>
</ul>
</div>
<div class="field">
<label>Illustration du projet</label>
<img
v-if="thumbnailFileSrc.length || form.thumbnail.length"
id="form-input-file-logo"
class="ui small image"
:src="
thumbnailFileSrc
? thumbnailFileSrc
: DJANGO_BASE_URL + form.thumbnail
"
alt="Thumbnail du projet"
>
<label
class="ui icon button"
for="thumbnail"
>
<i
class="file icon"
aria-hidden="true"
/>
<span class="label">{{
form.thumbnail_name ? form.thumbnail_name : fileToImport.name
}}</span>
</label>
<input
id="thumbnail"
class="file-selection"
type="file"
accept="image/jpeg, image/png"
style="display: none"
name="thumbnail"
@change="onFileChange"
>
<ul
v-if="errorThumbnail.length"
id="errorlist-thumbnail"
class="errorlist"
>
<li>
{{ errorThumbnail[0] }}
</li>
</ul>
</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>
</div>
<div class="ui horizontal divider">
PARAMÈTRES
</div>
<div class="two fields">
<div
id="published-visibility"
class="required field"
>
<label
for="access_level_pub_feature"
>Visibilité des signalements publiés</label>
<Dropdown
:options="levelPermissionsPub"
: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"
>
{{ error }}
</li>
</ul>
</div>
<div
id="archived-visibility"
class="required field"
>
<label for="access_level_arch_feature">
Visibilité des signalements archivés
</label>
<Dropdown
:options="levelPermissionsArc"
: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"
>
{{ error }}
</li>
</ul>
</div>
</div>
<div class="two fields">
<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>
<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="#"
target="_blank"
rel="noopener"
>
<i class="question icon" />
</a>
</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 class="field">
<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>
</div>
</div>
</div>
<div class="ui divider" />
<button
id="send-project"
type="button"
class="ui teal icon button"
@click="postForm"
>
<i
class="white save icon"
aria-hidden="true"
/> Enregistrer les changements
</button>
</form>
</div>
</template>
<script>
import axios from '@/axios-client.js';
import Dropdown from '@/components/Dropdown.vue';
import mapService from '@/services/map-service';
import TextareaMarkdown from 'textarea-markdown';
import { mapActions, mapState } from 'vuex';
export default {
name: 'ProjectEdit',
components: {
Dropdown,
},
data() {
return {
loading: false,
action: 'create',
fileToImport: {
name: 'Sélectionner une image ...',
size: 0,
},
errors_archive_feature: [],
errors: {
title: [],
access_level_pub_feature: [],
access_level_arch_feature: [],
},
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: {
title: '',
slug: '',
created_on: '',
updated_on: '',
description: '',
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)
creator: null,
access_level_pub_feature: { name: '', value: '' },
access_level_arch_feature: { name: '', value: '' },
archive_feature: 0,
delete_feature: 0,
map_max_zoom_level: 22,
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,
fast_edition_mode: false,
feature_browsing_default_filter: '',
feature_browsing_default_sort: 'created_on'
},
thumbnailFileSrc: '',
scalesTable: [
'1:500 Mio',
'1:250 Mio',
'1:150 Mio',
'1:70 Mio',
'1:35 Mio',
'1:15 Mio',
'1:10 Mio',
'1:4 Mio',
'1:2 Mio',
'1:1 Mio',
'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: {
...mapState([
'levelsPermissions',
]),
...mapState('projects', ['project']),
DJANGO_BASE_URL: function () {
return this.$store.state.configuration.VUE_APP_DJANGO_BASE;
},
levelPermissionsArc(){
const levels = new Array();
if(this.levelsPermissions) {
this.levelsPermissions.forEach((item) => {
if (item.user_type_id !== 'super_contributor') {
levels.push({
name: this.translateRoleToFrench(item.user_type_id),
value: item.user_type_id,
});
}
if (!this.form.moderation && item.user_type_id === 'moderator') {
levels.pop();
}
});
}
return levels;
},
levelPermissionsPub(){
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'
) {
levels.push({
name: this.translateRoleToFrench(item.user_type_id),
value: item.user_type_id,
});
}
});
}
return levels;
}
},
watch: {
'form.moderation': function (newValue){
if(!newValue && this.form.access_level_arch_feature.value === 'moderator') {
this.form.access_level_arch_feature = { name: '', value: '' };
}
}
},
mounted() {
this.definePageType();
if (this.action === 'create') {
this.thumbnailFileSrc = require('@/assets/img/default.png');
this.initPreviewMap();
} else if (this.action === 'edit' || this.action === 'create_from') {
if (!this.project) {
this.$store.dispatch('projects/GET_PROJECT', this.$route.params.slug)
.then((projet) => {
if (projet) {
this.fillProjectForm();
}
});
} else {
this.fillProjectForm();
}
}
let textarea = document.querySelector('textarea');
new TextareaMarkdown(textarea);
},
methods: {
...mapActions('map', [
'INITIATE_MAP'
]),
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';
}
},
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';
}
},
truncate(n, len) {
const ext = n.substring(n.lastIndexOf('.') + 1, n.length).toLowerCase();
let filename = n.replace('.' + ext, '');
if (filename.length <= len) {
return n;
}
filename = filename.substr(0, len) + (n.length > len ? '[...]' : '');
return filename + '.' + ext;
},
validateImgFile(files, handleFile) {
const url = window.URL || window.webkitURL;
const image = new Image();
image.onload = function () {
handleFile(true);
URL.revokeObjectURL(image.src);
};
image.onerror = function () {
handleFile(false);
URL.revokeObjectURL(image.src);
};
image.src = url.createObjectURL(files);
},
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);
}
},
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) {
Promise.all([
this.$store.dispatch('GET_USER_LEVEL_PROJECTS'), //* refresh projects user levels
this.$store.dispatch('GET_USER_LEVEL_PERMISSIONS'), //* refresh projects permissions
this.$store.dispatch('projects/GET_PROJECT', slug), //* refresh current project
]).then(() =>
// * go back to project list
this.$router.push({
name: 'project_detail',
params: { slug },
})
);
},
postProjectThumbnail(projectSlug) {
//* send img to the backend when feature_type is created
if (this.fileToImport) {
const formData = new FormData();
formData.append('file', this.fileToImport);
const url =
this.$store.state.configuration.VUE_APP_DJANGO_API_BASE +
'projects/' +
projectSlug +
'/thumbnail/';
return axios
.put(url, formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
})
.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;
});
}
},
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) {
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;
},
async postForm() {
if (!this.checkForm()) {
return;
}
const projectData = {
title: this.form.title,
description: this.form.description,
access_level_arch_feature: this.form.access_level_arch_feature.value,
access_level_pub_feature: this.form.access_level_pub_feature.value,
archive_feature: this.form.archive_feature,
delete_feature: this.form.delete_feature,
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,
feature_browsing_default_sort: this.form.feature_browsing_default_sort.value,
feature_browsing_default_filter: this.form.feature_browsing_default_filter.value,
};
let url = `${this.$store.state.configuration.VUE_APP_DJANGO_API_BASE}projects/`;
if (this.action === 'edit') {
await axios
.put((url += `${this.project.slug}/`), projectData)
.then((response) => {
if (response && response.status === 200) {
//* send thumbnail after feature_type was updated
if (this.fileToImport.size > 0) {
this.postProjectThumbnail(this.project.slug);
} else {
this.goBackNrefresh(this.project.slug);
}
}
})
.catch((error) => {
if (error.response && error.response.data.title[0]) {
this.errors.title.push(error.response.data.title[0]);
}
throw error;
});
} else {
if (this.action === 'create_from') {
url += `${this.project.slug}/duplicate/`;
}
this.loading = true;
await axios
.post(url, projectData)
.then((response) => {
if (response && response.status === 201 && response.data) {
//* send thumbnail after feature_type was created
if (this.fileToImport.size > 0) {
this.postProjectThumbnail(response.data.slug);
} else {
this.goBackNrefresh(response.data.slug);
}
}
this.loading = false;
})
.catch((error) => {
if (error.response && error.response.data.title[0]) {
this.errors.title.push(error.response.data.title[0]);
}
this.loading = false;
throw error;
});
}
},
fillProjectForm() {
//* 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') {
this.form.title =
this.project.title +
` (Copie-${new Date()
.toLocaleString()
.slice(0, -3)
.replace(',', '')})`;
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
this.form.feature_browsing_default_filter = this.featureBrowsingOptions.filter.find(
(el) => el.value === this.project.feature_browsing_default_filter
);
// fill dropdown current selection for feature browsing default sorting
this.form.feature_browsing_default_sort = this.featureBrowsingOptions.sort.find(
(el) => el.value === this.project.feature_browsing_default_sort
);
this.initPreviewMap();
},
initPreviewMap () {
const map = mapService.getMap();
if (map) mapService.destroyMap();
this.INITIATE_MAP({
el: this.$refs.map,
zoom: this.project ? this.project.map_max_zoom_level : 22,
center: this.$store.state.configuration.MAP_PREVIEW_CENTER,
maxZoom: 22,
controls: [],
zoomControl: false,
});
},
zoomMap() {
mapService.zoom(this.form.map_max_zoom_level);
}
},
};
</script>
<style media="screen" lang="less">
#form-input-file-logo {
margin-left: auto;
margin-right: auto;
}
.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;
i.icon, i.icons {
font-size: .9em;
}
}
}
.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: 1.85714em;
}
#feature_browsing_filter {
margin-top: -2.25em;
}
#feature_browsing_filter,
#feature_browsing_sort {
float: right;
}
</style>