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
<template>
<div id="projects-types">
<h3 class="ui header">
Créer un projet à partir d'un modèle disponible:
</h3>
<div class="ui divided items">
<div
v-for="project in project_types"
:key="project.slug"
class="item"
>
<div class="ui tiny image">
<img
:src="
project.thumbnail.includes('default')
? require('@/assets/img/default.png')
: DJANGO_BASE_URL + project.thumbnail + refreshId()
"
alt="Image associé au projet"
>
</div>
<div class="middle aligned content">
<div class="description">
<router-link
:to="{
name: 'project_create_from',
params: {
slug: project.slug,
},
}"
>
{{ project.title }}
</router-link>
<p>{{ project.description }}</p>
<strong>Projet {{ project.moderation ? '' : 'non' }} modéré</strong>
</div>
<div class="meta">
<span data-tooltip="Date de création">
{{ project.created_on }}&nbsp;
<i
class="calendar icon"
aria-hidden="true"
/>
</span>
</div>
<div class="meta">
<span data-tooltip="Visibilité des signalement publiés">
{{ project.access_level_pub_feature }}&nbsp;<i
class="eye icon"
aria-hidden="true"
/>
</span>
<span data-tooltip="Visibilité des signalement archivés">
{{ project.access_level_arch_feature }}&nbsp;<i
class="archive icon"
aria-hidden="true"
/>
</span>
</div>
</div>
</div>
<span
v-if="!project_types || project_types.length === 0"
>Aucun projet type n'est défini.</span>
</div>
</div>
</template>
<script>
import projectAPI from '@/services/project-api';
export default {
name: 'ProjectTypeList',
data() {
return {
project_types: null,
};
},
computed: {
DJANGO_BASE_URL: function () {
return this.$store.state.configuration.VUE_APP_DJANGO_BASE;
},
API_BASE_URL() {
return this.$store.state.configuration.VUE_APP_DJANGO_API_BASE;
},
},
mounted() {
projectAPI.getProjectTypes(this.API_BASE_URL)
.then((data) => {
if (data) {
this.project_types = data;
}
});
},
methods: {
refreshId() {
const crypto = window.crypto || window.msCrypto;
var array = new Uint32Array(1);
return '?ver=' + crypto.getRandomValues(array); // Compliant for security-sensitive use cases
},
},
};
</script>
<style lang="less" scoped>
#projects-types {
max-width: 800px !important;
}
</style>
\ No newline at end of file
<template>
<div v-frag>
<div v-frag v-if="feature">
<div class="row">
<div class="fourteen wide column">
<h1 class="ui header">
<div class="content">
{{ feature.title || feature.feature_id }}
<div class="ui icon right floated compact buttons">
<router-link
v-if="permissions && permissions.can_create_feature"
:to="{
name: 'ajouter-signalement',
params: {
slug_type_signal: $route.params.slug_type_signal,
},
}"
class="ui button button-hover-orange"
data-tooltip="Ajouter un signalement"
data-position="bottom left"
>
<i class="plus fitted icon"></i>
</router-link>
<router-link
v-if="
(permissions && permissions.can_update_feature) ||
isFeatureCreator
"
:to="{
name: 'editer-signalement',
params: {
slug_signal: $route.params.slug_signal,
slug_type_signal: $route.params.slug_type_signal,
},
}"
class="ui button button-hover-orange"
>
<i class="inverted grey pencil alternate icon"></i>
</router-link>
<!-- (permissions && permissions.can_delete_feature) || -->
<a
v-if="isFeatureCreator"
@click="isCanceling = true"
id="feature-delete"
class="ui button button-hover-red"
>
<i class="inverted grey trash alternate icon"></i>
</a>
</div>
<div class="ui hidden divider"></div>
<div class="sub header">
{{ feature.description }}
</div>
</div>
</h1>
</div>
</div>
<div class="row">
<div class="seven wide column">
<table class="ui very basic table">
<tbody>
<div
v-frag
v-for="(field, index) in feature.feature_data"
:key="'field' + index"
>
<tr v-if="field">
<td>
<b>{{ field.label }}</b>
</td>
<td>
<b>
<i
v-if="field.field_type === 'boolean'"
:class="[
'icon',
field.value ? 'olive check' : 'grey times',
]"
></i>
<span v-else>
{{ field.value }}
</span>
</b>
</td>
</tr>
</div>
<tr>
<td>Auteur</td>
<td>{{ feature.display_creator }}</td>
</tr>
<tr>
<td>Statut</td>
<td>
<i v-if="feature.status" :class="['icon', statusIcon]"></i>
{{ statusLabel }}
</td>
</tr>
<tr>
<td>Date de création</td>
<td v-if="feature.created_on">
{{ feature.created_on | formatDate }}
</td>
</tr>
<tr>
<td>Date de dernière modification</td>
<td v-if="feature.updated_on">
{{ feature.updated_on | formatDate }}
</td>
</tr>
<tr>
<td>Date d'archivage automatique</td>
<td v-if="feature.archived_on">
{{ feature.archived_on }}
</td>
</tr>
<tr>
<td>Date de suppression automatique</td>
<td v-if="feature.deletion_on">
{{ feature.deletion_on }}
</td>
</tr>
</tbody>
</table>
<h3>Liaison entre signalements</h3>
<table class="ui very basic table">
<tbody>
<tr
v-for="(link, index) in linked_features"
:key="link.feature_to.title + index"
>
<td v-if="link.feature_to.feature_type_slug">
{{ link.relation_type_display }}
<a @click="pushNgo(link)">{{ link.feature_to.title }} </a>
({{ link.feature_to.display_creator }} -
{{ link.feature_to.created_on }})
</td>
</tr>
</tbody>
</table>
</div>
<div class="seven wide column">
<div id="map"></div>
</div>
</div>
<div class="row">
<div class="seven wide column">
<h2 class="ui header">Pièces jointes</h2>
<div v-for="pj in attachments" :key="pj.id" class="ui divided items">
<div class="item">
<a
class="ui tiny image"
target="_blank"
:href="pj.attachment_file"
>
<img
:src="
pj.extension === '.pdf'
? require('@/assets/img/pdf.png')
: pj.attachment_file
"
/>
</a>
<div class="middle aligned content">
<a class="header" target="_blank" :href="pj.attachment_file">{{
pj.title
}}</a>
<div class="description">
{{ pj.info }}
</div>
</div>
</div>
</div>
<i v-if="attachments.length === 0"
>Aucune pièce jointe associée au signalement.</i
>
</div>
<div class="seven wide column">
<h2 class="ui header">Activité et commentaires</h2>
<div id="feed-event" class="ui feed">
<div v-frag v-for="(event, index) in events" :key="'event' + index">
<div v-frag v-if="event.event_type === 'create'">
<div v-if="event.object_type === 'feature'" class="event">
<div class="content">
<div class="summary">
<div class="date">
{{ event.created_on }}
</div>
Création du signalement
<span v-if="user">par {{ event.display_user }}</span>
</div>
</div>
</div>
<div v-else-if="event.object_type === 'comment'" class="event">
<div class="content">
<div class="summary">
<div class="date">
{{ event.created_on }}
</div>
Commentaire
<span v-if="user">par {{ event.display_user }}</span>
</div>
<div class="extra text">
{{ event.related_comment.comment }}
<div v-frag v-if="event.related_comment.attachment">
<br /><a
:href="
DJANGO_BASE_URL +
event.related_comment.attachment.url
"
target="_blank"
><i class="paperclip fitted icon"></i>
{{ event.related_comment.attachment.title }}</a
>
</div>
</div>
</div>
</div>
</div>
<div v-else-if="event.event_type === 'update'" class="event">
<div class="content">
<div class="summary">
<div class="date">
{{ event.created_on }}
</div>
Signalement mis à jour
<span v-if="user">par {{ event.display_user }}</span>
</div>
</div>
</div>
</div>
</div>
<div
v-if="permissions && permissions.can_create_feature"
class="ui segment"
>
<form
id="form-comment"
class="ui form"
method="POST"
enctype="multipart/form-data"
>
<div class="required field">
<label :for="comment_form.comment.id_for_label"
>Ajouter un commentaire</label
>
{{ comment_form.comment.errors }}
<textarea
v-model="comment_form.comment.value"
:name="comment_form.comment.html_name"
rows="2"
></textarea>
</div>
<label>Pièce jointe (facultative)</label>
<div class="two fields">
<div class="field">
<label class="ui icon button" for="attachment_file">
<i class="paperclip icon"></i>
<span class="label">{{
comment_form.attachment_file.value
? comment_form.attachment_file.value
: "Sélectionner un fichier ..."
}}</span>
</label>
<input
type="file"
accept="application/pdf, image/jpeg, image/png"
style="display: none"
name="attachment_file"
id="attachment_file"
@change="onFileChange"
/>
</div>
<div class="field">
<input
v-model="comment_form.title.value"
type="text"
:name="comment_form.title.html_name"
:id="comment_form.title.id_for_label"
/>
{{ comment_form.title.errors }}
</div>
</div>
<ul v-if="comment_form.attachment_file.errors" class="errorlist">
<li>
{{ comment_form.attachment_file.errors }}
</li>
</ul>
<button
@click="postComment"
type="button"
class="ui compact green icon button"
>
<i class="plus icon"></i> Poster le commentaire
</button>
</form>
</div>
</div>
</div>
<div
v-if="isCanceling"
class="ui dimmer modals page transition visible active"
style="display: flex !important"
>
<div
:class="[
'ui mini modal subscription',
{ 'active visible': isCanceling },
]"
>
<i @click="isCanceling = false" class="close icon"></i>
<div class="ui icon header">
<i class="trash alternate icon"></i>
Supprimer le signalement
</div>
<div class="actions">
<form
action="{% url 'geocontrib:feature_delete' slug=feature.project.slug feature_type_slug=feature.feature_type.slug feature_id=feature.feature_id %}"
method="POST"
>
<input type="hidden" name="_method" value="delete" />
<button
@click="deleteFeature"
type="button"
class="ui red compact fluid button"
>
Confirmer la suppression
</button>
</form>
</div>
</div>
</div>
</div>
<div v-frag v-else>Pas de signalement correspondant trouvé</div>
</div>
</template>
<script>
import frag from "vue-frag";
import { mapGetters, mapState } from "vuex";
import { mapUtil } from "@/assets/js/map-util.js";
import featureAPI from "@/services/feature-api";
const axios = require("axios");
export default {
name: "Feature_detail",
directives: {
frag,
},
data() {
return {
isCanceling: false,
attachments: [],
events: [],
comment_form: {
attachment_file: {
errors: null,
value: null,
file: null,
},
title: {
id_for_label: "title",
html_name: "title",
errors: null,
value: null,
},
comment: {
id_for_label: "add-comment",
html_name: "add-comment",
errors: null,
value: null,
},
non_field_errors: [],
},
};
},
computed: {
...mapState(["user"]),
...mapGetters(["permissions"]),
...mapState("feature", ["linked_features", "statusChoices"]),
DJANGO_BASE_URL: function () {
return this.$store.state.configuration.VUE_APP_DJANGO_BASE;
},
feature: function () {
const result = this.$store.state.feature.features.find(
(el) => el.feature_id === this.$route.params.slug_signal
);
return result;
},
isFeatureCreator() {
if (this.feature && this.user) {
return this.feature.creator === this.user.id;
}
return false;
},
statusIcon() {
switch (this.feature.status) {
case "archived":
return "grey archive";
case "pending":
return "teal hourglass outline";
case "published":
return "olive check";
case "draft":
return "orange pencil alternate";
default:
return "";
}
},
statusLabel() {
const status = this.statusChoices.find(
(el) => el.value === this.feature.status
);
return status ? status.name : "";
},
},
filters: {
formatDate(value) {
let date = new Date(value);
date = date.toLocaleString().replace(",", "");
return date.substr(0, date.length - 3); //* quick & dirty way to remove seconds from date
},
},
methods: {
pushNgo(link) {
this.$router.push({
name: "details-signalement",
params: {
slug_type_signal: link.feature_to.feature_type_slug,
slug_signal: link.feature_to.feature_id,
},
});
this.getFeatureEvents();
this.getFeatureAttachments();
this.getLinkedFeatures();
this.addFeatureToMap();
},
postComment() {
featureAPI
.postComment({
featureId: this.$route.params.slug_signal,
comment: this.comment_form.comment.value,
})
.then((response) => {
if (response && this.comment_form.attachment_file.file) {
featureAPI
.postCommentAttachment({
featureId: this.$route.params.slug_signal,
file: this.comment_form.attachment_file.file,
fileName: this.comment_form.title.file,
commentId: response.data.id,
title: response.data.comment,
})
.then(() => {
this.confirmComment();
});
} else {
this.confirmComment();
}
});
},
confirmComment() {
this.$store.commit("DISPLAY_MESSAGE", "Ajout du commentaire confirmé");
this.getFeatureEvents(); //* display new comment on the page
this.comment_form.attachment_file.file = null;
this.comment_form.attachment_file.value = null;
this.comment_form.title.file = null;
this.comment_form.title.value = null;
this.comment_form.comment.value = null;
},
validateImgFile(files, handleFile) {
let url = window.URL || window.webkitURL;
let image = new Image();
image.onload = function () {
handleFile(true);
};
image.onerror = function () {
handleFile(false);
};
image.src = url.createObjectURL(files);
URL.revokeObjectURL(image.src);
},
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) {
const period = files[0].name.lastIndexOf(".");
const fileName = files[0].name.substring(0, period);
const fileExtension = files[0].name.substring(period + 1);
const shortName = fileName.slice(0, 10) + "[...]." + fileExtension;
_this.comment_form.attachment_file.file = files[0]; //* store the file to post later
_this.comment_form.attachment_file.value = shortName; //* for display
_this.comment_form.title.value = shortName;
_this.comment_form.attachment_file.errors = null;
} else {
_this.comment_form.attachment_file.errors =
"Transférez une image valide. Le fichier que vous avez transféré n'est pas une image, ou il est corrompu.";
}
}
if (files.length) {
//* exception for pdf
if (files[0].type === "application/pdf") {
handleFile(true);
} else {
this.comment_form.attachment_file.errors = null;
//* check if file is an image and pass callback to handle file
this.validateImgFile(files[0], handleFile);
}
}
},
goBackToProject(message) {
this.$router.push({
name: "project_detail",
params: {
slug: this.$store.state.project_slug,
message,
},
});
},
deleteFeature() {
this.$store
.dispatch("feature/DELETE_FEATURE", this.feature.feature_id)
.then((response) => {
if (response.status === 204) {
this.$store.dispatch(
"feature/GET_PROJECT_FEATURES",
this.$route.params.slug
);
this.goBackToProject();
}
});
},
initMap() {
var mapDefaultViewCenter =
this.$store.state.configuration.DEFAULT_MAP_VIEW.center;
var mapDefaultViewZoom =
this.$store.state.configuration.DEFAULT_MAP_VIEW.zoom;
this.map = mapUtil.createMap({
mapDefaultViewCenter,
mapDefaultViewZoom,
});
// Update link to feature list with map zoom and center
mapUtil.addMapEventListener("moveend", function () {
// update link to feature list with map zoom and center
/*var $featureListLink = $("#feature-list-link")
var baseUrl = $featureListLink.attr("href").split("?")[0]
$featureListLink.attr("href", baseUrl +`?zoom=${this.map.getZoom()}&lat=${this.map.getCenter().lat}&lng=${this.map.getCenter().lng}`)*/
});
// Load the layers.
// - if one basemap exists, we load the layers of the first one
// - if not, load the default map and service options
let layersToLoad = null;
var baseMaps = this.$store.state.map.basemaps;
var layers = this.$store.state.map.availableLayers;
if (baseMaps && baseMaps.length > 0) {
const basemapIndex = 0;
layersToLoad = baseMaps[basemapIndex].layers;
layersToLoad.forEach((layerToLoad) => {
layers.forEach((layer) => {
if (layer.id === layerToLoad.id) {
layerToLoad = Object.assign(layerToLoad, layer);
}
});
});
layersToLoad.reverse();
}
mapUtil.addLayers(
layersToLoad,
this.$store.state.configuration.DEFAULT_BASE_MAP.SERVICE,
this.$store.state.configuration.DEFAULT_BASE_MAP.OPTIONS
);
mapUtil.getMap().dragging.disable();
mapUtil.getMap().doubleClickZoom.disable();
mapUtil.getMap().scrollWheelZoom.disable();
this.addFeatureToMap();
},
addFeatureToMap() {
const currentFeatureId = this.$route.params.slug_signal;
const url = `${this.$store.state.configuration.VUE_APP_DJANGO_API_BASE}features/${currentFeatureId}/?feature_type__slug=${this.$route.params.slug_type_signal}&output=geojson`;
axios
.get(url)
.then((response) => {
const feature = response.data;
if (feature) {
const currentFeature = [feature];
const featureGroup = mapUtil.addFeatures(currentFeature,{},true,this.$store.state.feature_type.feature_types);
mapUtil
.getMap()
.fitBounds(featureGroup.getBounds(), { padding: [25, 25] });
}
})
.catch((error) => {
throw error;
});
},
getFeatureEvents() {
featureAPI
.getFeatureEvents(this.$route.params.slug_signal)
.then((data) => (this.events = data));
},
getFeatureAttachments() {
featureAPI
.getFeatureAttachments(this.$route.params.slug_signal)
.then((data) => (this.attachments = data));
},
getLinkedFeatures() {
featureAPI
.getFeatureLinks(this.$route.params.slug_signal)
.then((data) =>
this.$store.commit("feature/SET_LINKED_FEATURES", data)
);
},
},
created() {
this.$store.commit(
"feature_type/SET_CURRENT_FEATURE_TYPE_SLUG",
this.$route.params.slug_type_signal
);
this.getFeatureEvents();
this.getFeatureAttachments();
this.getLinkedFeatures();
},
mounted() {
if (!this.project) {
this.$store
.dispatch("GET_PROJECT_INFO", this.$route.params.slug)
.then((data) => {
console.log(data);
this.initMap();
});
} else {
this.initMap();
}
},
beforeDestroy() {
this.$store.commit("CLEAR_MESSAGES");
},
};
</script>
<style>
#map {
width: 100%;
height: 100%;
min-height: 250px;
max-height: 70vh;
}
#feed-event .event {
margin-bottom: 1em;
}
#feed-event .event .date {
margin-right: 1em !important;
}
#feed-event .event .extra.text {
margin-left: 107px;
margin-top: 0;
}
</style>
\ No newline at end of file
<template>
<div v-frag>
<script
type="application/javascript"
:src="
baseUrl +
'resources/leaflet-control-geocoder-1.13.0/Control.Geocoder.js'
"
></script>
<div class="fourteen wide column">
<h1 v-if="feature && currentRouteName === 'editer-signalement'">
Mise à jour du signalement "{{ feature.title || feature.feature_id }}"
</h1>
<h1
v-else-if="feature_type && currentRouteName === 'ajouter-signalement'"
>
Création d'un signalement <small>[{{ feature_type.title }}]</small>
</h1>
<form
id="form-feature-edit"
action=""
method="post"
enctype="multipart/form-data"
class="ui form"
>
<!-- Feature Fields -->
<div class="two fields">
<div :class="field_title">
<label :for="form.title.id_for_label">{{ form.title.label }}</label>
<input
type="text"
required
:maxlength="form.title.field.max_length"
:name="form.title.html_name"
:id="form.title.id_for_label"
v-model="form.title.value"
@blur="updateStore"
/>
<ul id="errorlist-title" class="errorlist">
<li v-for="error in form.title.errors" :key="error">
{{ error }}
</li>
</ul>
</div>
<div class="required field">
<label :for="form.status.id_for_label">{{
form.status.label
}}</label>
<Dropdown
:options="allowedStatusChoices"
:selected="selected_status.name"
:selection.sync="selected_status"
/>
</div>
</div>
<div class="field">
<label :for="form.description.id_for_label">{{
form.description.label
}}</label>
<textarea
:name="form.description.html_name"
rows="5"
v-model="form.description.value"
@blur="updateStore"
></textarea>
</div>
<!-- Geom Field -->
<div class="field">
<label :for="form.geom.id_for_label">{{ form.geom.label }}</label>
<!-- Import GeoImage -->
<div v-frag v-if="feature_type && feature_type.geom_type === 'point'">
<p v-if="isOffline() !== true">
<button
@click="toggleGeoRefModal"
id="add-geo-image"
type="button"
class="ui compact button"
>
<i class="file image icon"></i>Importer une image géoréférencée
</button>
Vous pouvez utiliser une image géoréférencée pour localiser le
signalement.
</p>
<div
v-if="showGeoRef"
class="ui dimmer modals page transition visible active"
style="display: flex !important"
>
<div
class="ui mini modal transition visible active"
style="display: block !important"
>
<i class="close icon" @click="toggleGeoRefModal"></i>
<div class="content">
<h3>Importer une image géoréférencée</h3>
<form
id="form-geo-image"
class="ui form"
enctype="multipart/form-data"
>
<p>
Attention, si vous avez déjà saisi une géométrie, celle
issue de l'image importée l'écrasera.
</p>
<div class="field georef-btn">
<label>Image (png ou jpeg)</label>
<label class="ui icon button" for="image_file">
<i class="file icon"></i>
<span class="label">{{ geoRefFileLabel }}</span>
</label>
<input
type="file"
accept="image/jpeg, image/png"
style="display: none"
ref="file"
v-on:change="handleFileUpload"
name="image_file"
class="image_file"
id="image_file"
/>
<ul v-if="erreurUploadMessage" class="errorlist">
<li>
{{ erreurUploadMessage }}
</li>
</ul>
</div>
<button
@click="georeferencement"
id="get-geom-from-image-file"
type="button"
:class="[
'ui compact button',
file && !erreurUploadMessage ? 'green' : 'disabled',
{ red: erreurUploadMessage },
]"
>
<i class="plus icon"></i>
Importer
</button>
</form>
</div>
</div>
</div>
<p v-if="showGeoPositionBtn">
<button
@click="create_point_geoposition"
id="create-point-geoposition"
type="button"
class="ui compact button"
>
<i class="ui map marker alternate icon"></i>Positionner le
signalement à partir de votre géolocalisation
</button>
</p>
<span
id="erreur-geolocalisation"
v-if="erreurGeolocalisationMessage"
>
<div class="ui negative message">
<div class="header">
Une erreur est survenue avec la fonctionnalité de
géolocalisation
</div>
<p id="erreur-geolocalisation-message">
{{ erreurGeolocalisationMessage }}
</p>
</div>
<br />
</span>
</div>
<ul id="errorlist-geom" class="errorlist">
<li v-for="error in form.geom.errors" :key="error">
{{ error }}
</li>
</ul>
<!-- Map -->
<input
type="hidden"
:name="form.geom.html_name"
:id="form.geom.id_for_label"
v-model="form.geom.value"
@blur="updateStore"
/>
<div class="ui tab active map-container" data-tab="map">
<div id="map"></div>
<!-- // todo: ajouter v-if -->
<!-- {% if serialized_base_maps|length > 0 %} {% include
"geocontrib/map-layers/sidebar-layers.html" with
basemaps=serialized_base_maps layers=serialized_layers
project=project.slug%} {% endif %} -->
<SidebarLayers v-if="basemaps && map" />
</div>
</div>
<!-- Extra Fields -->
<div class="ui horizontal divider">DONNÉES MÉTIER</div>
<div
v-for="(field, index) in orderedCustomFields"
:key="field.field_type + index"
class="field"
>
<FeatureExtraForm :field="field" />
{{ field.errors }}
</div>
<!-- Pièces jointes -->
<div v-if="isOffline() !== true">
<div class="ui horizontal divider">PIÈCES JOINTES</div>
<div v-if="isOffline() !== true" id="formsets-attachment">
<FeatureAttachmentForm
v-for="form in attachmentFormset"
:key="form.dataKey"
:attachmentForm="form"
ref="attachementForm"
/>
</div>
<button
@click="add_attachement_formset"
id="add-attachment"
type="button"
class="ui compact basic button button-hover-green"
>
<i class="ui plus icon"></i>Ajouter une pièce jointe
</button>
</div>
<!-- Signalements liés -->
<div v-if="isOffline() !== true">
<div class="ui horizontal divider">SIGNALEMENTS LIÉS</div>
<div id="formsets-link">
<FeatureLinkedForm
v-for="form in linkedFormset"
:key="form.dataKey"
:linkedForm="form"
:features="features"
ref="linkedForm"
/>
</div>
<button
@click="add_linked_formset"
id="add-link"
type="button"
class="ui compact basic button button-hover-green"
>
<i class="ui plus icon"></i>Ajouter une liaison
</button>
</div>
<div class="ui divider"></div>
<button @click="postForm" type="button" class="ui teal icon button">
<i class="white save icon"></i> Enregistrer les changements
</button>
</form>
</div>
</div>
</template>
<script>
import frag from "vue-frag";
import { mapState, mapGetters } from "vuex";
import FeatureAttachmentForm from "@/components/feature/FeatureAttachmentForm";
import FeatureLinkedForm from "@/components/feature/FeatureLinkedForm";
import FeatureExtraForm from "@/components/feature/FeatureExtraForm";
import Dropdown from "@/components/Dropdown.vue";
import SidebarLayers from "@/components/map-layers/SidebarLayers";
import featureAPI from "@/services/feature-api";
import L from "leaflet";
import "leaflet-draw";
import { mapUtil } from "@/assets/js/map-util.js";
const axios = require("axios");
import flip from "@turf/flip";
axios.defaults.headers.common["X-CSRFToken"] = ((name) => {
var re = new RegExp(name + "=([^;]+)");
var value = re.exec(document.cookie);
return value !== null ? unescape(value[1]) : null;
})("csrftoken");
export default {
name: "Feature_edit",
directives: {
frag,
},
components: {
FeatureAttachmentForm,
FeatureLinkedForm,
Dropdown,
SidebarLayers,
FeatureExtraForm,
},
data() {
return {
map: null,
baseUrl: this.$store.state.configuration.BASE_URL,
file: null,
showGeoRef: false,
showGeoPositionBtn: true,
erreurGeolocalisationMessage: null,
erreurUploadMessage: null,
attachmentDataKey: 0,
linkedDataKey: 0,
form: {
title: {
errors: [],
id_for_label: "name",
field: {
max_length: 30,
},
html_name: "name",
label: "Nom",
value: "",
},
status: {
id_for_label: "status",
html_name: "status",
label: "Statut",
value: {
value: "draft",
name: "Brouillon",
},
},
description: {
errors: [],
id_for_label: "description",
html_name: "description",
label: "Description",
value: "",
},
geom: {
errors: [],
label: "Localisation",
value: null,
},
},
};
},
computed: {
...mapGetters(["project"]),
...mapGetters("feature_type", ["feature_type"]),
...mapState(["user", "USER_LEVEL_PROJECTS"]),
...mapState("map", ["basemaps"]),
...mapState("feature", [
"attachmentFormset",
"attachmentsToDelete",
"attachmentsToPut",
"linkedFormset",
"features",
"extra_form",
"linked_features",
"statusChoices",
]),
field_title() {
if (this.feature_type) {
if (this.feature_type.title_optional) {
return "field";
}
}
return "required field";
},
currentRouteName() {
return this.$route.name;
},
feature: function () {
return this.$store.state.feature.features.find(
(el) => el.feature_id === this.$route.params.slug_signal
);
},
orderedCustomFields() {
return [...this.extra_form].sort((a, b) => a.position - b.position);
},
geoRefFileLabel() {
if (this.file) {
return this.file.name;
}
return "Sélectionner une image ...";
},
selected_status: {
get() {
return this.form.status.value;
},
set(newValue) {
this.form.status.value = newValue;
this.updateStore();
},
},
allowedStatusChoices() {
if (this.project) {
const isModerate = this.project.moderation;
const userStatus = this.USER_LEVEL_PROJECTS[this.project.slug];
const isOwnFeature = this.feature
? this.feature.creator === this.user.id //* prevent undefined feature
: false; //* si le contributeur est l'auteur du signalement
if (
//* si admin ou modérateur, statuts toujours disponible : Brouillon, Publié, Archivé
userStatus === "Modérateur" ||
userStatus === "Administrateur projet"
) {
return this.statusChoices.filter((el) => el.value !== "pending");
} else if (userStatus === "Contributeur") {
//* cas particuliers du contributeur
if (
this.currentRouteName === "ajouter-signalement" ||
!isOwnFeature
) {
//* même cas à l'ajout d'une feature ou si feature n'a pas été créé par le contributeur
return isModerate
? this.statusChoices.filter(
(el) => el.value === "draft" || el.value === "pending"
)
: this.statusChoices.filter(
(el) => el.value === "draft" || el.value === "published"
);
} else {
//* à l'édition d'une feature et si le contributeur est l'auteur de la feature
return isModerate
? this.statusChoices.filter(
(el) => el.value !== "published" //* toutes sauf "Publié"
)
: this.statusChoices.filter(
(el) => el.value !== "pending" //* toutes sauf "En cours de publication"
);
}
}
}
return [];
},
},
methods: {
isOffline() {
return navigator.onLine == false;
},
initForm() {
if (this.currentRouteName === "editer-signalement") {
for (let key in this.feature) {
if (key && this.form[key]) {
if (key === "status") {
const value = this.feature[key];
this.form[key].value = this.statusChoices.find(
(key) => key.value === value
);
} else {
this.form[key].value = this.feature[key];
}
}
}
this.updateStore();
}
},
create_point_geoposition() {
function success(position) {
const latitude = position.coords.latitude;
const longitude = position.coords.longitude;
var layer = L.circleMarker([latitude, longitude]);
this.add_layer_call_back(layer);
this.map.setView([latitude, longitude]);
}
function error(err) {
this.erreurGeolocalisationMessage = err.message;
if (err.message === "User denied geolocation prompt") {
this.erreurGeolocalisationMessage =
"La géolocalisation a été désactivée par l'utilisateur";
}
}
this.erreurGeolocalisationMessage = null;
if (!navigator.geolocation) {
this.erreurGeolocalisationMessage =
"La géolocalisation n'est pas supportée par votre navigateur.";
} else {
navigator.geolocation.getCurrentPosition(
success.bind(this),
error.bind(this)
);
}
},
toggleGeoRefModal() {
if (this.showGeoRef) {
//* when popup closes, empty form
this.erreurUploadMessage = "";
this.file = null;
}
this.showGeoRef = !this.showGeoRef;
},
handleFileUpload() {
this.erreurUploadMessage = "";
this.file = this.$refs.file.files[0];
},
georeferencement() {
console.log("georeferencement");
const url = `${this.$store.state.configuration.VUE_APP_DJANGO_API_BASE}exif-geom-reader/`;
let formData = new FormData();
formData.append("image_file", this.file);
console.log(">> formData >> ", formData);
axios
.post(url, formData, {
headers: {
"Content-Type": "multipart/form-data",
},
})
.then((response) => {
console.log("SUCCESS!!", response.data);
if (response.data.geom.indexOf("POINT") >= 0) {
let regexp = /POINT\s\((.*)\s(.*)\)/;
let arr = regexp.exec(response.data.geom);
let json = {
type: "Feature",
geometry: {
type: "Point",
coordinates: [parseFloat(arr[1]), parseFloat(arr[2])],
},
properties: {},
};
this.updateMap(json);
this.updateGeomField(json);
// Set Attachment
this.addAttachment({
title: "Localisation",
info: "",
id: "loc",
attachment_file: this.file.name,
fileToImport: this.file,
});
}
})
.catch((error) => {
console.log({ error });
if (error && error.response && error.response) {
this.erreurUploadMessage = error.response.data.error;
} else {
this.erreurUploadMessage =
"Une erreur est survenue pendant l'import de l'image géoréférencée";
}
});
},
initExtraForms(feature) {
function findCurrentValue(label) {
const field = feature.feature_data.find((el) => el.label === label);
return field ? field.value : null;
}
let extraForm = this.feature_type.customfield_set.map((field) => {
return {
...field,
//* add value field to extra forms from feature_type and existing values if feature is defined
value: feature ? findCurrentValue(field.label) : null,
};
});
this.$store.commit("feature/SET_EXTRA_FORM", extraForm);
},
add_attachement_formset() {
this.$store.commit("feature/ADD_ATTACHMENT_FORM", {
dataKey: this.attachmentDataKey,
}); // * create an object with the counter in store
this.attachmentDataKey += 1; // * increment counter for key in v-for
},
addAttachment(attachment) {
console.log(attachment);
this.$store.commit("feature/ADD_ATTACHMENT_FORM", {
dataKey: this.attachmentDataKey,
title: attachment.title,
attachment_file: attachment.attachment_file,
info: attachment.info,
fileToImport: attachment.fileToImport,
id: attachment.id,
});
this.attachmentDataKey += 1;
},
addExistingAttachementFormset(attachementFormset) {
for (const attachment of attachementFormset) {
this.addAttachment(attachment);
}
},
add_linked_formset() {
this.$store.commit("feature/ADD_LINKED_FORM", {
dataKey: this.linkedDataKey,
}); // * create an object with the counter in store
this.linkedDataKey += 1; // * increment counter for key in v-for
},
addExistingLinkedFormset(linkedFormset) {
for (const linked of linkedFormset) {
this.$store.commit("feature/ADD_LINKED_FORM", {
dataKey: this.linkedDataKey,
relation_type: linked.relation_type,
feature_to: linked.feature_to,
});
this.linkedDataKey += 1;
}
},
updateStore() {
this.$store.commit("feature/UPDATE_FORM", {
title: this.form.title.value,
status: this.form.status.value,
description: this.form.description,
geometry: this.form.geom.value,
feature_id: this.feature ? this.feature.feature_id : "",
});
},
checkFormTitle() {
if (this.form.title.value) {
this.form.title.errors = [];
return true;
} else if (
!this.form.title.errors.includes("Veuillez compléter ce champ.")
) {
this.form.title.errors.push("Veuillez compléter ce champ.");
document
.getElementById("errorlist-title")
.scrollIntoView({ block: "end", inline: "nearest" });
}
return false;
},
checkFormGeom() {
if (this.form.geom.value) {
this.form.geom.errors = [];
return true;
} else if (
!this.form.geom.errors.includes("Valeur géométrique non valide.")
) {
this.form.geom.errors.push("Valeur géométrique non valide.");
document
.getElementById("errorlist-geom")
.scrollIntoView({ block: "end", inline: "nearest" });
}
return false;
},
checkAddedForm() {
let isValid = true; //* fallback if all customForms returned true
if (this.$refs.attachementForm) {
for (const attachementForm of this.$refs.attachementForm) {
if (attachementForm.checkForm() === false) {
isValid = false;
}
}
}
if (this.$refs.linkedForm) {
for (const linkedForm of this.$refs.linkedForm) {
if (linkedForm.checkForm() === false) {
isValid = false;
}
}
}
return isValid;
},
postForm() {
let is_valid = true;
if (!this.feature_type.title_optional) {
is_valid =
this.checkFormTitle() &&
this.checkFormGeom() &&
this.checkAddedForm();
} else {
is_valid = this.checkFormGeom() && this.checkAddedForm();
}
if (is_valid) {
this.$store.dispatch("feature/SEND_FEATURE", this.currentRouteName);
}
},
//* ************* MAP *************** *//
onFeatureTypeLoaded() {
var geomLeaflet = {
point: "circlemarker",
linestring: "polyline",
polygon: "polygon",
};
var geomType = this.feature_type.geom_type;
var drawConfig = {
polygon: false,
marker: false,
polyline: false,
rectangle: false,
circle: false,
circlemarker: false,
};
drawConfig[geomLeaflet[geomType]] = true;
L.drawLocal = {
draw: {
toolbar: {
actions: {
title: "Annuler le dessin",
text: "Annuler",
},
finish: {
title: "Terminer le dessin",
text: "Terminer",
},
undo: {
title: "Supprimer le dernier point dessiné",
text: "Supprimer le dernier point",
},
buttons: {
polyline: "Dessiner une polyligne",
polygon: "Dessiner un polygone",
rectangle: "Dessiner un rectangle",
circle: "Dessiner un cercle",
marker: "Dessiner une balise",
circlemarker: "Dessiner un point",
},
},
handlers: {
circle: {
tooltip: {
start: "Cliquer et glisser pour dessiner le cercle.",
},
radius: "Rayon",
},
circlemarker: {
tooltip: {
start: "Cliquer sur la carte pour placer le point.",
},
},
marker: {
tooltip: {
start: "Cliquer sur la carte pour placer la balise.",
},
},
polygon: {
tooltip: {
start: "Cliquer pour commencer à dessiner.",
cont: "Cliquer pour continuer à dessiner.",
end: "Cliquer sur le premier point pour terminer le dessin.",
},
},
polyline: {
error: "<strong>Error:</strong> shape edges cannot cross!",
tooltip: {
start: "Cliquer pour commencer à dessiner.",
cont: "Cliquer pour continuer à dessiner.",
end: "Cliquer sur le dernier point pour terminer le dessin.",
},
},
rectangle: {
tooltip: {
start: "Cliquer et glisser pour dessiner le rectangle.",
},
},
simpleshape: {
tooltip: {
end: "Relâcher la souris pour terminer de dessiner.",
},
},
},
},
edit: {
toolbar: {
actions: {
save: {
title: "Sauver les modifications",
text: "Sauver",
},
cancel: {
title:
"Annuler la modification, annule toutes les modifications",
text: "Annuler",
},
clearAll: {
title: "Effacer l'objet",
text: "Effacer",
},
},
buttons: {
edit: "Modifier l'objet",
editDisabled: "Aucun objet à modifier",
remove: "Supprimer l'objet",
removeDisabled: "Aucun objet à supprimer",
},
},
handlers: {
edit: {
tooltip: {
text: "Faites glisser les marqueurs ou les balises pour modifier l'élément.",
subtext: "Cliquez sur Annuler pour annuler les modifications..",
},
},
remove: {
tooltip: {
text: "Cliquez sur un élément pour le supprimer.",
},
},
},
},
};
this.drawnItems = new L.FeatureGroup();
this.map.addLayer(this.drawnItems);
this.drawControlFull = new L.Control.Draw({
position: "topright",
edit: {
featureGroup: this.drawnItems,
},
draw: drawConfig,
});
this.drawControlEditOnly = new L.Control.Draw({
position: "topright",
edit: {
featureGroup: this.drawnItems,
},
draw: false,
});
if (this.currentRouteName === "editer-signalement") {
this.map.addControl(this.drawControlEditOnly);
} else this.map.addControl(this.drawControlFull);
this.map.on(
"draw:created",
function (e) {
var layer = e.layer;
this.add_layer_call_back(layer);
}.bind(this)
);
//var wellknown;// TODO Remplacer par autre chose
this.map.on(
"draw:edited",
function (e) {
var layers = e.layers;
let self = this;
layers.eachLayer(function (layer) {
//this.updateGeomField(wellknown.stringify(layer.toGeoJSON()))
self.updateGeomField(layer.toGeoJSON());
});
}.bind(this)
);
this.map.on(
"draw:deleted",
function () {
this.drawControlEditOnly.remove(this.map);
this.drawControlFull.addTo(this.map);
this.updateGeomField("");
if (geomType === "point") {
this.showGeoPositionBtn = true;
this.erreurGeolocalisationMessage = "";
}
}.bind(this)
);
},
updateMap(geomFeatureJSON) {
if (this.drawnItems) this.drawnItems.clearLayers();
console.log("update map");
var geomType = this.feature_type.geom_type;
if (geomFeatureJSON) {
var geomJSON = flip(geomFeatureJSON.geometry); //turf.flip(geomFeatureJSON)
if (geomType === "point") {
L.circleMarker(geomJSON.coordinates).addTo(this.drawnItems);
} else if (geomType === "linestring") {
L.polyline(geomJSON.coordinates).addTo(this.drawnItems);
} else if (geomType === "polygon") {
L.polygon(geomJSON.coordinates).addTo(this.drawnItems);
}
this.map.fitBounds(this.drawnItems.getBounds(), { padding: [25, 25] });
} else {
this.map.setView(
this.$store.state.configuration.DEFAULT_MAP_VIEW.center,
this.$store.state.configuration.DEFAULT_MAP_VIEW.zoom
);
}
},
updateGeomField(newGeom) {
//this.geometry = newGeom;
this.form.geom.value = newGeom.geometry;
this.updateStore();
},
initMap() {
var mapDefaultViewCenter =
this.$store.state.configuration.DEFAULT_MAP_VIEW.center;
var mapDefaultViewZoom =
this.$store.state.configuration.DEFAULT_MAP_VIEW.zoom;
// Create the map, then init the layers and features
this.map = mapUtil.createMap({
mapDefaultViewCenter,
mapDefaultViewZoom,
});
const currentFeatureId = this.$route.params.slug_signal;
setTimeout(
function () {
let project_id=this.$route.params.slug.split('-')[0];
const mvtUrl = `${this.$store.state.configuration.VUE_APP_DJANGO_API_BASE}/features.mvt/?tile={z}/{x}/{y}&project_id=${project_id}`;
mapUtil.addVectorTileLayer(mvtUrl,this.$route.params.slug,this.$store.state.feature_type.feature_types);
}.bind(this),
1000
);
const url = `${this.$store.state.configuration.VUE_APP_DJANGO_API_BASE}projects/${this.$route.params.slug}/feature/?feature_type__slug=${this.$route.params.slug_type_signal}&output=geojson`;
axios
.get(url)
.then((response) => {
console.log(response.data.features);
const features = response.data.features;
if (features) {
const allFeaturesExceptCurrent = features.filter(
(feat) => feat.id !== currentFeatureId
);
mapUtil.addFeatures(allFeaturesExceptCurrent,{},false,this.$store.state.feature_type.feature_types);
if (this.currentRouteName === "editer-signalement") {
const currentFeature = features.filter(
(feat) => feat.id === currentFeatureId
)[0];
this.updateMap(currentFeature);
}
}
})
.catch((error) => {
throw error;
});
document.addEventListener("change-layers-order", (event) => {
// Reverse is done because the first layer in order has to be added in the map in last.
// Slice is done because reverse() changes the original array, so we make a copy first
mapUtil.updateOrder(event.detail.layers.slice().reverse());
});
},
add_layer_call_back(layer) {
layer.addTo(this.drawnItems);
this.drawControlFull.remove(this.map);
this.drawControlEditOnly.addTo(this.map);
//var wellknown;// TODO Remplacer par autre chose
//this.updateGeomField(wellknown.stringify(layer.toGeoJSON()))
this.updateGeomField(layer.toGeoJSON());
if (this.feature_type.geomType === "point") {
this.showGeoPositionBtn = false;
this.erreurGeolocalisationMessage = "";
}
},
getFeatureAttachments() {
featureAPI
.getFeatureAttachments(this.$route.params.slug_signal)
.then((data) => this.addExistingAttachementFormset(data));
},
getLinkedFeatures() {
featureAPI
.getFeatureLinks(this.$route.params.slug_signal)
.then((data) => this.addExistingLinkedFormset(data));
},
},
created() {
this.$store.commit(
"feature_type/SET_CURRENT_FEATURE_TYPE_SLUG",
this.$route.params.slug_type_signal
);
if (this.$route.params.slug_signal) {
this.getFeatureAttachments();
this.getLinkedFeatures();
}
},
mounted() {
this.$store
.dispatch("GET_PROJECT_INFO", this.$route.params.slug)
.then((data) => {
console.log(data);
this.initForm();
this.initMap();
this.onFeatureTypeLoaded();
this.initExtraForms(this.feature);
setTimeout(
function () {
mapUtil.addGeocoders(this.$store.state.configuration);
}.bind(this),
1000
);
});
},
destroyed() {
//* be sure that previous Formset have been cleared for creation
this.$store.commit("feature/CLEAR_ATTACHMENT_FORM");
this.$store.commit("feature/CLEAR_LINKED_FORM");
},
};
</script>
<style>
#map {
height: 70vh;
width: 100%;
border: 1px solid grey;
}
#get-geom-from-image-file {
margin-bottom: 5px;
}
.georef-btn {
max-width: 400px;
}
@media only screen and (max-width: 767px) {
#map {
height: 80vh;
}
}
/* // ! missing style in semantic.min.css, je ne comprends pas comment... */
.ui.right.floated.button {
float: right;
margin-right: 0;
margin-left: 0.25em;
}
/* // ! margin écrasé par class last-child first-child, pas normal ... */
.ui.segment {
margin: 1rem 0 !important;
}
/* override to display buttons under the dimmer of modal */
.leaflet-top,
.leaflet-bottom {
z-index: 800;
}
</style>
<template>
<div class="fourteen wide column">
<script
type="application/javascript"
:src="
baseUrl +
'resources/leaflet-control-geocoder-1.13.0/Control.Geocoder.js'
"
></script>
<div id="feature-list-container" class="ui grid mobile-column">
<div class="four wide column mobile-fullwidth">
<h1>Signalements</h1>
</div>
<div class="twelve-wide column no-padding-mobile mobile-fullwidth">
<div class="ui large text loader">Chargement</div>
<div class="ui secondary menu no-margin">
<a
@click="showMap = true"
:class="['item no-margin', { active: showMap }]"
data-tab="map"
data-tooltip="Carte"
><i class="map fitted icon"></i
></a>
<a
@click="showMap = false"
:class="['item no-margin', { active: !showMap }]"
data-tab="list"
data-tooltip="Liste"
><i class="list fitted icon"></i
></a>
<div class="item">
<h4>
{{ filteredFeatures.length }} signalement{{
filteredFeatures.length > 1 ? "s" : ""
}}
</h4>
</div>
<div
v-if="
project &&
feature_types.length > 0 &&
permissions.can_create_feature
"
class="item right"
>
<div
@click="showAddFeature = !showAddFeature"
class="ui dropdown button compact button-hover-green"
data-tooltip="Ajouter un signalement"
data-position="bottom left"
>
<i class="plus fitted icon"></i>
<div
v-if="showAddFeature"
class="menu transition visible"
style="z-index: 9999"
>
<div class="header">Ajouter un signalement du type</div>
<div class="scrolling menu text-wrap">
<router-link
:to="{
name: 'ajouter-signalement',
params: { slug_type_signal: type.slug },
}"
v-for="(type, index) in feature_types"
:key="type.slug + index"
class="item"
>
{{ type.title }}
</router-link>
</div>
</div>
</div>
<div
v-if="checkedFeatures.length"
@click="modalAllDelete"
class="ui button compact button-hover-red margin-left-25"
data-tooltip="Effacer tous les types de signalements sélectionnés"
data-position="left center"
data-variation="mini"
>
<i class="grey trash fitted icon"></i>
</div>
</div>
</div>
</div>
</div>
<form id="form-filters" class="ui form grid" action="" method="get">
<div class="field wide four column no-margin-mobile">
<label>Type</label>
<Dropdown
:options="form.type.choices"
:selected="form.type.selected"
:selection.sync="form.type.selected"
:search="true"
:clearable="true"
/>
</div>
<div class="field wide four column no-padding-mobile no-margin-mobile">
<label>Statut</label>
<!-- //* giving an object mapped on key name -->
<Dropdown
:options="statusChoices"
:selected="form.status.selected.name"
:selection.sync="form.status.selected"
:search="true"
:clearable="true"
/>
</div>
<div class="field wide four column">
<label>Nom</label>
<div class="ui icon input">
<i class="search icon"></i>
<div class="ui action input">
<input
type="text"
name="title"
v-model="form.title"
@input="onFilterChange"
/>
<button
type="button"
class="ui teal icon button"
id="submit-search"
>
<i class="search icon"></i>
</button>
</div>
</div>
</div>
<!-- map params, updated on map move -->
<input type="hidden" name="zoom" v-model="zoom" />
<input type="hidden" name="lat" v-model="lat" />
<input type="hidden" name="lng" v-model="lng" />
</form>
<div v-show="showMap" class="ui tab active map-container" data-tab="map">
<div id="map"></div>
<SidebarLayers v-if="baseMaps && map" />
</div>
<FeatureListTable
v-show="!showMap"
:filteredFeatures="filteredFeatures"
:user="user"
:checkedFeatures.sync="checkedFeatures"
/>
<!-- MODAL ALL DELETE FEATURE TYPE -->
<div
v-if="modalAllDeleteOpen"
class="ui dimmer modals page transition visible active"
style="display: flex !important"
>
<div
:class="[
'ui mini modal subscription',
{ 'active visible': modalAllDeleteOpen },
]"
>
<i @click="modalAllDeleteOpen = false" class="close icon"></i>
<div class="ui icon header">
<i class="trash alternate icon"></i>
Êtes-vous sûr de vouloir effacer
<span v-if="checkedFeatures.length === 1"> un signalement ? </span>
<span v-else> ces {{ checkedFeatures.length }} signalements ? </span>
</div>
<div class="actions">
<button
@click="deleteAllFeatureSelection"
type="button"
class="ui red compact fluid button no-margin"
>
Confirmer la suppression
</button>
</div>
</div>
</div>
</div>
</template>
<script>
import { mapGetters, mapState } from "vuex";
import { mapUtil } from "@/assets/js/map-util.js";
import SidebarLayers from "@/components/map-layers/SidebarLayers";
import FeatureListTable from "@/components/feature/FeatureListTable";
import Dropdown from "@/components/Dropdown.vue";
const axios = require("axios");
export default {
name: "Feature_list",
components: {
SidebarLayers,
Dropdown,
FeatureListTable,
},
data() {
return {
modalAllDeleteOpen: false,
form: {
type: {
selected: null,
choices: [],
},
status: {
selected: {
name: null,
value: null,
},
choices: [
{
name: "Brouillon",
value: "draft",
},
{
name: "En attente de publication",
value: "pending",
},
{
name: "Publié",
value: "published",
},
{
name: "Archivé",
value: "archived",
},
],
},
title: null,
},
geojsonFeatures: [],
filterStatus: null,
filterType: null,
baseUrl: this.$store.state.configuration.BASE_URL,
map: null,
zoom: null,
lat: null,
lng: null,
showMap: true,
showAddFeature: false,
};
},
computed: {
...mapGetters(["project", "permissions"]),
...mapState(["user"]),
...mapState("feature", ["features", "checkedFeatures"]),
...mapState("feature_type", ["feature_types"]),
baseMaps() {
return this.$store.state.map.basemaps;
},
filteredFeatures() {
let results = this.geojsonFeatures;
if (this.form.type.selected) {
results = results.filter(
(el) => el.properties.feature_type.title === this.form.type.selected
);
}
if (this.form.status.selected.value) {
console.log("filter by" + this.form.status.selected.value);
results = results.filter(
(el) => el.properties.status.value === this.form.status.selected.value
);
}
if (this.form.title) {
results = results.filter((el) => {
if (el.properties.title) {
return el.properties.title
.toLowerCase()
.includes(this.form.title.toLowerCase());
} else
return el.id.toLowerCase().includes(this.form.title.toLowerCase());
});
}
return results;
},
statusChoices() {
//* if project is not moderate, remove pending status
return this.form.status.choices.filter((el) =>
this.project.moderation ? true : el.value !== "pending"
);
},
},
watch: {
filteredFeatures(newValue, oldValue) {
if (newValue && newValue !== oldValue) {
this.onFilterChange();
}
},
},
methods: {
modalAllDelete() {
this.modalAllDeleteOpen = !this.modalAllDeleteOpen;
},
deleteFeature(feature) {
const url = `${this.$store.state.configuration.VUE_APP_DJANGO_API_BASE}features/${feature.feature_id}/?project__slug=${this.project.slug}`;
axios
.delete(url, {})
.then(() => {
if (!this.modalAllDeleteOpen) {
this.$store
.dispatch("feature/GET_PROJECT_FEATURES", this.project.slug)
.then(() => this.getFeaturesAsGeojson());
}
})
.catch(() => {
return false;
});
},
deleteAllFeatureSelection() {
let feature = {};
this.checkedFeatures.forEach((feature_id) => {
feature = { feature_id: feature_id };
this.deleteFeature(feature);
});
this.modalAllDelete();
},
onFilterChange() {
if (this.featureGroup) {
const features = this.filteredFeatures;
this.featureGroup.clearLayers();
this.featureGroup = mapUtil.addFeatures(
features,
{},
false,
this.$store.state.feature_type.feature_types
);
mapUtil.getMap().invalidateSize();
mapUtil.getMap()._onResize(); // force refresh for vector tiles
if (this.featureGroup.getLayers().length > 0) {
mapUtil
.getMap()
.fitBounds(this.featureGroup.getBounds(), { padding: [25, 25] });
} else {
mapUtil.getMap().zoomOut(1);
}
}
},
initMap() {
console.log(this);
this.zoom = this.$route.query.zoom || "";
this.lat = this.$route.query.lat || "";
this.lng = this.$route.query.lng || "";
var mapDefaultViewCenter =
this.$store.state.configuration.DEFAULT_MAP_VIEW.center;
var mapDefaultViewZoom =
this.$store.state.configuration.DEFAULT_MAP_VIEW.zoom;
this.map = mapUtil.createMap({
zoom: this.zoom,
lat: this.lat,
lng: this.lng,
mapDefaultViewCenter,
mapDefaultViewZoom,
});
document.addEventListener("change-layers-order", (event) => {
// Reverse is done because the first layer in order has to be added in the map in last.
// Slice is done because reverse() changes the original array, so we make a copy first
mapUtil.updateOrder(event.detail.layers.slice().reverse());
});
// --------- End sidebar events ----------
this.getFeaturesAsGeojson();
setTimeout(
function () {
let project_id = this.$route.params.slug.split("-")[0];
const mvtUrl = `${this.$store.state.configuration.VUE_APP_DJANGO_API_BASE}/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.form
);
mapUtil.addGeocoders(this.$store.state.configuration);
}.bind(this),
1000
);
},
getFeaturesAsGeojson() {
const url = `${this.$store.state.configuration.VUE_APP_DJANGO_API_BASE}projects/${this.$route.params.slug}/feature/?output=geojson`;
this.$store.commit(
"DISPLAY_LOADER",
"Récupération des signalements en cours..."
);
axios
.get(url)
.then((response) => {
if (response.status === 200 && response.data.features.length > 0) {
this.loadFeatures(response.data.features);
}
this.$store.commit("DISCARD_LOADER");
})
.catch((error) => {
this.$store.commit("DISCARD_LOADER");
throw error;
});
},
loadFeatures(features) {
this.geojsonFeatures = features;
const urlParams = new URLSearchParams(window.location.search);
const featureType = urlParams.get("feature_type");
const featureStatus = urlParams.get("status");
const featureTitle = urlParams.get("title");
this.featureGroup = mapUtil.addFeatures(
this.geojsonFeatures,
{
featureType,
featureStatus,
featureTitle,
},
false,
this.$store.state.feature_type.feature_types
);
// Fit the map to bound only if no initial zoom and center are defined
if (
(this.lat === "" || this.lng === "" || this.zoom === "") &&
this.geojsonFeatures.length > 0
) {
mapUtil
.getMap()
.fitBounds(this.featureGroup.getBounds(), { padding: [25, 25] });
}
this.form.type.choices = [
//* converting Set to an Array with spread "..."
...new Set(
this.geojsonFeatures.map((el) => el.properties.feature_type.title)
), //* use Set to eliminate duplicate values
];
},
},
created() {
if (!this.project) {
//this.$store.dispatch("GET_PROJECT_MESSAGES", this.$route.params.slug);
this.$store.dispatch("GET_PROJECT_INFO", this.$route.params.slug);
}
},
mounted() {
this.initMap();
},
destroyed() {
//* allow user to change page if ever stuck on loader
this.$store.commit("DISCARD_LOADER");
},
};
</script>
<style scoped>
#map {
width: 100%;
min-height: 300px;
height: calc(100vh - 300px);
border: 1px solid grey;
/* To not hide the filters */
z-index: 1;
}
.center {
text-align: center !important;
}
#feature-list-container {
justify-content: flex-start;
}
#feature-list-container .ui.menu:not(.vertical) .right.item {
padding-right: 0;
}
.map-container {
width: 80vw;
transform: translateX(-50%);
margin-left: 50%;
}
.margin-left-25 {
margin-left: 0.25em !important;
}
.no-margin {
margin: 0 !important;
}
.no-padding {
padding: 0 !important;
}
@media screen and (min-width: 767px) {
.twelve-wide {
width: 75% !important;
}
}
@media screen and (max-width: 767px) {
#feature-list-container > .mobile-fullwidth {
width: 100% !important;
}
.no-margin-mobile {
margin: 0 !important;
}
.no-padding-mobile {
padding-top: 0 !important;
padding-bottom: 0 !important;
}
.mobile-column {
flex-direction: column !important;
}
#form-filters > .field.column {
width: 100% !important;
}
.map-container {
width: 100%;
}
}
</style>
<template>
<div v-frag>
Erreur Réseau lors de l'envoi du signalement. Votre signalement devra être envoyé au serveur quand vous aurez de nouveau accès à internet.
Veuillez à ce moment là cliquer sur Envoyer sur la page principale du projet
<router-link
:to="{
name: 'project_detail',
params: { slug: $route.params.slug },
}"
class="header"
>Retour au projet</router-link
>
</div>
</template>
<script>
import frag from "vue-frag";
export default {
name: "Feature_offline",
directives: {
frag,
},
data() {
return {
};
},
computed: {
},
methods: {
}
};
</script>
<style>
</style>
\ No newline at end of file
<template>
<div v-if="structure" class="row">
<div class="five wide column">
<div class="ui attached secondary segment">
<h1 class="ui center aligned header">
<img
v-if="structure.geom_type === 'point'"
class="ui medium image"
src="@/assets/img/marker.png"
/>
<img
v-if="structure.geom_type === 'linestring'"
class="ui medium image"
src="@/assets/img/line.png"
/>
<img
v-if="structure.geom_type === 'polygon'"
class="ui medium image"
src="@/assets/img/polygon.png"
/>
{{ structure.title }}
</h1>
</div>
<div class="ui attached segment">
<div class="ui basic segment">
<div class="ui horizontal tiny statistic">
<div class="value">
{{ feature_type_features.length }}
</div>
<div class="label">
Signalement{{ features.length > 1 ? "s" : "" }}
</div>
</div>
<h3 class="ui header">Champs</h3>
<div class="ui divided list">
<div
v-for="(field, index) in orderedCustomFields"
:key="field.name + index"
class="item"
>
<div class="right floated content">
<div class="description">{{ field.field_type }}</div>
</div>
<div class="content">{{ field.label }} ({{ field.name }})</div>
</div>
</div>
</div>
</div>
<div class="ui bottom attached secondary segment">
<div v-if="permissions.can_create_feature" class="ui styled accordion">
<div
@click="toggleShowImport"
:class="['title', { active: showImport }]"
>
<i class="dropdown icon"></i>
Importer des signalements
</div>
<div :class="['content', { active: showImport }]">
<div id="form-import-features" class="ui form">
<div class="field">
<label class="ui icon button" for="json_file">
<i class="file icon"></i>
<span class="label">{{ fileToImport.name }}</span>
</label>
<input
@change="onFileChange"
type="file"
accept="application/json, .json, .geojson"
style="display: none"
name="json_file"
id="json_file"
/>
</div>
<ul v-if="importError" class="errorlist">
<li>
{{ importError }}
</li>
</ul>
<button
:disabled="fileToImport.size === 0"
@click="importGeoJson"
class="ui fluid teal icon button"
>
<i class="upload icon"></i> Lancer l'import
</button>
<ImportTask
v-if="importFeatureTypeData && importFeatureTypeData.length"
:data="importFeatureTypeData"
/>
</div>
</div>
</div>
<div class="ui styled accordion">
<div
@click="toggleShowImport"
:class="['title', { active: !showImport }]"
>
<i class="dropdown icon"></i>
Exporter les signalements
</div>
<div :class="['content', { active: !showImport }]">
<p>
Vous pouvez télécharger tous les signalements qui vous sont accessibles.
</p>
<button
type="button"
class="ui fluid teal icon button"
@click="exportFeatures"
>
<i class="download icon"></i> Exporter
</button>
</div>
</div>
</div>
</div>
<div class="nine wide column">
<h3 class="ui header">Derniers signalements</h3>
<div
v-for="(feature, index) in lastFeatures"
:key="feature.feature_id + index"
class="ui small header"
>
<span v-if="feature.status === 'archived'" data-tooltip="Archivé">
<i class="grey archive icon"></i>
</span>
<span
v-else-if="feature.status === 'pending'"
data-tooltip="En attente de publication"
>
<i class="teal hourglass outline icon"></i>
</span>
<span v-else-if="feature.status === 'published'" data-tooltip="Publié">
<i class="olive check icon"></i>
</span>
<span v-else-if="feature.status === 'draft'" data-tooltip="Brouillon">
<i class="orange pencil alternate icon"></i>
</span>
<router-link
:to="{
name: 'details-signalement',
params: {
slug: project.slug,
slug_type_signal: feature.feature_type.slug,
slug_signal: feature.feature_id,
},
}"
>
{{ feature.title || feature.feature_id }}
</router-link>
<div class="sub header">
<div>
{{
feature.description
? feature.description.substring(0, 200)
: "Pas de description disponible"
}}
</div>
<div>
[ Créé le {{ feature.created_on | formatDate }}
<span v-if="$store.state.user">
par {{ feature.display_creator }}</span
>
]
</div>
</div>
</div>
<router-link
v-if="project"
:to="{ name: 'liste-signalements', params: { slug: project.slug } }"
class="ui right labeled icon button margin-25"
>
<i class="right arrow icon"></i>
Voir tous les signalements
</router-link>
<router-link
v-if="permissions.can_create_feature"
:to="{
name: 'ajouter-signalement',
params: { slug_type_signal: structure.slug },
}"
class="ui icon button button-hover-green margin-25"
>
Ajouter un signalement
</router-link>
<br />
</div>
</div>
</template>
<script>
import { mapGetters, mapState } from "vuex";
import ImportTask from "@/components/ImportTask";
export default {
name: "Feature_type_detail",
components: {
ImportTask: ImportTask,
},
data() {
return {
importError: "",
fileToImport: {
name: "Sélectionner un fichier GeoJSON ...",
size: 0,
},
showImport: false,
};
},
filters: {
formatDate(value) {
let date = new Date(value);
date = date.toLocaleString().replace(",", " à");
return date.substr(0, date.length - 3); //* quick & dirty way to remove seconds from date
},
},
computed: {
...mapGetters(["project", "permissions"]),
...mapState("feature", ["features"]),
...mapState("feature_type", ["feature_types", "importFeatureTypeData"]),
structure: function () {
if (this.feature_types) {
return this.feature_types.find(
(el) => el.slug === this.$route.params.feature_type_slug
);
}
return null;
},
feature_type_features: function () {
return this.features.filter(
(el) => el.feature_type.slug === this.$route.params.feature_type_slug
);
},
lastFeatures: function () {
return this.feature_type_features.slice(0, 5);
},
orderedCustomFields() {
return [...this.structure.customfield_set].sort(
(a, b) => a.position - b.position
);
},
},
methods: {
toggleShowImport() {
this.showImport = !this.showImport;
if (this.showImport) {
this.$store.dispatch("feature_type/GET_IMPORTS", this.structure.slug);
}
},
transformProperties(prop) {
const type = typeof prop;
const date = new Date(prop);
if (type === "boolean") {
return "boolean";
} else if (Number.isSafeInteger(prop)) {
return "integer";
} else if (
type === "string" &&
date instanceof Date &&
!isNaN(date.valueOf())
) {
return "date";
} else if (type === "number" && !isNaN(parseFloat(prop))) {
return "decimal";
}
return "char"; //* string by default, most accepted type in database
},
checkJsonValidity(json) {
this.importError = "";
const fields = this.structure.customfield_set.map((el) => {
return {
name: el.name,
field_type: el.field_type,
options: el.options,
};
});
console.log({ json, fields });
for (const feature of json.features) {
console.log(feature.properties);
for (const { name, field_type, options } of fields) {
console.log("name", name, "field_type", field_type);
//* check if custom field is present
if (!(name in feature.properties)) {
console.log("NOT present");
return false;
}
const fieldInFeature = feature.properties[name];
const customType = this.transformProperties(fieldInFeature);
//* if custom field value is not null, then check validity of field
if (fieldInFeature !== null) {
//* if field type is list, it's not possible to guess from value type
if (field_type === "list") {
//*then check if the value is an available option
if (!options.includes(fieldInFeature)) {
console.log("NOT an element of list options");
return false;
}
} else if (customType !== field_type) {
//* check if custom field value match
console.log("NOT matched");
this.importError = `Le fichier est invalide: Un champ de type ${field_type} ne peut pas avoir la valeur [ ${fieldInFeature} ]`;
return false;
}
}
}
}
return true;
},
onFileChange(e) {
const files = e.target.files || e.dataTransfer.files;
if (!files.length) return;
let reader = new FileReader();
reader.addEventListener("load", (e) => {
if (this.checkJsonValidity(JSON.parse(e.target.result))) {
this.fileToImport = files[0]; // todo : remove this value from state as it stored (first attempt didn't work)
this.$store.commit(
"feature_type/SET_FILE_TO_IMPORT",
this.fileToImport
);
}
});
reader.readAsText(files[0]);
},
importGeoJson() {
this.$store.dispatch("feature_type/SEND_FEATURES_FROM_GEOJSON", {
slug: this.$route.params.slug,
feature_type_slug: this.$route.params.feature_type_slug,
fileToImport: this.fileToImport,
});
},
exportFeatures() {
const url = `${this.$store.state.configuration.VUE_APP_DJANGO_API_BASE}projects/${this.$route.params.slug}/feature-type/${this.$route.params.feature_type_slug}/export/`;
console.log(url);
window.open(url);
},
},
created() {
if (!this.project) {
this.$store.dispatch("GET_PROJECT_INFO", this.$route.params.slug);
}
this.$store.commit(
"feature_type/SET_CURRENT_FEATURE_TYPE_SLUG",
this.$route.params.slug_type_signal
);
},
};
</script>
<style scoped>
.margin-25 {
margin: 0 0.25em 0.25em 0 !important;
}
</style>
\ No newline at end of file
<template>
<div v-frag>
<div v-frag v-if="permissions && permissions.can_view_project && project">
<div id="message" class="fullwidth">
<div v-if="tempMessage" class="ui positive message">
<!-- <i class="close icon"></i> -->
<!-- <div class="header">You are eligible for a reward</div> -->
<p><i class="check icon"></i> {{ 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"></i> 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>
<div class="ui basic teal label" data-tooltip="Membres">
<i class="user icon"></i>{{ project.nb_contributors }}
</div>
<div class="ui basic teal label" data-tooltip="Signalements">
<i class="map marker icon"></i>{{ project.nb_published_features }}
</div>
<div class="ui basic teal label" data-tooltip="Commentaires">
<i class="comment icon"></i
>{{ project.nb_published_features_comments }}
</div>
</div>
<div class="ten wide column">
<h1 class="ui header">
<div class="content">
{{ project.title }}
<div v-if="arraysOffline.length>0">{{arraysOffline.length}} modifications en attente
<button
:disabled="isOffline()"
@click="sendOfflineFeatures()"
class="ui fluid teal icon button"
>
<i class="upload icon"></i> Envoyer au serveur
</button>
</div>
<div class="ui icon right floated compact buttons">
<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="isModalOpen = true"
>
<i class="inverted grey envelope icon"></i>
</a>
<router-link
v-if="permissions && permissions.can_update_project && isOffline()!=true"
:to="{ name: 'project_edit', params: { slug: project.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"></i>
</router-link>
</div>
<div class="ui hidden divider"></div>
<div class="sub header">
{{ project.description }}
</div>
</div>
</h1>
</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
v-for="(type, index) in feature_types"
:key="type.title + '-' + index"
class="item"
>
<div class="middle aligned content">
<router-link
:to="{
name: 'details-type-signalement',
params: { feature_type_slug: type.slug },
}"
>
<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>
<!-- {% if project and feature_types and
permissions|lookup:'can_create_feature' %} -->
<!-- // ? should we get type.is_editable ? -->
<!-- v-if="
project &&
permissions.can_create_feature &&
type.is_editable
" -->
<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="left center"
data-variation="mini"
>
<i class="ui plus icon"></i>
</router-link>
<router-link
:to="{
name: 'dupliquer-type-signalement',
params: { slug_type_signal: type.slug },
}"
v-if="
project &&
permissions &&
permissions.can_create_feature_type && isOffline()!=true
"
class="
ui
compact
small
icon
right
floated
button button-hover-green
"
data-tooltip="Dupliquer un type de signalement"
data-position="left center"
data-variation="mini"
>
<i class="inverted grey copy alternate icon"></i>
</router-link>
<router-link
:to="{
name: 'editer-type-signalement',
params: { slug_type_signal: type.slug },
}"
v-if="
project &&
type.is_editable &&
permissions &&
permissions.can_create_feature_type && isOffline()!=true
"
class="
ui
compact
small
icon
right
floated
button button-hover-green
"
data-tooltip="Éditer le type de signalement"
data-position="left center"
data-variation="mini"
>
<i class="inverted grey pencil alternate icon"></i>
</router-link>
</div>
</div>
<div v-if="feature_types.length === 0">
<i> Le projet ne contient pas encore de type de signalements. </i>
</div>
</div>
<div class="nouveau-type-signalement">
<router-link
v-if="permissions && permissions.can_update_project && isOffline()!=true"
:to="{
name: 'ajouter-type-signalement',
params: { slug: project.slug },
}"
class="ui compact basic button button-hover-green"
>
<i class="ui plus icon"></i>Créer un nouveau type de signalement
</router-link>
</div>
<div class="nouveau-type-signalement">
<a
v-if="permissions && permissions.can_update_project && isOffline()!=true"
class="
ui
compact
basic
button button-hover-green
important-flex
align-center
text-left
"
>
<i class="ui plus icon"></i>
<label class="ui" for="json_file">
<span class="label"
>Créer un nouveau type de signalement à partir d'un
GeoJSON</span
>
</label>
<input
type="file"
accept="application/json, .json, .geojson"
style="display: none"
name="json_file"
id="json_file"
@change="onFileChange"
/>
</a>
<br />
<div id="button-import" v-if="fileToImport.size > 0">
<button
:disabled="fileToImport.size === 0"
@click="toNewFeatureType"
class="ui fluid teal icon button"
>
<i class="upload icon"></i> Lancer l'import avec le fichier
{{ fileToImport.name }}
</button>
</div>
</div>
</div>
<div class="seven wide column">
<div id="map"></div>
</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="ui relaxed list">
<div
v-for="(item, index) in last_features"
:key="item.title + index"
class="item"
>
<div class="content">
<div>
<router-link
:to="{
name: 'details-signalement',
params: {
slug: project.slug,
slug_type_signal: item.feature_type.slug,
slug_signal: item.feature_id,
},
}"
>{{ item.title || item.feature_id }}</router-link
>
</div>
<div class="description">
<i
>[{{ item.created_on | setDate
}}<span v-if="user && item.display_creator"
>, par {{ item.display_creator }}
</span>
]</i
>
</div>
</div>
</div>
<i v-if="last_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="ui relaxed list">
<div
v-for="(item, index) in last_comments"
:key="'comment ' + index"
class="item"
>
<div class="content">
<div>
<router-link :to="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"></i>
<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"></i>
<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"></i>
<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"></i>
<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"></i>
<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"></i>
<span
>Vous ne disposez pas des droits nécessaires pour consulter ce
projet.</span
>
</span>
<div
v-if="isModalOpen"
class="ui dimmer modals page transition visible active"
style="display: flex !important"
>
<div
:class="[
'ui mini modal subscription',
{ 'transition visible active': isModalOpen },
]"
>
<i @click="isModalOpen = false" class="close icon"></i>
<div class="ui icon header">
<i class="envelope icon"></i>
Notifications du projet
</div>
<div class="content">
<button
@click="subsribeProject"
:class="['ui compact fluid button', is_suscriber ? 'red' : 'green']"
>
{{
is_suscriber
? "Se désabonner de ce projet"
: "S'abonner à ce projet"
}}
</button>
</div>
</div>
</div>
</div>
</template>
<script>
import frag from "vue-frag";
import { mapUtil } from "@/assets/js/map-util.js";
import { mapGetters, mapState } from "vuex";
import projectAPI from "@/services/project-api";
const axios = require("axios");
export default {
name: "Project_details",
props: ["message"],
directives: {
frag,
},
filters: {
setDate: function (value) {
let date = new Date(value);
let d = date.toLocaleDateString("fr", {
year: "2-digit",
month: "numeric",
day: "numeric",
});
return d;
},
},
data() {
return {
infoMessage: "",
arraysOffline: [],
geojsonImport: [],
fileToImport: { name: "", size: 0 },
slug: this.$route.params.slug,
isModalOpen: false,
is_suscriber: false,
tempMessage: null,
};
},
computed: {
...mapGetters(["project", "permissions"]),
...mapState("feature_type", ["feature_types"]),
...mapState("feature", ["features"]),
...mapState(["last_comments", "user"]),
DJANGO_BASE_URL: function () {
return this.$store.state.configuration.VUE_APP_DJANGO_BASE;
},
last_features: function () {
// * limit to last five element of array (looks sorted chronologically, but not sure...)
return this.$store.state.feature.features.slice(-5);
},
},
methods: {
refreshId() {
return "?ver=" + Math.random();
},
isOffline(){
return navigator.onLine==false;
},
checkForOfflineFeature(){
let arraysOffline=[];
let localStorageArray=localStorage.getItem("geocontrib_offline");
if(localStorageArray){
arraysOffline=JSON.parse(localStorageArray);
this.arraysOffline=arraysOffline.filter(x=>x.project==this.project.slug);
}
},
sendOfflineFeatures(){
var promises = [];
this.arraysOffline.forEach((feature, index, object)=>{
console.log(feature);
if(feature.type=='post') {
promises.push(
axios
.post(`${this.$store.state.configuration.VUE_APP_DJANGO_API_BASE}features/`, feature.geojson)
.then((response) => {
console.log(response)
if (response.status === 201 && response.data) {
object.splice(index, 1);
}
})
.catch((error) => {
console.log(error);
}));
}
else if(feature.type=='put') {
promises.push(
axios
.put(`${this.$store.state.configuration.VUE_APP_DJANGO_API_BASE}features/${feature.featureId}`, feature.geojson)
.then((response) => {
console.log(response)
if (response.status === 200 && response.data) {
object.splice(index, 1);
}
})
.catch((error) => {
console.log(error);
}));
}
});
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.project.slug);
arraysOffline=arraysOfflineOtherProject.concat(this.arraysOffline);
localStorage.setItem("geocontrib_offline",JSON.stringify(arraysOffline));
},
toNewFeatureType() {
this.$router.push({
name: "ajouter-type-signalement",
params: {
geojson: this.geojsonImport,
fileToImport: this.fileToImport,
},
});
},
onFileChange(e) {
var files = e.target.files || e.dataTransfer.files;
if (!files.length) return;
this.fileToImport = files[0];
// TODO : VALIDATION IF FILE IS JSON
if (this.fileToImport.size > 0) {
const fr = new FileReader();
fr.onload = (e) => {
this.geojsonImport = JSON.parse(e.target.result);
};
fr.readAsText(this.fileToImport);
//* stock filename to import features afterward
this.$store.commit(
"feature_type/SET_FILE_TO_IMPORT",
this.fileToImport
);
}
},
subsribeProject() {
this.$store.state.configuration.VUE_APP_DJANGO_API_BASE;
projectAPI
.subscribeProject({
suscribe: !this.is_suscriber,
projectSlug: this.$route.params.slug,
})
.then((data) => {
this.is_suscriber = data.is_suscriber;
this.isModalOpen = 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);
});
},
initMap(){
if (this.project && this.permissions.can_view_project) {
this.$store.dispatch("map/INITIATE_MAP");
const url = `${this.$store.state.configuration.VUE_APP_DJANGO_API_BASE}projects/${this.$route.params.slug}/feature/?output=geojson`;
let self = this;
this.checkForOfflineFeature();
let project_id=this.$route.params.slug.split('-')[0];
const mvtUrl = `${this.$store.state.configuration.VUE_APP_DJANGO_API_BASE}/features.mvt/?tile={z}/{x}/{y}&project_id=${project_id}`;
mapUtil.addVectorTileLayer(mvtUrl,this.$route.params.slug,this.$store.state.feature_type.feature_types);
axios
.get(url)
.then((response) => {
let features = response.data.features;
self.arraysOffline.forEach(x=>x.geojson.properties.color="red");
features=response.data.features.concat(self.arraysOffline.map(x=>x.geojson));
const featureGroup = mapUtil.addFeatures(features,{},false,this.$store.state.feature_type.feature_types);
if (featureGroup && featureGroup.getLayers().length > 0) {
mapUtil
.getMap()
.fitBounds(featureGroup.getBounds(), { padding: [25, 25] });
this.$store.commit("map/SET_GEOJSON_FEATURES", features);
} else {
this.$store.commit("map/SET_GEOJSON_FEATURES", []);
}
})
.catch((error) => {
throw error;
});
}
},
},
created() {
if (this.user) {
projectAPI
.getProjectSubscription({ projectSlug: this.$route.params.slug })
.then((data) => (this.is_suscriber = data.is_suscriber));
}
},
mounted() {
let self=this;
this.$store.dispatch("GET_PROJECT_INFO", this.slug).then(setTimeout(self.initMap,1000));
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
}
},
};
</script>
<style>
@import "../../assets/resources/semantic-ui-2.4.2/semantic.min.css";
#map {
width: 100%;
height: 100%;
min-height: 250px;
}
.list-image-type {
margin-right: 5px;
height: 25px;
vertical-align: bottom;
}
/* // ! missing style in semantic.min.css, je ne comprends pas comment... */
.ui.right.floated.button {
float: right;
margin: 0 0 0 1em;
}
.nouveau-type-signalement {
padding-top: 1em;
}
#button-import {
padding-top: 0.5em;
}
.fullwidth {
width: 100%;
}
.important-flex {
display: flex !important;
}
.align-center {
align-items: center !important;
}
.text-left {
text-align: left !important;
}
</style>
\ No newline at end of file
<template>
<div class="fourteen wide column">
<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
type="text"
required
maxlength="128"
name="title"
id="title"
v-model="form.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
class="ui small image"
id="form-input-file-logo"
:src="
thumbnailFileSrc
? thumbnailFileSrc
: DJANGO_BASE_URL + form.thumbnail
"
/>
<label class="ui icon button" for="thumbnail">
<i class="file icon"></i>
<span class="label">{{
form.thumbnail_name ? form.thumbnail_name : fileToImport.name
}}</span>
</label>
<input
@change="onFileChange"
class="file-selection"
type="file"
accept="image/jpeg, image/png"
style="display: none"
name="thumbnail"
id="thumbnail"
/>
<ul
v-if="errorThumbnail.length"
id="errorlist-thumbnail"
class="errorlist"
>
<li>
{{ errorThumbnail[0] }}
</li>
</ul>
</div>
</div>
<div class="field">
<label for="description">Description</label>
<textarea
v-model="form.description"
name="description"
rows="5"
></textarea>
<!-- {{ form.description.errors }} -->
</div>
<div class="ui horizontal divider">PARAMÈTRES</div>
<div class="four fields">
<div class="field">
<label for="archive_feature">Délai avant archivage</label>
<div class="ui right labeled input">
<input
type="number"
min="0"
oninput="validity.valid||(value=0);"
style="padding: 1px 2px"
name="archive_feature"
id="archive_feature"
@blur="checkEmpty"
v-model="form.archive_feature"
/>
<div class="ui label">jour(s)</div>
</div>
<ul
v-if="errors_archive_feature.length"
id="errorlist-achivage"
class="errorlist"
>
<li>
{{ errors_archive_feature[0] }}
</li>
</ul>
</div>
<div class="field">
<label for="delete_feature">Délai avant suppression</label>
<div class="ui right labeled input">
<input
type="number"
min="0"
oninput="validity.valid||(value=0);"
style="padding: 1px 2px"
name="delete_feature"
id="delete_feature"
@blur="checkEmpty"
v-model="form.delete_feature"
/>
<div class="ui label">jour(s)</div>
</div>
</div>
<div class="required field">
<label for="access_level_pub_feature"
>Visibilité des signalements publiés</label
>
<Dropdown
:options="levelPermissions"
: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 class="required field">
<label for="access_level_arch_feature">
Visibilité des signalements archivés
</label>
<Dropdown
:options="levelPermissions"
: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="field">
<div class="ui checkbox">
<input
type="checkbox"
v-model="form.moderation"
name="moderation"
id="moderation"
/>
<label for="moderation">Modération</label>
</div>
<!-- {{ form.moderation.errors }} -->
</div>
<div class="field">
<div class="ui checkbox">
<input
type="checkbox"
v-model="form.is_project_type"
name="is_project_type"
id="is_project_type"
/>
<label for="is_project_type">Est un projet type</label>
</div>
<!-- {{ form.is_project_type.errors }} -->
</div>
<div class="ui divider"></div>
<button @click="postForm" type="button" class="ui teal icon button">
<i class="white save icon"></i> Enregistrer les changements
</button>
</form>
</div>
</template>
<script>
const axios = require("axios");
import Dropdown from "@/components/Dropdown.vue";
import { mapGetters } from "vuex";
axios.defaults.headers.common["X-CSRFToken"] = ((name) => {
var re = new RegExp(name + "=([^;]+)");
var value = re.exec(document.cookie);
return value !== null ? unescape(value[1]) : null;
})("csrftoken");
export default {
name: "Project_edit",
components: {
Dropdown,
},
data() {
return {
loading: false,
action: "create",
levelPermissions: [
{ name: "Utilisateur anonyme", value: "anonymous" },
{ name: "Utilisateur connecté", value: "logged_user" },
{ name: "Contributeur", value: "contributor" },
],
fileToImport: {
name: "Sélectionner une image ...",
size: 0,
},
errors_archive_feature: [],
errors: {
title: [],
access_level_pub_feature: [],
access_level_arch_feature: [],
},
errorThumbnail: [],
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,
nb_features: 0,
nb_published_features: 0,
nb_comments: 0,
nb_published_features_comments: 0,
nb_contributors: 0,
is_project_type: false,
},
thumbnailFileSrc: "",
};
},
computed: {
...mapGetters(["project"]),
DJANGO_BASE_URL: function () {
return this.$store.state.configuration.VUE_APP_DJANGO_BASE;
},
},
methods: {
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";
}
},
truncate(n, len) {
let 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) {
let url = window.URL || window.webkitURL;
let image = new Image();
image.onload = function () {
handleFile(true);
};
image.onerror = function () {
handleFile(false);
};
image.src = url.createObjectURL(files);
URL.revokeObjectURL(image.src);
},
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
let 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_PERMISSIONS"), //* refresh projects permissions
this.$store.dispatch("GET_ALL_PROJECTS"), //* & refresh project list
]).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) {
let 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,
is_project_type: this.form.is_project_type,
moderation: this.form.moderation,
};
if (this.action === "create" || this.action === "create_from") {
this.loading = true;
await axios
.post(
`${this.$store.state.configuration.VUE_APP_DJANGO_API_BASE}projects/`,
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;
});
} else if (this.action === "edit") {
await axios
.put(
`${this.$store.state.configuration.VUE_APP_DJANGO_API_BASE}projects/${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;
});
}
},
},
created() {
this.definePageType();
console.log(this.action);
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("GET_PROJECT_INFO", this.$route.params.slug);
}
this.form = { ...this.project }; //* create a new object to avoid modifying original one
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 for dropdowns display (could be in a computed)
this.form.access_level_pub_feature = {
name: this.project.access_level_pub_feature,
value: this.levelPermissions.find(
(el) => (el.name === this.project.access_level_pub_feature)
).value,
};
this.form.access_level_arch_feature = {
name: this.project.access_level_arch_feature,
value: this.levelPermissions.find(
(el) => (el.name === this.project.access_level_arch_feature)
).value,
};
}
},
};
</script>
<style media="screen">
#form-input-file-logo {
margin-left: auto;
margin-right: auto;
}
.close.icon:hover {
cursor: pointer;
}
</style>
\ No newline at end of file
<template>
<div class="row">
<div class="seven wide column">
<h3 class="ui header">
Créer un projet à partir d'un modèle disponible:
</h3>
<div class="ui divided items">
<div v-for="project in project_types" :key="project.slug" class="item">
<div class="ui tiny image">
<img
:src="
project.thumbnail.includes('default')
? require('@/assets/img/default.png')
: DJANGO_BASE_URL + project.thumbnail + refreshId()
"
/>
</div>
<div class="middle aligned content">
<div class="description">
<router-link
:to="{
name: 'project_create_from',
params: {
slug: project.slug,
},
}"
>{{ project.title }}</router-link
>
<p>{{ project.description }}</p>
<strong v-if="project.moderation">Projet modéré</strong>
<strong v-else>Projet non modéré</strong>
</div>
<div class="meta">
<span data-tooltip="Délai avant archivage">
{{ project.archive_feature }}&nbsp;<i class="box icon"></i>
</span>
<span data-tooltip="Délai avant suppression">
{{ project.archive_feature }}&nbsp;<i
class="trash alternate icon"
></i>
</span>
<span data-tooltip="Date de création">
{{ project.created_on }}&nbsp;<i class="calendar icon"></i>
</span>
</div>
<div class="meta">
<span data-tooltip="Visibilité des signalement publiés">
{{ project.access_level_pub_feature }}&nbsp;<i
class="eye icon"
></i>
</span>
<span data-tooltip="Visibilité des signalement archivés">
{{ project.access_level_arch_feature }}&nbsp;<i
class="archive icon"
></i>
</span>
</div>
</div>
</div>
<span v-if="!project_types || project_types.length === 0"
>Aucun projet type n'est défini.</span
>
</div>
</div>
</div>
</template>
<script>
import { mapGetters } from "vuex";
export default {
name: "Project_type_list",
computed: {
...mapGetters(["project_types"]),
DJANGO_BASE_URL: function () {
return this.$store.state.configuration.VUE_APP_DJANGO_BASE;
},
},
methods: {
refreshId() {
return "?ver=" + Math.random();
},
},
};
</script>
\ No newline at end of file
<template>
<div>
<div class="row">
<div class="fourteen wide column">
<img
class="ui centered small image"
src="@/assets/img/logo-neogeo-circle.png"
/>
<h2 class="ui center aligned icon header">
<div class="content">
{{ APPLICATION_NAME }}
<div class="sub header">{{ APPLICATION_ABSTRACT }}</div>
</div>
</h2>
</div>
</div>
<div class="row">
<div class="six wide column">
<h3 class="ui horizontal divider header">CONNEXION</h3>
<div v-if="form.errors" class="ui warning message">
<div class="header">
Les informations d'identification sont incorrectes.
</div>
NB: Seuls les comptes actifs peuvent se connecter.
</div>
<form class="ui form" role="form" type="post" @submit.prevent="login">
<div class="ui stacked secondary segment">
<div class="six field required">
<div class="ui left icon input">
<i class="user icon"></i>
<input
v-model="username_value"
type="text"
name="username"
placeholder="Utilisateur"
/>
</div>
</div>
<div class="six field required">
<div class="ui left icon input">
<i class="lock icon"></i>
<input
v-model="password_value"
type="password"
name="password"
placeholder="Mot de passe"
/>
</div>
</div>
<button class="ui fluid large teal submit button" type="submit">
Login
</button>
</div>
</form>
</div>
</div>
</div>
</template>
<script>
export default {
name: "Login",
data() {
return {
username_value: null,
password_value: null,
logged: false,
form: {
errors: null,
},
};
},
computed: {
LOGO_PATH: function () {
return this.$store.state.configuration.VUE_APP_LOGO_PATH;
},
APPLICATION_NAME: function () {
return this.$store.state.configuration.VUE_APP_APPLICATION_NAME;
},
APPLICATION_ABSTRACT: function () {
return this.$store.state.configuration.VUE_APP_APPLICATION_ABSTRACT;
},
},
methods: {
login() {
this.$store
.dispatch("LOGIN", {
username: this.username_value,
password: this.password_value,
})
.then((status) => {
if (status === 200) {
this.form.errors = null;
} else if (status === "error") {
this.form.errors = status;
}
})
.catch();
},
},
};
</script>
\ No newline at end of file
const webpack = require('webpack')
const fs = require('fs')
const packageJson = fs.readFileSync('./package.json')
const version = JSON.parse(packageJson).version || 0
const webpack = require('webpack');
const fs = require('fs');
const packageJson = fs.readFileSync('./package.json');
const version = JSON.parse(packageJson).version || 0;
module.exports = {
publicPath: '/geocontrib/',
devServer: {
proxy: {
'^/api': {
target: 'https://geocontrib.dev.neogeo.fr/api',
ws: true,
changeOrigin: true
}
}
publicPath: '/geocontrib/',
devServer: {
proxy: {
'^/api': {
target: 'https://geocontrib.dev.neogeo.fr/api',
ws: true,
changeOrigin: true
}
}
},
pwa: {
workboxPluginMode: 'InjectManifest',
workboxOptions: {
swSrc: 'src/service-worker.js',
exclude: [
/\.map$/,
/config\/config.*\.json$/,
/manifest\.json$/
],
},
pwa: {
workboxPluginMode: 'InjectManifest',
workboxOptions: {
swSrc: 'src/service-worker.js',
exclude: [
/\.map$/,
/config\/config.*\.json$/,
/manifest\.json$/
],
},
themeColor: '#1da025'
},
configureWebpack: {
plugins: [
new webpack.DefinePlugin({
'process.env': {
PACKAGE_VERSION: '"' + version + '"'
}
})
]
iconPaths: {
faviconSVG: null,
favicon32: null,
favicon16: null,
appleTouchIcon: null,
maskIcon: null,
msTileImage: null,
},
// the rest of your original module.exports code goes here
}
themeColor: '#1da025'
},
configureWebpack: {
devtool: 'source-map',
plugins: [
new webpack.DefinePlugin({
'process.env': {
PACKAGE_VERSION: '"' + version + '"'
}
})
]
},
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