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
Commits on Source (51)
Showing
with 520 additions and 176 deletions
# intro
ceci permet de faire tourner le front en local sur /geocontrib
et de faire pointer /api sur n'importe quel backend (dev, local ou autre )
# configuration apache
dans la configuration apache generale (httpd.conf ou commande a2enmod ), activer les modules :
* mod_headers
* mod_proxy
* mod_ssl
* mod_proxy_http
```
<Location /geocontrib >
ProxyPass http://localhost:8080/geocontrib
</Location>
SSLProxyEngine On
<Location /api >
ProxyPass https://geocontrib.dev.neogeo.fr/geocontrib/api
RequestHeader set Referer https://geocontrib.dev.neogeo.fr/
</Location>
```
# configuration projet vueJS
remplacer dans le fichier config.json du projet
```
DOMAIN":"http://localhost:8010/", par "DOMAIN":"http://localhost/",
```
et
```
"VUE_APP_DJANGO_API_BASE":"http://localhost:8010/api/", par "VUE_APP_DJANGO_API_BASE":"http://localhost/api/",
```
......@@ -27,6 +27,18 @@ server {
proxy_pass http://geocontrib_site;
}
location /geocontrib/cas {
proxy_pass_header Set-Cookie;
proxy_set_header X-NginX-Proxy true;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
proxy_read_timeout 300s;
proxy_redirect off;
proxy_pass http://geocontrib_site;
}
location /geocontrib/admin {
proxy_pass_header Set-Cookie;
proxy_set_header X-NginX-Proxy true;
......
{
"name": "geocontrib-frontend",
"version": "2.3.2-rc2",
"version": "2.3.2",
"private": true,
"scripts": {
"serve": "npm run init-proxy & npm run init-serve",
......
{
"BASE_URL":"/geocontrib/",
"DOMAIN":"http://localhost:8010/",
"DOMAIN":"http://localhost/",
"NODE_ENV":"development",
"VUE_APP_LOCALE":"fr-FR",
"VUE_APP_APPLICATION_NAME":"GéoContrib",
......
......@@ -152,13 +152,14 @@
<main>
<div id="content" class="ui stackable grid centered container">
<transition name="fadeDownUp">
<div v-if="messages && messages.length > 0" class="row">
<div v-if="messages && messages.length > 0" class="row over-content">
<div class="fourteen wide column">
<div
v-for="(message, index) in messages"
:key="'message-' + index"
class="ui info message"
:class="['ui', message.level ? message.level : 'info', 'message']"
>
<i class="close icon" @click="DISCARD_MESSAGE(message)"></i>
<div class="header">
<i class="info circle icon"></i> Informations
</div>
......@@ -193,8 +194,7 @@
<script>
import frag from "vue-frag";
import { mapState } from "vuex";
import { mapGetters } from "vuex";
import { mapMutations, mapState, mapGetters } from "vuex";
export default {
name: "App",
......@@ -249,6 +249,7 @@ export default {
},
methods: {
...mapMutations(['DISCARD_MESSAGE']),
logout() {
this.$store.dispatch("LOGOUT");
},
......@@ -376,23 +377,10 @@ footer {
transition: none !important;
}
.bounce-enter-active {
animation: bounce-in .5s;
}
.bounce-leave-active {
animation: bounce-in .5s reverse;
}
@keyframes bounce-in {
0% {
transform: scale(0);
}
50% {
transform: scale(1.5);
}
100% {
transform: scale(1);
}
.ui.grid > .row.over-content {
position: absolute;
z-index: 99;
opacity: 0.95;
}
.fadeDownUp-enter-active {
......@@ -424,5 +412,16 @@ footer {
}
}
.ui.message > .close.icon {
cursor: pointer;
position: absolute;
margin: 0em;
top: 0.78575em;
right: 0.5em;
opacity: 0.7;
-webkit-transition: opacity 0.1s ease;
transition: opacity 0.1s ease;
}
</style>
\ No newline at end of file
const faIcons = [
'circle',
'address-book',
'address-card',
'adjust',
......
......@@ -298,8 +298,7 @@ const mapUtil = {
const currentValue = properties[colorsStyle.custom_field_name];
const colorStyle = colorsStyle.colors[currentValue];
return colorStyle ? colorStyle : featureType.color
}
else{
} else {
return featureType.color;
}
},
......@@ -310,7 +309,12 @@ const mapUtil = {
vectorTileLayerStyles: {
"default": (properties) => {
const featureType = featureTypes.find((x) => x.slug.split('-')[0] === '' + properties.feature_type_id);
const color = this.retrieveFeatureColor(featureType, properties)
const color = this.retrieveFeatureColor(featureType, properties);
const colorValue =
color.value && color.value.length ?
color.value : typeof color === 'string' && color.length ?
color : '#000000';
const hiddenStyle = ({
radius: 0,
......@@ -318,7 +322,17 @@ const mapUtil = {
weight: 0,
fill: false,
color: featureType.color,
})
});
const defaultStyle = {
radius: 4,
fillOpacity: 0.5,
weight: 3,
fill: true,
color: colorValue,
};
// Filtre sur le feature type
if (form_filters && form_filters.type.selected) {
if (featureType.title !== form_filters.type.selected) {
......@@ -337,13 +351,7 @@ const mapUtil = {
return hiddenStyle;
}
}
return ({
radius: 4,
fillOpacity: 0.5,
weight: 3,
fill: true,
color: color,
});
return defaultStyle;
},
},
// subdomains: "0123",
......@@ -398,7 +406,10 @@ const mapUtil = {
if (color == undefined){
color = featureType.color;
}
const colorValue = color.value ? color.value : color;
const colorValue =
color.value && color.value.length ?
color.value : typeof color === 'string' && color.length ?
color : '#000000';
if (geomJSON.type === 'Point') {
if (
customFieldOption &&
......@@ -407,7 +418,10 @@ const mapUtil = {
featureType.colors_style.value.icons &&
!!Object.keys(featureType.colors_style.value.icons).length
) {
if (featureType.colors_style.value.icons[customFieldOption]) {
if (
featureType.colors_style.value.icons[customFieldOption] &&
featureType.colors_style.value.icons[customFieldOption] !== 'circle'
) {
const iconHTML = `
<i
class="fas fa-${featureType.colors_style.value.icons[customFieldOption]} fa-lg"
......@@ -428,16 +442,16 @@ const mapUtil = {
.addTo(featureGroup);
} else {
L.circleMarker(geomJSON.coordinates, {
color: color,
color: colorValue,
radius: 4,
fillOpacity: 0.5,
weight: 3,
})
.bindPopup(popupContent)
.addTo(featureGroup);
.bindPopup(popupContent)
.addTo(featureGroup);
}
} else {
if (featureType.icon) {
if (featureType.icon && featureType.icon !== 'circle') {
const iconHTML = `
<i
class="fas fa-${featureType.icon} fa-lg"
......
......@@ -10,6 +10,13 @@ main {
padding: 2em 0em;
}
/* ---------------------------------- */
/* UTILS */
/* ---------------------------------- */
.ellipsis {
text-overflow: ellipsis;
overflow: hidden;
}
/* ---------------------------------- */
/* MAIN */
/* ---------------------------------- */
......
......@@ -46,7 +46,7 @@
>
<i
v-on:click="fetchImports()"
:class="['orange icon', ready ? 'sync' : 'hourglass half']"
:class="['orange icon', ready && !reloading ? 'sync' : 'hourglass half rotate']"
/>
</span>
</td>
......@@ -68,7 +68,7 @@ export default {
};
},
props: ["data"],
props: ["data", "reloading"],
filters: {
setDate: function (value) {
......@@ -122,23 +122,19 @@ export default {
padding-top: 1em;
}
@keyframes rotateIn {
from {
transform: rotate3d(0, 0, 1, -200deg);
opacity: 0;
}
to {
transform: translate3d(0, 0, 0);
opacity: 1;
}
i.icon {
width: 20px !important;
height: 20px !important;
}
.rotateIn {
animation-name: rotateIn;
transform-origin: center;
animation: 2s;
.rotate {
-webkit-animation:spin 1s linear infinite;
-moz-animation:spin 1s linear infinite;
animation:spin 1s linear infinite;
}
@-moz-keyframes spin { 100% { -moz-transform: rotate(360deg); } }
@-webkit-keyframes spin { 100% { -webkit-transform: rotate(360deg); } }
@keyframes spin { 100% { -webkit-transform: rotate(360deg); transform:rotate(360deg); } }
/*
Max width before this PARTICULAR table gets nasty
......
<template>
<div data-tab="list" class="dataTables_wrapper no-footer">
<table id="table-features" class="ui compact table">
<table id="table-features" class="ui compact table dataTable">
<thead>
<tr>
<th class="center"></th>
<th class="dt-center">
<div @click="switchMode" class="switch-buttons pointer" :data-tooltip="`Passer en mode ${mode === 'modify' ? 'suppression':'édition'}`">
<div><i :class="['icon pencil', {disabled: mode !== 'modify'}]"></i></div>
<span class="grey">|&nbsp;</span>
<div><i :class="['icon trash', {disabled: mode !== 'delete'}]"></i></div>
</div>
</th>
<th class="dt-center">
<div class="pointer" @click="changeSort('status')">
<th class="center">
Statut
<i
:class="{
......@@ -14,21 +22,23 @@
up: isSortedDesc('status'),
}"
class="icon sort"
@click="changeSort('status')"
/>
</div>
</th>
<th class="center">
Type
<i
:class="{
down: isSortedAsc('feature_type'),
up: isSortedDesc('feature_type'),
}"
class="icon sort"
@click="changeSort('feature_type')"
/>
<th class="dt-center">
<div class="pointer" @click="changeSort('feature_type')">
Type
<i
:class="{
down: isSortedAsc('feature_type'),
up: isSortedDesc('feature_type'),
}"
class="icon sort"
/>
</div>
</th>
<th class="center">
<th class="dt-center">
<div class="pointer" @click="changeSort('title')">
Nom
<i
:class="{
......@@ -36,10 +46,11 @@
up: isSortedDesc('title'),
}"
class="icon sort"
@click="changeSort('title')"
/>
</div>
</th>
<th class="center">
<th class="dt-center">
<div class="pointer" @click="changeSort('updated_on')">
Dernière modification
<i
:class="{
......@@ -47,62 +58,57 @@
up: isSortedDesc('updated_on'),
}"
class="icon sort"
@click="changeSort('updated_on')"
/>
</div>
</th>
<th class="center" v-if="user">
Auteur
<i
:class="{
down: isSortedAsc('display_creator'),
up: isSortedDesc('display_creator'),
}"
class="icon sort"
@click="changeSort('display_creator')"
/>
<th class="dt-center" v-if="user">
<div class="pointer" @click="changeSort('display_creator')">
Auteur
<i
:class="{
down: isSortedAsc('display_creator'),
up: isSortedDesc('display_creator'),
}"
class="icon sort"
/>
</div>
</th>
<th class="center" v-if="user">
Dernier éditeur
<i
:class="{
down: isSortedAsc('display_last_editor'),
up: isSortedDesc('display_last_editor'),
}"
class="icon sort"
@click="changeSort('display_last_editor')"
/>
<th class="dt-center" v-if="user">
<div class="pointer" @click="changeSort('display_last_editor')">
Dernier éditeur
<i
:class="{
down: isSortedAsc('display_last_editor'),
up: isSortedDesc('display_last_editor'),
}"
class="icon sort"
/>
</div>
</th>
</tr>
</thead>
<tbody>
<tr v-for="(feature, index) in paginatedFeatures" :key="index">
<td class="center">
<td class="dt-center">
<div
class="ui checkbox"
:class="
feature.properties.creator.username !== user.username &&
!user.is_superuser &&
!isUserProjectAdministrator
? 'disabled'
: ''
"
:class="['ui checkbox', {disabled: !checkRights(feature)}]"
>
<input
type="checkbox"
v-model="checked"
@input="storeClickedFeature(feature)"
:id="feature.id"
:value="feature.id"
v-model="checked"
:disabled="
feature.properties.creator.username !== user.username &&
!user.is_superuser &&
!isUserProjectAdministrator
"
:disabled="!checkRights(feature)"
name="select"
/>
<label></label>
<label for="select"></label>
</div>
<!-- {{canDeleteFeature(feature)}}
{{canEditFeature(feature)}} -->
</td>
<td class="center">
<td class="dt-center">
<div
v-if="feature.properties.status.value === 'archived'"
data-tooltip="Archivé"
......@@ -128,7 +134,7 @@
<i class="orange pencil alternate icon"></i>
</div>
</td>
<td class="center">
<td class="dt-center">
<router-link
:to="{
name: 'details-type-signalement',
......@@ -140,7 +146,7 @@
{{ feature.properties.feature_type.title }}
</router-link>
</td>
<td class="center">
<td class="dt-center">
<router-link
:to="{
name: 'details-signalement',
......@@ -152,13 +158,13 @@
>{{ getFeatureDisplayName(feature) }}</router-link
>
</td>
<td class="center">
<td class="dt-center">
{{ feature.properties.updated_on }}
</td>
<td class="center" v-if="user">
<td class="dt-center" v-if="user">
{{ getUserName(feature) }}
</td>
<td class="center" v-if="user">
<td class="dt-center" v-if="user">
{{ feature.properties.display_last_editor }}
</td>
</tr>
......@@ -261,18 +267,17 @@ export default {
props: [
"paginatedFeatures",
"checkedFeatures",
"clickedFeatures",
"featuresCount",
"pagination",
"sort",
"mode"
],
computed: {
...mapState(["user"]),
...mapGetters(["project", "permissions"]),
isUserProjectAdministrator() {
return this.permissions.is_project_administrator;
},
...mapState(["user", "USER_LEVEL_PROJECTS"]),
checked: {
get() {
......@@ -304,16 +309,18 @@ export default {
},
displayedPageNumbers() {
//* s'il y a moins de 5 pages, renvoyer toutes les pages
if (this.lastPageNumber < 5) return this.pageNumbers
//* si la page courante est inférieur à 5, la liste commence à l'index 0 et on retourne 5 pages
let firstPageInList = 0;
let pagesQuantity = 5;
//* à partir de la 5ième page et jusqu'à la 4ième page avant la fin : n'afficher que 3 page entre les ellipses et la page courante doit être au milieu
if (this.pagination.currentPage >= 5 && !(this.lastPageNumber - this.pagination.currentPage < 4)) {
//* à partir de la 5ième page et jusqu'à la 4ième page avant la fin : n'afficher que 3 page entre les ellipses et la page courante doit être au milieu
if (this.pagination.currentPage >= 5 && !((this.lastPageNumber - this.pagination.currentPage) < 4)) {
firstPageInList = this.pagination.currentPage - 2;
pagesQuantity = 3
}
//* a partir de 4 résultat avant la fin afficher seulement les 5 derniers résultats
if (this.lastPageNumber - this.pagination.currentPage < 4) {
//* à partir de 4 résultat avant la fin afficher seulement les 5 derniers résultats
if ((this.lastPageNumber - this.pagination.currentPage) < 4) {
firstPageInList = this.lastPageNumber - 5;
}
return this.pageNumbers.slice(firstPageInList, firstPageInList + pagesQuantity);
......@@ -321,6 +328,50 @@ export default {
},
methods: {
storeClickedFeature(feature) {
this.clickedFeatures.push({feature_id: feature.id, feature_type: feature.properties.feature_type.slug})
},
canDeleteFeature(feature) {
return feature.properties.creator.username !== this.user.username &&
!this.user.is_superuser &&
!this.permissions.is_project_administrator
},
canEditFeature(feature) {
const userStatus = this.USER_LEVEL_PROJECTS[this.project.slug];
const access = {
"Administrateur projet" : ["draft", "published", "archived"],
"Modérateur" : ["pending"],
"Super Contributeur" : ["draft", this.project.moderation ? "pending" : "published"],
"Contributeur" : ["draft", this.project.moderation ? "pending" : "published"],
};
//if (userStatus === "Super Contributeur" || userStatus === "Contributeur") { //? should super contributeur behave the same, I don't think so
if (userStatus === "Contributeur" && feature.properties.creator.username !== this.user.username) {
return false;
} else if (access[userStatus]) {
return access[userStatus].includes(feature.properties.status.value);
} else {
return false
}
},
checkRights(feature) {
switch (this.mode) {
case 'modify':
return this.canEditFeature(feature)
case 'delete':
return this.canDeleteFeature(feature)
}
},
switchMode() {
this.$emit('update:mode', this.mode === 'modify' ? 'delete' : 'modify');
this.$emit('update:clickedFeatures', []);
this.$store.commit("feature/UPDATE_CHECKED_FEATURES", []);
},
getUserName(feature) {
if (!feature.properties.creator) {
return " ---- ";
......@@ -352,6 +403,9 @@ export default {
}
},
},
destroyed() {
this.$store.commit("feature/UPDATE_CHECKED_FEATURES", []);
},
};
</script>
......@@ -361,6 +415,9 @@ export default {
position: relative;
clear: both;
}
table.dataTable th.dt-center, table.dataTable td.dt-center, table.dataTable td.dataTables_empty {
text-align: center;
}
.dataTables_wrapper .dataTables_length,
.dataTables_wrapper .dataTables_filter,
.dataTables_wrapper .dataTables_info,
......@@ -443,7 +500,23 @@ export default {
i.icon.sort:not(.down):not(.up) {
color: rgb(220, 220, 220);
}
.pointer:hover {
cursor: pointer;
}
.switch-buttons {
display: flex;
justify-content: center;
align-items: baseline;
}
.grey {
color: #bbbbbb;
}
.ui.dropdown .menu .left.menu, .ui.dropdown > .left.menu .menu {
margin-right: 0 !important;
}
/*
Max width before this PARTICULAR table gets nasty
This query will take effect for any screen smaller than 760px
......@@ -513,8 +586,8 @@ and also iPads specifically.
content: "Auteur";
}
.center {
text-align: right !important;
table.dataTable th.dt-center, table.dataTable td.dt-center, table.dataTable td.dataTables_empty {
text-align: right;
}
#table-features {
......
......@@ -14,7 +14,7 @@
v-model.lazy="form.color.value"
/>
</div>
<div class="required inline field">
<!-- <div class="required inline field">
<label>Symbole</label>
<button
class="ui icon button picker-button"
......@@ -27,7 +27,7 @@
class="icon alt"
/>
</button>
</div>
</div> -->
</div>
<div
:class="isIconPickerModalOpen ? 'active' : ''"
......
......@@ -14,7 +14,7 @@ if (workbox) {
// Since we have a SPA here, this should be index.html always.
// https://stackoverflow.com/questions/49963982/vue-router-history-mode-with-pwa-in-offline-mode
workbox.routing.registerNavigationRoute('/geocontrib/index.html', {
blacklist: [/\/api/,/\/admin/,/\/media/],
blacklist: [/\/api/,/\/admin/,/\/media/,/\/cas/],
})
workbox.routing.registerRoute(
......
......@@ -62,6 +62,21 @@ const featureAPI = {
}
},
async updateFeature({ feature_id, feature_type__slug, project__slug, newStatus }) {
let url = `${baseUrl}features/${feature_id}/?feature_type__slug=${feature_type__slug}&project__slug=${project__slug}`
const response = await axios({
url,
method: "PATCH",
data: { id: feature_id, status: newStatus, feature_type: feature_type__slug }
})
if (response.status === 200 && response.data) {
return response;
} else {
return null;
}
},
async postComment({ featureId, comment }) {
const response = await axios.post(
`${baseUrl}features/${featureId}/comments/`, { comment }
......
import axios from "@/axios-client.js";
import store from '../store'
const baseUrl = store.state.configuration.VUE_APP_DJANGO_API_BASE;
const featureTypeAPI = {
async deleteFeatureType(featureType_slug) {
const response = await axios.delete(
`${baseUrl}feature-types/${featureType_slug}`
);
if (
response.status === 204
) {
return 'success'
} else {
return null;
}
},
}
export default featureTypeAPI;
......@@ -35,6 +35,17 @@ const projectAPI = {
return null;
}
},
async deleteProject(projectSlug) {
const response = await axios.delete(
`${baseUrl}projects/${projectSlug}`
);
if ( response.status === 204 ) {
return 'success';
} else {
return null;
}
},
}
export default projectAPI;
......@@ -93,13 +93,16 @@ export default new Vuex.Store({
SET_EVENTS(state, events) {
state.events = events;
},
DISPLAY_MESSAGE(state, comment) {
state.messages = [{ comment }, ...state.messages];
DISPLAY_MESSAGE(state, message) {
state.messages = [message, ...state.messages];
if (document.getElementById("content")) document.getElementById("content").scrollIntoView({ block: "start", inline: "nearest" });
setTimeout(() => {
state.messages = [];
}, 3000);
},
DISCARD_MESSAGE(state, message) {
state.messages = state.messages.filter((el) => el.comment !== message.comment)
},
CLEAR_MESSAGES(state) {
state.messages = [];
},
......
......@@ -172,7 +172,6 @@ const feature = {
SEND_FEATURE({ state, rootState, commit, dispatch }, routeName) {
commit("DISPLAY_LOADER", "Le signalement est en cours de création", { root: true })
const message = routeName === "editer-signalement" ? "Le signalement a été mis à jour" : "Le signalement a été crée";
function redirect(featureId) {
dispatch(
'GET_PROJECT_FEATURE',
......@@ -188,7 +187,7 @@ const feature = {
params: {
slug_type_signal: rootState.feature_type.current_feature_type_slug,
slug_signal: featureId,
message,
message: routeName === "editer-signalement" ? "Le signalement a été mis à jour" : "Le signalement a été crée"
},
});
dispatch("GET_ALL_PROJECTS", null, {root:true}) //* & refresh project list
......@@ -201,30 +200,32 @@ const feature = {
redirect(featureId);
}
//* prepare feature data to send
let extraFormObject = {}; //* prepare an object to be flatten in properties of geojson
for (const field of state.extra_form) {
extraFormObject[field.name] = field.value;
}
const geojson = {
"id": state.form.feature_id,
"type": "Feature",
"geometry": state.form.geometry,
"properties": {
"title": state.form.title,
"description": state.form.description.value,
"status": state.form.status.value,
"project": rootState.project_slug,
"feature_type": rootState.feature_type.current_feature_type_slug,
...extraFormObject
function createGeojson() { //* prepare feature data to send
let extraFormObject = {}; //* prepare an object to be flatten in properties of geojson
for (const field of state.extra_form) {
extraFormObject[field.name] = field.value;
}
return {
"id": state.form.feature_id,
"type": "Feature",
"geometry": state.form.geometry,
"properties": {
"title": state.form.title,
"description": state.form.description.value,
"status": state.form.status.value,
"project": rootState.project_slug,
"feature_type": rootState.feature_type.current_feature_type_slug,
...extraFormObject
}
}
}
const geojson = createGeojson();
let url = `${rootState.configuration.VUE_APP_DJANGO_API_BASE}features/`
if (routeName === "editer-signalement") {
url += `${state.form.feature_id}/?` +
`feature_type__slug=${rootState.feature_type.current_feature_type_slug}` +
`&project__slug=${rootState.project_slug}`
url += `${state.form.feature_id}/?
feature_type__slug=${rootState.feature_type.current_feature_type_slug}
&project__slug=${rootState.project_slug}`
}
return axios({
......@@ -250,7 +251,7 @@ const feature = {
}
let updateMsg = {
project: rootState.project_slug,
type: 'put',
type: routeName === "editer-signalement" ? "put" : "post",
featureId: state.form.feature_id,
geojson: geojson
};
......
......@@ -194,7 +194,7 @@ const feature_type = {
})
.then((response) => {
if (response && response.status === 200) {
dispatch("GET_IMPORTS", {
return dispatch("GET_IMPORTS", {
feature_type: feature_type_slug
});
}
......@@ -214,12 +214,13 @@ const feature_type = {
if (feature_type) {
url = url.concat('', `${url.includes('?') ? '&' : '?'}feature_type_slug=${feature_type}`);
}
axios
return axios
.get(url)
.then((response) => {
if (response) {
commit("SET_IMPORT_FEATURE_TYPES_DATA", response.data);
}
return response;
})
.catch((error) => {
throw (error);
......
......@@ -504,7 +504,7 @@ export default {
},
confirmComment() {
this.$store.commit("DISPLAY_MESSAGE", "Ajout du commentaire confirmé");
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 = "";
......
......@@ -36,18 +36,19 @@
feature_types.length > 0 &&
permissions.can_create_feature
"
id="button-dropdown"
class="item right"
>
<div
@click="showAddFeature = !showAddFeature"
@click="toggleAddFeature"
class="ui dropdown button compact button-hover-green"
data-tooltip="Ajouter un signalement"
data-position="bottom left"
data-position="bottom right"
>
<i class="plus fitted icon"></i>
<div
v-if="showAddFeature"
class="menu transition visible"
class="menu left transition visible"
style="z-index: 9999"
>
<div class="header">Ajouter un signalement du type</div>
......@@ -67,13 +68,40 @@
</div>
</div>
<div
v-if="checkedFeatures.length"
v-if="checkedFeatures.length > 0 && mode === 'modify'"
@click="toggleModifyStatus"
class="ui dropdown button compact button-hover-green margin-left-25"
data-tooltip="Modifier le statut des Signalements"
data-position="bottom right"
>
<i class="pencil fitted icon"></i>
<div
v-if="showModifyStatus"
class="menu left transition visible"
style="z-index: 9999"
>
<div class="header">Modifier le statut des Signalements</div>
<div class="scrolling menu text-wrap">
<span
v-for="status in availableStatus"
:key="status.value"
@click="modifyStatus(status.value)"
class="item"
>
{{ status.name }}
</span>
</div>
</div>
</div>
<div
v-if="checkedFeatures.length > 0 && mode === 'delete'"
@click="modalAllDelete"
class="ui button compact button-hover-red margin-left-25"
data-tooltip="Effacer tous les types de signalements sélectionnés"
data-position="left center"
data-variation="mini"
data-position="bottom right"
>
<i class="grey trash fitted icon"></i>
</div>
......@@ -136,12 +164,15 @@
<SidebarLayers v-if="basemaps && map" />
</div>
<!-- | -->
<!-- v-on:update:clickedFeatures="handleClickedFeatures" -->
<FeatureListTable
v-show="!showMap"
v-on:update:page="handlePageChange"
v-on:update:sort="handleSortChange"
:paginatedFeatures="paginatedFeatures"
:checkedFeatures.sync="checkedFeatures"
:checkedFeatures="checkedFeatures"
:clickedFeatures.sync="clickedFeatures"
:mode.sync="mode"
:featuresCount="featuresCount"
:pagination="pagination"
:sort="sort"
......@@ -228,7 +259,9 @@ export default {
title: null,
},
baseUrl: this.$store.state.configuration.BASE_URL,
clickedFeatures: [],
modalAllDeleteOpen: false,
mode: "modify",
map: null,
zoom: null,
lat: null,
......@@ -250,6 +283,7 @@ export default {
},
showMap: true,
showAddFeature: false,
showModifyStatus: false,
};
},
......@@ -298,6 +332,7 @@ export default {
},
computed: {
...mapState(["user", "USER_LEVEL_PROJECTS"]),
...mapGetters([
'project', 'permissions'
]),
......@@ -322,6 +357,58 @@ export default {
);
},
availableStatus() { //* attente de réponse sur le ticket
//return this.statusChoices.filter((status) => status)
if (this.project) {
const isModerate = this.project.moderation;
const userStatus = this.USER_LEVEL_PROJECTS[this.project.slug];
const isOwnFeature = this.feature
? this.feature.creator === this.user.id //* prevent undefined feature
: false; //* si le contributeur est l'auteur du signalement
if (
//* si admin, modérateur ou super contributeur, statuts toujours disponible: Brouillon, Publié, Archivé
userStatus === "Administrateur projet" ||
(userStatus === "Super Contributeur" && !isModerate)
) {
return this.statusChoices.filter((el) => el.value !== "pending");
} else if (userStatus === "Super Contributeur" && isModerate) {
return this.statusChoices.filter(
(el) => el.value === "draft" || el.value === "pending"
);
} else if (userStatus === "Modérateur") {
return this.statusChoices.filter(
(el) => el.value === "draft" || el.value === "published"
);
} else if (userStatus === "Contributeur") {
//* cas particuliers du contributeur
if (
this.currentRouteName === "ajouter-signalement" ||
!isOwnFeature
) {
//* même cas à l'ajout d'une feature ou si feature n'a pas été créé par le contributeur
return isModerate
? this.statusChoices.filter(
(el) => el.value === "draft" || el.value === "pending"
)
: this.statusChoices.filter(
(el) => el.value === "draft" || el.value === "published"
);
} else {
//* à l'édition d'une feature et si le contributeur est l'auteur de la feature
return isModerate
? this.statusChoices.filter(
(el) => el.value !== "published" //* toutes sauf "Publié"
)
: this.statusChoices.filter(
(el) => el.value !== "pending" //* toutes sauf "En cours de publication"
);
}
}
}
return [];
},
featureTypeChoices() {
return this.feature_types.map((el) => el.title);
},
......@@ -329,15 +416,76 @@ export default {
methods: {
...mapActions('feature', [
'GET_PROJECT_FEATURES'
'GET_PROJECT_FEATURES',
'SEND_FEATURE'
]),
toggleAddFeature() {
this.showAddFeature = !this.showAddFeature;
this.showModifyStatus = false;
},
toggleModifyStatus() {
this.showModifyStatus = !this.showModifyStatus;
this.showAddFeature = false;
},
modalAllDelete() {
this.modalAllDeleteOpen = !this.modalAllDeleteOpen;
},
clickOutsideDropdown(e) {
if (!e.target.closest("#button-dropdown")) {
this.showModifyStatus = false;
setTimeout(() => { //* timout necessary to give time to click on link to add feature
this.showAddFeature = false;
}, 500);
}
},
async modifyStatus(newStatus) {
let errorCount = 0
const promises = this.checkedFeatures.map((feature_id) => {
let feature = this.clickedFeatures.find((el) => el.feature_id === feature_id)
if (feature) {
return featureAPI.updateFeature({
feature_id,
feature_type__slug: feature.feature_type,
project__slug: this.$route.params.slug, newStatus
})
} else {
errorCount += 1;
}
})
const promisesResult = await Promise.all(promises)
promisesResult.forEach((response) => {
if (response && response.data && response.status === 200) {
this.checkedFeatures.splice(this.checkedFeatures.indexOf(response.data.id), 2);
} else {
errorCount += 1;
}
})
let message = {
comment: "Tous les signalements ont été modifié avec succès.",
level: "positive"
}
if (errorCount) {
//* display error message
if(errorCount === 1) {
message.comment = "Un signalement n'a pas pu être modifié. (Il reste sélectionné)"
} else {
message.comment = `${errorCount} signalements n'ont pas pu être modifiés. (Ils restent sélectionnés)`
}
message.level = "negative"
}
this.fetchPagedFeatures();
this.$store.commit("DISPLAY_MESSAGE", message);
},
deleteFeature(feature_id) {
const url = `${this.API_BASE_URL}features/${feature_id}/?project__slug=${this.project.slug}`;
axios
axios //TODO: REFACTO -> Delete function already exist in store
.delete(url, {})
.then(() => {
if (!this.modalAllDeleteOpen) {
......@@ -346,7 +494,6 @@ export default {
})
.then(() => {
this.fetchPagedFeatures();
this.getNloadGeojsonFeatures();
this.checkedFeatures.splice(feature_id);
});
}
......@@ -359,8 +506,8 @@ export default {
deleteAllFeatureSelection() {
let feature = {};
this.checkedFeatures.forEach((feature_id) => {
feature = { feature_id: feature_id };
this.deleteFeature(feature.feature_id);
feature = { feature_id: feature_id }; // ? Is this usefull ?
this.deleteFeature(feature.feature_id); //? since property feature_id is directly used after...
});
this.modalAllDelete();
},
......@@ -461,8 +608,8 @@ export default {
fetchPagedFeatures(newUrl) {
this.onFilterChange(); //* use paginated event to watch change in filters and modify features on map
let url = `${this.API_BASE_URL}projects/${this.$route.params.slug}/feature-paginated/?output=geojson&limit=${this.pagination.pagesize}&offset=${this.pagination.start}`;
//* if receiving next & previous url
if (newUrl && typeof newUrl === "string") {
//* if receiving next & previous url
//newUrl = newUrl.replace("8000", "8010"); //* for dev uncomment to use proxy link
url = newUrl;
}
......@@ -550,9 +697,11 @@ export default {
this.initMap();
}
this.fetchPagedFeatures();
window.addEventListener("mousedown", this.clickOutsideDropdown);
},
destroyed() {
window.removeEventListener("mousedown", this.clickOutsideDropdown);
//* allow user to change page if ever stuck on loader
this.$store.commit("DISCARD_LOADER");
},
......@@ -570,10 +719,6 @@ export default {
z-index: 1;
}
.center {
text-align: center !important;
}
#feature-list-container {
justify-content: flex-start;
}
......@@ -600,20 +745,23 @@ export default {
padding: 0 !important;
}
.ui.dropdown .menu .left.menu, .ui.dropdown > .left.menu .menu {
margin-right: 0 !important;
}
@media screen and (min-width: 767px) {
.twelve-wide {
width: 75% !important;
}
}
@media screen and (max-width: 767px) {
#feature-list-container > .mobile-fullwidth {
width: 100% !important;
}
.no-margin-mobile {
margin: 0 !important;
}
.no-padding-mobile {
padding-top: 0 !important;
padding-bottom: 0 !important;
......@@ -621,10 +769,12 @@ export default {
.mobile-column {
flex-direction: column !important;
}
#button-dropdown {
transform: translate(-50px, -60px);
}
#form-filters > .field.column {
width: 100% !important;
}
.map-container {
width: 100%;
}
......