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 (189)
Showing
with 532 additions and 237 deletions
......@@ -5,8 +5,6 @@ stages:
- deploy
variables:
SONAR_PROJECTKEY: "$CI_PROJECT_NAME"
SONAR_HOST_URL: "https://sonarqube.neogeo.fr"
GIT_DEPTH: 0
test build:
......@@ -83,4 +81,4 @@ sonarqube-check:
- develop
stage: Static analysis
script:
- sonar-scanner -Dsonar.qualitygate.wait=true -Dsonar.projectKey=$CI_PROJECT_NAME -Dsonar.projectName=$CI_PROJECT_NAME -Dsonar.projectVersion=$CI_COMMIT_BRANCH
- sonar-scanner -Dsonar.qualitygate.wait=true -Dsonar.projectKey=id-$CI_PROJECT_ID -Dsonar.projectName="$CI_PROJECT_PATH" -Dsonar.projectVersion=$CI_COMMIT_BRANCH -Dsonar.coverage.exclusions=**/src/**/*
......@@ -35,7 +35,12 @@ NODE_ENV=development
"VUE_APP_RELOAD_INTERVAL": 15000,
"VUE_APP_DISABLE_LOGIN_BUTTON":false,
"VUE_APP_LOGIN_URL":"",
"VUE_APP_SSO_LOGIN_URL_WITH_REDIRECT":"",
"VUE_APP_FONT_FAMILY":"",
"VUE_APP_HEADER_COLOR":"",
"VUE_APP_PRIMARY_COLOR":"",
"VUE_APP_PRIMARY_HIGHLIGHT_COLOR":"",
"VUE_APP_PROJECT_FILTERS": "access_level,user_access_level,moderation,search",
"DEFAULT_BASE_MAP_SCHEMA_TYPE": "tms",
"DEFAULT_BASE_MAP_SERVICE": "https://{s}.tile.openstreetmap.fr/osmfr/{z}/{x}/{y}.png",
"DEFAULT_BASE_MAP_OPTIONS": {
......@@ -47,16 +52,30 @@ NODE_ENV=development
"center": [47.0, 1.0],
"zoom": 4
},
"GEOCODER_PROVIDERS" : {
"ADDOK": "addok",
"NOMINATIM": "nominatim",
"PHOTON": "photon"
},
"SELECTED_GEOCODER" : {
"PROVIDER": "addok"
"MAP_PREVIEW_CENTER": [43.60882653785945, 1.447888090697796],
"DISPLAY_FORBIDDEN_PROJECTS": true,
"DISPLAY_FORBIDDEN_PROJECTS_DEFAULT": true
}
```
### Configuration additionnelle pour geOrchestra
Afin de mieux s'intégrer au SDI geOrchestra, il est possible de modifier le header pour afficher celui de geOrchestra.
Dans le fichier config.json, ajouter :
```json
{
"GEORCHESTRA_INTEGRATION": {
"HEADER": {
"LEGACY_HEADER": false,
"LEGACY_URL": "/header/",
"STYLE": "",
"STYLESHEET": "",
"HEADER_SCRIPT": ""
}
}
}
```
### Utilisation sans installation du backend
Il est possible d'utiliser un serveur HTTP local pour utiliser l'API des instances en ligne, sans avoir de problèmes de CORS, pour cela se référer fichier : conf_apache_dev.md
......@@ -125,6 +144,77 @@ docker-compose up -d
### Version
Node => v14.18.2
### Changement par rapport au projet django
- Base.html => App.vue
- Découpage en composants
<br>
# Système de notifications
## Vue d'ensemble
Notre système de notifications est conçu pour informer les utilisateurs des événements significatifs au sein de leurs projets, tels que la création, la mise à jour et la suppression de signalements, de commentaires et de pièces jointes. Ce système est configurable, permettant d'adapter les notifications aux besoins spécifiques des projets et aux préférences des utilisateurs.
### Caractéristiques principales
- **Modèles personnalisables** : Vous pouvez personnaliser le contenu des notifications selon vos besoins directement via l'interface d'administration, grâce à des modèles modifiables.
## Types de notifications
### Notifications groupées
- **Objectif** : Ces notifications visent à informer les abonnés de tous les événements importants au sein des projets, incluant les mises à jour des signalements, les publications de commentaires ou de documents et les modifications majeures des projets.
- **Déclencheur** : Les notifications sont envoyées périodiquement selon la configuration de la tâche périodique associée.
- **Caractéristiques Configurables** :
- **Niveau d'envoi des notifications** : Les administrateurs peuvent configurer l'envoi des notifications pour les documents clés à un niveau globale ou par projet.
- **Désactivation des notifications** : Vous pouvez désactiver les notifications pour un type de signalement via l'interface d'administration ou dans l'application frontend. L'envoi des notifications de publication de documents clés ne sont pas impactés par ce pramétrage.
### Notifications de publication de documents clés
- **Objectif** : S'assurer que tous les abonnés d'un projet sont alertés lors de la publication de documents jugés essentiels ou critiques.
- **Déclencheur** : Les notifications sont envoyées périodiquement selon la configuration de la tâche périodique associée.
- **Fonctionnement** : Lors de la publication ou de l'édition d'un signalement, ainsi que lors de l'ajout de commentaires avec pièces jointes, les utilisateurs verront une option `Envoyer une notification de publication`. Cette option est décochée par défaut pour éviter les envois non désirés. En cochant cette option lors de la publication d'une pièce jointe, un événement `Document clé` est généré. Cet événement est lié à la notification spécifique qui sera envoyée aux abonnés du projet.
- **Caractéristiques Configurables** :
- **Activation des Notifications** : Les administrateurs peuvent activer les notifications pour les publications de documents clés directement lors de la création d'un type de signalement en sélectionnant l'option `Activer la notification de publication de pièces jointes`, désactivée par défaut. L'activation de cette notification peut également être modifiée ultérieurement dans l'interface d'édition du type de signalement. Ceci est accessible via l'icône de pinceau sur la page d'accueil du projet.
### Notifications pour signalements en attente de modération
- **Objectif** : Informer les modérateurs des projets lorsque des signalements requièrent leur attention pour validation, dans le contexte des projets ayant activé la modération.
- **Fonctionnement** : Les notifications sont déclenchées automatiquement lorsque le statut d'un signalement passe à "En attente de publication", nécessitant une action de la part des modérateurs.
### Notifications de publication de signalements après modération
- **Objectif** : Communiquer avec l'auteur d'un signalement pour l'informer que sa contribution a été approuvée et publiée par un modérateur.
- **Fonctionnement** : Ces notifications sont envoyées lorsqu'un signalement change de statut de "En attente de publication" à "Publié", fournissant un retour immédiat et valorisant pour les contributeurs
<br>
<br>
DEVELOPPEMENT
=============
Général
-------
Geocontrib est un projet initié par NeoGeo.
Le code source de l'application est maintenu sur la plateforme https://git.neogeo.fr/geocontrib/.
les Mainteneurs actuels :
- Timothée POUSSARD (Neogeo)
- Camille BLANCHON (Neogeo)
- Matthieu ETOURNEAU (Neogeo)
- Angela Escobar (Neogeo)
La documentation de l'application
*********************************
https://www.onegeosuite.fr/docs/team_geocontrib
Pratiques et règles de developpement
------------------------------------
Afin de partager des règles communes de développement et faciliter l'intégration de
nouveau code, veuillez lire les recommandations et bonnes pratiques recommandées pour contribuer
au projet GeoContrib.
Git
***
- Faire une demande de contribution en envoyant un mail à metourneau@neogeo.fr
- Un compte vous sera créé sur notre plateforme gitlab
- Faire un fork de l'application
- Faire des merge requests vers la branch ``develop``
- Faire des ``git pull`` avant chaque développement et avant chaque commit
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
[
'@babel/preset-env',
{
targets: {
esmodules: true
}
}
]
]
};
{
"name": "geocontrib-frontend",
"version": "6.2.0-rc1",
"version": "6.4.5-rc6",
"private": true,
"scripts": {
"serve": "npm run init-proxy & npm run init-serve",
......@@ -16,12 +16,13 @@
"@fortawesome/free-solid-svg-icons": "^5.15.4",
"@fortawesome/vue-fontawesome": "^2.0.6",
"@mapbox/vector-tile": "^1.3.1",
"@sentry/vue": "^9.10.1",
"axios": "^0.21.1",
"core-js": "^3.20.2",
"csvtojson": "^2.0.10",
"lodash": "^4.17.21",
"ol": "6.8.1",
"ol-mapbox-style": "^6.8.3",
"ol": "^9.1.0",
"ol-mapbox-style": "^12.3.0",
"pbf": "^3.2.1",
"qrcode": "^1.5.1",
"register-service-worker": "^1.7.1",
......@@ -34,13 +35,16 @@
"vuex": "^3.6.2"
},
"devDependencies": {
"@babel/core": "^7.25.2",
"@babel/eslint-parser": "^7.15.8",
"@babel/preset-env": "^7.25.4",
"@fortawesome/fontawesome-free": "^5.15.4",
"@vue/cli-plugin-babel": "~4.5.0",
"@vue/cli-plugin-eslint": "^5.0.0-beta.6",
"@vue/cli-plugin-pwa": "~4.5.0",
"@vue/cli-plugin-router": "~4.5.0",
"@vue/cli-service": "~4.5.0",
"babel-loader": "^9.2.1",
"eslint": "^7.32.0",
"eslint-plugin-vue": "^7.20.0",
"less": "^3.0.4",
......
......@@ -17,6 +17,7 @@
"VUE_APP_FONT_FAMILY":"",
"VUE_APP_HEADER_COLOR":"",
"VUE_APP_PRIMARY_COLOR":"",
"VUE_APP_PRIMARY_HIGHLIGHT_COLOR" : "",
"DEFAULT_BASE_MAP_SCHEMA_TYPE": "tms",
"DEFAULT_BASE_MAP_SERVICE": "https://{s}.tile.openstreetmap.fr/osmfr/{z}/{x}/{y}.png",
"DEFAULT_BASE_MAP_OPTIONS": {
......@@ -28,18 +29,8 @@
"zoom": 4
},
"MAP_PREVIEW_CENTER" : [43.60882653785945, 1.447888090697796],
"GEOCODER_PROVIDERS" : {
"ADDOK": "addok",
"NOMINATIM": "nominatim",
"PHOTON": "photon"
},
"SELECTED_GEOCODER" : {
"PROVIDER": "addok"
},
"DISPLAY_FORBIDDEN_PROJECTS": true,
"DISPLAY_FORBIDDEN_PROJECTS_DEFAULT": true,
"VUE_APP_URL_DOCUMENTATION": "https://www.onegeosuite.fr/docs/module-geocontrib/intro",
"VUE_APP_URL_DOCUMENTATION_FEATURE": "https://www.onegeosuite.fr/docs/module-geocontrib/project_settings"
}
\ No newline at end of file
public/img/favicon_gc.png

122 KiB | W: 0px | H: 0px

public/img/favicon_gc.png

1.12 KiB | W: 0px | H: 0px

public/img/favicon_gc.png
public/img/favicon_gc.png
public/img/favicon_gc.png
public/img/favicon_gc.png
  • 2-up
  • Swipe
  • Onion skin
<template>
<div id="app">
<AppHeader />
<GeorchestraHeader
v-if="isGeorchestra"
:key="$route.fullPath"
/>
<AppHeader v-else />
<div id="app-content">
<span id="scroll-top-anchor" />
......@@ -26,11 +31,13 @@ import { mapState } from 'vuex';
import AppHeader from '@/components/AppHeader';
import AppFooter from '@/components/AppFooter';
import GeorchestraHeader from '@/components/GeorchestraHeader.vue';
export default {
name: 'App',
components: {
GeorchestraHeader,
AppHeader,
AppFooter,
},
......@@ -54,7 +61,10 @@ export default {
...mapState('projects', [
'projects',
'project',
])
]),
isGeorchestra() {
return this.configuration.GEORCHESTRA_INTEGRATION?.HEADER;
},
},
};
</script>
......@@ -63,4 +73,4 @@ export default {
.ui.active.dimmer {
position: fixed;
}
</style>
\ No newline at end of file
</style>
......@@ -20033,10 +20033,10 @@ ol.ui.list li[value]:before {
padding: 1em 1.5em;
line-height: 1.4285em;
color: #252525;
-webkit-transition: opacity .5s ease, color .5s ease, background .5s ease, -webkit-box-shadow .5s ease;
transition: opacity .5s ease, color .5s ease, background .5s ease, -webkit-box-shadow .5s ease;
transition: opacity .5s ease, color .5s ease, background .5s ease, box-shadow .5s ease;
transition: opacity .5s ease, color .5s ease, background .5s ease, box-shadow .5s ease, -webkit-box-shadow .5s ease;
-webkit-transition: padding .5s ease, max-height .5s ease, opacity .5s ease, color .5s ease, background .5s ease, -webkit-box-shadow .5s ease;
transition: padding .5s ease, max-height .5s ease, opacity .5s ease, color .5s ease, background .5s ease, -webkit-box-shadow .5s ease;
transition: padding .5s ease, max-height .5s ease, opacity .5s ease, color .5s ease, background .5s ease, box-shadow .5s ease;
transition: padding .5s ease, max-height .5s ease, opacity .5s ease, color .5s ease, background .5s ease, box-shadow .5s ease, -webkit-box-shadow .5s ease;
border-radius: .07142857rem;
-webkit-box-shadow: 0 0 0 1px rgba(34, 36, 38, .22) inset, 0 0 0 0 transparent;
box-shadow: 0 0 0 1px rgba(34, 36, 38, .22) inset, 0 0 0 0 transparent
......
......@@ -408,6 +408,8 @@ body {
cursor: pointer;
z-index: 9;
background-color: #fff;
padding: 0 4px;
text-align: center;
}
.multiselect__spinner {
......@@ -440,11 +442,6 @@ body {
color: #35495e !important;
}
.multiselect__clear {
padding: 0 4px;
text-align: center;
}
.multiselect__clear i.icon {
font-size: .75em;
color: #999;
......
......@@ -47,8 +47,6 @@
}
.ol-popup #popup-content #customFields h5 {
max-height: 20;
}
.ol-popup #popup-content #customFields h5 {
margin: .5em 0;
}
......
......@@ -54,7 +54,10 @@
v-if="qrcode"
class="qrcode"
>
<img :src="qrcode">
<img
:src="qrcode"
alt="qrcode"
>
<p>
Ce QR code vous permet de vous connecter à l'application mobile GéoContrib (bientôt disponible)
</p>
......
......@@ -76,9 +76,7 @@
</router-link>
<router-link
v-if="
project && isOnline &&
(user.is_administrator || user.is_superuser || isAdmin)
"
project && isOnline && hasAdminRights"
:to="{
name: 'project_mapping',
params: { slug: project.slug },
......@@ -92,9 +90,7 @@
</router-link>
<router-link
v-if="
project && isOnline &&
(user.is_administrator || user.is_superuser || isAdmin)
"
project && isOnline && hasAdminRights"
:to="{
name: 'project_members',
params: { slug: project.slug },
......@@ -123,7 +119,7 @@
class="item ui label vertical no-hover"
>
<!-- super user rights are higher than others -->
{{ user.is_superuser ? 'Administrateur' : USER_LEVEL_PROJECTS[project.slug] }}
{{ user && user.is_superuser ? 'Administrateur' : USER_LEVEL_PROJECTS[project.slug] }}
<br>
</div>
<div
......@@ -155,6 +151,7 @@
v-else
class="item"
:href="SSO_LOGIN_URL"
target="_self"
>Se connecter</a>
</div>
</div>
......@@ -168,7 +165,7 @@
<div class="desktop flex push-right-desktop">
<div
v-if="!isOnline"
class="item"
class="item network-icon"
>
<span
data-tooltip="Vous êtes hors-ligne,
......@@ -199,7 +196,7 @@
class="item ui label vertical no-hover"
>
<!-- super user rights are higher than others -->
{{ user.is_superuser ? 'Administrateur' : USER_LEVEL_PROJECTS[project.slug] }}
{{ user && user.is_superuser ? 'Administrateur' : USER_LEVEL_PROJECTS[project.slug] }}
<br>
</div>
<div
......@@ -231,6 +228,7 @@
v-else
class="item log-item"
:href="SSO_LOGIN_URL"
target="_self"
>Se connecter</a>
</div>
</div>
......@@ -276,13 +274,17 @@ export default {
return this.configuration.VUE_APP_APPLICATION_NAME;
},
APPLICATION_ABSTRACT() {
return this.$store.state.configuration.VUE_APP_APPLICATION_ABSTRACT;
return this.configuration.VUE_APP_APPLICATION_ABSTRACT;
},
DISABLE_LOGIN_BUTTON() {
return this.configuration.VUE_APP_DISABLE_LOGIN_BUTTON;
},
SSO_LOGIN_URL() {
return this.configuration.VUE_APP_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() {
......@@ -300,6 +302,9 @@ export default {
? true
: false;
},
hasAdminRights() {
return this.user && (this.user.is_administrator || this.user.is_superuser) || this.isAdmin;
},
isSharedProject() {
return this.$route.path.includes('projet-partage');
}
......@@ -356,6 +361,10 @@ export default {
text-align: center;
}
.network-icon {
padding: .5rem !important;
}
.crossed-out {
position: relative;
padding: .2em;
......
......@@ -82,8 +82,8 @@
>
<Dropdown
v-else-if="field && field.field_type === 'list'"
:options="field.options"
v-else-if="field && (field.field_type === 'list' || field.field_type === 'notif_group')"
:options="field.field_type === 'notif_group' ? usersGroupsFeatureOptions : field.options"
:selected="selected_extra_form_list"
:selection.sync="selected_extra_form_list"
:required="field.is_mandatory"
......@@ -158,7 +158,7 @@
</template>
<script>
import { mapState, mapActions, mapMutations } from 'vuex';
import { mapState, mapActions, mapMutations, mapGetters } from 'vuex';
import Multiselect from 'vue-multiselect';
import Dropdown from '@/components/Dropdown.vue';
......@@ -199,9 +199,14 @@ export default {
...mapState('feature', [
'extra_forms',
]),
...mapGetters(['usersGroupsFeatureOptions']),
selected_extra_form_list: {
get() {
if (this.field.field_type === 'notif_group') {
const usersGroup = this.usersGroupsFeatureOptions.find((group) => group.value === this.field.value);
return usersGroup ? usersGroup.name : '';
}
return this.field.value || '';
},
set(newValue) {
......@@ -210,7 +215,7 @@ export default {
this.$emit('update:value', newValue.id || newValue);
} else {
const newExtraForm = this.field;
newExtraForm['value'] = newValue;
newExtraForm['value'] = this.field.field_type === 'notif_group' ? newValue.value : newValue;
this.$store.commit('feature/UPDATE_EXTRA_FORM', newExtraForm);
}
},
......
......@@ -142,6 +142,21 @@
{{ comment_form.attachment_file.errors }}
</div>
</div>
<div
v-if="enableKeyDocNotif"
class="field"
>
<div class="ui checkbox">
<input
id="is_key_document"
v-model="comment_form.attachment_file.isKeyDocument"
class="hidden"
name="is_key_document"
type="checkbox"
>
<label for="is_key_document">Envoyer une notification de publication aux abonnés du projet</label>
</div>
</div>
<ul
v-if="comment_form.attachment_file.errors"
class="errorlist"
......@@ -181,6 +196,10 @@ export default {
default: () => {
return [];
}
},
enableKeyDocNotif: {
type: Boolean,
default: false,
}
},
......@@ -191,6 +210,7 @@ export default {
errors: null,
title: '',
file: null,
isKeyDocument: false
},
comment: {
id_for_label: 'add-comment',
......@@ -244,9 +264,12 @@ export default {
file: this.comment_form.attachment_file.file,
fileName: this.comment_form.attachment_file.fileName,
title: this.comment_form.attachment_file.title,
isKeyDocument: this.comment_form.attachment_file.isKeyDocument,
commentId: response.data.id,
})
.then(() => {
// Reset isKeyDocument to default
this.comment_form.attachment_file.isKeyDocument = false;
this.confirmComment();
});
} else {
......
......@@ -45,6 +45,9 @@
<span v-else-if="field.value && field.field_type === 'multi_choices_list'">
{{ field.value.join(', ') }}
</span>
<span v-else-if="field.value && field.field_type === 'notif_group'">
{{ usersGroupLabel(field) }}
</span>
<span v-else>
{{ field.value && field.value.label ? field.value.label : field.value }}
</span>
......@@ -80,6 +83,19 @@
</span>
</td>
</tr>
<tr v-if="project && project.feature_assignement">
<td>
Membre assigné
</td>
<td>
<ProjectMemberSelect
:selected-user-id="assignedMemberId"
:disabled="!fastEditionMode || !canEditFeature"
class="inline"
@update:user="$store.commit('feature/UPDATE_FORM_FIELD', { name: 'assigned_member', value: $event })"
/>
</td>
</tr>
<tr>
<td>
Date de création
......@@ -143,11 +159,12 @@
<script>
import { mapState } from 'vuex';
import { mapState, mapGetters } from 'vuex';
import FeatureTypeLink from '@/components/FeatureType/FeatureTypeLink';
import FeatureEditStatusField from '@/components/Feature/FeatureEditStatusField';
import ExtraForm from '@/components/ExtraForm';
import FeatureFetchOffsetRoute from '@/components/Feature/FeatureFetchOffsetRoute';
import ProjectMemberSelect from '@/components/ProjectMemberSelect';
import { statusChoices, formatStringDate, checkDeactivatedValues } from '@/utils';
......@@ -166,6 +183,7 @@ export default {
FeatureEditStatusField,
ExtraForm,
FeatureFetchOffsetRoute,
ProjectMemberSelect,
},
props: {
......@@ -184,12 +202,16 @@ export default {
},
computed: {
...mapState('projects', [
'project'
]),
...mapState('feature', [
'currentFeature',
'linked_features',
'form',
'extra_forms',
]),
...mapGetters(['usersGroupsFeatureOptions']),
statusIcon() {
switch (this.currentFeature.properties.status.value || this.currentFeature.properties.status) {
......@@ -236,6 +258,20 @@ export default {
featureFields() {
return this.fastEditionMode ? this.extra_forms : this.featureData;
},
assignedMemberId() {
if (this.form && this.form.assigned_member) {
return this.form.assigned_member.value;
}
return this.currentFeature.properties.assigned_member;
},
},
methods: {
usersGroupLabel(field) {
const usersGroup = this.usersGroupsFeatureOptions.find((group) => group.value === field.value);
return usersGroup ? usersGroup.name : field.value;
}
}
};
......
<template>
<div>
<div class="ui teal segment">
<h4>
Pièce jointe
<button
class="ui small compact right floated icon button remove-formset"
type="button"
@click="removeAttachmentFormset(form.dataKey)"
>
<i
class="ui times icon"
aria-hidden="true"
/>
</button>
</h4>
<!-- {{ form.errors }} -->
<div class="visible-fields">
<div class="two fields">
<div class="required field">
<label :for="form.title.id_for_label">{{ form.title.label }}</label>
<input
:id="form.title.id_for_label"
v-model="form.title.value"
type="text"
required
:maxlength="form.title.field.max_length"
:name="form.title.html_name"
<section class="ui teal segment">
<h4>
Pièce jointe
<button
class="ui small compact right floated icon button remove-formset"
type="button"
@click="removeAttachmentForm(form.dataKey)"
>
<i
class="ui times icon"
aria-hidden="true"
/>
</button>
</h4>
<div class="visible-fields">
<div class="two fields">
<div class="required field">
<label :for="form.title.id_for_label">{{ form.title.label }}</label>
<input
:id="form.title.id_for_label"
v-model="form.title.value"
type="text"
required
:maxlength="form.title.field.max_length"
:name="form.title.html_name"
>
<ul
:id="form.title.id_for_error"
class="errorlist"
>
<li
v-for="error in form.title.errors"
:key="error"
>
<ul
:id="form.title.id_for_error"
class="errorlist"
>
<li
v-for="error in form.title.errors"
:key="error"
>
{{ error }}
</li>
</ul>
</div>
<div class="required field">
<label>Fichier (PDF, PNG, JPEG)</label>
<label
class="ui icon button"
:for="'attachment_file' + attachmentForm.dataKey"
>
<i
class="file icon"
aria-hidden="true"
/>
<span
v-if="form.attachment_file.value"
class="label"
>{{
form.attachment_file.value
}}</span>
<span
v-else
class="label"
>Sélectionner un fichier ... </span>
</label>
<input
:id="'attachment_file' + attachmentForm.dataKey"
type="file"
accept="application/pdf, image/jpeg, image/png"
style="display: none"
:name="form.attachment_file.html_name"
@change="onFileChange"
>
<ul
:id="form.attachment_file.id_for_error"
class="errorlist"
{{ error }}
</li>
</ul>
</div>
<div class="required field">
<label>Fichier (PDF, PNG, JPEG)</label>
<label
class="ui icon button"
:for="'attachment_file' + attachmentForm.dataKey"
>
<i
class="file icon"
aria-hidden="true"
/>
<span
v-if="form.attachment_file.value"
class="label"
>{{
form.attachment_file.value
}}</span>
<span
v-else
class="label"
>Sélectionner un fichier ... </span>
</label>
<input
:id="'attachment_file' + attachmentForm.dataKey"
type="file"
accept="application/pdf, image/jpeg, image/png"
style="display: none"
:name="form.attachment_file.html_name"
@change="onFileChange"
>
<ul
:id="form.attachment_file.id_for_error"
class="errorlist"
>
<li
v-for="error in form.attachment_file.errors"
:key="error"
>
<li
v-for="error in form.attachment_file.errors"
:key="error"
>
{{ error }}
</li>
</ul>
</div>
{{ error }}
</li>
</ul>
</div>
<div class="field">
<label for="form.info.id_for_label">{{ form.info.label }}</label>
<textarea
v-model="form.info.value"
name="form.info.html_name"
rows="5"
/>
<!-- {{ form.info.errors }} -->
</div>
<div class="field">
<label for="form.info.id_for_label">{{ form.info.label }}</label>
<textarea
v-model="form.info.value"
name="form.info.html_name"
rows="5"
/>
</div>
<div
v-if="enableKeyDocNotif"
class="field"
>
<div class="ui checkbox">
<input
:id="'is_key_document-' + attachmentForm.dataKey"
v-model="form.is_key_document.value"
:name="'is_key_document-' + attachmentForm.dataKey"
class="hidden"
type="checkbox"
@change="updateStore"
>
<label :for="'is_key_document-' + attachmentForm.dataKey">
Envoyer une notification de publication aux abonnés du projet
</label>
</div>
</div>
</div>
</div>
</section>
</template>
<script>
export default {
name: 'FeatureAttachmentFormset',
name: 'FeatureAttachmentForm',
props: {
attachmentForm: {
type: Object,
default: null,
},
enableKeyDocNotif: {
type: Boolean,
default: false,
}
},
......@@ -138,6 +157,9 @@ export default {
errors: null,
label: 'Info',
},
is_key_document: {
value: false,
},
},
};
},
......@@ -180,7 +202,7 @@ export default {
}
},
removeAttachmentFormset() {
removeAttachmentForm() {
this.$store.commit(
'feature/REMOVE_ATTACHMENT_FORM',
this.attachmentForm.dataKey
......@@ -198,6 +220,7 @@ export default {
attachment_file: this.form.attachment_file.value,
info: this.form.info.value,
fileToImport: this.fileToImport,
is_key_document: this.form.is_key_document.value
};
this.$store.commit('feature/UPDATE_ATTACHMENT_FORM', data);
},
......
......@@ -634,10 +634,17 @@ export default {
return true;
},
/**
* Ensures the name entered in the form is unique among all custom field names.
* This function prevents duplicate names, including those with only case differences,
* to avoid conflicts during automatic view generation where names are slugified.
*
* @returns {boolean} - Returns true if the name is unique (case insensitive), false otherwise.
*/
checkUniqueName() {
const occurences = this.customForms
.map((el) => el.name)
.filter((el) => el === this.form.name.value);
.map((el) => el.name.toLowerCase())
.filter((el) => el === this.form.name.value.toLowerCase());
return occurences.length === 1;
},
......
<template>
<div>
<geor-header
:legacy-header="legacyHeader"
:legacy-url="legacyUrl"
:style="customStyle"
:logo-url="logo"
:stylesheet="customStylesheet"
:active-app="activeApp"
/>
</div>
</template>
<script>
import { mapState } from 'vuex';
export default {
name: 'GeorchestraHeader',
computed: {
...mapState([
'configuration'
]),
headerConfig() {
return this.configuration.GEORCHESTRA_INTEGRATION?.HEADER;
},
activeApp() {
return this.$route.path.includes('my_account') ? 'geocontrib-account' : 'geocontrib';
},
logo() {
return this.configuration.VUE_APP_LOGO_PATH;
},
legacyHeader() {
return this.headerConfig.LEGACY_HEADER;
},
legacyUrl() {
return this.headerConfig.LEGACY_URL;
},
customStyle() {
return this.headerConfig.STYLE;
},
customStylesheet() {
return this.headerConfig.STYLESHEET;
},
headerScript() {
return this.headerConfig.HEADER_SCRIPT;
},
},
mounted() {
const headerScript = document.createElement('script');
headerScript.setAttribute('src', this.headerScript ? this.headerScript : 'https://cdn.jsdelivr.net/gh/georchestra/header@dist/header.js');
document.head.appendChild(headerScript);
},
};
</script>
......@@ -2,20 +2,7 @@
<div class="editionToolbar">
<div class="leaflet-bar">
<a
v-if="showDrawTool || isEditing"
:class="{ active: isSnapEnabled }"
:title="`${ isSnapEnabled ? 'Désactiver' : 'Activer' } l'accrochage aux points`"
@click="toggleSnap"
>
<i
class="magnet icon"
aria-hidden="true"
/>
<span class="sr-only">{{ isSnapEnabled ? 'Désactiver' : 'Activer' }} l'accrochage aux points</span>
</a>
<a
v-if="showDrawTool"
v-if="noExistingFeature"
class="leaflet-draw-draw-polygon active"
:title="`Dessiner ${editionService.geom_type === 'linestring' ? 'une' : 'un'} ${toolbarGeomTitle}`
"
......@@ -24,36 +11,53 @@
v-if="editionService.geom_type === 'linestring'"
class="list-image-type"
src="@/assets/img/line.png"
alt="line"
>
<img
v-if="editionService.geom_type === 'point'"
class="list-image-type"
src="@/assets/img/marker.png"
alt="marker"
>
<img
v-if="editionService.geom_type === 'polygon'"
class="list-image-type"
src="@/assets/img/polygon.png"
alt="polygon"
>
</a>
<div v-else>
<a
:class="{ active: isEditing }"
:title="`Modifier ${toolbarEditGeomTitle}`"
@click="update"
>
<i class="edit outline icon" />
<span class="sr-only">Modifier {{ toolbarEditGeomTitle }}</span>
</a>
<a
:title="`Supprimer ${toolbarEditGeomTitle}`"
@click="deleteObj"
>
<i class="trash alternate outline icon" />
<span class="sr-only">Supprimer {{ toolbarEditGeomTitle }}</span>
</a>
</div>
<a
v-else
:class="{ active: isEditing }"
:title="`Modifier ${toolbarEditGeomTitle}`"
@click="toggleEdition"
>
<i class="edit outline icon" />
<span class="sr-only">Modifier {{ toolbarEditGeomTitle }}</span>
</a>
<a
v-if="noExistingFeature || isEditing"
:class="{ active: isSnapEnabled }"
:title="`${ isSnapEnabled ? 'Désactiver' : 'Activer' } l'accrochage aux points`"
@click="toggleSnap"
>
<i
class="magnet icon"
aria-hidden="true"
/>
<span class="sr-only">{{ isSnapEnabled ? 'Désactiver' : 'Activer' }} l'accrochage aux points</span>
</a>
<a
v-if="!noExistingFeature"
:title="`Supprimer ${toolbarEditGeomTitle}`"
@click="deleteObj"
>
<i class="trash alternate outline icon" />
<span class="sr-only">Supprimer {{ toolbarEditGeomTitle }}</span>
</a>
</div>
</div>
</template>
......@@ -81,8 +85,8 @@ export default {
},
computed: {
showDrawTool() {
return this.editionService && this.editionService.editing_feature === undefined;
noExistingFeature() {
return this.editionService?.editing_feature === undefined;
},
toolbarGeomTitle() {
switch (this.editionService.geom_type) {
......@@ -99,13 +103,16 @@ export default {
},
methods: {
update() {
editionService.activeUpdateFeature();
this.isEditing = true;
toggleEdition() {
this.isEditing = !this.isEditing;
editionService.activeUpdateFeature(this.isEditing);
},
deleteObj() {
editionService.removeFeatureFromMap();
this.isEditing = false;
const hasBeenRemoved = editionService.removeFeatureFromMap();
// Vérifie que l'utilisateur a bien confirmé la suppression avant de réinitialiser le bouton d'édition
if (this.isEditing && hasBeenRemoved) {
this.isEditing = false;
}
},
toggleSnap() {
if (this.isSnapEnabled) {
......
......@@ -103,61 +103,89 @@ export default {
},
mounted() {
// clone object to not modify data in store, using json parse instead of spread operator that modifies data, for instance when navigating to basemap administration page
this.baseMaps = JSON.parse(JSON.stringify(this.basemaps));
const mapOptions =
JSON.parse(localStorage.getItem('geocontrib-map-options')) || {};
if (mapOptions && mapOptions[this.projectSlug]) {
// If already in the storage, we need to check if the admin did some
// modification in the basemaps on the server side. The rule is: if one layer has been added
// or deleted in the server, then we reset the localstorage.
const baseMapsFromLocalstorage = mapOptions[this.projectSlug]['basemaps'];
const areChanges = this.areChangesInBasemaps(
this.baseMaps,
baseMapsFromLocalstorage
);
this.initBasemaps();
},
methods: {
/**
* Initializes the basemaps and handles their state.
* This function checks if the basemaps stored in the local storage match those fetched from the server.
* If changes are detected, it resets the local storage with updated basemaps.
* It also sets the first basemap as active by default and adds the corresponding layers to the map.
*/
initBasemaps() {
// Clone object to not modify data in store, using JSON parse instead of spread operator
// that modifies data, for instance when navigating to basemap administration page
this.baseMaps = JSON.parse(JSON.stringify(this.basemaps));
if (areChanges) {
mapOptions[this.projectSlug] = {
basemaps: this.baseMaps,
'current-basemap-index': 0,
};
localStorage.setItem(
'geocontrib-map-options',
JSON.stringify(mapOptions)
// Retrieve map options from local storage
const mapOptions = JSON.parse(localStorage.getItem('geocontrib-map-options')) || {};
// Check if map options exist for the current project
if (mapOptions && mapOptions[this.projectSlug]) {
// If already in the storage, we need to check if the admin did some
// modification in the basemaps on the server side.
// The rule is: if one layer has been added or deleted on the server,
// then we reset the local storage.
const baseMapsFromLocalstorage = mapOptions[this.projectSlug]['basemaps'];
const areChanges = this.areChangesInBasemaps(
this.baseMaps,
baseMapsFromLocalstorage
);
} else if (baseMapsFromLocalstorage) {
this.baseMaps = baseMapsFromLocalstorage;
// If changes are detected, update local storage with the latest basemaps
if (areChanges) {
mapOptions[this.projectSlug] = {
basemaps: this.baseMaps,
'current-basemap-index': 0,
};
localStorage.setItem(
'geocontrib-map-options',
JSON.stringify(mapOptions)
);
} else if (baseMapsFromLocalstorage) {
// If no changes, use the basemaps from local storage
this.baseMaps = baseMapsFromLocalstorage;
}
}
}
if (this.baseMaps.length > 0) {
// if an active layers has been set earlier...
if (mapOptions[this.projectSlug] && mapOptions[this.projectSlug]['current-basemap-index']) {
// ...set the active layer with active property at true and reset others to false in order to prevent errors from wrong mapOptions
this.baseMaps.forEach((baseMap) => {
if (baseMap.id === mapOptions[this.projectSlug]['current-basemap-index']) {
baseMap.active = true;
} else {
baseMap.active = false;
}
});
} else { // set the first basemap as active
if (this.baseMaps.length > 0) {
// Set the first basemap as active by default
this.baseMaps[0].active = true;
// Check if an active layer has been set previously by the user
const activeBasemapId = mapOptions[this.projectSlug]
? mapOptions[this.projectSlug]['current-basemap-index']
: null;
// Ensure the active layer ID exists in the current basemaps in case id does not exist anymore or has changed
if (activeBasemapId >= 0 && this.baseMaps.some(bm => bm.id === activeBasemapId)) {
this.baseMaps.forEach((baseMap) => {
// Set the active layer by matching the ID and setting the active property to true
if (baseMap.id === mapOptions[this.projectSlug]['current-basemap-index']) {
baseMap.active = true;
} else {
// Reset others to false to prevent errors from incorrect mapOptions
baseMap.active = false;
}
});
}
// Add layers for the active basemap
this.addLayers(this.activeBasemap);
this.setSelectedQueryLayer();
} else {
// If no basemaps are available, add the default base map layers from the configuration
mapService.addLayers(
null,
this.$store.state.configuration.DEFAULT_BASE_MAP_SERVICE,
this.$store.state.configuration.DEFAULT_BASE_MAP_OPTIONS,
this.$store.state.configuration.DEFAULT_BASE_MAP_SCHEMA_TYPE
);
}
this.addLayers(this.activeBasemap);
this.setSelectedQueryLayer();
} else {
mapService.addLayers(
null,
this.$store.state.configuration.DEFAULT_BASE_MAP_SERVICE,
this.$store.state.configuration.DEFAULT_BASE_MAP_OPTIONS,
this.$store.state.configuration.DEFAULT_BASE_MAP_SCHEMA_TYPE
);
}
},
},
methods: {
toggleSidebar(value) {
this.expanded = value !== undefined ? value : !this.expanded;
},
......