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
Showing
with 4017 additions and 72 deletions
/* OPENLAYERS */
.ol-zoom{
right: 5px !important;
left:unset !important;
}
.ol-popup {
position: absolute;
background-color: white;
padding: 15px 5px 15px 15px;
border-radius: 10px;
bottom: 12px;
left: -120px;
width: 240px;
line-height: 1.4;
-webkit-box-shadow: 0 3px 14px rgba(0,0,0,.4);
box-shadow: 0 3px 14px rgba(0,0,0,.4);
}
.ol-popup #popup-content {
line-height: 1.3;
font-size: .95em;
}
.ol-popup #popup-content h4 {
margin-right: 15px;
margin-bottom: .5em;
color: #cacaca;
}
.ol-popup #popup-content h4,
.ol-popup #popup-content div {
text-overflow: ellipsis;
overflow: hidden;
}
.ol-popup #popup-content div {
color: #434343;
}
.ol-popup #popup-content .fields {
max-height: 200px;
overflow-y: scroll;
overflow-x: hidden;
padding-right: 10px;
display: block; /* overide .ui.form.fields rule conflict in featureEdit page */
margin: 0; /* overide .ui.form.fields rule conflict in featureEdit page */
}
.ol-popup #popup-content .divider {
margin-bottom: 0;
}
.ol-popup #popup-content #customFields h5 {
max-height: 20;
margin: .5em 0;
}
.ol-popup:after, .ol-popup:before {
top: 100%;
border: solid transparent;
content: " ";
height: 0;
width: 0;
position: absolute;
pointer-events: none;
}
.ol-popup:after {
border-top-color: white;
border-width: 10px;
left: 50%;
margin-left: -10px;
}
.ol-popup:before {
border-top-color: #cccccc;
border-width: 11px;
left: 50%;
margin-left: -11px;
}
.ol-popup-closer {
text-decoration: none;
position: absolute;
top: 4px;
right: 0;
width: 24px;
height: 24px;
font: 18px/24px Tahoma,Verdana,sans-serif;
color: #757575;
}
.ol-popup-closer:after {
content: "×";
}
.ol-scale-line {
left: 2em;
background: hsla(0,0%,100%,.5);
padding: 0;
}
.ol-scale-line-inner {
color: #333;
font-size: 0.9em;
text-align: left;
padding-left: 0.5em;
border: 2px solid #777;
border-top: none;
}
.ol-control {
border: 2px solid rgba(0,0,0,.2);
background-clip: padding-box;
padding: 0;
}
.ol-control button {
background-color: #fff;
color: #000;
height: 30px;
width: 30px;
font: 700 18px Lucida Console,Monaco,monospace;
margin: 0;
}
.ol-control button:hover {
cursor: pointer;
background-color: #ebebeb;
}
.ol-control button:focus {
background-color: #ebebeb;
}
/* hide the popup before the map get loaded */
.map-container > #popup.ol-popup {
display: none;
}
.ol-full-screen {
top: calc(1em + 60px);
right: 5px !important;
}
/* Geolocation button */
div.geolocation-container {
position: absolute;
right: 6px;
z-index: 9;
border: 2px solid rgba(0,0,0,.2);
background-clip: padding-box;
padding: 0;
border-radius: 4px;
}
button.button-geolocation {
border: none;
padding: 0;
margin: 0;
text-align: center;
background-color: #fff;
color: rgb(39, 39, 39);
width: 30px;
height: 30px;
font: 700 18px Lucida Console,Monaco,monospace;
border-radius: 2px;
line-height: 1.15;
cursor: pointer;
}
button.button-geolocation:hover {
background-color: #ebebeb;
}
button.button-geolocation.tracking {
background-color: rgba(255, 145, 0, 0.904);
color: #fff;
}
button.button-geolocation i {
margin: 0;
vertical-align: top; /* strangely top is the only value that center at middle */
background-image: url(../img/geolocation-icon.png);
background-size: cover;
width: 25px;
height: 25px;
}
\ No newline at end of file
......@@ -13,8 +13,7 @@
border: 1px solid grey;
top: 0;
position: absolute;
/* Under this value, the map hide the sidebar */
z-index: 400;
z-index: 9;
}
.sidebar-layers {
......@@ -62,7 +61,7 @@
.sidebar-container.expanded .layers-icon svg path,
.sidebar-container.closing .layers-icon svg path {
fill: #00b5ad;
fill: var(--primary-color, #00b5ad);
}
@keyframes open-sidebar {
......@@ -132,7 +131,7 @@
}
.layers-icon:hover svg path {
fill: #00b5ad;
fill: var(--primary-color, #00b5ad);
}
.basemaps-title {
......@@ -144,7 +143,6 @@
}
/* Layer item */
.layer-item {
padding-bottom: 0.5rem;
}
......@@ -164,6 +162,7 @@
.range-container {
display: flex;
min-width: 15em; /* give space for the bubble since adding a min-width to keep its shape */
}
.range-output-bubble {
......@@ -172,6 +171,8 @@
padding: 4px 7px;
border-radius: 40px;
background-color: #2c3e50;
min-width: 2em;
text-align: center;
}
/* Overrides default padding of semantic-ui accordion */
......
import axios from 'axios';
axios.defaults.withCredentials = true;
// Add a request interceptor
axios.interceptors.request.use(function (config) {
config.headers['X-CSRFToken'] = (name => {
const re = new RegExp(name + '=([^;]+)');
const value = re.exec(document.cookie);
return (value != null) ? unescape(value[1]) : null;
})('csrftoken');
return config;
}, function (error) {
return Promise.reject(error);
});
// Add a response interceptor
axios.interceptors.response.use(function (response) {
response.headers['X-CSRFToken'] = (name => {
const re = new RegExp(name + '=([^;]+)');
const value = re.exec(document.cookie);
return (value != null) ? unescape(value[1]) : null;
})('csrftoken');
return response;
}, function (error) {
return Promise.reject(error);
});
export default axios;
<template>
<div
id="user-activity"
class="ui stackable cards"
>
<!-- EVENTS -->
<div class="red card">
<div class="content">
<div class="center aligned header">
Mes dernières notifications reçues
</div>
<div class="center aligned description">
<div class="ui relaxed list">
<div
v-for="item in events"
:key="item.id"
class="item"
>
<div :class="['content', { 'ellipsis nowrap': item.related_feature.title }]">
{{ getNotificationName(item.event_type, item.object_type) }}
<div
v-if="item.object_type === 'project'"
>
<router-link
v-if="item.project_title"
:to="{
name: 'project_detail',
params: { slug: item.project_slug },
}"
>
{{ item.project_title }}
</router-link>
<span
v-else
class="meta"
><del>{{ item.project_slug }}</del>&nbsp;(supprimé)</span>
</div>
<div v-else>
<FeatureFetchOffsetRoute
v-if="item.related_feature.deletion_on === 'None'"
:feature-id="item.feature_id"
:properties="{
feature_type: {
slug: item.feature_type_slug
},
title: item.related_feature.title,
...item
}"
/>
<span
v-else
class="meta"
><del>{{ item.data.feature_title || item.feature_id }}</del>&nbsp;(supprimé)</span>
</div>
<div class="description">
<em>[ {{ item.created_on }}
<span v-if="user">
, par {{ item.display_user }}
</span>
]</em>
</div>
</div>
</div>
<em
v-if="!events || events.length === 0"
>Aucune notification pour le moment.</em>
</div>
</div>
</div>
</div>
<!-- FEATURES -->
<div class="orange card">
<div class="content">
<div class="center aligned header">
Mes derniers signalements
</div>
<div class="center aligned description">
<div class="ui relaxed list">
<div
v-for="item in features"
:key="item.id"
class="item"
>
<div class="content">
<div>
<FeatureFetchOffsetRoute
v-if="item.related_feature.deletion_on === 'None'"
:feature-id="item.feature_id"
:properties="{
feature_type: {
slug: item.feature_type_slug
},
title: item.related_feature.title,
...item
}"
/>
<span
v-else
class="meta"
>
<del>{{ item.data.feature_title || item.feature_id }}</del>&nbsp;(supprimé)
</span>
</div>
<div class="description">
<em>[ {{ item.created_on }}
<span v-if="user">
, par {{ item.display_user }}
</span>
]</em>
</div>
</div>
</div>
<em
v-if="!features || features.length === 0"
>Aucun signalement pour le moment.</em>
</div>
</div>
</div>
</div>
<!-- COMMENTS -->
<div class="yellow card">
<div class="content">
<div class="center aligned header">
Mes derniers commentaires
</div>
<div class="center aligned description">
<div class="ui relaxed list">
<div
v-for="item in comments"
:key="item.id"
class="item"
>
<div class="content">
<div>
<FeatureFetchOffsetRoute
v-if="item.related_feature.deletion_on === 'None'"
:feature-id="item.feature_id"
:properties="{
feature_type: {
slug: item.feature_type_slug
},
title: quoteComment(item.data.comment),
...item
}"
/>
<span
v-else
class="meta"
>
<del>{{ item.data.comment }}</del>&nbsp;(supprimé)
</span>
</div>
<div class="description">
<em>[ {{ item.created_on }}
<span v-if="user">
, par {{ item.display_user }}
</span>
]</em>
</div>
</div>
</div>
<em
v-if="!comments || comments.length === 0"
>Aucun commentaire pour le moment.</em>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import { mapState } from 'vuex';
import miscAPI from '@/services/misc-api';
import FeatureFetchOffsetRoute from '@/components/Feature/FeatureFetchOffsetRoute';
export default {
name: 'UserActivity',
components: {
FeatureFetchOffsetRoute,
},
data() {
return {
events: [],
features: [],
comments: [],
};
},
computed: {
...mapState([
'user',
]),
isSharedProject() {
return this.$route.path.includes('projet-partage');
},
},
created(){
this.getEvents();
// unset project to avoid interfering with generating query in feature links
this.$store.commit('projects/SET_PROJECT', null);
},
methods: {
getEvents(){
miscAPI.getUserEvents(this.$route.params.slug)
.then((data)=>{
this.events = data.events;
this.features = data.features;
this.comments = data.comments;
});
},
getNotificationName(eventType, objectType) {
if (eventType === 'create') {
if (objectType === 'feature') {
return 'Signalement créé';
} else if (objectType === 'comment') {
return 'Commentaire créé';
} else if (objectType === 'attachment') {
return 'Pièce jointe ajoutée';
} else if (objectType === 'project') {
return 'Projet créé';
}
} else if (eventType === 'update') {
if (objectType === 'feature') {
return 'Signalement mis à jour';
} else if (objectType === 'project') {
return 'Projet mis à jour';
}
} else if (eventType === 'delete') {
if (objectType === 'feature') {
return 'Signalement supprimé';
} else if (objectType === 'project') {
return 'Projet mis à jour';
} else {
return 'Événement inconnu';
}
}
},
quoteComment(comment) {
return `"${comment}"`;
},
}
};
</script>
<style scoped lang="less">
#user-activity {
flex-flow: column;
margin: 1em 0;
.card {
margin: .875em 0;
}
}
</style>
<template>
<div>
<h4 class="ui horizontal divider header">
PROFIL
</h4>
<div class="ui divided list">
<div class="item">
<div class="right floated content">
<div class="description">
<span v-if="user.username">{{ user.username }} </span>
</div>
</div>
<div class="content">
Nom d'utilisateur
</div>
</div>
<div class="item">
<div class="right floated content">
<div class="description">
{{ userFullname }}
</div>
</div>
<div class="content">
Nom complet
</div>
</div>
<div class="item">
<div class="right floated content">
<div class="description">
{{ user.email }}
</div>
</div>
<div class="content">
Adresse e-mail
</div>
</div>
<div class="item">
<div class="right floated content">
<div class="description">
{{ user.is_superuser ? "Oui" : "Non" }}
</div>
</div>
<div class="content">
Administrateur
</div>
</div>
</div>
<div
v-if="qrcode"
class="qrcode"
>
<img
:src="qrcode"
alt="qrcode"
>
<p>
Ce QR code vous permet de vous connecter à l'application mobile GéoContrib (bientôt disponible)
</p>
</div>
</div>
</template>
<script>
import { mapState, mapActions } from 'vuex';
import QRCode from 'qrcode';
export default {
name: 'UserProfile',
data() {
return {
qrcode: null
};
},
computed: {
...mapState([
'configuration',
'user',
'userToken'
]),
userFullname() {
if (this.user.first_name || this.user.last_name) {
return `${this.user.first_name} ${this.user.last_name}`;
}
return null;
},
},
created() {
this.GET_USER_TOKEN()
.then(async () => {
try {
const qrcodeData = {
url: `${this.configuration.VUE_APP_DJANGO_BASE}/geocontrib/`,
token: this.userToken
};
this.qrcode = await QRCode.toDataURL(JSON.stringify(qrcodeData));
} catch (err) {
console.error(err);
}
})
.catch((err) => {
console.error(err);
});
},
methods: {
...mapActions([
'GET_USER_TOKEN'
])
}
};
</script>
<style scoped lang="less">
.qrcode {
img {
display: block;
margin: auto;
width: 12rem;
}
p {
font-size: 0.8rem;
font-style: italic;
text-align: center;
}
}
</style>
<template>
<div>
<h4 class="ui horizontal divider header">
MES PROJETS
</h4>
<div class="ui divided items">
<div
:class="['ui inverted dimmer', { active: projectsLoading }]"
>
<div class="ui text loader">
Récupération des projets en cours...
</div>
</div>
<div
v-for="project in projectsArray"
:key="project.slug"
class="item"
>
<div
v-if="user_permissions[project.slug].can_view_project"
class="item-content-wrapper"
>
<div class="ui tiny image">
<img
v-if="project.thumbnail"
class="ui small image"
alt="Thumbnail projet"
:src="
project.thumbnail.includes('default')
? require('@/assets/img/default.png')
: DJANGO_BASE_URL + project.thumbnail + refreshId()
"
height="200"
>
</div>
<div class="middle aligned content">
<router-link
:to="{
name: 'project_detail',
params: { slug: project.slug },
}"
class="header"
>
{{ project.title }}
</router-link>
<div class="description">
<p>{{ project.description }}</p>
</div>
<div class="meta top">
<span
class="right floated"
>
<strong>Projet {{ project.moderation ? "" : "non" }} modéré</strong>
</span>
<span>
Niveau d'autorisation requis : {{ project.access_level_pub_feature }}
</span><br>
<span>
Mon niveau d'autorisation :
<span v-if="USER_LEVEL_PROJECTS && project">
{{ USER_LEVEL_PROJECTS[project.slug] }}
</span>
<span v-if="user && user.is_administrator">
{{ "+ Gestionnaire métier" }}
</span>
</span>
</div>
<div class="meta">
<span
class="right floated"
:data-tooltip="`Projet créé le ${project.created_on}`"
>
<i
class="calendar icon"
aria-hidden="true"
/>
&nbsp;{{ project.created_on }}
</span>
<span data-tooltip="Membres">
{{ project.nb_contributors }}&nbsp;
<i
class="user icon"
aria-hidden="true"
/>
</span>
<span data-tooltip="Signalements publiés">
{{ project.nb_published_features }}&nbsp;
<i
class="map marker icon"
aria-hidden="true"
/>
</span>
<span data-tooltip="Commentaires">
{{ project.nb_published_features_comments }}&nbsp;
<i
class="comment icon"
aria-hidden="true"
/>
</span>
</div>
</div>
</div>
</div>
<!-- PAGINATION -->
<Pagination
v-if="count"
:nb-pages="Math.ceil(count/10)"
@page-update="changePage"
/>
</div>
</div>
</template>
<script>
import { mapState, mapMutations, mapActions } from 'vuex';
import Pagination from '@/components/Pagination.vue';
export default {
name: 'UserProjectList',
components: {
Pagination,
},
data() {
return {
projectsLoading: true,
};
},
computed: {
...mapState([
'user',
'USER_LEVEL_PROJECTS',
'user_permissions',
]),
// todo : filter projects to user
...mapState('projects', [
'projects',
'count',
]),
DJANGO_BASE_URL() {
return this.$store.state.configuration.VUE_APP_DJANGO_BASE;
},
isSharedProject() {
return this.$route.path.includes('projet-partage');
},
availableProjects() {
if (this.isSharedProject) {
return this.projects.filter((el) => el.slug === this.$route.params.slug);
}
return this.projects;
},
projectsArray() { //* if only one project, only project object is returned
return Array.isArray(this.projects) ? this.projects : [this.projects];
}
},
created(){
this.SET_PROJECTS([]); //* empty previous project to avoid undefined user_permissions[project.slug]
this.getData();
},
methods: {
...mapMutations('projects', [
'SET_CURRENT_PAGE',
'SET_PROJECTS',
]),
...mapActions('projects', [
'GET_PROJECTS',
]),
refreshId() {
const crypto = window.crypto || window.msCrypto;
var array = new Uint32Array(1);
return '?ver=' + crypto.getRandomValues(array); // Compliant for security-sensitive use cases
},
getData(page) {
this.loading = true;
this.GET_PROJECTS({ ismyaccount: true, projectSlug: this.$route.params.slug, page })
.then(() => this.projectsLoading = false)
.catch(() => this.projectsLoading = false);
},
changePage(e) {
this.getData(e);
},
}
};
</script>
<style lang="less" scoped>
.ui.divided.items {
.item {
.item-content-wrapper {
width: 100%;
margin: 0;
padding: 1em 0;
display: flex;
.middle.aligned.content {
.header {
font-size: 1.28571429em;
font-weight: 600;
color: rgb(31, 31, 31)
}
}
}
}
> .item:nth-child(2) {
border: none !important;
}
}
.description {
p {
text-align: justify;
}
}
@media only screen and (min-width: 767px) {
.item-content-wrapper {
align-items: flex-start;
.middle.aligned.content {
width: 100%;
padding: 0 0 0 1.5em;
.meta.top {
span {
line-height: 1.2em;
}
}
}
}
}
@media only screen and (max-width: 767px) {
.item-content-wrapper {
flex-direction: column;
align-items: center;
.middle.aligned.content {
width: 80%;
padding: 1.5em 0 0;
.meta.top {
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: center;
.right.floated {
float: none !important;
margin-left: 0 !important;
margin-bottom: 0.5em;
}
span {
margin: 0.15em 0;
}
}
}
}
}
</style>
<template>
<div id="app-footer">
<div class="ui compact text menu">
<router-link
:to="{name: 'mentions'}"
class="item"
>
Mentions légales
</router-link>
<router-link
:to="{name: 'aide'}"
class="item"
>
Aide
</router-link>
<p class="item">
Version {{ PACKAGE_VERSION }}
</p>
</div>
</div>
</template>
<script>
export default {
name: 'AppFooter',
computed: {
PACKAGE_VERSION: () => process.env.PACKAGE_VERSION || '0',
}
};
</script>
<template>
<div
id="app-header"
:class="$route.name"
>
<div class="menu container">
<div class="ui inverted icon menu">
<router-link
:is="isSharedProject ? 'span' : 'router-link'"
:to="isSharedProject ? '' : '/'"
:class="['header item', {disable: isSharedProject}]"
>
<img
class="ui right spaced image"
alt="Logo de l'application"
:src="logo"
>
<span class="desktop">
{{ APPLICATION_NAME }}
</span>
</router-link>
<div
v-if="width <= 560 || (width > 560 && project)"
id="menu-dropdown"
:class="['ui dropdown item', { 'active visible': menuIsOpen }]"
@click="menuIsOpen = !menuIsOpen"
>
<div
v-if="!isOnline"
class="crossed-out mobile"
>
<i
class="wifi icon"
aria-hidden="true"
/>
</div>
<span class="expand-center">
<span v-if="project"> Projet : {{ project.title }} </span>
</span>
<i
class="dropdown icon"
aria-hidden="true"
/>
<div
:class="[
'menu dropdown-list transition',
{ 'visible': menuIsOpen },
]"
style="z-index: 401"
>
<router-link
v-if="project"
:to="{
name: 'project_detail',
params: { slug: project.slug },
}"
class="item"
>
<i
class="home icon"
aria-hidden="true"
/>Accueil
</router-link>
<router-link
v-if="project"
:to="{
name: 'liste-signalements',
params: { slug: project.slug },
}"
class="item"
>
<i
class="list icon"
aria-hidden="true"
/>Liste & Carte
</router-link>
<router-link
v-if="
project && isOnline && hasAdminRights"
:to="{
name: 'project_mapping',
params: { slug: project.slug },
}"
class="item"
>
<i
class="map icon"
aria-hidden="true"
/>Fonds cartographiques
</router-link>
<router-link
v-if="
project && isOnline && hasAdminRights"
:to="{
name: 'project_members',
params: { slug: project.slug },
}"
class="item"
>
<i
class="users icon"
aria-hidden="true"
/>Membres
</router-link>
<div class="mobile">
<router-link
:is="isOnline ? 'router-link' : 'span'"
v-if="user"
:to="{
name: 'my_account',
params: { slug: isSharedProject && $route.params.slug ? $route.params.slug : null }
}"
class="item"
>
{{ userFullname || user.username || "Utilisateur inconnu" }}
</router-link>
<div
v-if="USER_LEVEL_PROJECTS && project"
class="item ui label vertical no-hover"
>
<!-- super user rights are higher than others -->
{{ user && user.is_superuser ? 'Administrateur' : USER_LEVEL_PROJECTS[project.slug] }}
<br>
</div>
<div
v-if="user && user.is_administrator"
class="item ui label vertical no-hover"
>
Gestionnaire métier
</div>
<div
v-if="!DISABLE_LOGIN_BUTTON"
>
<a
v-if="user"
class="item"
@click="logout"
><i
class="ui logout icon"
aria-hidden="true"
/>
</a>
<router-link
v-else-if="!user && !SSO_LOGIN_URL"
:to="{ name : 'login' }"
class="item"
>
Se connecter
</router-link>
<a
v-else
class="item"
:href="SSO_LOGIN_URL"
target="_self"
>Se connecter</a>
</div>
</div>
</div>
</div>
<div class="desktop flex push-right-desktop item title abstract">
<span>
{{ APPLICATION_ABSTRACT }}
</span>
</div>
<div class="desktop flex push-right-desktop">
<div
v-if="!isOnline"
class="item network-icon"
>
<span
data-tooltip="Vous êtes hors-ligne,
vos changements pourront être envoyés au serveur au retour de la connexion"
data-position="bottom right"
>
<div class="crossed-out">
<i
class="wifi icon"
aria-hidden="true"
/>
</div>
</span>
</div>
<router-link
:is="isOnline ? 'router-link' : 'span'"
v-if="user"
:to="{
name: 'my_account',
params: { slug: isSharedProject && $route.params.slug ? $route.params.slug : null }
}"
class="item"
>
{{ userFullname || user.username || "Utilisateur inconnu" }}
</router-link>
<div
v-if="USER_LEVEL_PROJECTS && project"
class="item ui label vertical no-hover"
>
<!-- super user rights are higher than others -->
{{ user && user.is_superuser ? 'Administrateur' : USER_LEVEL_PROJECTS[project.slug] }}
<br>
</div>
<div
v-if="user && user.is_administrator"
class="item ui label vertical no-hover"
>
Gestionnaire métier
</div>
<div
v-if="!DISABLE_LOGIN_BUTTON"
>
<a
v-if="user"
class="item log-item"
@click="logout"
><i
class="ui logout icon"
aria-hidden="true"
/>
</a>
<router-link
v-else-if="!user && !SSO_LOGIN_URL"
:to="{ name : 'login' }"
class="item log-item"
>
Se Connecter
</router-link>
<a
v-else
class="item log-item"
:href="SSO_LOGIN_URL"
target="_self"
>Se connecter</a>
</div>
</div>
</div>
<MessageInfoList />
</div>
</div>
</template>
<script>
import { mapState } from 'vuex';
import MessageInfoList from '@/components/MessageInfoList';
export default {
name: 'AppHeader',
components: {
MessageInfoList
},
data() {
return {
menuIsOpen: false,
width: window.innerWidth > 0 ? window.innerWidth : screen.width,
};
},
computed: {
...mapState([
'user',
'USER_LEVEL_PROJECTS',
'configuration',
'loader',
'isOnline'
]),
...mapState('projects', [
'projects',
'project',
]),
APPLICATION_NAME() {
return this.configuration.VUE_APP_APPLICATION_NAME;
},
APPLICATION_ABSTRACT() {
return this.configuration.VUE_APP_APPLICATION_ABSTRACT;
},
DISABLE_LOGIN_BUTTON() {
return this.configuration.VUE_APP_DISABLE_LOGIN_BUTTON;
},
SSO_LOGIN_URL() {
if (this.configuration.VUE_APP_LOGIN_URL) {
// add a next parameter with the pathname as expected by OGS to redirect after login
return `${this.configuration.VUE_APP_LOGIN_URL}?next=${encodeURIComponent(window.location.pathname)}`;
}
return null;
},
logo() {
return this.configuration.VUE_APP_LOGO_PATH;
},
userFullname() {
if (this.user.first_name || this.user.last_name) {
return `${this.user.first_name} ${this.user.last_name}`;
}
return null;
},
isAdmin() {
return this.USER_LEVEL_PROJECTS &&
this.USER_LEVEL_PROJECTS[this.project.slug] === 'Administrateur projet'
? true
: false;
},
hasAdminRights() {
return this.user && (this.user.is_administrator || this.user.is_superuser) || this.isAdmin;
},
isSharedProject() {
return this.$route.path.includes('projet-partage');
}
},
created() {
window.addEventListener('mousedown', this.clickOutsideMenu);
},
beforeDestroy() {
window.removeEventListener('mousedown', this.clickOutsideMenu);
},
methods: {
logout() {
this.$store.dispatch('LOGOUT');
},
clickOutsideMenu(e) {
if (e.target.closest && !e.target.closest('#menu-dropdown')) {
this.menuIsOpen = false;
}
},
}
};
</script>
<style lang="less" scoped>
.menu.container .header {
padding-top: 5px !important;
padding-bottom: 5px !important;
&> img {
max-height: 30px;
}
}
.vertical {
flex-direction: column;
justify-content: center;
}
.flex {
display: flex;
}
/* keep above loader */
#menu-dropdown {
z-index: 1001;
}
.expand-center {
width: 100%;
text-align: center;
}
.network-icon {
padding: .5rem !important;
}
.crossed-out {
position: relative;
padding: .2em;
&::before {
content: "";
position: absolute;
top: 45%;
left: -8%;
width: 100%;
border-top: 2px solid #ee2e24;
transform: rotate(45deg);
box-shadow: 0px 0px 0px 1px #373636;
border-radius: 3px;
}
}
@media screen and (max-width: 1110px) {
.abstract{
display: none !important;
}
}
@media screen and (min-width: 726px) {
.mobile {
display: none !important;
}
#app-header {
min-width: 560px;
}
.menu.container {
width: auto !important;
}
.push-right-desktop {
margin-left: auto;
}
}
@media screen and (max-width: 725px) {
.desktop {
display: none !important;
}
div.dropdown-list {
width: 100vw;
}
.menu.container .header {
//width: 70px;
width: 100%;
}
#app-header:not(.index) {
/* make the logo disappear on scroll */
position: sticky;
top: -90px;
height: 80px;
.menu.container {
/* make the logo disappear on scroll */
height: 30px;
position: sticky;
top: 0;
}
}
.menu.container .header > img {
margin: 0;
margin: auto;
max-width: 100%;
}
#menu-dropdown {
width: 100%;
}
#menu-dropdown > span {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
}
.menu.container {
position: relative;
}
.disable:hover {
cursor: default !important;
background-color: #373636 !important;
}
/* keep above map controls or buttons */
#app-header {
z-index: 9999;
.menu.container .ui.inverted.icon.menu { /* avoid adding space when messages are displayed */
margin: 0;
display: flex;
flex-wrap: wrap;
}
}
.ui.menu .ui.dropdown .menu {
.item.no-hover:hover {
cursor: auto !important;
background: white !important;
}
}
/* copy style to apply inside nested div */
.ui.menu .ui.dropdown .menu .item {
margin: 0;
text-align: left;
font-size: 1em !important;
padding: 0.78571429em 1.14285714em !important;
background: 0 0 !important;
color: #252525 !important;
text-transform: none !important;
font-weight: 400 !important;
box-shadow: none !important;
transition: none !important;
}
.item.title::before {
background: none !important;
}
.log-item {
height: 100% !important;
}
</style>
\ No newline at end of file
<template>
<div
id="custom-dropdown"
:id="`custom-dropdown${identifier}`"
:class="[
'ui fluid search selection dropdown',
'ui search selection dropdown',
{ 'active visible': isOpen },
{ disabled },
]"
......@@ -10,30 +10,54 @@
>
<input
v-if="search"
ref="input"
v-model="input"
class="search"
autocomplete="off"
tabindex="0"
@input="handelInput"
v-on:keyup.enter="select(0)"
v-model="input"
:placeholder="placeholder"
/>
<div v-if="!placeholder" class="default text">{{ selected }}</div>
:placeholder="placehold"
@input="isOpen = true"
@keyup.enter="select(0)"
@keyup.esc="toggleDropdown(false)"
>
<div
v-if="!input"
class="default text"
>
<div v-if="Array.isArray(selected)">
<span v-if="selected[0]"> {{ selected[0] }} - </span>
<span class="italic">{{ selected[1] }}</span>
</div>
<div v-else>
{{ selectedDisplay }}
</div>
</div>
<i
:class="['dropdown icon', { clear: search && selected }]"
:class="['dropdown icon', { clear: clearable && selected }]"
aria-hidden="true"
@click="clear"
></i>
/>
<div :class="['menu', { 'visible transition': isOpen }]">
<div
v-for="(option, index) in processedOptions || ['No results found.']"
@click="select(index)"
v-for="(option, index) in filteredOptions || ['No results found.']"
:id="option.name && Array.isArray(option.name) ? option.name[0] : option.name || option"
:key="option + index"
:class="[
processedOptions ? 'item' : 'message',
{ 'active selected': option === selected },
filteredOptions ? 'item' : 'message',
{ 'active selected': option.name === selected || option.id === selected },
]"
@click="select(index)"
>
{{ option }}
<div v-if="option.name && Array.isArray(option.name)">
<span v-if="option.name[0]"> {{ option.name[0] }} - </span>
<span class="italic">{{ option.name[1] }}</span>
</div>
<span v-else-if="option.name">
{{ option.name }}
</span>
<span v-else>
{{ option }}
</span>
</div>
</div>
</div>
......@@ -41,67 +65,131 @@
<script>
export default {
name: "Dropdown",
name: 'Dropdown',
props: ["options", "selected", "disabled", "search", "placeholder"],
computed: {
processedOptions: function () {
let options = this.options.map((el) =>
el.constructor == Object ? el.name : el
);
if (this.search && this.input !== "") {
options = this.searchOptions(options);
}
return options.length > 0 ? options : null;
props: {
clearable: {
type: Boolean,
default: null,
},
disabled: {
type: Boolean,
default: null,
},
options: {
type: Array,
default: null,
},
placeholder: {
type: String,
default: null,
},
selected: {
type: [String, Array],
default: null,
},
search: {
type: Boolean,
default: null,
},
},
data() {
return {
isOpen: false,
input: "",
input: '',
identifier: 0,
};
},
computed: {
filteredOptions: function () {
let options = this.options;
if (this.search && this.input !== '') {
options = this.options.filter(this.matchInput);
}
return options.length > 0 ? options : null;
},
placehold() {
return this.input ? '' : this.placeholder;
},
selectedDisplay() { // for project attributes, option are object and selected is an id
if (this.options[0] && this.options[0].name) {
const option = this.options.find(opt => opt.id === this.selected);
if (option) return option.name;
}
return this.selected;
}
},
created() {
const crypto = window.crypto || window.msCrypto;
const array = new Uint32Array(1);
this.identifier = Math.floor(crypto.getRandomValues(array) * 10000);
window.addEventListener('mousedown', this.clickOutsideDropdown);
},
beforeDestroy() {
window.removeEventListener('mousedown', this.clickOutsideDropdown);
},
methods: {
toggleDropdown() {
this.isOpen = !this.isOpen;
toggleDropdown(val) {
if (this.isOpen) { //* if dropdown is open :
this.input = ''; // * -> clear input field when closing dropdown
} else if (this.search) { //* if dropdown is closed is a search dropdown:
this.$refs.input.focus({ //* -> focus on input field
preventScroll: true,
});
} else if (this.clearable && val.target && this.selected) {
this.clear(); //* clear selected and input
}
this.isOpen = typeof val === 'boolean' ? val : !this.isOpen;
},
select(index) {
// * toggle dropdown is called several time, timeout delay this function to be the last
setTimeout(() => {
this.isOpen = false; // * quick & dirty, car toggle dropdown est rappelé plusieurs fois aileurs, à creuser...
}, 500);
this.$emit("update:selection", this.options[index]);
this.input = "";
this.isOpen = false;
}, 0);
if (this.filteredOptions) {
this.$emit('update:selection', this.filteredOptions[index]);
}
this.input = '';
},
searchOptions(options) {
return options.filter((el) =>
el.toLowerCase().includes(this.input.toLowerCase())
);
matchInput(el) {
let match;
if (el.name && Array.isArray(el.name)) {
match =
el.name[0].toLowerCase().includes(this.input.toLowerCase()) ||
el.name[1].toLowerCase().includes(this.input.toLowerCase());
} else {
match = el.name
? el.name.toLowerCase().includes(this.input.toLowerCase())
: el.toLowerCase().includes(this.input.toLowerCase());
}
return match;
},
clear() {
this.input = "";
this.clearSelected();
},
clearSelected() {
this.$emit("update:selection", "");
},
handelInput() {
this.isOpen = true;
this.clearSelected();
if (this.clearable && this.selected) {
this.input = '';
this.$emit('update:selection', '');
if (this.isOpen) {
this.toggleDropdown(false);
}
}
},
clickOutsideDropdown(e) {
if (!e.target.closest("#custom-dropdown")) this.isOpen = false;
if (!e.target.closest(`#custom-dropdown${this.identifier}`) && this.isOpen) {
this.toggleDropdown(false);
}
},
},
created() {
window.addEventListener("mousedown", this.clickOutsideDropdown);
},
beforeDestroy() {
window.removeEventListener("mousedown", this.clickOutsideDropdown);
},
};
</script>
......@@ -109,4 +197,7 @@ export default {
.ui.selection.dropdown .menu > .item {
white-space: nowrap;
}
</style>
\ No newline at end of file
.italic {
font-style: italic;
}
</style>
This diff is collapsed.
<template>
<div>
<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
"
alt="Pièce jointe au signalement"
>
</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>
<em v-if="attachments.length === 0">
Aucune pièce jointe associée au signalement.
</em>
</div>
</template>
<script>
export default {
name: 'FeatureAttachements',
props: {
attachments: {
type: Array,
default: () => {
return [];
}
}
}
};
</script>
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
<template>
<div class="ui mini modal">
<i class="close icon"></i>
<i
class="close icon"
aria-hidden="true"
/>
<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">
<form
id="form-geo-image"
class="ui form"
enctype="multipart/form-data"
>
{% csrf_token %}
<p>
Attention, si vous avez déjà saisi une géométrie, celle issue de
......@@ -13,19 +20,28 @@
<div class="field">
<label>Image (png ou jpeg)</label>
<label class="ui icon button" for="image_file">
<i class="file icon"></i>
<label
class="ui icon button"
for="image_file"
>
<i
class="file icon"
aria-hidden="true"
/>
<span class="label">Sélectionner une image ...</span>
</label>
<input
id="image_file"
type="file"
accept="image/jpeg, image/png"
style="display: none"
name="image_file"
class="image_file"
id="image_file"
>
<p
class="error-message"
style="color: red"
/>
<p class="error-message" style="color: red"></p>
</div>
<button
......@@ -34,7 +50,10 @@
class="ui positive right labeled icon button"
>
Importer
<i class="checkmark icon"></i>
<i
class="checkmark icon"
aria-hidden="true"
/>
</button>
</form>
</div>
......@@ -43,6 +62,6 @@
<script>
export default {
name: "Feature_edit_modal"
}
</script>
\ No newline at end of file
name: 'FeatureEditModal'
};
</script>
<template>
<div
id="status"
class="field"
>
<Dropdown
v-if="selectedStatus"
:options="allowedStatusChoices"
:selected="selectedStatus.name"
:selection.sync="selectedStatus"
/>
</div>
</template>
<script>
import Dropdown from '@/components/Dropdown.vue';
import { statusChoices, allowedStatus2change } from '@/utils';
import { mapState } from 'vuex';
export default {
name: 'FeatureEditStatusField',
components: {
Dropdown,
},
props: {
status: {
type: String,
default: '',
},
},
computed: {
...mapState([
'user',
'USER_LEVEL_PROJECTS',
]),
...mapState('projects', [
'project'
]),
...mapState('feature', [
'currentFeature'
]),
statusObject() {
return statusChoices.find((key) => key.value === this.status);
},
selectedStatus: {
get() {
return this.statusObject;
},
set(newValue) {
this.$store.commit('feature/UPDATE_FORM_FIELD', { name: 'status', value: newValue.value });
},
},
isFeatureCreator() {
if (this.currentFeature && this.currentFeature.properties && this.user) {
return this.currentFeature.properties.creator === this.user.id ||
this.currentFeature.properties.creator.username === this.user.username;
}
return false;
},
allowedStatusChoices() {
if (this.project && this.currentFeature && this.user) {
const isModerate = this.project.moderation;
const userStatus = this.USER_LEVEL_PROJECTS && this.USER_LEVEL_PROJECTS[this.project.slug];
return allowedStatus2change(this.user, isModerate, userStatus, this.isFeatureCreator);
}
return [];
},
},
};
</script>
\ No newline at end of file
<template>
<router-link
:is="query && Number.isInteger(query.offset) ? 'router-link' : 'span'"
:to="{
name: 'details-signalement-filtre',
params: { slug },
query,
}"
>
{{ properties.title || featureId }}
</router-link>
</template>
<script>
import { mapState } from 'vuex';
import axios from '@/axios-client.js';
import projectAPI from '@/services/project-api';
export default {
name: 'FeatureFetchOffsetRoute',
props: {
featureId: {
type: String,
default: '',
},
properties: {
type: Object,
default: () => {},
},
},
data() {
return {
position: null,
slug: this.$route.params.slug || this.properties.project_slug,
ordering: null,
filter: null,
};
},
computed: {
...mapState('projects', [
'project'
]),
query() {
if (this.ordering) {
const searchParams = { ordering: this.ordering };
if (this.filter === 'feature_type_slug') { // when feature_type is the default filter of the project,
searchParams['feature_type_slug'] = this.properties.feature_type.slug; // get its slug for the current feature
}
if (Number.isInteger(this.position)) {
searchParams['offset'] = this.position; // get its slug for the current feature
}
return searchParams;
}
return null;
},
},
watch: {
featureId() {
this.initData();
}
},
created() {
this.initData();
},
methods: {
async initData() {
if (this.project) {
this.setProjectParams(this.project);
} else {
await this.getProjectFilterAndSort();
}
this.getFeaturePosition(this.featureId)
.then((position) => {
if (Number.isInteger(position)) {
this.position = position;
}
})
.catch((error) => {
console.error(error);
});
},
setProjectParams(project) {
this.ordering = project.feature_browsing_default_sort;
if (project.feature_browsing_default_filter === 'feature_type_slug') { // when feature_type is the default filter of the project,
this.filter = this.properties.feature_type.slug;
}
},
async getFeaturePosition(featureId) {
const searchParams = new URLSearchParams(this.query);
const response = await axios.get(`${this.$store.state.configuration.VUE_APP_DJANGO_API_BASE}projects/${this.slug}/feature/${featureId}/position-in-list/?${searchParams.toString()}`);
if (response && response.status === 200) {
return response.data;
}
return null;
},
async getProjectFilterAndSort() {
const project = await projectAPI.getProject(this.$store.state.configuration.VUE_APP_DJANGO_API_BASE, this.slug);
if (project) this.setProjectParams(project);
}
}
};
</script>
This diff is collapsed.