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 0910a9aac51e1d483ac0dfd470833f00f33d1a22..5f39dbee276a51b9372a1517a72c26cd2aa9dab7 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,6 +1,6 @@
 {
   "name": "geocontrib-frontend",
-  "version": "3.0.2",
+  "version": "3.1.0",
   "lockfileVersion": 1,
   "requires": true,
   "dependencies": {
@@ -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"
+          }
         }
       }
     },
@@ -3119,7 +3186,6 @@
       "version": "1.0.10",
       "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
       "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
-      "dev": true,
       "requires": {
         "sprintf-js": "~1.0.2"
       }
@@ -3561,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",
@@ -5207,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",
@@ -8321,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",
@@ -8695,6 +8775,14 @@
       "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
       "dev": true
     },
+    "linkify-it": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-2.2.0.tgz",
+      "integrity": "sha512-GnAl/knGn+i1U/wjBz3akz2stz+HrHLsxMwHQGofCDfPvlf+gDKN58UtfmUquTY4/MXeE2x7k19KQmeoZi94Iw==",
+      "requires": {
+        "uc.micro": "^1.0.1"
+      }
+    },
     "loader-runner": {
       "version": "2.4.0",
       "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-2.4.0.tgz",
@@ -8887,6 +8975,25 @@
       "resolved": "https://registry.npmjs.org/mapbox-to-css-font/-/mapbox-to-css-font-2.4.1.tgz",
       "integrity": "sha512-QQ/iKiM43DM9+aujTL45Iz5o7gDeSFmy4LPl3HZmNcwCE++NxGazf+yFpY+wCb+YS23sDa1ghpo3zrNFOcHlow=="
     },
+    "markdown-it": {
+      "version": "8.4.2",
+      "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-8.4.2.tgz",
+      "integrity": "sha512-GcRz3AWTqSUphY3vsUqQSFMbgR38a4Lh3GWlHRh/7MRwz8mcu9n2IO7HOh+bXHrR9kOPDl5RNCaEsrneb+xhHQ==",
+      "requires": {
+        "argparse": "^1.0.7",
+        "entities": "~1.1.1",
+        "linkify-it": "^2.0.0",
+        "mdurl": "^1.0.1",
+        "uc.micro": "^1.0.5"
+      },
+      "dependencies": {
+        "entities": {
+          "version": "1.1.2",
+          "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz",
+          "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w=="
+        }
+      }
+    },
     "md5.js": {
       "version": "1.3.5",
       "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz",
@@ -8904,6 +9011,11 @@
       "integrity": "sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA==",
       "dev": true
     },
+    "mdurl": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz",
+      "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g=="
+    },
     "media-typer": {
       "version": "0.3.0",
       "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
@@ -12029,8 +12141,7 @@
     "sprintf-js": {
       "version": "1.0.3",
       "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
-      "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=",
-      "dev": true
+      "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw="
     },
     "sshpk": {
       "version": "1.17.0",
@@ -12273,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",
@@ -12543,6 +12662,15 @@
       "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=",
       "dev": true
     },
+    "textarea-markdown": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/textarea-markdown/-/textarea-markdown-1.3.1.tgz",
+      "integrity": "sha512-1rCSrFwnRE++oM5NT+t7GEbT647geFVPcnhD4y0gSwpIKIpWwy7UbBCTowt7pum+CZB2Hv5niNmO54bP+wyjMA==",
+      "requires": {
+        "markdown-it": "^8.4.0",
+        "whatwg-fetch": "^2.0.3"
+      }
+    },
     "thenify": {
       "version": "3.3.1",
       "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
@@ -12810,6 +12938,11 @@
       "integrity": "sha512-VAH4IvQ7BDFYglMd7BPRDfLgxZZX4O4TFcRDA6EN5X7erNJJq+McIEp8np9aVtxrCJ6qx4GTYVfOWNjcqwZgRw==",
       "dev": true
     },
+    "uc.micro": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz",
+      "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA=="
+    },
     "uglify-js": {
       "version": "3.4.10",
       "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.4.10.tgz",
@@ -13257,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",
@@ -14149,6 +14213,11 @@
       "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==",
       "dev": true
     },
+    "whatwg-fetch": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-2.0.4.tgz",
+      "integrity": "sha512-dcQ1GWpOD/eEQ97k66aiEVpNnapVj90/+R+SXTPYGHpYBBypfKJEQjLrvMZ7YXbKm21gXd4NcuxUTjiv1YtLng=="
+    },
     "whatwg-url": {
       "version": "5.0.0",
       "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
diff --git a/package.json b/package.json
index e354da93825ab0839f017a113dacfe453884496e..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",
@@ -28,6 +29,7 @@
     "register-service-worker": "^1.7.1",
     "rxjs": "^6.6.7",
     "sortablejs": "^1.14.0",
+    "textarea-markdown": "^1.3.1",
     "vue": "^2.6.11",
     "vue-multiselect": "~2.1.6",
     "vue-router": "^3.5.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/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/Edit/FeatureExtraForm.vue b/src/components/Feature/Edit/FeatureExtraForm.vue
index 8d09d514788bca922e52c7a66dce5982522a5ec2..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 === 'editer-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 === 'editer-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 === 'editer-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 === 'editer-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 === 'editer-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 === 'editer-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 === 'editer-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/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 e71b17585637336eace2b1f6e7fe029e79f4afcc..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 =
@@ -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/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 fa88de3e66e19b648fc845c0f5258eff69964bc2..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 {
@@ -665,7 +666,11 @@ export default {
                 .filter(Boolean);
             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/Detail/ProjectHeader.vue b/src/components/Project/Detail/ProjectHeader.vue
index 610e46a06da6166f87938f102fec0172b3b5093c..c9588d7a414bf0bace02235f02b105b7fb50b144 100644
--- a/src/components/Project/Detail/ProjectHeader.vue
+++ b/src/components/Project/Detail/ProjectHeader.vue
@@ -51,7 +51,14 @@
           {{ project.title }}
         </h1>
         <div class="sub header">
-          {{ project.description }}
+          <!-- {{ project.description }} -->
+          <div id="preview" />
+          <textarea
+            id="editor"
+            v-model="project.description"
+            data-preview="#preview"
+            hidden
+          />
         </div>
       </div>
 
@@ -158,6 +165,7 @@
 </template>
 
 <script>
+import TextareaMarkdown from 'textarea-markdown';
 
 import { mapState, mapGetters, mapMutations } from 'vuex';
 
@@ -213,6 +221,11 @@ export default {
 
   },
 
+  mounted() {
+    let textarea = document.querySelector('textarea');
+    new TextareaMarkdown(textarea);
+  },
+
   methods: {
     ...mapMutations('modals', [
       'OPEN_PROJECT_MODAL'
@@ -290,6 +303,11 @@ export default {
   }
 }
 
+#preview {
+  max-height: 10em;
+  overflow: scroll;
+}
+
 @media  screen and (max-width: 767px) {
   .middle.aligned.column {
     text-align: center;
diff --git a/src/components/Project/FeaturesListAndMap/FeatureListTable.vue b/src/components/Project/FeaturesListAndMap/FeatureListTable.vue
index 69cd85d84687014a27a3c979b22cff889214d757..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',
@@ -212,7 +268,10 @@
                 {{ feature.feature_type.title }}
               </router-link>
             </td>
-            <td class="dt-center">
+            <td
+              id="name"
+              class="dt-center"
+            >
               <router-link
                 :to="{
                   name: 'details-signalement-filtre',
@@ -226,17 +285,22 @@
                 {{ 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 || ' ---- ' }}
@@ -268,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"
       >
@@ -337,7 +401,6 @@
 
 <script>
 import { mapState, mapGetters, mapMutations } from 'vuex';
-import FeatureListMassToggle from '@/components/Feature/FeatureListMassToggle';
 import { formatStringDate } from '@/utils';
 
 export default {
@@ -349,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,
@@ -381,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];
     },
@@ -434,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 }
@@ -467,7 +556,11 @@ export default {
         Contributeur : ['draft', 'pending', 'published'],
       };
 
-      if (this.user.is_superuser) {
+      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;
@@ -479,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;
     },
@@ -501,6 +587,7 @@ export default {
     },
 
     changeSort(column) {
+      if (!this.isOnline) return;
       if (this.sort.column === column) {
         //changer only order
         this.$emit('update:sort', {
@@ -616,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;
@@ -692,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";
   }
 
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 fe3e4d4e908ed04287a73b0e99a2c57f616d1c44..1059a36c29463f8528fb450f0137b0c571f18757 100644
--- a/src/components/Projects/ProjectsListItem.vue
+++ b/src/components/Projects/ProjectsListItem.vue
@@ -21,7 +21,16 @@
         {{ project.title }}
       </router-link>
       <div class="description">
-        <p>{{ project.description }}</p>
+        <textarea
+          :id="`editor-${project.slug}`"
+          :value="project.description"
+          :data-preview="`#preview-${project.slug}`"
+          hidden
+        />
+        <div
+          :id="`preview-${project.slug}`"
+          class="preview"
+        />
       </div>
       <div class="meta top">
         <span class="right floated">
@@ -74,6 +83,7 @@
 </template>
 
 <script>
+import TextareaMarkdown from 'textarea-markdown';
 
 import { mapState } from 'vuex';
 
@@ -100,6 +110,11 @@ export default {
     },
   },
 
+  mounted() {
+    let textarea = document.getElementById(`editor-${this.project.slug}`);
+    new TextareaMarkdown(textarea);
+  },
+
   methods: {
     refreshId() {
       const crypto = window.crypto || window.msCrypto;
@@ -112,6 +127,11 @@ export default {
 </script>
 
 <style lang="less" scoped>
+.preview {
+  max-height: 10em;
+  overflow: scroll;
+  margin-bottom: 0.8em;
+}
 
 .description {
   p {
diff --git a/src/main.js b/src/main.js
index 0bae0e95ba2fa6a2b0778efdde6df71763a4fe88..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) {
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/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 2466c7c6ad6e4b2c81188aadf9fe694b73d96e6f..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,7 +210,7 @@ const feature = {
             feature_id: featureId
           })
           .then(() => {
-            if (routeName.includes('details-signalement')) return;
+            if (routeName === 'details-signalement') return response;
             router.push({
               name: 'details-signalement',
               params: {
@@ -218,24 +219,25 @@ const feature = {
               },
             });
           });
+        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;
         }
         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,
@@ -264,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);
           }
         }
       })
@@ -296,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/FeatureEdit.vue b/src/views/Feature/FeatureEdit.vue
index 45a27e77dee91061d9e130e3abdd18173f0c32e3..af2dff46d4196e9546e18284e4664295276932e2 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,17 +276,18 @@
       <div
         v-for="(field, index) in orderedCustomFields"
         :key="field.field_type + index"
-        class="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>
@@ -304,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>
@@ -333,7 +346,7 @@
       <button
         type="button"
         :class="['ui teal icon button', { loading: sendingFeature }]"
-        @click="postForm"
+        @click="onSave"
       >
         <i
           class="white save icon"
@@ -439,9 +452,11 @@ export default {
     ]),
     ...mapState('feature', [
       'attachmentFormset',
-      'linkedFormset',
-      'features',
+      'checkedFeatures',
+      'currentFeature',
       'extra_forms',
+      'features',
+      'linkedFormset',
     ]),
     ...mapState('feature-type', [
       'feature_types'
@@ -520,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', []);
     }
 
@@ -531,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', {
@@ -545,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');
     });
   },
@@ -562,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') {
@@ -753,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) {
@@ -770,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
@@ -794,11 +826,60 @@ 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 *************** *//
 
     onFeatureTypeLoaded() {
@@ -847,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 315acba70feeaf31c5d1beb13cbc259fbb08a0a3..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',
@@ -639,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() {
@@ -665,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 c48c19edff621efe27fed3c0309026c3e0463d23..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"
@@ -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,7 +183,19 @@ 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
       Promise.all([
@@ -196,6 +214,9 @@ export default {
   },
 
   methods: {
+    ...mapMutations([
+      'DISPLAY_MESSAGE',
+    ]),
     ...mapActions('feature', [
       'DELETE_FEATURE',
     ]),
@@ -207,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 };
@@ -243,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'
               });
@@ -253,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'
         });
@@ -282,6 +303,10 @@ export default {
       this.toggleDeleteModal();
     },
 
+    modifyFeaturesAttributes() {
+      console.log('modifyFeaturesAttributes');
+    },
+
     onFilterChange() {
       if (mapService.getMap() && mapService.mvtLayer) {
         mapService.mvtLayer.changed();
@@ -384,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') {
diff --git a/src/views/Project/ProjectEdit.vue b/src/views/Project/ProjectEdit.vue
index 0e788f3f84a33b747d170945eb5d315fef982b37..b36976a70387053c06941516c006205576a03a8b 100644
--- a/src/views/Project/ProjectEdit.vue
+++ b/src/views/Project/ProjectEdit.vue
@@ -91,14 +91,26 @@
           </ul>
         </div>
       </div>
-      <div class="field">
-        <label for="description">Description</label>
-        <textarea
-          v-model="form.description"
-          name="description"
-          rows="5"
-        />
-        <!-- {{ form.description.errors }} -->
+      <div class="two fields">
+        <div class="field">
+          <label for="description">Description</label>
+          <textarea
+            id="editor"
+            v-model="form.description"
+            data-preview="#preview"
+            name="description"
+            rows="5"
+          />
+          <!-- {{ form.description.errors }} -->
+        </div>
+        <div class="field">
+          <label for="preview">Aperçu</label>
+          <div
+            id="preview"
+            class="description preview"
+            name="preview"
+          />
+        </div>
       </div>
 
       <div class="ui horizontal divider">
@@ -248,6 +260,8 @@
 import axios from '@/axios-client.js';
 import Dropdown from '@/components/Dropdown.vue';
 
+import TextareaMarkdown from 'textarea-markdown';
+
 import { mapState } from 'vuex';
 
 export default {
@@ -371,6 +385,11 @@ export default {
     }
   },
 
+  mounted() {
+    let textarea = document.querySelector('textarea');
+    new TextareaMarkdown(textarea);
+  },
+
   methods: {
     definePageType() {
       if (this.$router.history.current.name === 'project_create') {
@@ -642,4 +661,15 @@ export default {
 .close.icon:hover {
   cursor: pointer;
 }
+
+textarea {
+  height: 10em;
+}
+
+.description.preview {
+  height: 10em;
+  overflow: scroll;
+  border: 1px solid rgba(34, 36, 38, .15);
+  padding: .78571429em 1em;
+}
 </style>
\ No newline at end of file