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="project-members"
class="page"
>
<div id="project-members">
<h1 class="ui header">
Gérer les membres
</h1>
......@@ -49,7 +46,10 @@
:disabled="!newMember.user.name"
@click="addMember"
>
<i class="white add icon" />
<i
class="white add icon"
aria-hidden="true"
/>
<span class="padding-1">Ajouter</span>
</button>
</div>
......@@ -59,10 +59,13 @@
id="form-members"
class="ui form"
>
<table class="ui red table">
<table
class="ui red table"
aria-describedby="Table des membres du projet"
>
<thead>
<tr>
<th>
<th scope="col">
Membre
<i
:class="{
......@@ -70,10 +73,11 @@
up: isSortedDesc('member'),
}"
class="icon sort"
aria-hidden="true"
@click="changeSort('member')"
/>
</th>
<th>
<th scope="col">
Niveau d'autorisation
<i
:class="{
......@@ -81,6 +85,7 @@
up: isSortedDesc('role'),
}"
class="icon sort"
aria-hidden="true"
@click="changeSort('role')"
/>
</th>
......@@ -92,7 +97,7 @@
:key="member.username"
>
<td>
{{ member.user.last_name }} {{ member.user.first_name }}<br><i>{{ member.user.username }}</i>
{{ member.user.last_name }} {{ member.user.first_name }}<br><em>{{ member.user.username }}</em>
</td>
<td>
<div class="required field online">
......@@ -108,7 +113,10 @@
data-tooltip="Retirer ce membre"
@click="removeMember(member)"
>
<i class="times icon" />
<i
class="times icon"
aria-hidden="true"
/>
</button>
</div>
</td>
......@@ -123,7 +131,10 @@
class="ui teal icon button"
@click="saveMembers"
>
<i class="white save icon" />&nbsp;Enregistrer les changements
<i
class="white save icon"
aria-hidden="true"
/>&nbsp;Enregistrer les changements
</button>
</div>
</div>
......@@ -132,9 +143,10 @@
<script>
import axios from '@/axios-client.js';
import { mapState } from 'vuex';
import { mapMutations, mapState } from 'vuex';
import Dropdown from '@/components/Dropdown.vue';
import { formatUserOption } from '@/utils';
export default {
name: 'ProjectMembers',
......@@ -177,16 +189,7 @@ export default {
userOptions: function () {
return this.projectUsers
.filter((el) => el.userLevel.value === 'logged_user')
.map((el) => {
let name = el.user.first_name || '';
if (el.user.last_name) {
name = name + ' ' + el.user.last_name;
}
return {
name: [name, el.user.username],
value: el.user.id,
};
});
.map((el) => formatUserOption(el.user)); // Format user data to fit dropdown option structure
},
levelOptions: function () {
......@@ -236,10 +239,16 @@ export default {
destroyed() {
//* allow user to change page if ever stuck on loader
this.$store.commit('DISCARD_LOADER');
this.DISCARD_LOADER();
},
methods: {
...mapMutations([
'DISPLAY_MESSAGE',
'DISPLAY_LOADER',
'DISCARD_LOADER'
]),
validateNewMember() {
this.newMember.errors = [];
if (!this.newMember.user.value) {
......@@ -289,60 +298,69 @@ export default {
});
},
/**
* Saves the updated members and their roles for a project.
* Displays a loader while the update is in progress and provides feedback upon completion or error.
*/
saveMembers() {
// Display a loader to indicate that the update process is ongoing
this.DISPLAY_LOADER('Mise à jour des membres du projet en cours ...');
// Prepare the data to be sent in the API request
const data = this.projectUsers.map((member) => {
return {
user: member.user,
level: {
display: member.userLevel.name,
codename: member.userLevel.value,
display: member.userLevel.name, // Display name of the user level
codename: member.userLevel.value, // Codename of the user level
},
};
});
// Make an API request to update the project members
axios
.put(
`${this.$store.state.configuration.VUE_APP_DJANGO_API_BASE}projects/${this.project.slug}/utilisateurs/`,
data
)
.then((response) => {
// Check if the response status is 200 (OK)
if (response.status === 200) {
this.$store.dispatch('GET_USER_LEVEL_PROJECTS'); //* update user status in top right menu
this.$store.commit('DISPLAY_MESSAGE', { comment: 'Permissions mises à jour', level: 'positive' });
// Dispatch an action to update the user status in the top right menu
this.$store.dispatch('GET_USER_LEVEL_PROJECTS');
// Display a positive message indicating success
this.DISPLAY_MESSAGE({ comment: 'Permissions mises à jour avec succès', level: 'positive' });
} else {
this.$store.commit(
'DISPLAY_MESSAGE',
{
comment : "Une erreur s'est produite pendant la mises à jour des permissions",
level: 'negative'
}
);
// Display a generic error message if the response status is not 200
this.DISPLAY_MESSAGE({
comment: "Une erreur s'est produite pendant la mises à jour des permissions",
level: 'negative'
});
}
// Hide the loader regardless of the request result
this.DISCARD_LOADER();
})
.catch((error) => {
throw error;
});
},
fetchMembers() {
// todo: move function to a service
return axios
.get(
`${this.$store.state.configuration.VUE_APP_DJANGO_API_BASE}projects/${this.$route.params.slug}/utilisateurs/`
)
.then((response) => response.data)
.catch((error) => {
throw error;
// Hide the loader if an error occurs
this.DISCARD_LOADER();
// Determine the error message to display
const errorMessage = error.response && error.response.data && error.response.data.error
? error.response.data.error
: "Une erreur s'est produite pendant la mises à jour des permissions";
// Display the error message
this.DISPLAY_MESSAGE({
comment: errorMessage,
level: 'negative'
});
// Log the error to the console for debugging
console.error(error);
});
},
populateMembers() {
this.$store.commit(
'DISPLAY_LOADER',
'Récupération des membres en cours...'
);
this.fetchMembers().then((members) => {
this.$store.commit('DISCARD_LOADER');
this.DISPLAY_LOADER('Récupération des membres en cours...');
this.$store.dispatch('projects/GET_PROJECT_USERS', this.$route.params.slug).then((members) => {
this.DISCARD_LOADER();
this.projectUsers = members.map((el) => {
return {
userLevel: { name: el.level.display, value: el.level.codename },
......
<template>
<div
id="projects"
class="page"
>
<div id="projects">
<h2 class="ui horizontal divider header">
PROJETS
</h2>
......@@ -12,8 +9,13 @@
v-if="user && user.can_create_project && isOnline"
:to="{ name: 'project_create', params: { action: 'create' } }"
class="ui green basic button"
data-test="create-project"
>
<i class="plus icon" /> Créer un nouveau projet
<i
class="plus icon"
aria-hidden="true"
/>
Créer un nouveau projet
</router-link>
<router-link
v-if="user && user.can_create_project && isOnline"
......@@ -21,25 +23,33 @@
name: 'project_type_list',
}"
class="ui blue basic button"
data-test="to-project-models"
>
<i class="copy icon" /> Accéder à la liste des modèles de projets
<i
class="copy icon"
aria-hidden="true"
/>
Accéder à la liste des modèles de projets
</router-link>
</div>
<!-- FILTRES DES PROJETS -->
<ProjectsMenu
:loading="loading"
@filter="setProjectsFilters"
@getData="getData"
@loading="setLoader"
/>
<div
v-if="configuration.DISPLAY_FORBIDDEN_PROJECTS"
id="forbidden-projects"
class="ui toggle checkbox"
class="ui toggle checkbox margin-top"
>
<input
v-model="displayForbiddenProjects"
:checked="displayForbiddenProjects"
type="checkbox"
@input="toggleForbiddenProjects"
>
<label>
N'afficher que les projets disponibles à la consultation
......@@ -50,14 +60,12 @@
<div
v-if="projects"
class="ui divided items dimmable dimmed"
data-test="project-list"
>
<div
:class="{ active: loading }"
class="ui inverted dimmer"
>
<div :class="['ui inverted dimmer', { active: loading }]">
<div class="ui loader" />
</div>
<ProjectsListItem
v-for="project in projects"
:key="project.slug"
......@@ -66,21 +74,15 @@
<span
v-if="!projects || projects.length === 0"
>Vous n'avez accès à aucun projet.</span>
<div
:class="{ active: loading }"
class="ui inverted dimmer"
>
<div class="ui loader" />
</div>
Vous n'avez accès à aucun projet.
</span>
<!-- PAGINATION -->
<Pagination
v-if="count"
:nb-pages="Math.ceil(count/10)"
:on-page-change="SET_CURRENT_PAGE"
@change-page="changePage"
:nb-pages="nbPages"
@page-update="changePage"
/>
</div>
</div>
......@@ -123,49 +125,30 @@ export default {
DJANGO_BASE_URL() {
return this.$store.state.configuration.VUE_APP_DJANGO_BASE;
},
},
watch: {
filters: {
deep: true,
handler(newValue) {
if (newValue) {
this.getData();
}
}
},
displayForbiddenProjects(newValue) {
if (newValue) {
this.SET_PROJECTS_FILTER({
filter: 'accessible',
value: 'true'
});
} else {
this.SET_PROJECTS_FILTER({
filter: 'accessible',
value: null
});
}
this.getData();
nbPages() {
return Math.ceil(this.count / 10);
}
},
created() {
this.SET_CURRENT_PAGE(1);
// Empty stored text to search
this.SET_PROJECTS_SEARCH_STATE({ text: null });
// Empty stored project list
this.$store.commit('projects/SET_PROJECT', null);
this.SET_PROJECTS_FILTER({
filter: 'accessible',
value: 'true'
});
// Init display of restricted access projects
this.displayForbiddenProjects = this.configuration.DISPLAY_FORBIDDEN_PROJECTS_DEFAULT;
this.setForbiddenProjectsFilter(true);
},
methods: {
...mapMutations('projects', [
'SET_CURRENT_PAGE',
'SET_PROJECTS_FILTER'
'SET_PROJECTS_FILTER',
'SET_PROJECTS_SEARCH_STATE',
]),
...mapActions('projects', [
'GET_PROJECTS'
'GET_PROJECTS',
]),
getData(page) {
......@@ -187,8 +170,26 @@ export default {
this.getData(e);
},
setProjectsFilters(e) {
setProjectsFilters(e, noUpdate) {
this.SET_PROJECTS_FILTER(e);
// Reset the page number at filter change
this.SET_CURRENT_PAGE(1);
// Wait that all filters are set in store to fetch data when component is created
if (!noUpdate) {
this.getData();
}
},
toggleForbiddenProjects(e) {
this.displayForbiddenProjects = e.target.checked;
this.setForbiddenProjectsFilter();
},
setForbiddenProjectsFilter(noUpdate) {
this.setProjectsFilters({
filter: 'accessible',
value: this.displayForbiddenProjects ? 'true' : null
}, noUpdate);
},
}
};
......@@ -198,6 +199,16 @@ export default {
#projects {
margin: 0 auto;
.dimmable {
.dimmer {
.loader {
top: 25%;
}
}
}
}
.flex {
......@@ -217,10 +228,10 @@ export default {
color: rgb(94, 94, 94);
}
input:checked ~ label::before {
background-color: teal !important;
background-color: var(--primary-color, #008c86) !important;
}
input:checked ~ label {
color: teal !important;
color: var(--primary-color, #008c86) !important;
}
}
......
<template>
<div
id="projects-types"
class="page"
>
<div id="projects-types">
<h3 class="ui header">
Créer un projet à partir d'un modèle disponible:
</h3>
......@@ -19,6 +16,7 @@
? require('@/assets/img/default.png')
: DJANGO_BASE_URL + project.thumbnail + refreshId()
"
alt="Image associé au projet"
>
</div>
<div class="middle aligned content">
......@@ -37,27 +35,25 @@
<strong>Projet {{ project.moderation ? '' : 'non' }} modéré</strong>
</div>
<div class="meta">
<span data-tooltip="Délai avant archivage">
{{ project.archive_feature }}&nbsp;<i class="box icon" />
</span>
<span data-tooltip="Délai avant suppression">
{{ project.archive_feature }}&nbsp;<i
class="trash alternate icon"
/>
</span>
<span data-tooltip="Date de création">
{{ project.created_on }}&nbsp;<i class="calendar icon" />
{{ 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>
......@@ -94,13 +90,17 @@ export default {
mounted() {
projectAPI.getProjectTypes(this.API_BASE_URL)
.then((data) => {
if (data) this.project_types = data;
if (data) {
this.project_types = data;
}
});
},
methods: {
refreshId() {
return '?ver=' + Math.random();
const crypto = window.crypto || window.msCrypto;
var array = new Uint32Array(1);
return '?ver=' + crypto.getRandomValues(array); // Compliant for security-sensitive use cases
},
},
};
......
<template>
<div v-frag>
<div class="row">
<div class="fourteen wide column">
<h1 class="ui header">
<div
v-if="feature"
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" />
</router-link>
<router-link
v-if="
(permissions && permissions.can_update_feature) ||
isFeatureCreator ||
isModerator
"
: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" />
</router-link>
<a
v-if="((permissions && permissions.can_update_feature) || isFeatureCreator) && isOnline"
id="feature-delete"
class="ui button button-hover-red"
@click="isCanceling = true"
>
<i class="inverted grey trash alternate icon" />
</a>
</div>
<div class="ui hidden divider" />
<div class="sub header prewrap">
{{ feature.description }}
</div>
</div>
</h1>
</div>
</div>
<div v-if="!feature && !loader.isLoading">
Pas de signalement correspondant trouvé
</div>
<div class="row">
<div class="seven wide column">
<table
v-if="feature"
class="ui very basic table"
>
<tbody>
<tr v-if="feature_type">
<td>
<b> Type de signalement </b>
</td>
<td>
<router-link
:to="{
name: 'details-type-signalement',
params: { feature_type_slug: feature_type.slug },
}"
class="feature-type-title"
>
<img
v-if="feature_type.geom_type === 'point'"
class="list-image-type"
src="@/assets/img/marker.png"
>
<img
v-if="feature_type.geom_type === 'linestring'"
class="list-image-type"
src="@/assets/img/line.png"
>
<img
v-if="feature_type.geom_type === 'polygon'"
class="list-image-type"
src="@/assets/img/polygon.png"
>
{{ feature_type.title }}
</router-link>
</td>
</tr>
<div
v-for="(field, index) in feature.feature_data"
:key="'field' + index"
v-frag
>
<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',
]"
/>
<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]"
/>
{{ 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>
</tbody>
</table>
<h3 v-if="feature">
Liaison entre signalements
</h3>
<table
v-if="feature"
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
class="pointer"
@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"
ref="map"
/>
</div>
</div>
<div
v-if="feature"
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-for="(event, index) in events"
:key="'event' + index"
v-frag
>
<div
v-if="event.event_type === 'create'"
v-frag
>
<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-if="event.related_comment.attachment"
v-frag
>
<br><a
:href="
DJANGO_BASE_URL +
event.related_comment.attachment.url
"
target="_blank"
><i class="paperclip fitted icon" />
{{ 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 && isOnline"
class="ui segment"
>
<form
id="form-comment"
class="ui form"
>
<div class="required field">
<label
:for="comment_form.comment.id_for_label"
>Ajouter un commentaire</label>
<ul
v-if="comment_form.comment.errors"
class="errorlist"
>
<li>
{{ comment_form.comment.errors }}
</li>
</ul>
<textarea
v-model="comment_form.comment.value"
:name="comment_form.comment.html_name"
rows="2"
/>
</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" />
<span class="label">{{
comment_form.attachment_file.value
? comment_form.attachment_file.value
: "Sélectionner un fichier ..."
}}</span>
</label>
<input
id="attachment_file"
type="file"
accept="application/pdf, image/jpeg, image/png"
style="display: none"
name="attachment_file"
@change="onFileChange"
>
</div>
<div class="field">
<input
id="title"
v-model="comment_form.attachment_file.title"
type="text"
name="title"
>
{{ comment_form.attachment_file.errors }}
</div>
</div>
<ul
v-if="comment_form.attachment_file.errors"
class="errorlist"
>
<li>
{{ comment_form.attachment_file.errors }}
</li>
</ul>
<button
type="button"
class="ui compact green icon button"
@click="postComment"
>
<i class="plus icon" /> 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
class="close icon"
@click="isCanceling = false"
/>
<div class="ui icon header">
<i class="trash alternate icon" />
Supprimer le signalement
</div>
<div class="actions">
<button
type="button"
class="ui red compact fluid button"
@click="deleteFeature"
>
Confirmer la suppression
</button>
</div>
</div>
</div>
</div>
</template>
<script>
import frag from 'vue-frag';
import { mapGetters, mapState, mapActions, mapMutations } from 'vuex';
import { formatStringDate } from '@/utils';
import { mapUtil } from '@/assets/js/map-util.js';
import featureAPI from '@/services/feature-api';
import axios from '@/axios-client.js';
export default {
name: 'FeatureDetail',
directives: {
frag,
},
filters: {
formatDate(value) {
return formatStringDate(value);
},
},
data() {
return {
attachments: [],
comment_form: {
attachment_file: {
errors: null,
title: '',
file: null,
},
comment: {
id_for_label: 'add-comment',
html_name: 'add-comment',
errors: '',
value: null,
},
},
events: [],
isCanceling: false,
projectSlug: this.$route.params.slug,
};
},
computed: {
...mapState([
'user',
'USER_LEVEL_PROJECTS',
'isOnline',
'loader',
]),
...mapState('projects', [
'project'
]),
...mapGetters([
'permissions',
]),
...mapGetters('feature_type', [
'feature_type',
]),
...mapState('feature', {
linked_features: 'linked_features',
statusChoices: 'statusChoices',
feature: 'current_feature',
}),
DJANGO_BASE_URL() {
return this.$store.state.configuration.VUE_APP_DJANGO_BASE;
},
isFeatureCreator() {
if (this.feature && this.user) {
return this.feature.creator === this.user.id;
}
return false;
},
isModerator() {
return this.USER_LEVEL_PROJECTS && this.project &&
this.USER_LEVEL_PROJECTS[this.projectSlug] === 'Modérateur'
? true
: 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 : '';
},
},
created() {
this.$store.commit(
'feature_type/SET_CURRENT_FEATURE_TYPE_SLUG',
this.$route.params.slug_type_signal
);
this.getFeatureEvents();
this.getFeatureAttachments();
this.getLinkedFeatures();
},
mounted() {
this.DISPLAY_LOADER('Recherche du signalement');
if (!this.project) {
// Chargements des features et infos projet en cas d'arrivée directe sur la page ou de refresh
axios.all([
this.GET_PROJECT(this.$route.params.slug),
this.GET_PROJECT_INFO(this.$route.params.slug),
this.GET_PROJECT_FEATURE({
project_slug: this.$route.params.slug,
feature_id: this.$route.params.slug_signal
})])
.then(() => {
this.DISCARD_LOADER();
this.initMap();
})
.catch(() => this.DISCARD_LOADER());
} else if (!this.feature || this.feature.feature_id !== this.$route.params.slug_signal) {
this.GET_PROJECT_FEATURE({
project_slug: this.$route.params.slug,
feature_id: this.$route.params.slug_signal
})
.then(() => {
this.DISCARD_LOADER();
this.initMap();
})
.catch(() => this.DISCARD_LOADER());
} else {
this.DISCARD_LOADER();
this.initMap();
}
},
beforeDestroy() {
this.$store.commit('CLEAR_MESSAGES');
},
methods: {
...mapMutations([
'DISCARD_LOADER',
'DISPLAY_LOADER',
]),
...mapActions('projects', [
'GET_PROJECT',
'GET_PROJECT_INFO'
]),
...mapActions('feature', [
'GET_PROJECT_FEATURES',
'GET_PROJECT_FEATURE'
]),
pushNgo(link) {
this.DISPLAY_LOADER('Recherche du signalement');
this.$router.push({
name: 'details-signalement',
params: {
slug_type_signal: link.feature_to.feature_type_slug,
slug_signal: link.feature_to.feature_id,
},
});
this.GET_PROJECT_FEATURE({
project_slug: this.$route.params.slug,
feature_id: this.$route.params.slug_signal,
})
.then(()=> {
this.addFeatureToMap();
this.DISCARD_LOADER();
})
.catch(() => this.DISCARD_LOADER());
this.getFeatureEvents();
this.getFeatureAttachments();
this.getLinkedFeatures();
},
validateForm() {
this.comment_form.comment.errors = '';
if (!this.comment_form.comment.value) {
this.comment_form.comment.errors = 'Le commentaire ne peut pas être vide';
return false;
}
return true;
},
postComment() {
if (this.validateForm()) {
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.attachment_file.fileName,
title: this.comment_form.attachment_file.title,
commentId: response.data.id,
})
.then(() => {
this.confirmComment();
});
} else {
this.confirmComment();
}
});
}
},
confirmComment() {
this.$store.commit('DISPLAY_MESSAGE', { comment: 'Ajout du commentaire confirmé', level: 'positive' });
this.getFeatureEvents(); //* display new comment on the page
this.comment_form.attachment_file.file = null;
this.comment_form.attachment_file.fileName = '';
this.comment_form.attachment_file.title = '';
this.comment_form.comment.value = null;
},
validateImgFile(files, handleFile) {
let url = window.URL || window.webkitURL;
let image = new Image();
image.onload = function () {
handleFile(true);
URL.revokeObjectURL(image.src);
};
image.onerror = function () {
handleFile(false);
URL.revokeObjectURL(image.src);
};
image.src = url.createObjectURL(files);
},
onFileChange(e) {
// * read image file
const files = e.target.files || e.dataTransfer.files;
const handleFile = (isValid) => {
if (isValid) {
this.comment_form.attachment_file.file = files[0]; //* store the file to post afterwards
let title = files[0].name;
this.comment_form.attachment_file.fileName = title; //* name of the file
const fileExtension = title.substring(title.lastIndexOf('.') + 1);
if ((title.length - fileExtension.length) > 11) {
title = title.slice(0, 10) + '[...].' + fileExtension;
}
this.comment_form.attachment_file.title = title; //* title for display
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.$route.params.slug,
message,
},
});
},
deleteFeature() {
this.$store
.dispatch('feature/DELETE_FEATURE', { feature_id: this.feature.feature_id })
.then((response) => {
if (response.status === 204) {
this.GET_PROJECT_FEATURES({
project_slug: 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(this.$refs.map, {
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,
this.$store.state.configuration.DEFAULT_BASE_MAP_SCHEMA_TYPE,
);
mapUtil.getMap().dragging.disable();
mapUtil.getMap().doubleClickZoom.disable();
mapUtil.getMap().scrollWheelZoom.disable();
this.addFeatureToMap();
},
addFeatureToMap() {
const url = `${this.$store.state.configuration.VUE_APP_DJANGO_API_BASE}projects/${this.$route.params.slug}/feature/` +
`?feature_id=${this.$route.params.slug_signal}&output=geojson`;
axios
.get(url)
.then((response) => {
if (response.data.features.length > 0) {
const featureGroup = mapUtil.addFeatures(
response.data.features,
{},
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))
.catch((err)=> console.error(err));
},
getFeatureAttachments() {
featureAPI
.getFeatureAttachments(this.$route.params.slug_signal)
.then((data) => (this.attachments = data))
.catch((err)=> console.error(err));
},
getLinkedFeatures() {
featureAPI
.getFeatureLinks(this.$route.params.slug_signal)
.then((data) =>
this.$store.commit('feature/SET_LINKED_FEATURES', data)
)
.catch((err)=> console.error(err));
},
},
};
</script>
<style scoped>
#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;
}
.prewrap {
white-space: pre-wrap;
}
.feature-type-title {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
line-height: 1.5em;
}
.list-image-type {
margin-right: 5px;
height: 25px;
vertical-align: bottom;
}
</style>
\ No newline at end of file
<template>
<div v-frag>
<div
v-if="permissions && permissions.can_view_project && project"
v-frag
>
<div
id="message"
class="fullwidth"
>
<div
v-if="tempMessage"
class="ui positive message"
>
<p><i class="check icon" /> {{ tempMessage }}</p>
</div>
</div>
<div
id="message_info"
class="fullwidth"
>
<div
v-if="infoMessage"
class="ui info message"
style="text-align: left"
>
<div class="header">
<i class="info circle icon" /> Informations
</div>
<ul class="list">
{{
infoMessage
}}
</ul>
</div>
</div>
<div class="row">
<div class="four wide middle aligned column">
<img
class="ui small spaced image"
:src="
project.thumbnail.includes('default')
? require('@/assets/img/default.png')
: DJANGO_BASE_URL + project.thumbnail + refreshId()
"
>
<div class="ui hidden divider" />
<div
class="ui basic teal label"
data-tooltip="Membres"
>
<i class="user icon" />{{ project.nb_contributors }}
</div>
<div
class="ui basic teal label"
data-tooltip="Signalements publiés"
>
<i class="map marker icon" />{{ project.nb_published_features }}
</div>
<div
class="ui basic teal label"
data-tooltip="Commentaires"
>
<i class="comment icon" />{{
project.nb_published_features_comments
}}
</div>
</div>
<div class="ten wide column important-flex space-between">
<div>
<h1 class="ui header">
{{ project.title }}
</h1>
<div class="ui hidden divider" />
<div class="sub header">
{{ project.description }}
</div>
</div>
<div class="content flex flex-column-right">
<div class="flex flex-column-right">
<div class="ui icon right compact buttons flex-column-right">
<div>
<a
v-if="
user &&
permissions &&
permissions.can_view_project &&
isOnline
"
id="subscribe-button"
class="ui button button-hover-green"
data-tooltip="S'abonner au projet"
data-position="top center"
data-variation="mini"
@click="modalType = 'subscribe'"
>
<i class="inverted grey envelope icon" />
</a>
<router-link
v-if="
permissions &&
permissions.can_update_project &&
isOnline
"
:to="{ name: 'project_edit', params: { slug } }"
class="ui button button-hover-orange"
data-tooltip="Modifier le projet"
data-position="top center"
data-variation="mini"
>
<i class="inverted grey pencil alternate icon" />
</router-link>
<a
v-if="isProjectAdmin && isOnline"
id="delete-button"
class="ui button button-hover-red"
data-tooltip="Supprimer le projet"
data-position="top center"
data-variation="mini"
@click="modalType = 'deleteProject'"
>
<i class="inverted grey trash icon" />
</a>
</div>
<button
v-if="isProjectAdmin &&
!isSharedProject && project.generate_share_link"
class="ui teal left labeled icon button"
@click="copyLink"
>
<i class="left icon share square" />
Copier le lien de partage
</button>
</div>
<div v-if="confirmMsg">
<div class="ui positive tiny-margin message">
<span>
Le lien a été copié dans le presse-papier
</span>
&nbsp;
<i
class="close icon"
@click="confirmMsg = ''"
/>
</div>
</div>
</div>
</div>
</div>
<div v-if="arraysOffline.length > 0">
{{ arraysOffline.length }} modification<span v-if="arraysOffline.length>1">s</span> en attente
<button
:disabled="!isOnline"
class="ui fluid labeled teal icon button"
@click="sendOfflineFeatures"
>
<i class="upload icon" />
Envoyer au serveur
</button>
</div>
</div>
<div class="row">
<div class="seven wide column">
<h3 class="ui header">
Types de signalements
</h3>
<div class="ui middle aligned divided list">
<div
:class="{ active: projectInfoLoading }"
class="ui inverted dimmer"
>
<div class="ui text loader">
Récupération des types de signalements en cours...
</div>
</div>
<div
:class="{ active: featureTypeImporting }"
class="ui inverted dimmer"
>
<div class="ui text loader">
Traitement du fichier en cours ...
</div>
</div>
<div
v-for="(type, index) in feature_types"
:key="type.title + '-' + index"
class="item"
>
<div class="feature-type-container">
<router-link
:to="{
name: 'details-type-signalement',
params: { feature_type_slug: type.slug },
}"
class="feature-type-title"
>
<img
v-if="type.geom_type === 'point'"
class="list-image-type"
src="@/assets/img/marker.png"
>
<img
v-if="type.geom_type === 'linestring'"
class="list-image-type"
src="@/assets/img/line.png"
>
<img
v-if="type.geom_type === 'polygon'"
class="list-image-type"
src="@/assets/img/polygon.png"
>
{{ type.title }}
</router-link>
<div class="middle aligned content">
<router-link
v-if="
project && permissions && permissions.can_create_feature
"
:to="{
name: 'ajouter-signalement',
params: { slug_type_signal: type.slug },
}"
class="
ui
compact
small
icon
right
floated
button button-hover-green
"
data-tooltip="Ajouter un signalement"
data-position="top right"
data-variation="mini"
>
<i class="ui plus icon" />
</router-link>
<router-link
v-if="
project &&
permissions &&
permissions.can_create_feature_type &&
isOnline
"
:to="{
name: 'dupliquer-type-signalement',
params: { slug_type_signal: type.slug },
}"
class="
ui
compact
small
icon
right
floated
button button-hover-green
"
data-tooltip="Dupliquer un type de signalement"
data-position="top right"
data-variation="mini"
>
<i class="inverted grey copy alternate icon" />
</router-link>
<div
v-if="isImporting(type)"
class="import-message"
>
<i class="info circle icon" />
Import en cours
</div>
<div
v-else
v-frag
>
<a
v-if="isProjectAdmin && isOnline"
class="
ui
compact
small
icon
right
floated
button button-hover-red
"
data-tooltip="Supprimer le type de signalement"
data-position="top center"
data-variation="mini"
@click="toggleDeleteFeatureType(type)"
>
<i class="inverted grey trash alternate icon" />
</a>
<router-link
v-if="
project &&
permissions &&
permissions.can_create_feature_type &&
isOnline
"
:to="{
name: 'editer-symbologie-signalement',
params: { slug_type_signal: type.slug },
}"
class="
ui
compact
small
icon
right
floated
button button-hover-orange
"
data-tooltip="Éditer la symbologie du type de signalement"
data-position="top center"
data-variation="mini"
>
<i class="inverted grey paint brush alternate icon" />
</router-link>
<router-link
v-if="
project &&
type.is_editable &&
permissions &&
permissions.can_create_feature_type &&
isOnline
"
:to="{
name: 'editer-type-signalement',
params: { slug_type_signal: type.slug },
}"
class="
ui
compact
small
icon
right
floated
button button-hover-orange
"
data-tooltip="Éditer le type de signalement"
data-position="top center"
data-variation="mini"
>
<i class="inverted grey pencil alternate icon" />
</router-link>
</div>
</div>
</div>
</div>
<div v-if="feature_types.length === 0">
<i> Le projet ne contient pas encore de type de signalements. </i>
</div>
</div>
<div id="nouveau-type-signalement">
<router-link
v-if="
permissions &&
permissions.can_update_project &&
isOnline
"
:to="{
name: 'ajouter-type-signalement',
params: { slug },
}"
class="ui compact basic button"
>
<i class="ui plus icon" />Créer un nouveau type de signalement
</router-link>
</div>
<div class="nouveau-type-signalement">
<div
v-if="
permissions &&
permissions.can_update_project &&
isOnline
"
class="
ui
compact
basic
button
button-align-left
"
>
<i class="ui plus icon" />
<label
class="ui"
for="json_file"
>
<span
class="label"
>Créer un nouveau type de signalement à partir d'un
GeoJSON</span>
</label>
<input
id="json_file"
type="file"
accept="application/json, .json, .geojson"
style="display: none"
name="json_file"
@change="onGeoJSONFileChange"
>
</div>
</div>
<div class="nouveau-type-signalement">
<div
v-if="
permissions &&
permissions.can_update_project &&
isOnline
"
class="
ui
compact
basic
button
button-align-left
"
>
<i class="ui plus icon" />
<label
class="ui"
for="csv_file"
>
<span
class="label"
>Créer un nouveau type de signalement à partir d'un
CSV</span>
</label>
<input
id="csv_file"
type="file"
accept="application/csv, .csv"
style="display: none"
name="csv_file"
@change="onCSVFileChange"
>
</div>
</div>
<div class="nouveau-type-signalement">
<router-link
v-if="
IDGO &&
permissions &&
permissions.can_update_project &&
isOnline
"
:to="{
name: 'catalog-import',
params: {
slug,
feature_type_slug: 'create'
},
}"
class="ui compact basic button button-align-left"
>
<i class="ui plus icon" />
Créer un nouveau type de signalement à partir du catalogue {{ CATALOG_NAME|| 'IDGO' }}
</router-link>
</div>
<div
v-if="geojsonFileToImport.size > 0"
id="button-import"
>
<button
:disabled="geojsonFileToImport.size === 0"
class="ui fluid teal icon button"
@click="toNewGeojsonFeatureType"
>
<i class="upload icon" /> Lancer l'import avec le fichier
{{ geojsonFileToImport.name }}
</button>
</div>
<div
v-if="csvFileToImport.size > 0 && !csvError"
id="button-import"
>
<button
:disabled="csvFileToImport.size === 0"
class="ui fluid teal icon button"
@click="toNewCsvFeatureType"
>
<i class="upload icon" /> Lancer l'import avec le fichier
{{ csvFileToImport.name }}
</button>
</div>
<div
v-if="csvError"
class="ui negative message"
>
<i
class="close icon"
@click="csvError = null"
/>
{{ csvError }}
</div>
</div>
<div
id="map-column"
class="seven wide column"
>
<div
:class="{ active: mapLoading }"
class="ui inverted dimmer"
>
<div class="ui text loader">
Chargement de la carte...
</div>
</div>
<div
id="map"
ref="map"
/>
<div
class="ui button teal"
@click="$router.push({
name: 'liste-signalements',
params: { slug: slug },
})"
>
<i class="ui icon arrow right" />
Voir tous les signalements
</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="{ active: featuresLoading }"
class="ui inverted dimmer"
>
<div class="ui text loader">
Récupération des signalements en cours...
</div>
</div>
<div class="ui relaxed list">
<div
v-for="(item, index) in features.slice(-5)"
:key="item.properties.title + index"
class="item"
>
<div class="content">
<div>
<router-link
:to="{
name: 'details-signalement',
params: {
slug,
slug_type_signal:
item.properties.feature_type.slug,
slug_signal: item.id,
},
}"
>
{{ item.properties.title || item.id }}
</router-link>
</div>
<div class="description">
<i>
[{{ item.properties.created_on }}
<span v-if="user && item.properties.creator">
, par
{{
item.properties.creator.full_name
? item.properties.creator.full_name
: item.properties.creator.username
}}
</span>
]
</i>
</div>
</div>
</div>
<i
v-if="features.length === 0"
>Aucun signalement pour le moment.</i>
</div>
</div>
</div>
</div>
<div class="orange card">
<div class="content">
<div class="center aligned header">
Derniers commentaires
</div>
<div class="center aligned description">
<div
:class="{ active: projectInfoLoading }"
class="ui inverted dimmer"
>
<div class="ui text loader">
Récupération des commentaires en cours...
</div>
</div>
<div class="ui relaxed list">
<div
v-for="(item, index) in last_comments"
:key="'comment ' + index"
class="item"
>
<div class="content">
<div>
<router-link
:to="getRouteUrl(item.related_feature.feature_url)"
>
"{{ item.comment }}"
</router-link>
</div>
<div class="description">
<i>[ {{ item.created_on
}}<span
v-if="user && item.display_author"
>, par {{ item.display_author }}
</span>
]</i>
</div>
</div>
</div>
<i
v-if="!last_comments || last_comments.length === 0"
>Aucun commentaire pour le moment.</i>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="fourteen wide column">
<div class="ui grey segment">
<h3 class="ui header">
Paramètres du projet
</h3>
<div class="ui three stackable cards">
<!-- <div class="card">
<div class="center aligned content">
<h4 class="ui center aligned icon header">
<i class="disabled grey archive icon" />
<div class="content">
Délai avant archivage automatique
</div>
</h4>
</div>
<div class="center aligned extra content">
{{ project.archive_feature }} jours
</div>
</div>
<div class="card">
<div class="content">
<h4 class="ui center aligned icon header">
<i class="disabled grey trash alternate icon" />
<div class="content">
Délai avant suppression automatique
</div>
</h4>
</div>
<div class="center aligned extra content">
{{ project.delete_feature }} jours
</div>
</div> -->
<div class="card">
<div class="content">
<h4 class="ui center aligned icon header">
<i class="disabled grey eye icon" />
<div class="content">
Visibilité des signalements publiés
</div>
</h4>
</div>
<div class="center aligned extra content">
{{ project.access_level_pub_feature }}
</div>
</div>
<div class="card">
<div class="content">
<h4 class="ui center aligned icon header">
<i class="disabled grey eye icon" />
<div class="content">
Visibilité des signalements archivés
</div>
</h4>
</div>
<div class="center aligned extra content">
{{ project.access_level_arch_feature }}
</div>
</div>
<div class="card">
<div class="content">
<h4 class="ui center aligned icon header">
<i class="disabled grey cogs icon" />
<div class="content">
Modération
</div>
</h4>
</div>
<div class="center aligned extra content">
{{ project.moderation ? "Oui" : "Non" }}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<span v-else>
<i class="icon exclamation triangle" />
<span>Vous ne disposez pas des droits nécessaires pour consulter ce
projet.</span>
</span>
<div
v-if="modalType"
class="ui dimmer modals page transition visible active"
style="display: flex !important"
>
<div
:class="[
'ui mini modal subscription',
{ 'transition visible active': modalType },
]"
>
<i
class="close icon"
@click="modalType = false"
/>
<div class="ui icon header">
<i :class="[modalType === 'subscribe' ? 'envelope' : 'trash', 'icon']" />
{{
modalType === 'subscribe' ? 'Notifications' : 'Suppression'
}} du {{
modalType === 'deleteFeatureType' ? 'type de signalement ' + featureTypeToDelete.title : 'projet'
}}
</div>
<div class="content">
<div v-if="modalType !== 'subscribe'">
<p class="centered-text">
Confirmez vous la suppression du {{
modalType === 'deleteProject' ?
'projet, ainsi que les types de signalements' :
'type de signalement'
}} et tous les signalements associés&nbsp;?
</p>
<p class="centered-text alert">
Attention cette action est irreversible !
</p>
</div>
<button
:class="['ui compact fluid button', modalType === 'subscribe' && !is_suscriber ? 'green' : 'red']"
@click="handleModalClick"
>
<span v-if="modalType === 'subscribe'">
{{
is_suscriber
? "Se désabonner de ce projet"
: "S'abonner à ce projet"
}}
</span>
<span v-else>
Supprimer le
{{
modalType === 'deleteProject'
? 'projet'
: 'type de signalement'
}}
</span>
</button>
</div>
</div>
</div>
<div
:class="isFileSizeModalOpen ? 'active' : ''"
class="ui dimmer"
>
<div
:class="isFileSizeModalOpen ? 'active' : ''"
class="ui modal tiny"
style="top: 20%"
>
<div class="header">
Fichier trop grand!
</div>
<div class="content">
<p>
Impossible de créer un type de signalement à partir d'un fichier
GeoJSON de plus de 10Mo (celui importé fait {{ geojsonFileSize > 0 ? geojsonFileSize : csvFileSize }} Mo).
</p>
</div>
<div class="actions">
<div
class="ui button teal"
@click="closeFileSizeModal"
>
Fermer
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import { featureCollection, point } from '@turf/helpers';
import bbox from '@turf/bbox';
import frag from 'vue-frag';
import { mapUtil } from '@/assets/js/map-util.js';
import { mapGetters, mapState, mapActions, mapMutations } from 'vuex';
import projectAPI from '@/services/project-api';
import featureTypeAPI from '@/services/featureType-api';
import featureAPI from '@/services/feature-api';
import { fileConvertSizeToMo, csvToJson } from '@/assets/js/utils';
export default {
name: 'ProjectDetails',
directives: {
frag,
},
props: {
message: {
type: String,
default: ''
}
},
data() {
return {
infoMessage: '',
importMessage: null,
arraysOffline: [],
arraysOfflineErrors: [],
confirmMsg: false,
geojsonImport: [],
csvImport: null,
csvError: null,
geojsonFileToImport: { name: '', size: 0 },
csvFileToImport: { name: '', size: 0 },
slug: this.$route.params.slug,
modalType: false,
is_suscriber: false,
tempMessage: null,
projectInfoLoading: true,
featureTypeImporting: false,
featureTypeToDelete: null,
featuresLoading: true,
isFileSizeModalOpen: false,
mapLoading: true,
};
},
computed: {
...mapGetters([
'permissions'
]),
...mapState('projects', [
'project'
]),
...mapState([
'configuration',
'isOnline',
]),
...mapState('feature_type', [
'feature_types',
'importFeatureTypeData'
]),
...mapState('feature', [
'features'
]),
...mapState([
'last_comments',
'user',
'user_permissions',
'reloadIntervalId',
]),
...mapState('map', [
'map'
]),
DJANGO_BASE_URL() {
return this.configuration.VUE_APP_DJANGO_BASE;
},
API_BASE_URL() {
return this.configuration.VUE_APP_DJANGO_API_BASE;
},
CATALOG_NAME() {
return this.configuration.VUE_APP_CATALOG_NAME;
},
IDGO() {
return this.$store.state.configuration.VUE_APP_IDGO;
},
geojsonFileSize() {
return fileConvertSizeToMo(this.geojsonFileToImport.size);
},
csvFileSize() {
return fileConvertSizeToMo(this.csvFileToImport.size);
},
isSharedProject() {
return this.$route.path.includes('projet-partage');
},
isProjectAdmin() {
return this.user_permissions && this.user_permissions[this.slug] &&
this.user_permissions[this.slug].is_project_administrator;
},
},
watch: {
feature_types: {
deep: true,
handler(newValue, oldValue) {
if (newValue && newValue !== oldValue) {
this.GET_IMPORTS({
project_slug: this.$route.params.slug,
});
}
},
},
importFeatureTypeData: {
deep: true,
handler(newValue) {
if (
newValue &&
newValue.some((el) => el.status === 'pending') &&
!this.reloadIntervalId
) {
this.SET_RELOAD_INTERVAL_ID(
setInterval(() => {
this.GET_IMPORTS({
project_slug: this.$route.params.slug,
});
}, this.$store.state.configuration.VUE_APP_RELOAD_INTERVAL)
);
} else if (
newValue &&
!newValue.some((el) => el.status === 'pending') &&
this.reloadIntervalId
) {
this.GET_PROJECT_FEATURE_TYPES(this.slug);
this.CLEAR_RELOAD_INTERVAL_ID();
}
},
},
},
created() {
if (this.user) {
projectAPI
.getProjectSubscription({
baseUrl: this.$store.state.configuration.VUE_APP_DJANGO_API_BASE,
projectSlug: this.$route.params.slug
})
.then((data) => (this.is_suscriber = data.is_suscriber));
}
this.$store.commit('feature/SET_FEATURES', []); //* empty features remaining in case they were in geojson format and will be fetch after map initialization anyway
this.$store.commit('feature_type/SET_FEATURE_TYPES', []); //* empty feature_types remaining from previous project
},
mounted() {
this.retrieveProjectInfo();
if (this.message) {
this.tempMessage = this.message;
document
.getElementById('message')
.scrollIntoView({ block: 'end', inline: 'nearest' });
setTimeout(() => (this.tempMessage = null), 5000); //* hide message after 5 seconds
}
},
destroyed() {
this.CLEAR_RELOAD_INTERVAL_ID();
},
methods: {
...mapMutations([
'SET_RELOAD_INTERVAL_ID',
'CLEAR_RELOAD_INTERVAL_ID',
'DISPLAY_MESSAGE',
'DISPLAY_LOADER',
'DISCARD_LOADER',
]),
...mapActions('projects', [
'GET_PROJECT_INFO',
'GET_PROJECT',
]),
...mapActions('map', [
'INITIATE_MAP'
]),
...mapActions('feature_type', [
'GET_IMPORTS'
]),
...mapActions('feature', [
'GET_PROJECT_FEATURES'
]),
...mapActions('feature_type', [
'GET_PROJECT_FEATURE_TYPES'
]),
refreshId() {
return '?ver=' + Math.random();
},
getRouteUrl(url) {
if (this.isSharedProject) {
url = url.replace('projet', 'projet-partage');
}
return url.replace(this.$store.state.configuration.BASE_URL, ''); //* remove duplicate /geocontrib
},
isImporting(type) {
if (this.importFeatureTypeData) {
const singleImportData = this.importFeatureTypeData.find(
(el) => el.feature_type_title === type.slug
);
return singleImportData && singleImportData.status === 'pending';
}
return false;
},
copyLink() {
const sharedLink = window.location.href.replace('projet', 'projet-partage');
navigator.clipboard.writeText(sharedLink).then(()=> {
this.confirmMsg = true;
}, () => {
console.log('failed');
}
);
},
retrieveProjectInfo() {
this.DISPLAY_LOADER('Projet en cours de chargement.');
Promise.all([
this.GET_PROJECT(this.slug),
this.GET_PROJECT_INFO(this.slug)
])
.then(() => {
this.DISCARD_LOADER();
this.projectInfoLoading = false;
setTimeout(() => {
let map = mapUtil.getMap();
if (map) map.remove();
this.initMap();
}, 1000);
})
.catch((err) => {
console.error(err);
this.DISCARD_LOADER();
this.projectInfoLoading = false;
});
},
checkForOfflineFeature() {
let arraysOffline = [];
const localStorageArray = localStorage.getItem('geocontrib_offline');
if (localStorageArray) {
arraysOffline = JSON.parse(localStorageArray);
this.arraysOffline = arraysOffline.filter(
(x) => x.project === this.slug
);
}
},
sendOfflineFeatures() {
this.arraysOfflineErrors = [];
const promises = this.arraysOffline.map((feature) => featureAPI.postOrPutFeature({
data: feature.geojson,
feature_id: feature.featureId,
project__slug: feature.project,
feature_type__slug: feature.geojson.properties.feature_type,
method: feature.type.toUpperCase(),
})
.then((response) => {
if (!response) this.arraysOfflineErrors.push(feature);
})
.catch((error) => {
console.error(error);
this.arraysOfflineErrors.push(feature);
})
);
this.DISPLAY_LOADER('Envoi des signalements en cours.');
Promise.all(promises).then(() => {
this.updateLocalStorage();
this.retrieveProjectInfo();
});
},
updateLocalStorage() {
let arraysOffline = [];
const localStorageArray = localStorage.getItem('geocontrib_offline');
if (localStorageArray) {
arraysOffline = JSON.parse(localStorageArray);
}
const arraysOfflineOtherProject = arraysOffline.filter(
(x) => x.project !== this.slug
);
this.arraysOffline = [];
arraysOffline = arraysOfflineOtherProject.concat(
this.arraysOfflineErrors
);
localStorage.setItem('geocontrib_offline', JSON.stringify(arraysOffline));
},
toNewGeojsonFeatureType() {
this.featureTypeImporting = true;
this.$router.push({
name: 'ajouter-type-signalement',
params: {
geojson: this.geojsonImport,
fileToImport: this.geojsonFileToImport,
},
});
this.featureTypeImporting = false;
},
toNewCsvFeatureType() {
this.featureTypeImporting = true;
this.$router.push({
name: 'ajouter-type-signalement',
params: {
csv: this.csvImport,
fileToImport: this.csvFileToImport,
},
});
this.featureTypeImporting = false;
},
onGeoJSONFileChange(e) {
this.featureTypeImporting = true;
var files = e.target.files || e.dataTransfer.files;
if (!files.length) return;
this.geojsonFileToImport = files[0];
// TODO : VALIDATION IF FILE IS JSON
if (parseFloat(fileConvertSizeToMo(this.geojsonFileToImport.size)) > 10) {
this.isFileSizeModalOpen = true;
} else if (this.geojsonFileToImport.size > 0) {
const fr = new FileReader();
try {
fr.onload = (e) => {
this.geojsonImport = JSON.parse(e.target.result);
this.featureTypeImporting = false;
};
fr.readAsText(this.geojsonFileToImport);
//* stock filename to import features afterward
this.$store.commit(
'feature_type/SET_FILE_TO_IMPORT',
this.geojsonFileToImport
);
} catch (err) {
console.error(err);
this.featureTypeImporting = false;
}
} else {
this.featureTypeImporting = false;
}
},
onCSVFileChange(e) {
this.featureTypeImporting = true;
var files = e.target.files || e.dataTransfer.files;
if (!files.length) return;
this.csvFileToImport = files[0];
if (parseFloat(fileConvertSizeToMo(this.csvFileToImport.size)) > 10) {
this.isFileSizeModalOpen = true;
} else if (this.csvFileToImport.size > 0) {
const fr = new FileReader();
try {
fr.readAsText(this.csvFileToImport);
fr.onloadend = () => {
// Check if file contains 'lat' and 'long' fields
const headersLine =
fr.result
.split('\n')[0]
.split(',')
.filter(el => {
return el === 'lat' || el === 'lon';
});
// Look for 2 decimal fields in first line of csv
// corresponding to lon and lat
const sampleLine =
fr.result
.split('\n')[1]
.split(',')
.map(el => {
return !isNaN(el) && el.indexOf('.') != -1;
})
.filter(Boolean);
if (sampleLine.length > 1 && headersLine.length === 2) {
this.csvError = null;
this.csvImport = csvToJson(fr.result);
this.featureTypeImporting = false;
//* stock filename to import features afterward
this.$store.commit(
'feature_type/SET_FILE_TO_IMPORT',
this.csvFileToImport
);
} else {
// File doesn't seem to contain coords
this.csvError = `Le fichier ${this.csvFileToImport.name} ne semble pas contenir de coordonnées`;
this.featureTypeImporting = false;
}
};
} catch (err) {
console.error(err);
this.featureTypeImporting = false;
}
} else {
this.featureTypeImporting = false;
}
},
closeFileSizeModal() {
this.geojsonFileToImport = { name: '', size: 0 };
this.csvFileToImport = { name: '', size: 0 };
this.featureTypeImporting = false;
this.isFileSizeModalOpen = false;
},
subscribeProject() {
projectAPI
.subscribeProject({
baseUrl: this.$store.state.configuration.VUE_APP_DJANGO_API_BASE,
suscribe: !this.is_suscriber,
projectSlug: this.$route.params.slug,
})
.then((data) => {
this.is_suscriber = data.is_suscriber;
this.modalType = false;
if (this.is_suscriber)
this.infoMessage =
'Vous êtes maintenant abonné aux notifications de ce projet.';
else
this.infoMessage =
'Vous ne recevrez plus les notifications de ce projet.';
setTimeout(() => (this.infoMessage = ''), 3000);
});
},
deleteProject() {
projectAPI.deleteProject(this.API_BASE_URL, this.slug)
.then((response) => {
if (response === 'success') {
this.$router.push('/');
this.DISPLAY_MESSAGE({
comment: `Le projet ${this.project.title} a bien été supprimé.`, level: 'positive'
});
} else {
this.DISPLAY_MESSAGE({
comment: `Une erreur est survenu lors de la suppression du projet ${this.project.title}.`,
level: 'negative'
});
}
});
},
deleteFeatureType() {
featureTypeAPI.deleteFeatureType(this.featureTypeToDelete.slug)
.then((response) => {
this.modalType = false;
if (response === 'success') {
this.retrieveProjectInfo();
this.DISPLAY_MESSAGE({
comment: `Le type de signalement ${this.featureTypeToDelete.title} a bien été supprimé.`,
level: 'positive',
});
} else {
this.DISPLAY_MESSAGE({
comment: `Une erreur est survenu lors de la suppression du type de signalement ${this.featureTypeToDelete.title}.`,
level: 'negative',
});
}
this.featureTypeToDelete = null;
});
},
handleModalClick() {
switch (this.modalType) {
case 'subscribe':
this.subscribeProject();
break;
case 'deleteProject':
this.deleteProject();
break;
case 'deleteFeatureType':
this.deleteFeatureType();
break;
}
},
toggleDeleteFeatureType(featureType) {
this.featureTypeToDelete = featureType;
this.modalType = 'deleteFeatureType';
},
async initMap() {
if (this.project && this.permissions.can_view_project) {
await this.INITIATE_MAP(this.$refs.map);
this.checkForOfflineFeature();
let project_id = this.$route.params.slug.split('-')[0];
const mvtUrl = `${this.API_BASE_URL}features.mvt/?tile={z}/{x}/{y}&project_id=${project_id}`;
mapUtil.addVectorTileLayer(
mvtUrl,
this.$route.params.slug,
this.$store.state.feature_type.feature_types
);
this.mapLoading = false;
const featuresOffline = this.arraysOffline.map((x) => {
return { ...x.geojson, overideColor: '#ff0000' }; //* red (hex format is better for perf)
});
this.featuresLoading = false;
mapUtil.addFeatures(
featuresOffline,
{},
true,
this.$store.state.feature_type.feature_types,
this.$route.params.slug,
);
featureAPI.getFeaturesBbox(this.slug).then((featuresBbox) => {
if (featuresBbox) {
if (featuresOffline.length > 0) {//* add offline features to BBOX with Turf
const allFeatures = [...featuresBbox.map((coordinates) => point(coordinates)), ...featuresOffline];
const featureCollect = featureCollection(allFeatures);
const newBbox = bbox(featureCollect);
if (newBbox) featuresBbox = [[newBbox[0], newBbox[1]], [newBbox[2], newBbox[3]]];
}
mapUtil.getMap().fitBounds(featuresBbox, { padding: [25, 25] });
}
});
}
},
},
};
</script>
<style>
#map-column {
display: flex;
flex-direction: column;
}
#map {
width: 100%;
height: 100%;
min-height: 250px;
margin-bottom: 1em;
}
/* // ! missing style in semantic.min.css, je ne comprends pas comment... */
.ui.right.floated.button {
float: right;
}
</style>
<style scoped>
.list-image-type {
margin-right: 5px;
height: 25px;
vertical-align: bottom;
}
.feature-type-container {
display: flex;
justify-content: space-between;
align-items: center;
}
.feature-type-container > .middle.aligned.content {
width: 50%;
}
.feature-type-title {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
line-height: 1.5em;
}
.nouveau-type-signalement {
margin-top: 1em;
}
.nouveau-type-signalement .label{
cursor: pointer;
}
#button-import {
margin-top: 0.5em;
}
.fullwidth {
width: 100%;
}
.button-align-left {
display: flex;
align-items: center;
text-align: left;
width: fit-content;
}
.space-between {
justify-content: space-between;
}
.flex-column-right {
flex-direction: column !important;
align-items: flex-end;
}
.import-message {
width: fit-content;
line-height: 2em;
color: teal;
}
</style>
<style scoped>
.ui.button, .ui.button .button, .tiny-margin {
margin: 0.1rem 0 0.1rem 0.1rem !important;
}
.alert {
color: red;
}
.centered-text {
text-align: center;
}
</style>
......@@ -2,6 +2,7 @@ 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: {
......@@ -34,6 +35,7 @@ module.exports = {
themeColor: '#1da025'
},
configureWebpack: {
devtool: 'source-map',
plugins: [
new webpack.DefinePlugin({
'process.env': {
......@@ -42,5 +44,14 @@ module.exports = {
})
]
},
// the rest of your original module.exports code goes here
transpileDependencies: [
// Add dependencies that use modern JavaScript syntax, based on encountered errors
'ol',
'color-rgba',
'color-parse',
'@sentry/browser',
'@sentry/core',
'@sentry/vue',
'@sentry-internal'
]
};
module.exports = {
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader'
}
},
{
test: /\.vue$/,
loader: 'vue-loader'
}
]
}
};
\ No newline at end of file