diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index bd01e22edd47c77eb35b09eff7b41198f1463721..8ae10fb420e5c853a1b2b129380a52dffff24a5c 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -25,16 +25,14 @@ build testing docker image:
   only:
     - develop
   tags:
-    - build
-  image:
-    name: gcr.io/kaniko-project/executor:debug
-    entrypoint: [""]
+    - build_docker
+  variables:
+    DOCKER_TAG: testing
   script:
-    - mkdir -p /kaniko/.docker
-    - export
-    - echo "{\"auths\":{\"https://index.docker.io/v1/\":{\"auth\":\"$DOCKER_AUTH\"}}}" > /kaniko/.docker/config.json
-    - /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/Dockerfile --destination neogeo/geocontrib-front:testing
-    - echo Image docker neogeo/geocontrib-front:testing livrée
+    - cat $DOCKER_PASSWORD | docker login --username $DOCKER_LOGIN --password-stdin
+    - docker-compose build geocontrib-front
+    - docker-compose push geocontrib-front
+    - echo Image docker neogeo/geocontrib-front:${DOCKER_TAG} livrée
 
 deploy testing docker image:
   stage: deploy
@@ -52,36 +50,30 @@ build stable docker image:
   only:
     - master
   tags:
-    - build
-  image:
-    name: gcr.io/kaniko-project/executor:debug
-    entrypoint: [""]
+    - build_docker
+  variables:
+    DOCKER_TAG: latest
   script:
-    - mkdir -p /kaniko/.docker
-    - echo "{\"auths\":{\"https://index.docker.io/v1/\":{\"auth\":\"$DOCKER_AUTH\"}}}" > /kaniko/.docker/config.json
-
-    - /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/Dockerfile --destination neogeo/geocontrib-front:latest
-    - echo Image docker neogeo/geocontrib:latest livrée
-
+    - cat $DOCKER_PASSWORD | docker login --username $DOCKER_LOGIN --password-stdin
+    - docker-compose build geocontrib-front
+    - docker-compose push geocontrib-front
+    - echo Image docker neogeo/geocontrib-front:${DOCKER_TAG} livrée
 
 build tagged docker image:
   stage: build
   only:
     - tags
   tags:
-    - build
-  image:
-    name: gcr.io/kaniko-project/executor:debug
-    entrypoint: [""]
+    - build_docker
+  variables:
+    DOCKER_TAG: $CI_COMMIT_TAG
   script:
     # Don't build tag id package.json as wrong version
     - grep "\"version\":.\"$CI_COMMIT_TAG\"" package.json
-    - mkdir -p /kaniko/.docker
-    - echo "{\"auths\":{\"https://index.docker.io/v1/\":{\"auth\":\"$DOCKER_AUTH\"}}}" > /kaniko/.docker/config.json
-
-    - /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/Dockerfile --destination neogeo/geocontrib-front:$CI_COMMIT_TAG
-    - echo Image docker neogeo/geocontrib-front:$CI_COMMIT_TAG livrée
-
+    - cat $DOCKER_PASSWORD | docker login --username $DOCKER_LOGIN --password-stdin
+    - docker-compose build geocontrib-front
+    - docker-compose push geocontrib-front
+    - echo Image docker neogeo/geocontrib-front:${DOCKER_TAG} livrée
 
 sonarqube-check:
   image:
diff --git a/docker-compose.yaml b/docker-compose.yaml
index 18f164891441f5ecbbaa1bbf86ba056f826570f6..a250dbb007c4693ba0d4c4e033be50cc1ed6fa28 100644
--- a/docker-compose.yaml
+++ b/docker-compose.yaml
@@ -2,7 +2,7 @@
 version: "3"
 services:
   geocontrib-front:
-    image: neogeo/geocontrib-front:geocontrib-latest
+    image: neogeo/geocontrib-front:${DOCKER_TAG:-testing}
     build: .
     environment:
       - BASE_URL=${BASE_URL}
diff --git a/package-lock.json b/package-lock.json
index e64fcedf2f8ae61f7fb04c6094707c6a02256d76..5f39dbee276a51b9372a1517a72c26cd2aa9dab7 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -2562,6 +2562,16 @@
           "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==",
           "dev": true
         },
+        "ansi-styles": {
+          "version": "4.3.0",
+          "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+          "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "color-convert": "^2.0.1"
+          }
+        },
         "array-union": {
           "version": "1.0.2",
           "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz",
@@ -2571,6 +2581,34 @@
             "array-uniq": "^1.0.1"
           }
         },
+        "chalk": {
+          "version": "4.1.2",
+          "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+          "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "ansi-styles": "^4.1.0",
+            "supports-color": "^7.1.0"
+          }
+        },
+        "color-convert": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+          "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "color-name": "~1.1.4"
+          }
+        },
+        "color-name": {
+          "version": "1.1.4",
+          "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+          "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+          "dev": true,
+          "optional": true
+        },
         "dir-glob": {
           "version": "2.2.2",
           "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-2.2.2.tgz",
@@ -2642,6 +2680,13 @@
             "slash": "^2.0.0"
           }
         },
+        "has-flag": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+          "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+          "dev": true,
+          "optional": true
+        },
         "ignore": {
           "version": "4.0.6",
           "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz",
@@ -2679,6 +2724,28 @@
           "requires": {
             "minipass": "^3.1.1"
           }
+        },
+        "supports-color": {
+          "version": "7.2.0",
+          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+          "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "has-flag": "^4.0.0"
+          }
+        },
+        "vue-loader-v16": {
+          "version": "npm:vue-loader@16.8.3",
+          "resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-16.8.3.tgz",
+          "integrity": "sha512-7vKN45IxsKxe5GcVCbc2qFU5aWzyiLrYJyUuMz4BQLKctCj/fmCa0w6fGiiQ2cLFetNcek1ppGJQDCup0c1hpA==",
+          "dev": true,
+          "optional": true,
+          "requires": {
+            "chalk": "^4.1.0",
+            "hash-sum": "^2.0.0",
+            "loader-utils": "^2.0.0"
+          }
         }
       }
     },
@@ -3560,8 +3627,7 @@
     "bluebird": {
       "version": "3.7.2",
       "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
-      "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==",
-      "dev": true
+      "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg=="
     },
     "bn.js": {
       "version": "5.2.0",
@@ -5206,6 +5272,16 @@
         }
       }
     },
+    "csvtojson": {
+      "version": "2.0.10",
+      "resolved": "https://registry.npmjs.org/csvtojson/-/csvtojson-2.0.10.tgz",
+      "integrity": "sha512-lUWFxGKyhraKCW8Qghz6Z0f2l/PqB1W3AO0HKJzGIQ5JRSlR651ekJDiGJbBT4sRNNv5ddnSGVEnsxP9XRCVpQ==",
+      "requires": {
+        "bluebird": "^3.5.1",
+        "lodash": "^4.17.3",
+        "strip-bom": "^2.0.0"
+      }
+    },
     "cyclist": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/cyclist/-/cyclist-1.0.1.tgz",
@@ -8320,6 +8396,11 @@
       "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==",
       "dev": true
     },
+    "is-utf8": {
+      "version": "0.2.1",
+      "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz",
+      "integrity": "sha512-rMYPYvCzsXywIsldgLaSoPlw5PfoB/ssr7hY4pLfcodrA5M/eArza1a9VmTiNIBNMjOGr1Ow9mTyU2o69U6U9Q=="
+    },
     "is-weakref": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz",
@@ -12303,6 +12384,14 @@
         }
       }
     },
+    "strip-bom": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz",
+      "integrity": "sha512-kwrX1y7czp1E69n2ajbG65mIo9dqvJ+8aBQXOGVxqwvNbsXdFM6Lq37dLAY3mknUwru8CfcCbfOLL/gMo+fi3g==",
+      "requires": {
+        "is-utf8": "^0.2.0"
+      }
+    },
     "strip-comments": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/strip-comments/-/strip-comments-1.0.2.tgz",
@@ -13301,75 +13390,6 @@
         }
       }
     },
-    "vue-loader-v16": {
-      "version": "npm:vue-loader@16.8.3",
-      "resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-16.8.3.tgz",
-      "integrity": "sha512-7vKN45IxsKxe5GcVCbc2qFU5aWzyiLrYJyUuMz4BQLKctCj/fmCa0w6fGiiQ2cLFetNcek1ppGJQDCup0c1hpA==",
-      "dev": true,
-      "optional": true,
-      "requires": {
-        "chalk": "^4.1.0",
-        "hash-sum": "^2.0.0",
-        "loader-utils": "^2.0.0"
-      },
-      "dependencies": {
-        "ansi-styles": {
-          "version": "4.3.0",
-          "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
-          "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
-          "dev": true,
-          "optional": true,
-          "requires": {
-            "color-convert": "^2.0.1"
-          }
-        },
-        "chalk": {
-          "version": "4.1.2",
-          "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
-          "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
-          "dev": true,
-          "optional": true,
-          "requires": {
-            "ansi-styles": "^4.1.0",
-            "supports-color": "^7.1.0"
-          }
-        },
-        "color-convert": {
-          "version": "2.0.1",
-          "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
-          "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
-          "dev": true,
-          "optional": true,
-          "requires": {
-            "color-name": "~1.1.4"
-          }
-        },
-        "color-name": {
-          "version": "1.1.4",
-          "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
-          "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
-          "dev": true,
-          "optional": true
-        },
-        "has-flag": {
-          "version": "4.0.0",
-          "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
-          "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
-          "dev": true,
-          "optional": true
-        },
-        "supports-color": {
-          "version": "7.2.0",
-          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
-          "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
-          "dev": true,
-          "optional": true,
-          "requires": {
-            "has-flag": "^4.0.0"
-          }
-        }
-      }
-    },
     "vue-multiselect": {
       "version": "2.1.6",
       "resolved": "https://registry.npmjs.org/vue-multiselect/-/vue-multiselect-2.1.6.tgz",
diff --git a/package.json b/package.json
index b300d14a7021816265add7350667c3971d2f171f..e28623c0db2883316c4e62b115bf9773f70e4760 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "geocontrib-frontend",
-  "version": "3.1.0",
+  "version": "3.3.0",
   "private": true,
   "scripts": {
     "serve": "npm run init-proxy & npm run init-serve",
@@ -21,6 +21,7 @@
     "@turf/helpers": "^6.5.0",
     "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",
diff --git a/src/App.vue b/src/App.vue
index b45820b6b21df309dee4b1368a6b29b23238866e..56520c47cf02a578d7bc5d8d768073dcd549b360 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -66,71 +66,4 @@ export default {
     ])
   },
 };
-</script>
-
-<style>
-.vertical {
-  flex-direction: column;
-  justify-content: center;
-}
-
-.leaflet-container {
-  background: white !important;
-}
-
-.flex {
-  display: flex;
-}
-
-/* keep above loader */
-#menu-dropdown {
-  z-index: 1001;
-}
-
-@media screen and (max-width: 985px) {
-  .abstract{
-    display: none !important;
-  }
-}
-
-@media screen and (min-width: 560px) {
-  .mobile {
-    display: none !important;
-  }
-  #app-header {
-    min-width: 560px;
-  }
-  .menu.container {
-    width: auto !important;
-  }
-  .push-right-desktop {
-    margin-left: auto;
-  }
-}
-
-@media screen and (max-width: 590px) {
-  .desktop {
-    display: none !important;
-  }
-  div.dropdown-list {
-    width: 100vw;
-    left: -70px !important; /* should be the same than belows */
-  }
-  .menu.container a.header {
-    width: 70px;
-  }
-  .menu.container a.header > img {
-    margin: 0;
-  }
-  #menu-dropdown {
-    width: calc(100vw - 70px);
-    justify-content: space-between;
-  }
-  #menu-dropdown > span {
-    text-overflow: ellipsis;
-    overflow: hidden;
-    white-space: nowrap;
-  }
-}
-
-</style>
+</script>
\ No newline at end of file
diff --git a/src/assets/js/utils.js b/src/assets/js/utils.js
index 0680fcdfad82586d6325e4cc501bf9873abf4d7c..85e7a1b11496402eb57d688f13dcd2d187a90665 100644
--- a/src/assets/js/utils.js
+++ b/src/assets/js/utils.js
@@ -12,30 +12,4 @@ export function fileConvertSizeToMo(aSize){
   aSize = Math.abs(parseInt(aSize, 10));
   const def = [1024*1024, 'Mo', 1];
   return (aSize/def[0]).toFixed(def[2]);
-}
-
-export function csvToJson(csv, delimiter) {
-  const result = [];
-
-  const allLines = csv.split('\n');
-  const headers = allLines[0].split(delimiter).map(el => {
-    return el.replace('\r', '');
-  });
-  const [, ...lines] = allLines;
-
-  for (const line of lines) {
-    if (line) {
-      const obj = {};
-      const currentLine = line.split(delimiter).map(el => {
-        return el.replace('\r', '');
-      });
-
-      for (let i = 0; i < headers.length; i++) {
-        obj[headers[i]] = currentLine[i];
-      }
-
-      result.push(obj);
-    }
-  }
-  return JSON.parse(JSON.stringify(result));
-}
+}
\ No newline at end of file
diff --git a/src/components/Account/UserProjectsList.vue b/src/components/Account/UserProjectsList.vue
index 3d7302a19b701d5d3d7363424e1459f849b69017..4e26440a571bfb10b66cb2d96ac28b4d09b26378 100644
--- a/src/components/Account/UserProjectsList.vue
+++ b/src/components/Account/UserProjectsList.vue
@@ -47,11 +47,11 @@
             <div class="description">
               <p>{{ project.description }}</p>
             </div>
-            <div class="meta">
+            <div class="meta top">
               <span
                 class="right floated"
               >
-                Projet {{ project.moderation ? "" : "non" }} modéré
+                <strong>Projet {{ project.moderation ? "" : "non" }} modéré</strong>
               </span>
               <span>
                 Niveau d'autorisation requis : {{ project.access_level_pub_feature }}
@@ -228,6 +228,12 @@ export default {
   }
 }
 
+.description {
+  p {
+    text-align: justify;
+  }
+}
+
 @media only screen and (min-width: 767px) {
   .item-content-wrapper {
     align-items: flex-start;
@@ -235,6 +241,12 @@ export default {
     .middle.aligned.content {
       width: 100%;
       padding: 0 0 0 1.5em;
+
+      .meta.top {
+        span {
+          line-height: 1.2em;
+        }
+      }
     }
   }
 }
@@ -244,8 +256,25 @@ export default {
     align-items: center;
 
     .middle.aligned.content {
-      width: 70%;
+      width: 80%;
       padding: 1.5em 0 0;
+
+      .meta.top {
+        display: flex;
+        flex-direction: column;
+        align-items: flex-start;
+        justify-content: center;
+
+        .right.floated {
+          float: none !important;
+          margin-left: 0 !important;
+          margin-bottom: 0.5em;
+        }
+
+        span {
+          margin: 0.15em 0;
+        }
+      }
     }
   }
 }
diff --git a/src/components/AppHeader.vue b/src/components/AppHeader.vue
index 77f4c4ac316b16c6c5882abf6238d3b3775ac210..5b6fe4777530ebbe7898f2d45712b614b841f0a2 100644
--- a/src/components/AppHeader.vue
+++ b/src/components/AppHeader.vue
@@ -22,8 +22,15 @@
           :class="['ui dropdown item', { 'active visible': menuIsOpen }]"
           @click="menuIsOpen = !menuIsOpen"
         >
-          <!-- empty span to occupy space for style if no project -->
-          <span>
+          <div
+            v-if="!isOnline"
+            class="crossed-out mobile"
+          >
+            <i
+              class="wifi icon"
+            />
+          </div>
+          <span class="expand-center">
             <span v-if="project"> Projet : {{ project.title }} </span>
           </span>
           <i
@@ -155,6 +162,20 @@
           </span>
         </div>
         <div class="desktop flex push-right-desktop">
+          <div
+            v-if="!isOnline"
+            class="item"
+          >
+            <span
+              data-tooltip="Vous êtes hors-ligne,
+                vos changements pourront être envoyés au serveur au retour de la connexion"
+              data-position="bottom right"
+            >
+              <div class="crossed-out">
+                <i class="wifi icon" />
+              </div>
+            </span>
+          </div>
           <router-link
             :is="isOnline ? 'router-link' : 'span'"
             v-if="user"
@@ -207,21 +228,21 @@
           </div>
         </div>
       </div>
-      <MessageInfo />
+      <MessageInfoList />
     </div>
   </div>
 </template>
 
 <script>
 import { mapState } from 'vuex';
-import MessageInfo from '@/components/MessageInfo';
+import MessageInfoList from '@/components/MessageInfoList';
 
 export default {
 
   name: 'AppHeader',
 
   components: {
-    MessageInfo
+    MessageInfoList
   },
 
   data() {
@@ -301,6 +322,86 @@ export default {
 </script>
 
 <style lang="less" scoped>
+.vertical {
+  flex-direction: column;
+  justify-content: center;
+}
+
+.flex {
+  display: flex;
+}
+
+/* keep above loader */
+#menu-dropdown {
+  z-index: 1001;
+}
+
+.expand-center {
+  width: 100%;
+  text-align: center;
+}
+
+.crossed-out {
+  position: relative;
+  padding: .2em;
+  &::before {
+    content: "";
+    position: absolute;
+    top: 45%;
+    left: -8%;
+    width: 100%;
+    border-top: 2px solid #ee2e24;
+    transform: rotate(45deg);
+    box-shadow: 0px 0px 0px 1px #373636;
+    border-radius: 3px;
+  }
+}
+
+@media screen and (max-width: 985px) {
+  .abstract{
+    display: none !important;
+  }
+}
+
+@media screen and (min-width: 560px) {
+  .mobile {
+    display: none !important;
+  }
+  #app-header {
+    min-width: 560px;
+  }
+  .menu.container {
+    width: auto !important;
+  }
+  .push-right-desktop {
+    margin-left: auto;
+  }
+}
+
+@media screen and (max-width: 590px) {
+  .desktop {
+    display: none !important;
+  }
+  div.dropdown-list {
+    width: 100vw;
+    left: -70px !important; /* should be the same than belows */
+  }
+  .menu.container a.header {
+    width: 70px;
+  }
+  .menu.container a.header > img {
+    margin: 0;
+  }
+  #menu-dropdown {
+    width: calc(100vw - 70px);
+    //justify-content: space-between;
+  }
+  #menu-dropdown > span {
+    text-overflow: ellipsis;
+    overflow: hidden;
+    white-space: nowrap;
+  }
+}
 
 .menu.container {
   position: relative;
@@ -348,4 +449,4 @@ export default {
   height: 100% !important;
 }
 
-</style>
+</style>
\ No newline at end of file
diff --git a/src/components/Feature/Detail/FeatureHeader.vue b/src/components/Feature/Detail/FeatureHeader.vue
index d1ab51b94908bffa2dae66f0f15f177802f1c326..c3784191e44c1abd730f914ceadb8f5189003dc2 100644
--- a/src/components/Feature/Detail/FeatureHeader.vue
+++ b/src/components/Feature/Detail/FeatureHeader.vue
@@ -2,152 +2,163 @@
   <div>
     <h1 class="ui header">
       <div class="content">
-        <span
-          v-if="fastEditionMode && form"
-          class="form ui half-block"
-        >
-          <input
-            id="feature_detail_title_input"
-            :value="form.title"
-            type="text"
-            required
-            maxlength="128"
-            name="title"
-            @blur="updateTitle"
-          >
-        </span>
-        <span v-else>
-          {{ currentFeature.title || currentFeature.feature_id }}
-        </span>
-
-        <div class="ui icon right floated compact buttons">
-          <router-link
-            v-if="displayToListButton"
-            id="feature-detail-to-features-list"
-            :to="{
-              name: 'liste-signalements',
-              params: { slug: $route.params.slug },
-            }"
-            custom
-          >
-            <div class="ui button tiny-margin teal">
-              <i class="ui icon arrow right" />
-              Retour à la liste des signalements
-            </div>
-          </router-link>
-          <span
-            v-if="featuresCount"
-            id="feature-count"
-            class="ui button tiny-margin basic"
-          >
-            {{ parseInt($route.query.offset) + 1 }} sur {{ featuresCount }}
-          </span>
-          <button
-            v-if="queryparams"
-            id="previous-feature"
-            :class="['ui button button-hover-green tiny-margin', { disabled: queryparams.previous < 0 }]"
-            data-tooltip="Voir le précédent signalement"
-            data-position="bottom center"
-            @click="toFeature('previous')"
+        <div class="two-block">
+          <div
+            v-if="fastEditionMode && form && canEditFeature"
+            class="form ui half-block"
           >
-            <i
-              class="angle left fitted icon"  
-              aria-hidden="true"
-            />
-          </button>
-          <button
-            v-if="queryparams"
-            id="next-feature"
-            :class="[
-              'ui button button-hover-green tiny-margin',
-              { disabled: queryparams.next >= featuresCount }
-            ]"
-            data-tooltip="Voir le prochain signalement"
-            data-position="bottom center"
-            @click="toFeature('next')"
+            <input
+              id="feature_detail_title_input"
+              :value="form.title"
+              type="text"
+              required
+              maxlength="128"
+              name="title"
+              @blur="updateTitle"
+            >
+          </div>
+          <div
+            v-else
+            class="ellipsis"
           >
-            <i
-              class="angle right fitted icon"
-              aria-hidden="true"
-            />
-          </button>
+            {{ currentFeature.title || currentFeature.feature_id }}
+          </div>
 
-          <button
-            v-if="fastEditionMode && userCanFastEdit"
-            id="previous-feature"
-            :class="['ui button button-hover-orange tiny-margin', { disabled: false }]"
-            data-tooltip="Enregistrer les modifications"
-            data-position="bottom center"
-            @click="$store.dispatch('feature/SEND_FEATURE', $route.name)"
+          <div
+            id="feature-actions"
+            class="ui icon compact buttons"
           >
-            <i
-              class="save fitted icon"  
-              aria-hidden="true"
-            />
-          </button>
+            <div>
+              <router-link
+                v-if="displayToListButton"
+                id="feature-detail-to-features-list"
+                :to="{
+                  name: 'liste-signalements',
+                  params: { slug: $route.params.slug },
+                }"
+                custom
+              >
+                <div class="ui button tiny-margin teal">
+                  <i class="ui icon arrow right" />
+                  Retour à la liste des signalements
+                </div>
+              </router-link>
+            </div>
+            <div>
+              <span
+                v-if="featuresCount"
+                id="feature-count"
+                class="ui button tiny-margin basic disabled no-opacity"
+              >
+                {{ parseInt($route.query.offset) + 1 }} sur {{ featuresCount }}
+              </span>
+              <button
+                v-if="queryparams"
+                id="previous-feature"
+                :class="['ui button button-hover-green tiny-margin', { disabled: queryparams.previous < 0 }]"
+                data-tooltip="Voir le précédent signalement"
+                data-position="bottom center"
+                @click="toFeature('previous')"
+              >
+                <i
+                  class="angle left fitted icon"
+                  aria-hidden="true"
+                />
+              </button>
+              <button
+                v-if="queryparams"
+                id="next-feature"
+                :class="[
+                  'ui button button-hover-green tiny-margin',
+                  { disabled: queryparams.next >= featuresCount }
+                ]"
+                data-tooltip="Voir le prochain signalement"
+                data-position="bottom center"
+                @click="toFeature('next')"
+              >
+                <i
+                  class="angle right fitted icon"
+                  aria-hidden="true"
+                />
+              </button>
+            </div>
+            <div>
+              <button
+                v-if="fastEditionMode && canEditFeature"
+                id="save-fast-edit"
+                :class="['ui button button-hover-orange tiny-margin', { disabled: false }]"
+                data-tooltip="Enregistrer les modifications"
+                data-position="bottom center"
+                @click="validateFastEdition"
+              >
+                <i
+                  class="save fitted icon"
+                  aria-hidden="true"
+                />
+              </button>
 
-          <router-link
-            v-if="permissions && permissions.can_create_feature"
-            id="add-feature"
-            :to="{
-              name: 'ajouter-signalement',
-              params: {
-                slug_type_signal: $route.params.slug_type_signal || featureType.slug,
-              },
-            }"
-            class="ui button button-hover-green tiny-margin"
-            data-tooltip="Ajouter un signalement"
-            data-position="bottom center"
-          >
-            <i
-              class="plus icon"
-              aria-hidden="true"
-            />
-          </router-link>
+              <router-link
+                v-if="permissions && permissions.can_create_feature"
+                id="add-feature"
+                :to="{
+                  name: 'ajouter-signalement',
+                  params: {
+                    slug_type_signal: $route.params.slug_type_signal || featureType.slug,
+                  },
+                }"
+                class="ui button button-hover-green tiny-margin"
+                data-tooltip="Ajouter un signalement"
+                data-position="bottom center"
+              >
+                <i
+                  class="plus icon"
+                  aria-hidden="true"
+                />
+              </router-link>
 
-          <router-link
-            v-if="slugSignal &&
-              ((permissions && permissions.can_update_feature) ||
-                isFeatureCreator ||
-                isModerator)
-            "
-            id="edit-feature"
-            :to="{
-              name: 'editer-signalement',
-              params: {
-                slug_signal: slugSignal,
-                slug_type_signal: $route.params.slug_type_signal || featureType.slug,
-              },
-              query: $route.query
-            }"
-            class="ui button button-hover-orange tiny-margin"
-            data-tooltip="Éditer le signalement"
-            data-position="bottom center"
-          >
-            <i
-              class="inverted grey pencil alternate icon"
-              aria-hidden="true"
-            />
-          </router-link>
+              <router-link
+                v-if="slugSignal && canEditFeature"
+                id="edit-feature"
+                :to="{
+                  name: 'editer-signalement',
+                  params: {
+                    slug_signal: slugSignal,
+                    slug_type_signal: $route.params.slug_type_signal || featureType.slug,
+                  },
+                  query: $route.query
+                }"
+                class="ui button button-hover-orange tiny-margin"
+                data-tooltip="Éditer le signalement"
+                data-position="bottom center"
+              >
+                <i
+                  class="inverted grey pencil alternate icon"
+                  aria-hidden="true"
+                />
+              </router-link>
 
-          <a
-            v-if="((permissions && permissions.can_update_feature) || isFeatureCreator) && isOnline"
-            id="currentFeature-delete"
-            class="ui button button-hover-red tiny-margin"
-            data-tooltip="Supprimer le signalement"
-            data-position="bottom right"
-            @click="$emit('setIsCancelling')"
-          >
-            <i
-              class="inverted grey trash alternate icon"
-              aria-hidden="true"
-            />
-          </a>
+              <a
+                v-if="canDeleteFeature && isOnline"
+                id="currentFeature-delete"
+                class="ui button button-hover-red tiny-margin"
+                data-tooltip="Supprimer le signalement"
+                data-position="bottom right"
+                @click="$emit('setIsDeleting')"
+              >
+                <i
+                  class="inverted grey trash alternate icon"
+                  aria-hidden="true"
+                />
+              </a>
+            </div>
+          </div>
         </div>
-        <div class="ui hidden divider" />
+
+        <!-- <div class="ui hidden divider" /> -->
+
         <div class="sub header prewrap">
           <span
-            v-if="fastEditionMode && form"
+            v-if="fastEditionMode && canEditFeature && form"
             class="form ui half-block"
           >
             <textarea
@@ -196,12 +207,23 @@ export default {
       type: Boolean,
       default: false,
     },
+    isFeatureCreator: {
+      type: Boolean,
+      default: false,
+    },
+    canEditFeature: {
+      type: Boolean,
+      default: false,
+    },
+    canDeleteFeature: {
+      type: Boolean,
+      default: false,
+    },
   },
 
   computed: {
     ...mapState([
       'user',
-      'USER_LEVEL_PROJECTS',
       'isOnline',
     ]),
     ...mapState('feature', [
@@ -212,24 +234,6 @@ export default {
       'permissions',
     ]),
 
-    isFeatureCreator() {
-      if (this.currentFeature && this.user) {
-        return this.currentFeature.creator === this.user.id;
-      }
-      return false;
-    },
-
-    isModerator() {
-      return this.USER_LEVEL_PROJECTS && this.USER_LEVEL_PROJECTS[this.$route.params.slug] === 'Modérateur';
-    },
-
-    userCanFastEdit() {
-      const superiorRoles = ['contributor', 'super_contributor', 'moderator', 'admin'];
-      return this.USER_LEVEL_PROJECTS &&
-        superiorRoles.includes(this.USER_LEVEL_PROJECTS[this.$route.params.slug]) ||
-          this.user.is_superuser;
-    },
-
     queryparams() {
       return this.$route.query.offset >= 0 ? {
         previous: parseInt(this.$route.query.offset) - 1,
@@ -258,26 +262,45 @@ export default {
 
     updateDescription(e) {
       this.$store.commit('feature/UPDATE_FORM_FIELD', { name: 'description', value: e.target.value });
+    },
+
+    validateFastEdition() {
+      this.$store.dispatch('feature/SEND_FEATURE', this.$route.name)
+        .then(() => this.$emit('updateEvents'));
     }
   }
 };
 </script>
 
 <style>
-#next-feature {
-  margin-right: .5rem !important;
-}
 #feature-detail-to-features-list {
   line-height: 0;
   margin-right: 5px;
 }
-.half-block {
-  display: inline-block;
-  width: 50%;
-}
 #feature_detail_title_input {
   font-weight: bold;
   font-size: 2em;
   padding: .25em;
 }
+.two-block {
+  display: flex;
+  justify-content: space-between;
+  margin-bottom: .5em;
+}
+#feature-actions > div {
+  margin-left: .5rem;
+}
+#feature-actions .no-opacity {
+  opacity: 1 !important; /* overide disabled low opacity to customize button style */
+}
+
+@media screen and (max-width: 700px) {
+  .two-block {
+    flex-direction: column-reverse;
+  }
+  #feature-actions.ui.buttons {
+    flex-direction: column;
+    align-items: flex-end;
+  }
+}
 </style>
\ No newline at end of file
diff --git a/src/components/Feature/Detail/FeatureTable.vue b/src/components/Feature/Detail/FeatureTable.vue
index ef36f677aa2df6d4aa818ac0a5f065e6a020d132..b0e4a8b8e963be24bac5a544dd6464c03d244929 100644
--- a/src/components/Feature/Detail/FeatureTable.vue
+++ b/src/components/Feature/Detail/FeatureTable.vue
@@ -23,7 +23,8 @@
           <td>
             <strong class="ui form">
               <span
-                v-if="fastEditionMode && extra_forms.length > 0"
+                v-if="fastEditionMode && canEditFeature && extra_forms.length > 0"
+                :id="field.label"
               >
                 <FeatureExtraForm
                   :field="getExtraForm(field)"
@@ -60,7 +61,7 @@
               aria-hidden="true"
             />
             <FeatureEditStatusField
-              v-if="fastEditionMode && form"
+              v-if="fastEditionMode && canEditFeature && form"
               :status="form.status.value"
               class="inline"
             />
@@ -161,7 +162,11 @@ export default {
     fastEditionMode: {
       type: Boolean,
       default: false,
-    }
+    },
+    canEditFeature: {
+      type: Boolean,
+      default: false,
+    },
   },
 
   computed: {
diff --git a/src/components/Feature/Edit/FeatureExtraForm.vue b/src/components/Feature/Edit/FeatureExtraForm.vue
index 778c46539df03dbc82510be65fb9577478e5a3d4..3ae38ec519c52e664918ec5c33b58702c39cb126 100644
--- a/src/components/Feature/Edit/FeatureExtraForm.vue
+++ b/src/components/Feature/Edit/FeatureExtraForm.vue
@@ -1,10 +1,9 @@
 <template>
-  <div
-    v-if="field && field.field_type === 'char'"
-  >
+  <div v-if="field && field.field_type === 'char'">
     <label
-      v-if="$route.name !== 'details-signalement'"
+      v-if="displayLabels"
       :for="field.name"
+      :class="{ required: field.is_mandatory }"
     >
       {{ field.label }}
     </label>
@@ -13,16 +12,25 @@
       :value="field.value"
       type="text"
       :name="field.name"
+      :required="field.is_mandatory"
       @blur="updateStore_extra_form"
     >
+    <ul
+      v-if="field.is_mandatory && error"
+      :id="`errorlist-extra-form-${field.name}`"
+      class="errorlist"
+    >
+      <li>
+        {{ error }}
+      </li>
+    </ul>
   </div>
 
-  <div
-    v-else-if="field && field.field_type === 'list'"
-  >
+  <div v-else-if="field && field.field_type === 'list'">
     <label
-      v-if="$route.name !== 'details-signalement'"
+      v-if="displayLabels"
       :for="field.name"
+      :class="{ required: field.is_mandatory }"
     >
       {{ field.label }}
     </label>
@@ -30,14 +38,23 @@
       :options="field.options"
       :selected="selected_extra_form_list"
       :selection.sync="selected_extra_form_list"
+      :required="field.is_mandatory"
     />
+    <ul
+      v-if="field.is_mandatory && error"
+      :id="`errorlist-extra-form-${field.name}`"
+      class="errorlist"
+    >
+      <li>
+        {{ error }}
+      </li>
+    </ul>
   </div>
-  <div
-    v-else-if="field && field.field_type === 'integer'"
-  >
+  <div v-else-if="field && field.field_type === 'integer'">
     <label
-      v-if="$route.name !== 'details-signalement'"
+      v-if="displayLabels"
       :for="field.name"
+      :class="{ required: field.is_mandatory }"
     >
       {{ field.label }}
     </label>
@@ -48,13 +65,21 @@
         :value="field.value"
         type="number"
         :name="field.name"
+        :required="field.is_mandatory"
         @change="updateStore_extra_form"
       >
     </div>
+    <ul
+      v-if="field.is_mandatory && error"
+      :id="`errorlist-extra-form-${field.name}`"
+      class="errorlist"
+    >
+      <li>
+        {{ error }}
+      </li>
+    </ul>
   </div>
-  <div
-    v-else-if="field && field.field_type === 'boolean'"
-  >
+  <div v-else-if="field && field.field_type === 'boolean'">
     <div class="ui checkbox">
       <input
         :id="field.name"
@@ -64,16 +89,15 @@
         @change="updateStore_extra_form"
       >
       <label :for="field.name">
-        {{ $route.name !== 'details-signalement' ? field.label : '' }}
+        {{ displayLabels ? field.label : '' }}
       </label>
     </div>
   </div>
-  <div
-    v-else-if="field && field.field_type === 'date'"
-  >
+  <div v-else-if="field && field.field_type === 'date'">
     <label
-      v-if="$route.name !== 'details-signalement'"
+      v-if="displayLabels"
       :for="field.name"
+      :class="{ required: field.is_mandatory }"
     >
       {{ field.label }}
     </label>
@@ -82,15 +106,24 @@
       :value="field.value"
       type="date"
       :name="field.name"
+      :required="field.is_mandatory"
       @blur="updateStore_extra_form"
     >
+    <ul
+      v-if="field.is_mandatory && error"
+      :id="`errorlist-extra-form-${field.name}`"
+      class="errorlist"
+    >
+      <li>
+        {{ error }}
+      </li>
+    </ul>
   </div>
-  <div
-    v-else-if="field && field.field_type === 'decimal'"
-  >
+  <div v-else-if="field && field.field_type === 'decimal'">
     <label
-      v-if="$route.name !== 'details-signalement'"
+      v-if="displayLabels"
       :for="field.name"
+      :class="{ required: field.is_mandatory }"
     >
       {{ field.label }}
     </label>
@@ -101,25 +134,44 @@
         type="number"
         step=".01"
         :name="field.name"
+        :required="field.is_mandatory"
         @change="updateStore_extra_form"
       >
     </div>
+    <ul
+      v-if="field.is_mandatory && error"
+      :id="`errorlist-extra-form-${field.name}`"
+      class="errorlist"
+    >
+      <li>
+        {{ error }}
+      </li>
+    </ul>
   </div>
-  <div
-    v-else-if="field && field.field_type === 'text'"
-  >
+  <div v-else-if="field && field.field_type === 'text'">
     <label
-      v-if="$route.name !== 'details-signalement'"
+      v-if="displayLabels"
       :for="field.name"
+      :class="{ required: field.is_mandatory }"
     >
       {{ field.label }}
     </label>
     <textarea
       :value="field.value"
       :name="field.name"
+      :required="field.is_mandatory"
       rows="3"
       @blur="updateStore_extra_form"
     />
+    <ul
+      v-if="field.is_mandatory && error"
+      :id="`errorlist-extra-form-${field.name}`"
+      class="errorlist"
+    >
+      <li>
+        {{ error }}
+      </li>
+    </ul>
   </div>
 </template>
 
@@ -140,6 +192,12 @@ export default {
     }
   },
 
+  data() {
+    return {
+      error: null
+    };
+  },
+
   computed: {
     selected_extra_form_list: {
       get() {
@@ -152,6 +210,18 @@ export default {
         this.$store.commit('feature/UPDATE_EXTRA_FORM', newExtraForm);
       },
     },
+
+    displayLabels() {
+      return this.$route.name === 'editer-signalement' || this.$route.name === 'ajouter-signalement' || this.$route.name === 'editer-attribut-signalement';
+    }
+  },
+
+  watch: {
+    'field.value': function(newValue) {
+      if (newValue) {
+        this.error = null;
+      }
+    }
   },
 
   methods: {
@@ -164,6 +234,26 @@ export default {
       }
       this.$store.commit('feature/UPDATE_EXTRA_FORM', newExtraForm);
     },
+
+    checkForm() {
+      let isValid = true;
+      if (this.field.is_mandatory && !this.field.value) {
+        isValid = false;
+        this.error = 'Ce champ est obligatoire';
+      } else {
+        this.error = null;
+      }
+      return isValid;
+    }
   },
 };
 </script>
+
+<style lang="less" scoped>
+
+label.required:after {
+	content: ' *';
+	color: rgb(209, 0, 0);
+}
+
+</style>
diff --git a/src/components/Feature/FeatureEditStatusField.vue b/src/components/Feature/FeatureEditStatusField.vue
index 806182c3a835cd04a9b8a3d36c3531b7bb7b94bb..3c4133b5c7ff9b8a459ebca9733bd74268604c98 100644
--- a/src/components/Feature/FeatureEditStatusField.vue
+++ b/src/components/Feature/FeatureEditStatusField.vue
@@ -1,5 +1,8 @@
 <template>
-  <div class="field">
+  <div
+    id="status"
+    class="field"
+  >
     <Dropdown
       v-if="selectedStatus"
       :options="allowedStatusChoices"
@@ -56,7 +59,7 @@ export default {
     allowedStatusChoices() {
       if (this.project && this.currentFeature && this.user) {
         const isModerate = this.project.moderation;
-        const userStatus = this.USER_LEVEL_PROJECTS[this.project.slug];
+        const userStatus = this.USER_LEVEL_PROJECTS && this.USER_LEVEL_PROJECTS[this.project.slug];
         const isOwnFeature = this.currentFeature.creator === this.user.id; //* si le contributeur est l'auteur du signalement
         return allowedStatus2change(this.user, isModerate, userStatus, isOwnFeature, /* this.currentRouteName */);
       }
diff --git a/src/components/Feature/FeatureListMassToggle.vue b/src/components/Feature/FeatureListMassToggle.vue
deleted file mode 100644
index ff4b1da6ff6616933727a93e6595b10238a7bfd8..0000000000000000000000000000000000000000
--- a/src/components/Feature/FeatureListMassToggle.vue
+++ /dev/null
@@ -1,59 +0,0 @@
-<template>
-  <div
-    class="switch-buttons pointer"
-    :data-tooltip="`Passer en mode ${massMode === 'modify' ? 'suppression':'édition'}`"
-    @click="switchMode"
-  >
-    <div>
-      <i
-        :class="['icon pencil', {disabled: massMode !== 'modify'}]"
-        aria-hidden="true"
-      />
-    </div>
-    <span class="grey">|&nbsp;</span>
-    <div>
-      <i
-        :class="['icon trash', {disabled: massMode !== 'delete'}]"
-        aria-hidden="true"
-      />
-    </div>
-  </div>
-</template>
-
-<script>
-import { mapMutations, mapState } from 'vuex';
-
-export default {
-  name: 'FeatureListMassToggle',
-
-  computed: {
-    ...mapState('feature', ['massMode'])
-  },
-
-  methods: {
-    ...mapMutations('feature', [
-      'TOGGLE_MASS_MODE',
-      'UPDATE_CHECKED_FEATURES',
-      'UPDATE_CLICKED_FEATURES']),
-
-    switchMode() {
-      this.TOGGLE_MASS_MODE(this.massMode === 'modify' ? 'delete' : 'modify');
-      this.UPDATE_CLICKED_FEATURES([]);
-      this.UPDATE_CHECKED_FEATURES([]);
-    }
-  },
-};
-</script>
-
-<style scoped>
-.switch-buttons {
-  display: flex;
-  justify-content: center;
-  align-items: baseline;
-}
-
-.grey {
-  color: #bbbbbb;
-}
-
-</style>
diff --git a/src/components/FeatureType/FeatureTypeCustomForm.vue b/src/components/FeatureType/FeatureTypeCustomForm.vue
index d064ee6ca7a27c946f98d3543e65621a946da83b..17dfbc157fb94e7b51a79c298153911a453241be 100644
--- a/src/components/FeatureType/FeatureTypeCustomForm.vue
+++ b/src/components/FeatureType/FeatureTypeCustomForm.vue
@@ -3,19 +3,34 @@
     :id="`custom_form-${form.position.value}`"
     class="ui teal segment pers-field"
   >
-    <h4>
-      Champ personnalisé
-      <button
-        class="ui small compact right floated icon button remove-field"
-        type="button"
-        @click="removeCustomForm()"
-      >
-        <i
-          class="ui times icon"
-          aria-hidden="true"
-        />
-      </button>
-    </h4>
+    <div class="custom-field-header">
+      <h4>
+        Champ personnalisé
+      </h4>
+      <div class="top-right">
+        <div
+          v-if="(form.label.value || form.name.value) && selectedFieldType !== 'Booléen'"
+          class="ui checkbox"
+        >
+          <input
+            type="checkbox"
+            name="mandatory-custom-field"
+            @change="setIsFieldMandatory($event)"
+          >
+          <label>Champ obligatoire</label>
+        </div>
+        <button
+          class="ui small compact right floated icon button remove-field"
+          type="button"
+          @click="removeCustomForm()"
+        >
+          <i
+            class="ui times icon"
+            aria-hidden="true"
+          />
+        </button>
+      </div>
+    </div>
     <div class="visible-fields">
       <div class="two fields">
         <div class="required field">
@@ -193,6 +208,7 @@ export default {
       ],
       form: {
         dataKey: 0,
+        isFieldMandatory: false,
         label: {
           errors: [],
           id_for_label: 'label',
@@ -292,12 +308,28 @@ export default {
     },
   },
 
+  watch: {
+    'form.isFieldMandatory': {
+      deep: true,
+      handler(newValue) {
+        console.log(newValue);
+      }
+    }
+  },
+
   mounted() {
     //* add datas from store to state to avoid mutating directly store with v-model (not good practice), could have used computed with getter and setter as well
     this.fillCustomFormData(this.customForm);
   },
 
   methods: {
+
+    setIsFieldMandatory(e) {
+      this.form.isFieldMandatory = e.target.checked;
+      this.updateStore();
+      console.log(this.form.isFieldMandatory);
+    },
+
     hasDuplicateOptions() {
       this.form.options.errors = [];
       const isDup =
@@ -312,7 +344,7 @@ export default {
 
     fillCustomFormData(customFormData) {
       for (const el in customFormData) {
-        if (el && this.form[el] && customFormData[el]) {
+        if (el && this.form[el] && customFormData[el] !== undefined && customFormData[el] !== null) {
           //* check if is an object, because data from api is a string, while import from django is an object
           this.form[el].value = customFormData[el].value
             ? customFormData[el].value
@@ -332,6 +364,7 @@ export default {
     updateStore() {
       const data = {
         dataKey: this.customForm.dataKey,
+        isMandatory: this.form.isFieldMandatory,
         label: this.form.label.value,
         name: this.form.name.value,
         position: this.form.position.value,
@@ -365,54 +398,66 @@ export default {
       return occurences.length === 1;
     },
 
-    checkFilledOptions() {
-      if (this.form.field_type.value === 'list') {
-        if (this.form.options.value.length < 1) {
-          return false;
-        } else if (
-          this.form.options.value.length === 1 &&
-          this.form.options.value[0] === ''
-        ) {
-          return false;
-        }
-      }
-      return true;
+    checkListOptions() {
+      if (this.form.field_type.value !== 'list') return true;
+      return this.form.options.value.length >= 2 && !this.form.options.value.includes('');
     },
 
     checkCustomForm() {
       this.form.label.errors = [];
       this.form.name.errors = [];
       this.form.options.errors = [];
+      let isValid = true;
       if (!this.form.label.value) {
         //* vérifier que le label est renseigné
         this.form.label.errors = ['Veuillez compléter ce champ.'];
-        return false;
+        isValid = false;
       } else if (!this.form.name.value) {
         //* vérifier que le nom est renseigné
         this.form.name.errors = ['Veuillez compléter ce champ.'];
-        return false;
+        isValid = false;
       } else if (!this.hasRegularCharacters(this.form.name.value)) {
         //* vérifier qu'il n'y a pas de caractères spéciaux
         this.form.name.errors = [
           'Veuillez utiliser seulement les caratères autorisés.',
         ];
-        return false;
+        isValid = false;
       } else if (!this.checkUniqueName()) {
         //* vérifier si les noms sont pas dupliqués
         this.form.name.errors = [
           'Les champs personnalisés ne peuvent pas avoir des noms similaires.',
         ];
-        return false;
-      } else if (!this.checkFilledOptions()) {
+        isValid = false;
+      } else if (!this.checkListOptions()) {
         //* s'il s'agit d'un type liste, vérifier que le champ option est bien renseigné
         this.form.options.errors = ['Veuillez compléter ce champ.'];
-        return false;
+        isValid = false;
       } else if (this.hasDuplicateOptions()) {
         //* pour le cas d'options dupliqués
-        return false;
+        isValid = false;
       }
-      return true;
+      if (!isValid) document.getElementById(`custom_form-${this.form.position.value}`).scrollIntoView({ block: 'start', inline: 'nearest' });
+      return isValid;
     },
   },
 };
 </script>
+
+<style lang="less" scoped>
+
+.custom-field-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+
+  .top-right {
+    display: flex;
+    align-items: center;
+
+    .checkbox {
+      margin-right: 5rem;
+    }
+  }
+}
+
+</style>
diff --git a/src/components/ImportTask.vue b/src/components/ImportTask.vue
index 026b7cbfe30e8b8ffcc4e7354b33642f2a7b13ec..c5792c73764138b2c6d00795e1109a1222dda73f 100644
--- a/src/components/ImportTask.vue
+++ b/src/components/ImportTask.vue
@@ -26,7 +26,9 @@
           <td>
             <h4 class="ui header align-right">
               <div :data-tooltip="importFile.geojson_file_name">
-                {{ importFile.geojson_file_name | subString }}
+                <div class="ellipsis">
+                  {{ importFile.geojson_file_name | subString }}
+                </div>
                 <div class="sub header">
                   ajouté le {{ importFile.created_on | setDate }}
                 </div>
@@ -234,5 +236,9 @@ and also iPads specifically.
   .margin-left {
     margin-left: 94%;
   }
+  h4.ui.header {
+    margin-left: 60px;
+    white-space: nowrap;
+  }
 }
 </style>
diff --git a/src/components/MessageInfo.vue b/src/components/MessageInfo.vue
index 3996dc9de95d967afa14deee2d063ddf3dfeddf0..4fe49097eaeefdfd6129215601c6921ce2e80111 100644
--- a/src/components/MessageInfo.vue
+++ b/src/components/MessageInfo.vue
@@ -1,36 +1,30 @@
 <template>
-  <transition name="fadeDownUp">
-    <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', message.level ? message.level : 'info', 'message']"
-        >
+  <li
+    :ref="'message-' + message.counter"
+    :class="['list-container', { show }]"
+  >
+    <div :class="['list-item', { show}]">
+      <div :class="['ui', message.level ? message.level : 'info', 'message']">
+        <i
+          class="close icon"
+          aria-hidden="true"
+          @click="removeListItem"
+        />
+        <div class="header">
           <i
-            class="close icon"
+            class="info circle icon"
             aria-hidden="true"
-            @click="DISCARD_MESSAGE(message)"
           />
-          <div class="header">
-            <i
-              class="info circle icon"
-              aria-hidden="true"
-            />
-            Informations
-          </div>
-          <ul class="list">
-            {{
-              message.comment
-            }}
-          </ul>
+          Informations
         </div>
+        <ul class="list">
+          {{
+            message.comment
+          }}
+        </ul>
       </div>
     </div>
-  </transition>
+  </li>
 </template>
 
 <script>
@@ -39,54 +33,100 @@ import { mapState, mapMutations } from 'vuex';
 export default {
   name: 'MessageInfo',
 
+  props: {
+    message: {
+      type: Object,
+      default: () => {},
+    },
+  },
+
+  data() {
+    return {
+      listMessages: [],
+      show: false,
+    };
+  },
+
   computed: {
     ...mapState(['messages']),
   },
 
+  mounted() {
+    setTimeout(() => {
+      this.show = true;
+    }, 15);
+  },
+
   methods: {
     ...mapMutations(['DISCARD_MESSAGE']),
+
+    removeListItem(){
+      const container = this.$refs['message-' + this.message.counter];
+      container.ontransitionend = () => {
+        this.DISCARD_MESSAGE(this.message.counter);
+      };
+      this.show = false;
+    },
   },
   
 };
 </script>
 
-<style>
-.row.over-content {
-  position: absolute; /* to display message info over page content */
-  z-index: 99;
-  opacity: 0.95;
-  width: calc(100% - 4em); /* 4em is #content left + right paddings */
-  top: calc(61px + 1em); /* 61px is #app-header height */
-  right: 2em; /* 2em is #content left paddings */
+<style scoped>
+.list-container{
+    list-style: none;
+    width: 100%;
+    height: 0;
+    position: relative;
+    cursor: pointer;
+    overflow: hidden;
+    transition: all 0.6s ease-out;
 }
-
-.fadeDownUp-enter-active {
-  animation: fadeInDown .5s;
+.list-container.show{
+  height: 6em;
 }
-.fadeDownUp-leave-active {
-  animation: fadeOutUp .5s;
+.list-container.show:not(:first-child){
+  margin-top: 10px;
 }
-@keyframes fadeOutUp {
-  0% {
-    opacity: 1;
-  }
-
-  100% {
+.list-container .list-item{
+    padding: .5rem 0;
+    width: 100%;
+    position: absolute;
     opacity: 0;
-    transform: translate3d(0, -100%, 0);
-  }
+    top: 0;
+    left: 0;
+    transition: all 0.6s ease-out;
+}
+.list-container .list-item.show{
+    opacity: 1;
 }
 
-@keyframes fadeInDown {
-  from {
-    opacity: 0;
-    transform: translate3d(0, -100%, 0);
-  }
+ul.list{
+  overflow: scroll;
+  height: 2.2em;
+  margin-bottom: .5em !important;
+}
 
-  to {
-    opacity: 1;
-    transform: translate3d(0, 0, 0);
-  }
+.ui.message {
+  overflow: hidden;
+  padding-bottom: 0 !important;
+}
+.ui.message::after {
+  content: "";
+  position: absolute;
+  bottom: 0;
+  left: 1em;
+  right: 0;
+  width: calc(100% - 2em);
+}
+.ui.info.message::after {
+  box-shadow: 0px -8px 5px 3px rgb(248, 255, 255);
+}
+.ui.positive.message::after {
+  box-shadow: 0px -8px 5px 3px rgb(248, 255, 255);
+}
+.ui.negative.message::after {
+  box-shadow: 0px -8px 5px 3px rgb(248, 255, 255);
 }
 
 .ui.message > .close.icon {
diff --git a/src/components/MessageInfoList.vue b/src/components/MessageInfoList.vue
new file mode 100644
index 0000000000000000000000000000000000000000..5974a28d54314d12975ceb58e0cf6eee917aa603
--- /dev/null
+++ b/src/components/MessageInfoList.vue
@@ -0,0 +1,50 @@
+<template>
+  <div
+    v-if="messages && messages.length > 0"
+    class="row over-content"
+  >
+    <div class="fourteen wide column">
+      <ul
+        class="message-list"
+        aria-live="assertive"
+      >
+        <MessageInfo
+          v-for="message in messages"
+          :key="'message-' + message.counter"
+          :message="message"
+        />
+      </ul>
+    </div>
+  </div>
+</template>
+
+<script>
+import { mapState } from 'vuex';
+import MessageInfo from '@/components/MessageInfo';
+
+export default {
+  name: 'MessageInfoList',
+  
+  components: {
+    MessageInfo,
+  },
+  computed: {
+    ...mapState(['messages']),
+  },
+};
+</script>
+
+<style scoped>
+
+.row.over-content {
+  position: absolute; /* to display message info over page content */
+  z-index: 99;
+  opacity: 0.95;
+  width: calc(100% - 4em); /* 4em is #content left + right paddings */
+  top: calc(61px + 1em); /* 61px is #app-header height */
+  right: 2em; /* 2em is #content left paddings */
+}
+.message-list{
+    list-style: none;
+}
+</style>
\ No newline at end of file
diff --git a/src/components/Project/Detail/ProjectFeatureTypes.vue b/src/components/Project/Detail/ProjectFeatureTypes.vue
index 7cb7968533a7c1f3a17d9d919486a52e05b3075b..f98acb30e2b0eb3b59d8be7f9adca67a805e03a8 100644
--- a/src/components/Project/Detail/ProjectFeatureTypes.vue
+++ b/src/components/Project/Detail/ProjectFeatureTypes.vue
@@ -426,10 +426,11 @@
 </template>
 
 <script>
+import { csv } from 'csvtojson';
 
 import { mapState, mapGetters, mapMutations, mapActions } from 'vuex';
 
-import { fileConvertSizeToMo, csvToJson } from '@/assets/js/utils';
+import { fileConvertSizeToMo } from '@/assets/js/utils';
 import FeatureTypeLink from '@/components/FeatureType/FeatureTypeLink';
 
 export default {
@@ -633,7 +634,6 @@ export default {
         try {
           fr.readAsText(this.csvFileToImport);
           fr.onloadend = () => {
-
             // Find csv delimiter
             const commaDelimited = fr.result.split('\n')[0].includes(',');
             const semicolonDelimited = fr.result.split('\n')[0].includes(';');
@@ -644,19 +644,16 @@ export default {
               this.featureTypeImporting = false;
               return;
             }
-
             // Check if file contains 'lat' and 'long' fields
-            const headersLine =
-              fr.result
-                .split('\n')[0]
-                .split(delimiter)
-                .map(el => {
-                  return el.replace('\r', '');
-                })
-                .filter(el => {
-                  return el === 'lat' || el === 'lon';
-                });
-
+            const headers = fr.result
+              .split('\n')[0]
+              .split(delimiter)
+              .map(el => {
+                return el.replace('\r', '');
+              });
+            const headersCoord = headers.filter(el => {
+              return el === 'lat' || el === 'lon';
+            });
             // Look for 2 decimal fields in first line of csv
             // corresponding to lon and lat
             const sampleLine =
@@ -667,9 +664,13 @@ export default {
                   return !isNaN(el) && el.indexOf('.') !== -1;
                 })
                 .filter(Boolean);
-            if (sampleLine.length > 1 && headersLine.length === 2) {
+            if (sampleLine.length > 1 && headersCoord.length === 2) {
               this.csvError = null;
-              this.csvImport = csvToJson(fr.result, delimiter);
+              csv()
+                .fromString(fr.result)
+                .then((jsonObj)=>{
+                  this.csvImport = jsonObj;
+                });
               this.featureTypeImporting = false;
               //* stock filename to import features afterward
               this.SET_FILE_TO_IMPORT(this.csvFileToImport);
diff --git a/src/components/Project/FeaturesListAndMap/FeatureListTable.vue b/src/components/Project/FeaturesListAndMap/FeatureListTable.vue
index fff79a991ecb1479945455bb91747a99b68c5ed4..a522fc895075e010c2c9e902d4cc6e395bf1c636 100644
--- a/src/components/Project/FeaturesListAndMap/FeatureListTable.vue
+++ b/src/components/Project/FeaturesListAndMap/FeatureListTable.vue
@@ -1,8 +1,53 @@
 <template>
   <div>
-    <div class="table-mobile-buttons left-align">
-      <FeatureListMassToggle />
+    <div class="ui form">
+      <div
+        v-if="isOnline"
+        class="inline fields"
+      >
+        <label
+          data-tooltip="Choisir un type de sélection de signalements pour effectuer une action"
+          data-position="bottom left"
+        >Mode de sélection :</label>
+        <div class="field">
+          <div class="ui radio checkbox">
+            <input
+              id="edit-status"
+              v-model="mode"
+              type="radio"
+              name="mode"
+              value="edit-status"
+            >
+            <label for="edit-status">Édition de statut</label>
+          </div>
+        </div>
+        <div class="field">
+          <div class="ui radio checkbox">
+            <input
+              id="edit-attributes"
+              v-model="mode"
+              type="radio"
+              name="mode"
+              value="edit-attributes"
+            >
+            <label for="edit-attributes">Édition d'attribut</label>
+          </div>
+        </div>
+        <div class="field">
+          <div class="ui radio checkbox">
+            <input
+              id="delete-features"
+              v-model="mode"
+              type="radio"
+              name="mode"
+              value="delete-features"
+            >
+            <label for="delete-features">Suppression de signalement</label>
+          </div>
+        </div>
+      </div>
     </div>
+
     <div
       data-tab="list"
       class="dataTables_wrapper no-footer"
@@ -15,10 +60,11 @@
         <thead>
           <tr>
             <th
+              v-if="isOnline"
               scope="col"
               class="dt-center"
             >
-              <FeatureListMassToggle />
+              Sélection
             </th>
 
             <th
@@ -26,7 +72,7 @@
               class="dt-center"
             >
               <div
-                class="pointer"
+                :class="isOnline ? 'pointer' : 'disabled'"
                 @click="changeSort('status')"
               >
                 Statut
@@ -45,7 +91,7 @@
               class="dt-center"
             >
               <div
-                class="pointer"
+                :class="isOnline ? 'pointer' : 'disabled'"
                 @click="changeSort('feature_type')"
               >
                 Type
@@ -64,7 +110,7 @@
               class="dt-center"
             >
               <div
-                class="pointer"
+                :class="isOnline ? 'pointer' : 'disabled'"
                 @click="changeSort('title')"
               >
                 Nom
@@ -83,7 +129,7 @@
               class="dt-center"
             >
               <div
-                class="pointer"
+                :class="isOnline ? 'pointer' : 'disabled'"
                 @click="changeSort('updated_on')"
               >
                 Dernière modification
@@ -103,7 +149,7 @@
               class="dt-center"
             >
               <div
-                class="pointer"
+                :class="isOnline ? 'pointer' : 'disabled'"
                 @click="changeSort('display_creator')"
               >
                 Auteur
@@ -123,7 +169,7 @@
               class="dt-center"
             >
               <div
-                class="pointer"
+                :class="isOnline ? 'pointer' : 'disabled'"
                 @click="changeSort('display_last_editor')"
               >
                 Dernier éditeur
@@ -144,7 +190,11 @@
             v-for="(feature, index) in paginatedFeatures"
             :key="index"
           >
-            <td class="dt-center">
+            <td
+              v-if="isOnline"
+              id="select"
+              class="dt-center"
+            >
               <div
                 :class="['ui checkbox', {disabled: !checkRights(feature)}]"
               >
@@ -161,7 +211,10 @@
               </div>
             </td>
 
-            <td class="dt-center">
+            <td
+              id="status"
+              class="dt-center"
+            >
               <div
                 v-if="feature.status === 'archived'"
                 data-tooltip="Archivé"
@@ -199,7 +252,10 @@
                 />
               </div>
             </td>
-            <td class="dt-center">
+            <td
+              id="type"
+              class="dt-center"
+            >
               <router-link
                 :to="{
                   name: 'details-type-signalement',
@@ -207,11 +263,15 @@
                     feature_type_slug: feature.feature_type.slug,
                   },
                 }"
+                class="ellipsis space-left"
               >
                 {{ feature.feature_type.title }}
               </router-link>
             </td>
-            <td class="dt-center">
+            <td
+              id="name"
+              class="dt-center"
+            >
               <router-link
                 :to="{
                   name: 'details-signalement-filtre',
@@ -220,21 +280,27 @@
                   },
                   query: { ...queryparams, offset: queryparams.offset + index }
                 }"
+                class="ellipsis space-left"
               >
                 {{ feature.title || feature.feature_id }}
               </router-link>
             </td>
-            <td class="dt-center">
+            <td
+              id="update"
+              class="dt-center"
+            >
               {{ feature.updated_on | formatDate }}
             </td>
             <td
               v-if="user"
+              id="author"
               class="dt-center"
             >
               {{ feature.display_creator || ' ---- ' }}
             </td>
             <td
               v-if="user"
+              id="last_editor"
               class="dt-center"
             >
               {{ feature.display_last_editor || ' ---- ' }}
@@ -266,7 +332,7 @@
         sur {{ featuresCount }} éléments
       </div>
       <div
-        v-if="pageNumbers.length > 1"
+        v-if="pageNumbers.length > 1 && isOnline"
         id="table-features_paginate"
         class="dataTables_paginate paging_simple_numbers"
       >
@@ -335,7 +401,6 @@
 
 <script>
 import { mapState, mapGetters, mapMutations } from 'vuex';
-import FeatureListMassToggle from '@/components/Feature/FeatureListMassToggle';
 import { formatStringDate } from '@/utils';
 
 export default {
@@ -347,10 +412,13 @@ export default {
     },
   },
 
-  components: {
-    FeatureListMassToggle,
+  beforeRouteLeave (to, from, next) {
+    if (to.name !== 'editer-attribut-signalement') {
+      this.UPDATE_CHECKED_FEATURES([]); // empty if not needed anymore
+    }
+    next(); // continue navigation
   },
-
+  
   props: {
     paginatedFeatures: {
       type: Array,
@@ -379,15 +447,34 @@ export default {
     queryparams: {
       type: Object,
       default: null,
-    }
+    },
+    editAttributesFeatureType: {
+      type: String,
+      default: null,
+    },
   },
 
   computed: {
     ...mapGetters(['permissions']),
-    ...mapState(['user', 'USER_LEVEL_PROJECTS']),
+    ...mapState([
+      'user',
+      'USER_LEVEL_PROJECTS',
+      'isOnline'
+    ]),
     ...mapState('projects', ['project']),
     ...mapState('feature', ['clickedFeatures', 'massMode']),
 
+    mode: {
+      get() {
+        return this.massMode;
+      },
+      set(newMode) {
+        this.TOGGLE_MASS_MODE(newMode);
+        this.UPDATE_CLICKED_FEATURES([]);
+        this.UPDATE_CHECKED_FEATURES([]);
+      },
+    },
+
     userStatus() {
       return this.USER_LEVEL_PROJECTS[this.$route.params.slug];
     },
@@ -432,17 +519,21 @@ export default {
     },
   },
 
-  destroyed() {
-    this.UPDATE_CHECKED_FEATURES([]);
-  },
-
   methods: {
     ...mapMutations('feature', [
       'UPDATE_CLICKED_FEATURES',
       'UPDATE_CHECKED_FEATURES',
+      'TOGGLE_MASS_MODE',
     ]),
 
     storeClickedFeature(feature) {
+      if (this.massMode === 'edit-attributes') { // if modifying attributes
+        if (this.checkedFeatures.length === 0) { // store feature type slug to restrict selection for next selected features
+          this.$emit('update:editAttributesFeatureType', feature.feature_type.slug);
+        } else if (this.checkedFeatures.length === 1 && this.checkedFeatures[0] === feature.feature_id) {
+          this.$emit('update:editAttributesFeatureType', null); // delete feature type slug if last checkedFeatures is unselected, to allow other types selection
+        }
+      }
       this.UPDATE_CLICKED_FEATURES([
         ...this.clickedFeatures,
         { feature_id: feature.feature_id, feature_type: feature.feature_type.slug }
@@ -465,7 +556,13 @@ export default {
         Contributeur : ['draft', 'pending', 'published'],
       };
 
-      if (this.userStatus === 'Contributeur' && feature.display_creator !== this.user.username) {
+      if (this.checkedFeatures.length > 0 && // check if selection should be restricted to a specific feature type, for attributes modification
+        feature.feature_type.slug !== this.editAttributesFeatureType &&
+        this.massMode === 'edit-attributes') {
+        return false;
+      } else if (this.user.is_superuser) {
+        return true;
+      } else if (this.userStatus === 'Contributeur' && feature.display_creator !== `${this.user.first_name} ${this.user.last_name}`) {
         return false;
       } else if (permissions[this.userStatus]) {
         return permissions[this.userStatus].includes(feature.status);
@@ -475,20 +572,13 @@ export default {
     },
 
     checkRights(feature) {
-      switch (this.massMode) {
-      case 'modify':
+      if (this.massMode.includes('edit')) {
         return this.canEditFeature(feature);
-      case 'delete':
+      } else if (this.massMode === 'delete-features') {
         return this.canDeleteFeature(feature);
       }
     },
 
-    switchMode() {
-      this.$emit('update:mode', this.mode === 'modify' ? 'delete' : 'modify');
-      this.UPDATE_CLICKED_FEATURES([]);
-      this.$store.commit('feature/UPDATE_CHECKED_FEATURES', []);
-    },
-
     isSortedAsc(column) {
       return this.sort.column === column && this.sort.ascending;
     },
@@ -497,6 +587,7 @@ export default {
     },
 
     changeSort(column) {
+      if (!this.isOnline) return;
       if (this.sort.column === column) {
         //changer only order
         this.$emit('update:sort', {
@@ -612,6 +703,12 @@ i.icon.sort:not(.down):not(.up) {
 .table-mobile-buttons {
   margin-bottom: 1em;
 }
+
+/* increase contrast between available checkboxes and disabled ones */
+#table-features .ui.disabled.checkbox label::before {
+  background-color: #fbf5f5;;  
+}
+
 @media only screen and (min-width: 761px) {
   .table-mobile-buttons {
     display: none !important;
@@ -688,25 +785,25 @@ and also iPads specifically.
   /*
 	Label the data
 	*/
-  td:nth-of-type(1):before {
+  td#select:before {
     content: "";
   }
-  td:nth-of-type(2):before {
+  td#status:before {
     content: "Statut";
   }
-  td:nth-of-type(3):before {
+  td#type:before {
     content: "Type";
   }
-  td:nth-of-type(4):before {
+  td#name:before {
     content: "Nom";
   }
-  td:nth-of-type(5):before {
+  td#update:before {
     content: "Dernière modification";
   }
-  td:nth-of-type(6):before {
+  td#author:before {
     content: "Auteur";
   }
-  td:nth-of-type(7):before {
+  td#last_editor:before {
     content: "Dernier éditeur";
   }
 
@@ -725,6 +822,11 @@ and also iPads specifically.
     text-align: center;
     margin: .5em 0;
   }
+  .space-left {
+    max-width: 100%;
+    display: inline-block;
+    padding-left: 3em;
+  }
 }
 @media only screen and (max-width: 410px) {
   .ui.table tr td {
diff --git a/src/components/Project/FeaturesListAndMap/FeaturesListAndMapFilters.vue b/src/components/Project/FeaturesListAndMap/FeaturesListAndMapFilters.vue
index a2e8029578b979abb062e153e80da30acace7988..5d2c39aac1ef890d6bbb17b0d2e572ff9b2a0493 100644
--- a/src/components/Project/FeaturesListAndMap/FeaturesListAndMapFilters.vue
+++ b/src/components/Project/FeaturesListAndMap/FeaturesListAndMapFilters.vue
@@ -86,11 +86,11 @@
               </div>
             </div>
             <div
-              v-if="checkedFeatures.length > 0 && massMode === 'modify'"
+              v-if="checkedFeatures.length > 0 && massMode.includes('edit') && isOnline"
               class="ui dropdown button compact button-hover-green tiny-margin-left"
-              data-tooltip="Modifier le statut des Signalements"
+              :data-tooltip="`Modifier le${massMode.includes('status') ? ' statut' : 's attributs'} des signalements`"
               data-position="bottom right"
-              @click="toggleModifyStatus"
+              @click="editFeatures"
             >
               <i
                 class="pencil fitted icon"
@@ -109,7 +109,7 @@
                     v-for="status in availableStatus"
                     :key="status.value"
                     class="item"
-                    @click="$emit('modify-status', status.value)"
+                    @click="$emit('edit-status', status.value)"
                   >
                     {{ status.name }}
                   </span>
@@ -117,7 +117,7 @@
               </div>
             </div>
             <div
-              v-if="checkedFeatures.length > 0 && massMode === 'delete'"
+              v-if="checkedFeatures.length > 0 && massMode === 'delete-features' && isOnline"
               class="ui button compact button-hover-red tiny-margin-left"
               data-tooltip="Supprimer tous les signalements sélectionnés"
               data-position="bottom right"
@@ -138,7 +138,7 @@
     >
       <div
         id="type"
-        class="field column"
+        :class="['field column', { 'disabled': !isOnline }]"
       >
         <label>Type</label>
         <Dropdown
@@ -151,7 +151,7 @@
       </div>
       <div
         id="statut"
-        class="field column"
+        :class="['field column', { 'disabled': !isOnline }]"
       >
         <label>Statut</label>
         <!--  //* giving an object mapped on key name -->
@@ -165,7 +165,7 @@
       </div>
       <div
         id="name"
-        class="field column"
+        :class="['field column', { 'disabled': !isOnline }]"
       >
         <label>Nom</label>
         <div class="ui icon input">
@@ -235,7 +235,12 @@ export default {
           ...initialPagination
         };
       }
-    }
+    },
+    editAttributesFeatureType: {
+      type: String,
+      default: null,
+    },
+
   },
 
   data() {
@@ -260,7 +265,8 @@ export default {
   computed: {
     ...mapState([
       'user',
-      'USER_LEVEL_PROJECTS'
+      'USER_LEVEL_PROJECTS',
+      'isOnline'
     ]),
     ...mapState('feature', [
       'checkedFeatures',
@@ -333,11 +339,37 @@ export default {
       this.showModifyStatus = false;
     },
 
+    editFeatures() {
+      switch (this.massMode) {
+      case 'edit-status':
+        this.toggleModifyStatus();
+        break;
+      case 'edit-attributes':
+        this.displayAttributesForm();
+        break;
+      }
+    },
+
     toggleModifyStatus() {
       this.showModifyStatus = !this.showModifyStatus;
       this.showAddFeature = false;
     },
 
+    displayAttributesForm() {
+      if (this.checkedFeatures.length > 1) {
+        this.$router.push({
+          name: 'editer-attribut-signalement',
+          params: {
+            slug_type_signal: this.editAttributesFeatureType,
+          },
+        });
+      } else {
+        this.$store.commit('DISPLAY_MESSAGE', {
+          comment: 'Veuillez sélectionner au moins 2 signalements pour l\'édition multiple d\'attributs'
+        });
+      }
+    },
+
     clickOutsideDropdown(e) {
       if (!e.target.closest('#button-dropdown')) {
         this.showModifyStatus = false;
diff --git a/src/components/Projects/ProjectsListItem.vue b/src/components/Projects/ProjectsListItem.vue
index 37d03f00fa41b3dd90b186e78f239a449cba028c..1059a36c29463f8528fb450f0137b0c571f18757 100644
--- a/src/components/Projects/ProjectsListItem.vue
+++ b/src/components/Projects/ProjectsListItem.vue
@@ -32,7 +32,7 @@
           class="preview"
         />
       </div>
-      <div class="meta">
+      <div class="meta top">
         <span class="right floated">
           <strong v-if="project.moderation">Projet modéré</strong>
           <strong v-else>Projet non modéré</strong>
@@ -132,4 +132,34 @@ export default {
   overflow: scroll;
   margin-bottom: 0.8em;
 }
+
+.description {
+  p {
+    text-align: justify;
+  }
+}
+
+@media screen and (max-width: 767px) {
+  .content {
+    width: 90% !important;
+
+    .meta.top {
+      display: flex;
+      flex-direction: column;
+      align-items: flex-start;
+      justify-content: center;
+
+      .right.floated {
+        float: none !important;
+        margin-left: 0 !important;
+        margin-bottom: 0.5em;
+      }
+
+      span {
+        margin: 0.15em 0;
+      }
+    }
+  }
+}
+
 </style>
diff --git a/src/components/Projects/ProjectsMenu.vue b/src/components/Projects/ProjectsMenu.vue
index 442ab3a5a7e68e24e0c26a5a4ec8cad38f6d0a64..ece71b68de5d7d37283af90a410e65280395f4a9 100644
--- a/src/components/Projects/ProjectsMenu.vue
+++ b/src/components/Projects/ProjectsMenu.vue
@@ -1,18 +1,21 @@
 <template>
-  <div class="filters-container">
-    <div class="ui styled accordion">
+  <div id="filters-container">
+    <div
+      class="ui styled accordion"
+      @click="displayFilters = !displayFilters"
+    >
       <div
         id="filters"
         class="title collapsible-filters"
       >
         FILTRES
         <i
-          class="ui icon caret right down"
+          :class="['ui icon customcaret', { 'collapsed': !displayFilters }]"
           aria-hidden="true"
         />
       </div>
     </div>
-    <div class="ui menu filters hidden">
+    <div :class="['ui menu filters', { 'hidden': displayFilters }]">
       <div class="item">
         <label>
           Niveau d'autorisation requis
@@ -40,7 +43,7 @@
           v-on="$listeners"
         />
       </div>
-      <div class="right item">
+      <div class="item">
         <label>
           Recherche par nom
         </label>
@@ -69,6 +72,7 @@ export default {
 
   data() {
     return {
+      displayFilters: false,
       moderationOptions: [
         {
           label: 'Tous',
@@ -157,26 +161,10 @@ export default {
     }
   },
 
-  mounted() {
-    const el = document.getElementsByClassName('collapsible-filters');
-
-    el[0].addEventListener('click', function() {
-      const icon = document.getElementsByClassName('caret');
-      icon[0].classList.toggle('right');
-      const content = document.getElementsByClassName('filters');
-      content[0].classList.toggle('hidden');
-      if (content[0].style.maxHeight){
-        content[0].style.maxHeight = null;
-      } else {
-        content[0].style.maxHeight = content[0].scrollHeight + 5 + 'px';
-      }
-    });
-  },
-
   methods: {
     ...mapActions('projects', [
       'SEARCH_PROJECTS'
-    ])
+    ]),
   }
 };
 </script>
@@ -189,7 +177,7 @@ export default {
   transition: @arguments;
 }
 
-.filters-container {
+#filters-container {
 	width: 100%;
 	display: flex;
 	flex-direction: column;
@@ -200,6 +188,23 @@ export default {
 		.collapsible-filters {
 			font-size: 1.25em;
 			padding-right: 0;
+      .customcaret{
+        transition: transform .2s ease;
+        &.collapsed {
+          transform: rotate(180deg);
+        }
+        &::before{
+          position: relative;
+          right: 0;
+          top: 65%;
+          color: #999;
+          margin-top: 4px;
+          border-color: #999 transparent transparent;
+          border-style: solid;
+          border-width: 5px 5px 0;
+          content: "";
+        }
+      }
 		}
 	}
 	.filters {
@@ -207,16 +212,16 @@ export default {
 		height:auto;
 		min-height: 0;
 		max-height:75px;
+    opacity: 1;
 		margin: 0 0 1em 0;
     border: none;
     box-shadow: none;
-		.transition-properties(max-height 0.2s ease-out;);
+		.transition-properties(all 0.2s ease-out;);
 		.item {
 			display: flex;
 			flex-direction: column;
 			align-items: flex-start !important;
-
-			padding: 0.4em 0.6em 0.4em 0;
+			padding: 0.5em;
 
 			label {
 				margin-bottom: 0.2em;
@@ -230,20 +235,43 @@ export default {
     .item::before {
 			width: 0;
 		}
-    .right.item {
-      padding-right: 0;
-      #search-projects {
-        width: 100%;
-      }
+    #search-projects {
+      width: 100%;
     }
-		.right.item::before {
-			width: 0;
-		}
 	}
 	.filters.hidden {
-		max-height: 0;
 		overflow: hidden;
-		border: none;
+    opacity: 0;
+    max-height: 0;
 	}
 }
+
+@media screen and (min-width: 701px) {
+  .item {
+    &:first-child {
+      padding-left: 0;
+    }
+    &:last-child {
+      padding-right: 0;
+    }
+  }
+}
+
+@media screen and (max-width: 700px) {
+  #filters-container {
+
+    .filters {
+      display: flex;
+      flex-direction: column;
+      max-height: 275px;
+      .transition-properties(all 0.2s ease-out;);
+
+      .item {
+        width: 100%;
+        padding-right: 0;
+        padding-left: 0;
+      }
+    }
+  }
+}
 </style>
diff --git a/src/main.js b/src/main.js
index 4bf957d27033d17d20e97b20b73a92e60d13335f..cabb80a8ddc6293333db579cbb747d5852b9590b 100644
--- a/src/main.js
+++ b/src/main.js
@@ -26,7 +26,7 @@ Vue.config.productionTip = false;
 
 // gestion mise à jour du serviceWorker et du precache
 var refreshing=false;
-if(navigator.serviceWorker){
+if (navigator.serviceWorker) {
   navigator.serviceWorker.addEventListener('controllerchange', () => {
     // We'll also need to add 'refreshing' to our data originally set to false.
     if (refreshing) {
@@ -44,7 +44,7 @@ const onConfigLoaded = function(config){
   store.commit('SET_CONFIG', config);
   setInterval(() => { //* check if navigator is online
     store.commit('SET_IS_ONLINE', navigator.onLine);
-  }, 10000);
+  }, 2000);
 
   // set title and favico
   document.title = `${config.VUE_APP_APPLICATION_NAME} ${config.VUE_APP_APPLICATION_ABSTRACT}`;
diff --git a/src/router/index.js b/src/router/index.js
index 8fe8e9d76322ca63af83c24d4c95a744059aec64..56953714fbab4848193e56d9fab1c6dcd82e8f04 100644
--- a/src/router/index.js
+++ b/src/router/index.js
@@ -130,6 +130,11 @@ const routes = [
     name: 'editer-signalement',
     component: () => import('../views/Feature/FeatureEdit.vue')
   },
+  {
+    path: `/${projectBase}/:slug/type-signalement/:slug_type_signal/editer-signalements-attributs/`,
+    name: 'editer-attribut-signalement',
+    component: () => import('../views/Feature/FeatureEdit.vue')
+  },
 
   {
     path: '/projet/:slug/catalog/:feature_type_slug',
diff --git a/src/services/map-service.js b/src/services/map-service.js
index c64c318d9e024d9bb3900caff85d1ef8456df50c..63e4204c00689cba3b4120faf5aa77b7abc72ef1 100644
--- a/src/services/map-service.js
+++ b/src/services/map-service.js
@@ -561,7 +561,12 @@ const mapService = {
                   </div>
                   ${author}
                   `;
-    const featureId = feature.getProperties ? feature.getProperties().feature_id || feature.getId() : feature.id; //* feature.id was used with leaflet, with ol feature.getId replace it, but keeping it as fallback can prevent regression
+    const featureId =
+      feature.getId() ?
+        feature.getId() :
+        feature.getProperties ?
+          feature.getProperties().feature_id :
+          feature.id;
     return { html, feature_type, featureId };
   },
 
diff --git a/src/store/index.js b/src/store/index.js
index f5ed15a226fbf68d621784197be62e48aa7c1968..2be432d43f1c6c21dc1de190ecb8cc843cac176b 100644
--- a/src/store/index.js
+++ b/src/store/index.js
@@ -34,6 +34,7 @@ export default new Vuex.Store({
       message: 'En cours de chargement'
     },
     logged: false,
+    messageCount: 0,
     messages: [],
     reloadIntervalId: null,
     staticPages: null,
@@ -75,16 +76,18 @@ export default new Vuex.Store({
       state.levelsPermissions = levelsPermissions;
     },
     DISPLAY_MESSAGE(state, message) {
-      state.messages = [message, ...state.messages];
+      message['counter'] = state.messageCount;
+      state.messageCount += 1;
+      state.messages = [message, ...state.messages]; // add new message at the beginning of the list
       if (document.getElementById('scroll-top-anchor')) {
         document.getElementById('scroll-top-anchor').scrollIntoView({ block: 'start', inline: 'nearest' });
       }
       setTimeout(() => {
-        state.messages = [];
+        state.messages = state.messages.slice(0, -1); // remove one message from the end of the list
       }, 3000);
     },
-    DISCARD_MESSAGE(state, message) {
-      state.messages = state.messages.filter((el) => el.comment !== message.comment);
+    DISCARD_MESSAGE(state, messageCount) {
+      state.messages = state.messages.filter((mess) => mess.counter !== messageCount);
     },
     CLEAR_MESSAGES(state) {
       state.messages = [];
diff --git a/src/store/modules/feature-type.store.js b/src/store/modules/feature-type.store.js
index 49921f0132dfd260b45b198d359bd45fc4f4fd30..0fe6b25c8de60295e8d2ba2a166fdd592ceead4e 100644
--- a/src/store/modules/feature-type.store.js
+++ b/src/store/modules/feature-type.store.js
@@ -111,6 +111,7 @@ const feature_type = {
         customfield_set: state.customForms.map(el => {
           return {
             position: el.position,
+            is_mandatory: el.isMandatory,
             label: el.label,
             name: el.name,
             field_type: el.field_type,
diff --git a/src/store/modules/feature.store.js b/src/store/modules/feature.store.js
index ef93083a8691c11c3de37a5dbd79082f3058b67f..7f82a9c848fcf6fa8e385472d0cdc7c5b40b073f 100644
--- a/src/store/modules/feature.store.js
+++ b/src/store/modules/feature.store.js
@@ -15,7 +15,7 @@ const feature = {
     form: null,
     linkedFormset: [], //* used to edit in feature_edit
     linked_features: [], //* used to display in feature_detail
-    massMode: 'modify',
+    massMode: 'edit-status',
   },
   mutations: {
     SET_FEATURES(state, features) {
@@ -174,7 +174,6 @@ const feature = {
 
       const cancelToken = axios.CancelToken.source();
       commit('SET_CANCELLABLE_SEARCH_REQUEST', cancelToken, { root: true });
-      //commit('SET_CURRENT_FEATURE', null); //? Est-ce que c'est nécessaire ? -> fait sauter l'affichage au clic sur un signalement lié (feature_detail)
       const url = `${rootState.configuration.VUE_APP_DJANGO_API_BASE}projects/${project_slug}/feature/?id=${feature_id}`;
       return axios
         .get(url, { cancelToken: cancelToken.token })
@@ -186,18 +185,20 @@ const feature = {
           return response;
         })
         .catch((error) => {
+          console.error('Error while getting feature for id = ', feature_id, error);
           throw error;
         });
     },
 
     SEND_FEATURE({ state, rootState, commit, dispatch }, routeName) {
-      function redirect(featureId) {
+      function redirect(featureId, featureName, response) {
+        if (routeName === 'editer-attribut-signalement') return response; // exit function to avoid conflict with next feature call to GET_PROJECT_FEATURE when modifying more than 2 features
         commit(
           'DISPLAY_MESSAGE',
           {
             comment: routeName === 'ajouter-signalement' ?
               'Le signalement a été crée' :
-              'Le signalement a été mis à jour',
+              `Le signalement ${featureName} a été mis à jour`,
             level: 'positive'
           },
           { root: true },
@@ -209,35 +210,34 @@ const feature = {
             feature_id: featureId
           })
           .then(() => {
-            if (routeName.includes('details-signalement')) return;
+            if (routeName === 'details-signalement') return response;
             router.push({
               name: 'details-signalement',
               params: {
                 slug_type_signal: rootState['feature-type'].current_feature_type_slug,
                 slug_signal: featureId,
-                message: routeName === 'ajouter-signalement' ? 'Le signalement a été crée' : 'Le signalement a été mis à jour'
               },
             });
           });
+        return response;
       }
 
-      async function handleOtherForms(featureId) {
+      async function handleOtherForms(featureId, featureName, response) {
         await dispatch('SEND_ATTACHMENTS', featureId);
         await dispatch('PUT_LINKED_FEATURES', featureId);
-        redirect(featureId);
+        return redirect(featureId, featureName, response);
       }
 
       function createGeojson() { //* prepare feature data to send
         const extraFormObject = {}; //* prepare an object to be flatten in properties of geojson
         for (const field of state.extra_forms) {
-          extraFormObject[field.name] = field.value;
+          if (field.value !== null) extraFormObject[field.name] = field.value;
         }
-        //const feature = state.form || state.currentFeature;
         return {
           id: state.form.feature_id || state.currentFeature.feature_id,
           type: 'Feature',
           geometry: state.form.geometry || state.form.geom ||
-                      state.currentFeature.geometry || state.currentFeature.geom,
+          state.currentFeature.geometry || state.currentFeature.geom,
           properties: {
             title: state.form.title,
             description: state.form.description.value,
@@ -266,12 +266,14 @@ const feature = {
         data: geojson
       }).then((response) => {
         if ((response.status === 200 || response.status === 201) && response.data) {
+          const featureId = response.data.id;
+          const featureName = response.data.properties.title;
           if (state.attachmentFormset.length > 0 ||
             state.linkedFormset.length > 0 ||
             state.attachmentsToDelete.length > 0) {
-            handleOtherForms(response.data.id);
+            return handleOtherForms(featureId, featureName, response);
           } else {
-            redirect(response.data.id);
+            return redirect(featureId, featureName, response);
           }
         }
       })
@@ -298,7 +300,7 @@ const feature = {
             });
           }
           else {
-            console.error(error);
+            console.error('Error while sending feature', error);
             throw error;
           }
           throw error;
diff --git a/src/utils/index.js b/src/utils/index.js
index 432cd2970a33e4be66ba5293a3501b9e7f76b2d5..a89354b8305aaae4b1d12e04003990b7e25cda55 100644
--- a/src/utils/index.js
+++ b/src/utils/index.js
@@ -58,4 +58,29 @@ export function allowedStatus2change(user, isModerate, userStatus, isOwnFeature,
     }
   }
   return [];
+}
+
+export function transformProperties(prop) {
+  const type = typeof prop;
+  const date = new Date(prop);
+  const regInteger = /^-*?\d+$/;
+  const regFloat = /^-*?\d*?\.\d+$/;
+  const regText = /[\r\n]/;
+  if (type === 'boolean' || prop.toLowerCase() === 'true' || prop.toLowerCase() === 'False') {
+    return 'boolean';
+  } else if (regInteger.test(prop) || Number.isSafeInteger(prop)) {
+    return 'integer';
+  } else if (
+    type === 'string' &&
+    ['/', ':', '-'].some((el) => prop.includes(el)) && // check for chars found in datestring
+    date instanceof Date &&
+    !isNaN(date.valueOf())
+  ) {
+    return 'date';
+  } else if (regFloat.test(prop) ||  type === 'number' && !isNaN(parseFloat(prop))) {
+    return 'decimal';
+  } else if (regText.test(prop)) {
+    return 'text';
+  }
+  return 'char'; //* string by default, most accepted type in database
 }
\ No newline at end of file
diff --git a/src/views/Feature/FeatureDetail.vue b/src/views/Feature/FeatureDetail.vue
index 6926b84925ecad4dde6bd8fe2422beddc5bafdf5..fd547578dd6184020837ccb692fce61e37dcd08f 100644
--- a/src/views/Feature/FeatureDetail.vue
+++ b/src/views/Feature/FeatureDetail.vue
@@ -13,8 +13,12 @@
             :feature-type="featureType"
             :fast-edition-mode="project.fast_edition_mode"
             :display-to-list-button="displayToListButton"
-            @setIsCancelling="isCanceling = true"
+            :is-feature-creator="isFeatureCreator"
+            :can-edit-feature="canEditFeature"
+            :can-delete-feature="canDeleteFeature"
+            @setIsDeleting="isDeleting = true"
             @tofeature="pushNgo"
+            @updateEvents="getFeatureEvents"
           />
         </div>
       </div>
@@ -24,6 +28,7 @@
             v-if="project"
             :feature-type="featureType"
             :fast-edition-mode="project.fast_edition_mode"
+            :can-edit-feature="canEditFeature"
             @tofeature="pushNgo"
           />
         </div>
@@ -60,28 +65,33 @@
           />
         </div>
       </div>
+
       <div
-        v-if="isCanceling"
+        v-if="isDeleting"
         class="ui dimmer modals visible active"
       >
         <div
           :class="[
-            'ui mini modal subscription',
-            { 'active visible': isCanceling },
+            'ui mini modal',
+            { 'active visible': isDeleting },
           ]"
         >
           <i
             class="close icon"
             aria-hidden="true"
-            @click="isCanceling = false"
+            @click="isDeleting = false"
           />
-          <div class="ui icon header">
+          <div
+            v-if="isDeleting"
+            class="ui icon header"
+          >
             <i
               class="trash alternate icon"
               aria-hidden="true"
             />
             Supprimer le signalement
           </div>
+
           <div class="actions">
             <button
               type="button"
@@ -93,7 +103,61 @@
           </div>
         </div>
       </div>
+
+      <div
+        v-if="isLeaving"
+        class="ui dimmer modals visible active"
+      >
+        <div
+          :class="[
+            'ui mini modal',
+            { 'active visible': isLeaving },
+          ]"
+        >
+          <i
+            class="close icon"
+            aria-hidden="true"
+            @click="isLeaving = false"
+          />
+          <div class="ui icon header">
+            <i
+              :class="[project.fast_edition_mode && hasUnsavedChange ? 'sign-out' : 'random', 'icon']"
+              aria-hidden="true"
+            />
+            Abandonner {{
+              project.fast_edition_mode && hasUnsavedChange ?
+                'les modifications' :
+                'la vue signalement filtré'
+            }}
+          </div>
+          <div class="content">
+            {{
+              project.fast_edition_mode && hasUnsavedChange ?
+                'Les modifications apportées au signalement ne seront pas sauvegardées, continuer ?':
+                `Vous allez quittez la vue signalement filtré,
+                  l\'ordre des signalements pourrait changer après édition d\'un signalement.`
+            }}
+          </div>
+          <div class="actions">
+            <button
+              type="button"
+              class="ui green compact button"
+              @click="stayOnPage"
+            >
+              Annuler
+            </button>
+            <button
+              type="button"
+              class="ui red compact button"
+              @click="leavePage"
+            >
+              Continuer
+            </button>
+          </div>
+        </div>
+      </div>
     </div>
+
     <div v-else>
       Pas de signalement correspondant trouvé
     </div>
@@ -101,7 +165,7 @@
 </template>
 
 <script>
-import { mapState, mapActions, mapMutations } from 'vuex';
+import { mapState, mapActions, mapMutations, mapGetters } from 'vuex';
 import mapService from '@/services/map-service';
 
 import axios from '@/axios-client.js';
@@ -136,19 +200,19 @@ export default {
   },
 
   beforeRouteUpdate (to, from, next) {
-    let leaving = true; // by default navigate to next route
     if (this.hasUnsavedChange) {
-      leaving = this.confirmLeave(); // prompt user that there is unsaved changes or that features order might change
+      this.confirmLeave(next); // prompt user that there is unsaved changes or that features order might change
+    } else {
+      next(); // continue navigation
     }
-    next(leaving);
   },
 
   beforeRouteLeave (to, from, next) {
-    let leaving = true; // by default navigate to next route
     if (this.hasUnsavedChange || (from.query.offset >= 0 && to.name === 'editer-signalement')) {
-      leaving = this.confirmLeave(); // prompt user that there is unsaved changes or that features order might change
+      this.confirmLeave(next); // prompt user that there is unsaved changes or that features order might change
+    } else {
+      next(); // continue navigation
     }
-    next(leaving);
   },
 
   data() {
@@ -170,27 +234,37 @@ export default {
       events: [],
       featureType: {},
       featuresCount: null,
-      isCanceling: false,
+      isDeleting: false,
+      isLeaving: false,
       slugSignal: '',
       displayToListButton: false,
     };
   },
 
   computed: {
+    ...mapState([
+      'USER_LEVEL_PROJECTS',
+      'user'
+    ]),
     ...mapState('projects', [
       'project'
     ]),
     ...mapState('feature-type', [
       'feature_types',
-      'feature_type',
     ]),
     ...mapState('feature', [
       'currentFeature',
       'form',
     ]),
+    ...mapGetters('feature-type', [
+      'feature_type',
+    ]),
+    ...mapGetters([
+      'permissions',
+    ]),
 
     hasUnsavedChange() {
-      if (this.form) {
+      if (this.project.fast_edition_mode && this.form && this.currentFeature) {
         if (this.form.title !== this.currentFeature.title) return true;
         if (this.form.description.value !== this.currentFeature.description) return true;
         if (this.form.status.value !== this.currentFeature.status) return true;
@@ -200,6 +274,36 @@ export default {
         }
       }
       return false;
+    },
+
+    isFeatureCreator() {
+      if (this.currentFeature && this.user) {
+        return this.currentFeature.creator === this.user.id;
+      }
+      return false;
+    },
+
+    isModerator() {
+      return this.USER_LEVEL_PROJECTS && this.USER_LEVEL_PROJECTS[this.$route.params.slug] === 'Modérateur';
+    },
+
+    isAdministrator() {
+      return this.USER_LEVEL_PROJECTS && this.USER_LEVEL_PROJECTS[this.$route.params.slug] === 'Administrateur projet';
+    },
+
+    canEditFeature() {
+      return (this.permissions && this.permissions.can_update_feature) ||
+                this.isFeatureCreator ||
+                this.isModerator ||
+                this.user.is_superuser;
+    },
+
+    canDeleteFeature() {
+      return (this.permissions && this.permissions.can_delete_feature && this.isFeatureCreator) ||
+                this.isFeatureCreator ||
+                this.isModerator ||
+                this.isAdministrator ||
+                this.user.is_superuser;
     }
   },
 
@@ -283,10 +387,18 @@ export default {
       }
     },
 
-    confirmLeave() {
-      return window.confirm(this.project.fast_edition_mode && this.hasUnsavedChange ?
-        'Les modifications apportées au signalement ne seront pas sauvegardées, continuer ?':
-        'Vous allez quittez la vue signalement filtré, l\'ordre des signalements pourrait changer après édition d\'un signalement.');
+    confirmLeave(next) {
+      this.next = next;
+      this.isLeaving = true;
+    },
+
+    stayOnPage() {
+      this.isLeaving = false;
+    },
+
+    leavePage() {
+      this.isLeaving = false;
+      this.next();
     },
 
     async reloadPage() {
diff --git a/src/views/Feature/FeatureEdit.vue b/src/views/Feature/FeatureEdit.vue
index 68b7a5d39641400209748e13a8f2cc28b4f3cdf0..cf127b78cb27621d87db4914cb742293841fcc8b 100644
--- a/src/views/Feature/FeatureEdit.vue
+++ b/src/views/Feature/FeatureEdit.vue
@@ -1,12 +1,15 @@
 <template>
   <div id="feature-edit">
-    <h1 v-if="feature && currentRouteName === 'editer-signalement'">
-      Mise à jour du signalement "{{ feature.title || feature.feature_id }}"
-    </h1>
-    <h1
-      v-else-if="feature_type && currentRouteName === 'ajouter-signalement'"
-    >
-      Création d'un signalement <small>[{{ feature_type.title }}]</small>
+    <h1>
+      <span v-if="feature_type && currentRouteName === 'ajouter-signalement'">      
+        Création d'un signalement <small>[{{ feature_type.title }}]</small>
+      </span>
+      <span v-else-if="feature && currentRouteName === 'editer-signalement'">
+        Mise à jour du signalement "{{ feature.title || feature.feature_id }}"
+      </span>
+      <span v-else-if="feature_type && currentRouteName === 'editer-attribut-signalement'">
+        Mise à jour des attributs de {{ checkedFeatures.length }} signalements
+      </span>
     </h1>
 
     <form
@@ -17,7 +20,10 @@
       class="ui form"
     >
       <!-- Feature Fields -->
-      <div class="two fields">
+      <div
+        v-if="currentRouteName !== 'editer-attribut-signalement'"
+        class="two fields"
+      >
         <div :class="field_title">
           <label :for="form.title.id_for_label">{{ form.title.label }}</label>
           <input
@@ -64,7 +70,10 @@
         </div>
       </div>
 
-      <div class="field">
+      <div
+        v-if="currentRouteName !== 'editer-attribut-signalement'"
+        class="field"
+      >
         <label :for="form.description.id_for_label">{{
           form.description.label
         }}</label>
@@ -77,7 +86,10 @@
       </div>
 
       <!-- Geom Field -->
-      <div class="field">
+      <div
+        v-if="currentRouteName !== 'editer-attribut-signalement'"
+        class="field"
+      >
         <label :for="form.geom.id_for_label">{{ form.geom.label }}</label>
         <!-- Import GeoImage -->
         <div
@@ -264,14 +276,18 @@
       <div
         v-for="(field, index) in orderedCustomFields"
         :key="field.field_type + index"
-        class="field"
       >
-        <FeatureExtraForm :field="field" />
+        <FeatureExtraForm
+          :id="field.label"
+          ref="extraForm"
+          :field="field"
+          class="field"
+        />
         {{ field.errors }}
       </div>
 
       <!-- Pièces jointes -->
-      <div v-if="isOnline">
+      <div v-if="isOnline && currentRouteName !== 'editer-attribut-signalement'">
         <div class="ui horizontal divider">
           PIÈCES JOINTES
         </div>
@@ -301,7 +317,7 @@
       </div>
 
       <!-- Signalements liés -->
-      <div v-if="isOnline">
+      <div v-if="isOnline && currentRouteName !== 'editer-attribut-signalement'">
         <div class="ui horizontal divider">
           SIGNALEMENTS LIÉS
         </div>
@@ -330,7 +346,7 @@
       <button
         type="button"
         :class="['ui teal icon button', { loading: sendingFeature }]"
-        @click="postForm"
+        @click="onSave"
       >
         <i
           class="white save icon"
@@ -436,9 +452,11 @@ export default {
     ]),
     ...mapState('feature', [
       'attachmentFormset',
-      'linkedFormset',
-      'features',
+      'checkedFeatures',
+      'currentFeature',
       'extra_forms',
+      'features',
+      'linkedFormset',
     ]),
     ...mapState('feature-type', [
       'feature_types'
@@ -517,7 +535,7 @@ export default {
       this.$route.params.slug_type_signal
     );
     //* empty previous feature data, not emptying by itself since it doesn't update by itself anymore
-    if (this.currentRouteName === 'ajouter-signalement') {
+    if (this.currentRouteName === 'ajouter-signalement' || this.currentRouteName === 'editer-attribut-signalement') {
       this.$store.commit('feature/SET_CURRENT_FEATURE', []);
     }
 
@@ -528,10 +546,13 @@ export default {
   },
 
   mounted() {
-    const promises = [
-      this.$store.dispatch('projects/GET_PROJECT', this.$route.params.slug),
-      this.$store.dispatch('projects/GET_PROJECT_INFO', this.$route.params.slug),
-    ];
+    const promises = [];
+    if (!this.project) {
+      promises.push(
+        this.$store.dispatch('projects/GET_PROJECT', this.$route.params.slug),
+        this.$store.dispatch('projects/GET_PROJECT_INFO', this.$route.params.slug),
+      );
+    }
     if (this.$route.params.slug_signal) {
       promises.push(
         this.$store.dispatch('feature/GET_PROJECT_FEATURE', {
@@ -542,9 +563,11 @@ export default {
     }
 
     Promise.all(promises).then(() => {
-      this.initForm();
-      this.initMap();
-      this.onFeatureTypeLoaded();
+      if (this.currentRouteName !== 'editer-attribut-signalement') {
+        this.initForm();
+        this.initMap();
+        this.onFeatureTypeLoaded(); // init map tools
+      }
       this.$store.dispatch('feature/INIT_EXTRA_FORMS');
     });
   },
@@ -559,7 +582,7 @@ export default {
 
   methods: {
     initForm() {
-      if (this.currentRouteName === 'editer-signalement') {
+      if (this.currentRouteName.includes('editer')) {
         for (const key in this.feature) {
           if (key && this.form[key]) {
             if (key === 'status') {
@@ -750,6 +773,13 @@ export default {
 
     checkAddedForm() {
       let isValid = true; //* fallback if all customForms returned true
+      if (this.$refs.extraForm) {
+        for (const extraForm of this.$refs.extraForm) {
+          if (extraForm.checkForm() === false) {
+            isValid = false;
+          }
+        }
+      }
       if (this.$refs.attachementForm) {
         for (const attachementForm of this.$refs.attachementForm) {
           if (attachementForm.checkForm() === false) {
@@ -767,22 +797,27 @@ export default {
       return isValid;
     },
 
-    postForm() {
-      let is_valid = true;
-      if (!this.feature_type.title_optional) {
-        is_valid =
-          this.checkFormTitle() &&
-          this.checkFormGeom() &&
-          this.checkAddedForm();
+    onSave() {
+      if (this.currentRouteName === 'editer-attribut-signalement') {
+        this.postMultipleFeatures();
       } else {
-        is_valid = this.checkFormGeom() && this.checkAddedForm();
+        this.postForm();
       }
+    },
+
+    async postForm() {
+      let is_valid = true;
+      let response;
+      is_valid =
+        this.checkFormGeom() &&
+        this.checkAddedForm();
+      if (!this.feature_type.title_optional) is_valid = this.checkFormTitle() && is_valid;
 
       if (is_valid) {
         //* in a moderate project, at edition of a published feature by someone else than admin or moderator, switch published status to draft.
         if (
           this.project.moderation &&
-          this.currentRouteName === 'editer-signalement' &&
+          this.currentRouteName.includes('editer') &&
           this.form.status.value.value === 'published' &&
           !this.permissions.is_project_administrator &&
           !this.permissions.is_project_moderator
@@ -791,9 +826,58 @@ export default {
           this.updateStore();
         }
         this.sendingFeature = true;
-        this.$store.dispatch('feature/SEND_FEATURE', this.currentRouteName)
-          .then(() => this.sendingFeature = false);
+        response = await this.$store.dispatch('feature/SEND_FEATURE', this.currentRouteName);
+        this.sendingFeature = false;
+        return response;
+      }
+    },
+
+    async postMultipleFeatures() {
+      this.$store.commit('DISPLAY_LOADER', 'Envoi des signalements en cours...');
+      const extraForms = [...this.extra_forms];// store extra forms for multiple features to not be overide by current feature
+      let results = [];
+      for (const featureId of this.checkedFeatures) {
+        const response = await this.$store.dispatch('feature/GET_PROJECT_FEATURE', {
+          project_slug: this.$route.params.slug,
+          feature_id: featureId,
+        });
+        if (response.status === 200) {
+          this.initForm(); // fill title, status, description needed to send request
+          for (let xtraForm of extraForms) { // fill extra forms with features values, only if the value of the extra form for multiple features is null
+            if (xtraForm.value === null) { // if no value to overide in feature, keep the feature value
+              xtraForm['value'] = this.feature.feature_data.find((feat) => feat.label === xtraForm.label).value;
+              await this.$store.commit('feature/UPDATE_EXTRA_FORM', xtraForm);
+            }
+          }
+          const result = await this.postForm();
+          results.push(result);
+        }
       }
+      this.$store.commit('DISCARD_LOADER');
+      const errors = results.filter((res) => res === undefined || res.status !== 200);
+      if (errors.length > 0) {
+        this.$store.commit(
+          'DISPLAY_MESSAGE',
+          {
+            comment: 'Des signalements n\'ont pas pu être mis à jour',
+            level: 'negative'
+          },
+        );
+      } else {
+        this.$store.commit(
+          'DISPLAY_MESSAGE',
+          {
+            comment: 'Les signalements ont été mis à jour',
+            level: 'positive'
+          },
+        );
+      }
+      this.$router.push({
+        name: 'liste-signalements',
+        params: {
+          slug: this.$route.params.slug,
+        },
+      });
     },
 
     //* ************* MAP *************** *//
@@ -844,9 +928,9 @@ export default {
       }
     },
 
-    updateGeomField(newGeom) {
+    async updateGeomField(newGeom) {
       this.form.geom.value = newGeom;
-      this.updateStore();
+      await this.updateStore();
     },
 
     initMap() {
diff --git a/src/views/FeatureType/FeatureTypeDetail.vue b/src/views/FeatureType/FeatureTypeDetail.vue
index 2c20b6c90632cacfd92a6942e2b0539e14cb7a9c..97db52d8babd1925b82899592d1a4b3ee069e619 100644
--- a/src/views/FeatureType/FeatureTypeDetail.vue
+++ b/src/views/FeatureType/FeatureTypeDetail.vue
@@ -40,10 +40,12 @@
                 </div>
               </div>
               <div class="value">
-                {{ features_count }}
+                {{ isOnline ? features_count : '?' }}
               </div>
-              <div class="label">
-                Signalement{{ features.length > 1 ? "s" : "" }}
+              <div
+                class="label"
+              >
+                Signalement{{ features.length > 1 || !isOnline ? "s" : "" }}
               </div>
             </div>
 
@@ -221,7 +223,10 @@
         </div>
       </div>
 
-      <div class="nine wide column">
+      <div
+        v-if="isOnline"
+        class="nine wide column"
+      >
         <h3 class="ui header">
           Derniers signalements
         </h3>
@@ -347,17 +352,32 @@
         </router-link>
         <br>
       </div>
+      <div
+        v-else
+        class="nine wide column"
+      >
+        <h3 class="ui header">
+          Derniers signalements
+        </h3>
+        <div class="ui message info">
+          <p>
+            Information non disponible en mode déconnecté.
+          </p>
+        </div>
+      </div>
     </div>
   </div>
 </template>
 
 <script>
+import { csv } from 'csvtojson';
+
 import { mapActions, mapMutations, mapGetters, mapState } from 'vuex';
-import { formatStringDate } from '@/utils';
+import { formatStringDate, transformProperties } from '@/utils';
 import ImportTask from '@/components/ImportTask';
 import featureAPI from '@/services/feature-api';
 
-import { fileConvertSizeToMo, csvToJson } from '@/assets/js/utils';
+import { fileConvertSizeToMo } from '@/assets/js/utils'; // TODO: refactor with above utils, those files are similar
 
 export default {
   name: 'FeatureTypeDetail',
@@ -539,26 +559,6 @@ export default {
       }
     },
 
-    transformProperties(prop) {
-      const type = typeof prop;
-      const date = new Date(prop);
-      if (type === 'boolean') {
-        return 'boolean';
-      } else if (Number.isSafeInteger(prop)) {
-        return 'integer';
-      } else if (
-        type === 'string' &&
-        ['/', ':', '-'].some((el) => prop.includes(el)) && // check for chars found in datestring
-        date instanceof Date &&
-        !isNaN(date.valueOf())
-      ) {
-        return 'char';
-      } else if (type === 'number' && !isNaN(parseFloat(prop))) {
-        return 'decimal';
-      }
-      return 'char'; //* string by default, most accepted type in database
-    },
-
     checkJsonValidity(json) {
       this.importError = '';
       const fields = this.structure.customfield_set.map((el) => {
@@ -570,9 +570,10 @@ export default {
       });
       for (const feature of json.features) {
         for (const { name, field_type, options } of fields) {
-          if (name in feature.properties) {
-            const fieldInFeature = feature.properties[name];
-            const customType = this.transformProperties(fieldInFeature);
+          const properties = feature.properties || feature;
+          if (name in properties) {
+            const fieldInFeature = properties[name];
+            const customType = transformProperties(fieldInFeature);
             //* if custom field value is not null, then check validity of field
             if (fieldInFeature !== null) {
               //* if field type is list, it's not possible to guess from value type
@@ -595,22 +596,20 @@ export default {
       return true;
     },
 
-    checkCsvValidity(csv) {
+    async checkCsvValidity(csvString) {
       this.importError = '';
-
-      // Find csv delimiter
-      const commaDelimited = csv.split('\n')[0].includes(',');
-      const semicolonDelimited = csv.split('\n')[0].includes(';');
+      // Find csvString delimiter
+      const commaDelimited = csvString.split('\n')[0].includes(',');
+      const semicolonDelimited = csvString.split('\n')[0].includes(';');
       const delimiter = commaDelimited && !semicolonDelimited ? ',' : semicolonDelimited ?  ';' : false;
 
       if ((commaDelimited && semicolonDelimited) || !delimiter) {
         this.importError = `Le fichier ${this.csvFileToImport.name} n'est pas formaté correctement`;
         return false;
       }
-
       // Check if file contains 'lat' and 'long' fields
       const headersLine =
-        csv
+        csvString
           .split('\n')[0]
           .replace(/(\r\n|\n|\r)/gm, '')
           .split(delimiter)
@@ -622,7 +621,7 @@ export default {
         return false;
       }
       const sampleLine =
-        csv
+        csvString
           .split('\n')[1]
           .split(delimiter)
           .map(el => {
@@ -630,55 +629,8 @@ export default {
           })
           .filter(Boolean);
       if (sampleLine.length > 1 && headersLine.length === 2) {
-        const fields = this.structure.customfield_set.map((el) => {
-          return {
-            name: el.name,
-            field_type: el.field_type,
-            options: el.options,
-          };
-        });
-        const csvFeatures = csvToJson(csv, delimiter);
-        for (const feature of csvFeatures) {
-          for (let { name, field_type, options } of fields) {
-            if (name in feature) {
-              const fieldInFeature = feature[name];
-
-              // overide some specific cases on date type data
-              if (
-                typeof fieldInFeature === 'string' &&
-                ['/', ':', '-'].some((el) => fieldInFeature.includes(el)) &&
-                (new Date(fieldInFeature)) instanceof Date &&
-                !isNaN((new Date(fieldInFeature)).valueOf())
-              ) {
-                field_type = 'char';
-              } else if (
-                field_type === 'date' &&
-                ((new Date(fieldInFeature)) instanceof Date)
-              ) {
-                field_type = 'char';
-              }
-
-              const customType = this.transformProperties(fieldInFeature);
-              //* if custom field value is not null, then check validity of field
-              if (fieldInFeature !== null) {
-                //* if field type is list, it's not possible to guess from value type
-                if (field_type === 'list') {
-                  //*then check if the value is an available option
-                  if (fieldInFeature && !options.includes(fieldInFeature)) {
-                    this.importError = `Le fichier est invalide: la valeur [ ${fieldInFeature} ] n'est pas une option valide 
-                      pour le champ personnalisé "${name}".`;
-                    return false;
-                  }
-                } else if (customType !== field_type) {
-                  //* check if custom field value match
-                  this.importError = `Le fichier est invalide: Un champ de type ${field_type} ne peut pas avoir la valeur [ ${fieldInFeature} ]`;
-                  return false;
-                }
-              }
-            }
-          }
-        }
-        return true;
+        const features = await csv().fromString(csvString);
+        return this.checkJsonValidity({ features });
       } else {
         return false;
       }
diff --git a/src/views/FeatureType/FeatureTypeEdit.vue b/src/views/FeatureType/FeatureTypeEdit.vue
index 5682366fa74a2f7e5540ddcefbd9f87884d11fda..47bad6dceb3668063f9c76a1b9de51d7c13fdc7c 100644
--- a/src/views/FeatureType/FeatureTypeEdit.vue
+++ b/src/views/FeatureType/FeatureTypeEdit.vue
@@ -159,6 +159,7 @@ import { mapGetters, mapState, mapMutations, mapActions } from 'vuex';
 
 import Dropdown from '@/components/Dropdown.vue';
 import FeatureTypeCustomForm from '@/components/FeatureType/FeatureTypeCustomForm.vue';
+import { transformProperties } from'@/utils';
 
 export default {
   name: 'FeatureTypeEdit',
@@ -247,6 +248,7 @@ export default {
         'archived_on',
         'deletion_on',
         'feature_type',
+        'feature_id',
         'display_creator',
         'display_last_editor',
         'project',
@@ -638,23 +640,21 @@ export default {
       return 'point';
     },
 
-    transformProperties(prop) {
-      const type = typeof prop;
-      const date = new Date(prop);
-      if (type === 'boolean') {
-        return 'boolean';
-      } else if (Number.isSafeInteger(prop)) {
-        return 'integer';
-      } else if (
-        type === 'string' &&
-        date instanceof Date &&
-        !isNaN(date.valueOf())
-      ) {
-        return 'date';
-      } else if (type === 'number' && !isNaN(parseFloat(prop))) {
-        return 'decimal';
+    buildCustomForm(properties) {
+      for (const [key, val] of Object.entries(properties)) {
+        //* check that the property is not a keyword from the backend or map style
+        // todo: add map style keywords
+        if (!this.reservedKeywords.includes(key)) {
+          const customForm = {
+            label: { value: key || '' },
+            name: { value: key || '' },
+            position: this.dataKey, // * use dataKey already incremented by addCustomForm
+            field_type: { value: transformProperties(val) }, // * guessed from the type
+            options: { value: [] }, // * not available in export
+          };
+          this.addCustomForm(customForm);
+        }
       }
-      return 'char'; //* string by default, most accepted type in database
     },
 
     importGeoJsonFeatureType() {
@@ -664,93 +664,16 @@ export default {
         this.form.title.value = properties.feature_type;
         this.form.geom_type.value = this.translateLabel(geometry.type);
         this.updateStore(); //* register title & geom_type in store
-
-        //* loop properties to create a customForm for each of them
-        for (const [key, val] of Object.entries(properties)) {
-          //* check that the property is not a keyword from the backend or map style
-          // todo: add map style keywords
-          if (!this.reservedKeywords.includes(key)) {
-            const customForm = {
-              label: { value: key || '' },
-              name: { value: key || '' },
-              position: this.dataKey, // * use dataKey already incremented by addCustomForm
-              field_type: { value: this.transformProperties(val) }, // * guessed from the type
-              options: { value: [] }, // * not available in export
-            };
-            this.addCustomForm(customForm);
-          }
-        }
+        this.buildCustomForm(properties);
       }
     },
 
     importCSVFeatureType() {
       if (this.csv.length) {
         this.updateStore(); //* register title & geom_type in store
-        // List fileds for user to select coords fields
-        // this.csvFields =
-        //   Object.keys(this.csv[0])
-        //     .map(el => {
-        //       return {
-        //         field: el,
-        //         x: false,
-        //         y:false
-        //       };
-        //     });
-        for (const [key, val] of Object.entries(this.csv[0])) {
-          //* check that the property is not a keyword from the backend or map style
-          // todo: add map style keywords
-          if (!this.reservedKeywords.includes(key)) {
-            const customForm = {
-              label: { value: key || '' },
-              name: { value: key || '' },
-              position: this.dataKey, // * use dataKey already incremented by addCustomForm
-              field_type: { value: this.transformProperties(val) }, // * guessed from the type
-              options: { value: [] }, // * not available in export
-            };
-            this.addCustomForm(customForm);
-          }
-        }
+        this.buildCustomForm(this.csv[0]);
       }
     },
-
-    // pickXcsvCoordField(e) {
-    //   this.csvFields.forEach(el => {
-    //     if (el.field === e.field) {
-    //       el.x = true;
-    //     } else {
-    //       el.x = false;
-    //     }
-    //   });
-    // },
-    // pickYcsvCoordField(e) {
-    //   this.csvFields.forEach(el => {
-    //     if (el.field === e.field) {
-    //       el.y = true;
-    //     } else {
-    //       el.y = false;
-    //     }
-    //   });
-    // },
-    // setCSVCoordsFields() {
-    //   const xField = this.csvFields.find(el => el.x === true).field;
-    //   const yField = this.csvFields.find(el => el.y === true).field;
-    //   this.csvFields = null;
-
-    //   for (const [key, val] of Object.entries(this.csv[0])) {
-    //     //* check that the property is not a keyword from the backend or map style
-    //     // todo: add map style keywords
-    //     if (!this.reservedKeywords.includes(key) && key !== xField && key !== yField) {
-    //       const customForm = {
-    //         label: { value: key || '' },
-    //         name: { value: key || '' },
-    //         position: this.dataKey, // * use dataKey already incremented by addCustomForm
-    //         field_type: { value: this.transformProperties(val) }, // * guessed from the type
-    //         options: { value: [] }, // * not available in export
-    //       };
-    //       this.addCustomForm(customForm);
-    //     }
-    //   }
-    // }
   },
 };
 </script>
diff --git a/src/views/Project/FeaturesListAndMap.vue b/src/views/Project/FeaturesListAndMap.vue
index fde352a957e3980fea4e7b76329ca27f1d3fdd78..fd67094f18b6eb966acf80b64f9fd4dc60007f3b 100644
--- a/src/views/Project/FeaturesListAndMap.vue
+++ b/src/views/Project/FeaturesListAndMap.vue
@@ -5,16 +5,17 @@
         :show-map="showMap"
         :features-count="featuresCount"
         :pagination="pagination"
+        :edit-attributes-feature-type="editAttributesFeatureType"
         @set-filter="setFilters"
         @reset-pagination="resetPagination"
         @fetch-features="fetchPagedFeatures"
         @show-map="setShowMap"
-        @modify-status="modifyStatus"
+        @edit-status="modifyStatus"
         @toggle-delete-modal="toggleDeleteModal"
       />
 
       <div
-        :class="['ui tab active map-container', {visible: showMap}]"
+        :class="['ui tab active map-container', { 'visible': showMap }]"
         data-tab="map"
       >
         <div
@@ -48,6 +49,7 @@
         :features-count="featuresCount"
         :pagination="pagination"
         :sort="sort"
+        :edit-attributes-feature-type.sync="editAttributesFeatureType"
         :queryparams="queryparams"
         @update:page="handlePageChange"
         @update:sort="handleSortChange"
@@ -62,7 +64,7 @@
       >
         <div
           :class="[
-            'ui mini modal subscription',
+            'ui mini modal',
             { 'active visible': isDeleteModalOpen },
           ]"
         >
@@ -125,6 +127,7 @@ export default {
 
   data() {
     return {
+      editAttributesFeatureType: null,
       currentLayer: null,
       featuresCount: 0,
       form: {
@@ -154,6 +157,9 @@ export default {
   },
 
   computed: {
+    ...mapState([
+      'isOnline'
+    ]),
     ...mapState('projects', [
       'project',
     ]),
@@ -177,13 +183,26 @@ export default {
     },
   },
 
+
+  watch: {
+    isOnline(newValue, oldValue) {
+      if (newValue != oldValue && !newValue) {
+        this.DISPLAY_MESSAGE({
+          comment: 'Les signalements du projet non mis en cache ne sont pas accessibles en mode déconnecté',
+        });
+      }
+    },
+  },
+
   mounted() {
+    this.UPDATE_CHECKED_FEATURES([]); // empty for when turning back from edit attributes page
     if (!this.project) {
       // Chargements des features et infos projet en cas d'arrivée directe sur la page ou de refresh
-      this.$store.dispatch('projects/GET_PROJECT', this.projectSlug);
-      this.$store
-        .dispatch('projects/GET_PROJECT_INFO', this.projectSlug)
-        .then(() => this.initMap());
+      Promise.all([
+        this.$store.dispatch('projects/GET_PROJECT', this.projectSlug),
+        this.$store.dispatch('projects/GET_PROJECT_INFO', this.projectSlug)
+      ])
+        .then(()=> this.initMap());
     } else {
       this.initMap();
     }
@@ -195,6 +214,9 @@ export default {
   },
 
   methods: {
+    ...mapMutations([
+      'DISPLAY_MESSAGE',
+    ]),
     ...mapActions('feature', [
       'DELETE_FEATURE',
     ]),
@@ -206,7 +228,7 @@ export default {
     setShowMap(newValue) {
       this.showMap = newValue;
       //* expanded sidebar is visible under the list, even when the map is closed (position:absolute), solved by closing it whin switching to list
-      if (newValue === false) this.$refs.sidebar.toggleSidebar(false);
+      if (newValue === false && this.$refs.sidebar) this.$refs.sidebar.toggleSidebar(false);
     },
     resetPagination() {
       this.pagination = { ...initialPagination };
@@ -242,7 +264,7 @@ export default {
               this.UPDATE_CHECKED_FEATURES(newCheckedFeatures);
               this.modifyStatus(newStatus);
             } else {
-              this.$store.commit('DISPLAY_MESSAGE', {
+              this.DISPLAY_MESSAGE({
                 comment: `Le signalement ${feature.title} n'a pas pu être modifié`,
                 level: 'negative'
               });
@@ -252,7 +274,7 @@ export default {
         }
       } else {
         this.fetchPagedFeatures();
-        this.$store.commit('DISPLAY_MESSAGE', {
+        this.DISPLAY_MESSAGE({
           comment: 'Tous les signalements ont été modifié avec succès.',
           level: 'positive'
         });
@@ -281,6 +303,10 @@ export default {
       this.toggleDeleteModal();
     },
 
+    modifyFeaturesAttributes() {
+      console.log('modifyFeaturesAttributes');
+    },
+
     onFilterChange() {
       if (mapService.getMap() && mapService.mvtLayer) {
         mapService.mvtLayer.changed();
@@ -383,6 +409,12 @@ export default {
     },
 
     fetchPagedFeatures(newUrl) {
+      if (!navigator.onLine) {
+        this.DISPLAY_MESSAGE({
+          comment: 'Les signalements du projet non mis en cache ne sont pas accessibles en mode déconnecté',
+        });
+        return;
+      }
       let url = `${this.API_BASE_URL}projects/${this.projectSlug}/feature-paginated/?limit=${this.pagination.pagesize}&offset=${this.pagination.start}`;
       //* if receiving next & previous url (// todo : might be not used anymore, to check)
       if (newUrl && typeof newUrl === 'string') {
@@ -486,6 +518,9 @@ export default {
   margin-left: 50%;
   visibility: hidden;
   position: absolute;
+  #map {
+    min-height: 0;
+  }
 }
 .map-container.visible {
   visibility: visible;
diff --git a/src/views/Project/ProjectMembers.vue b/src/views/Project/ProjectMembers.vue
index 679f9a59f0296d909dc87961b118b035cbe6dad2..adf13ec5c98f698da9682758e614b4c514ee75c4 100644
--- a/src/views/Project/ProjectMembers.vue
+++ b/src/views/Project/ProjectMembers.vue
@@ -301,6 +301,11 @@ export default {
     },
 
     saveMembers() {
+      this.$store.commit(
+        'DISPLAY_LOADER',
+        'Mise à jour des membres du projet en cours ...'
+      );
+
       const data = this.projectUsers.map((member) => {
         return {
           user: member.user,
@@ -329,8 +334,10 @@ export default {
               }
             );
           }
+          this.$store.commit('DISCARD_LOADER');
         })
         .catch((error) => {
+          this.$store.commit('DISCARD_LOADER');
           throw error;
         });
     },