Skip to content
Snippets Groups Projects
Commit bb625ce7 authored by Timothee P's avatar Timothee P :sunflower:
Browse files

Merge branch 'develop' into redmine-issues/12779

parents 60f11dde e6035bd1
No related branches found
No related tags found
No related merge requests found
Showing
with 551 additions and 179 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",
......
......@@ -155,13 +155,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
......@@ -197,8 +198,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",
......@@ -256,6 +256,7 @@ export default {
},
methods: {
...mapMutations(['DISCARD_MESSAGE']),
logout() {
this.$store.dispatch("LOGOUT");
},
......@@ -383,23 +384,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 {
......@@ -431,6 +419,17 @@ 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>
<style scoped>
......
......@@ -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",
......
......@@ -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%;
}
......
......@@ -2,7 +2,7 @@
<div v-if="structure" class="row">
<div class="five wide column">
<div class="ui attached secondary segment">
<h1 class="ui center aligned header">
<h1 class="ui center aligned header ellipsis">
<img
v-if="structure.geom_type === 'point'"
class="ui medium image"
......@@ -72,7 +72,7 @@
:class="loadingImportFile ? 'loading' : ''"
>
<div class="field">
<label class="ui icon button" for="json_file">
<label class="ui icon button ellipsis" for="json_file">
<i class="file icon"></i>
<span class="label">{{ fileToImport.name }}</span>
</label>
......@@ -100,6 +100,7 @@
<ImportTask
v-if="importFeatureTypeData && importFeatureTypeData.length"
:data="importFeatureTypeData"
:reloading="reloadingImport"
/>
</div>
</div>
......@@ -149,6 +150,15 @@
Pour suivre le statut de l'import, cliquez sur "Importer des Signalements".
</p>
</div>
<div
v-else-if="waitMessage"
class="ui message info"
>
<p>
L'import des signalements a été lancé.
Vous pourrez suivre le statut de l'import dans quelques instants...
</p>
</div>
<div
v-for="(feature, index) in lastFeatures"
:key="feature.feature_id + index"
......@@ -243,7 +253,9 @@ export default {
},
showImport: false,
featuresLoading: true,
loadingImportFile: false
loadingImportFile: false,
waitMessage: false,
reloadingImport: false,
};
},
......@@ -260,6 +272,9 @@ export default {
'project',
'permissions'
]),
...mapState([
'reloadIntervalId'
]),
...mapState('feature', [
'features',
'features_count'
......@@ -307,9 +322,27 @@ export default {
if (newValue.slug){
this.GET_IMPORTS({
feature_type: this.$route.params.feature_type_slug
});
})
}
}
},
importFeatureTypeData: {
deep: true,
handler(newValue) {
if (newValue && newValue.some(el => el.status === 'pending')) {
setTimeout(() => {
this.reloadingImport = true;
this.GET_IMPORTS({
feature_type: this.$route.params.feature_type_slug
}).then(()=> {
setTimeout(() => {
this.reloadingImport = false;
}, 1000);
})
}, this.$store.state.configuration.VUE_APP_RELOAD_INTERVAL);
}
}
},
},
created() {
......@@ -341,9 +374,8 @@ export default {
toggleShowImport() {
this.showImport = !this.showImport;
if (this.showImport) {
this.$store.dispatch("feature_type/GET_IMPORTS", {
feature_type: this.$route.params.feature_type_slug
});
this.GET_IMPORTS({
feature_type: this.$route.params.feature_type_slug });
}
},
......@@ -432,11 +464,14 @@ export default {
},
importGeoJson() {
this.waitMessage = true;
this.$store.dispatch('feature_type/SEND_FEATURES_FROM_GEOJSON', {
slug: this.$route.params.slug,
feature_type_slug: this.$route.params.feature_type_slug,
fileToImport: this.fileToImport,
});
}).then(() => {
this.waitMessage = false;
})
},
exportFeatures() {
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment