changeset 389:e7d5383bc358

feat: Primitive validation and error messages Added simple error messages to the usermanagement Added simple validation rules for fields: * password rules according issue70 * email used the regex from gocode
author Thomas Junk <thomas.junk@intevation.de>
date Mon, 13 Aug 2018 16:21:26 +0200
parents af82a8989b44
children 6f77f33af651
files client/src/assets/application.scss client/src/components/Sidebar.vue client/src/components/Userdetail.vue client/src/stores/application.js client/src/stores/usermanagement.js client/src/views/Users.vue
diffstat 6 files changed, 203 insertions(+), 49 deletions(-) [+]
line wrap: on
line diff
--- a/client/src/assets/application.scss	Mon Aug 13 11:03:30 2018 +0200
+++ b/client/src/assets/application.scss	Mon Aug 13 16:21:26 2018 +0200
@@ -7,6 +7,9 @@
 $iconwidth: 20px;
 $basic-shadow: 1px 3px 8px 2px rgba(220, 220, 220, 0.75);
 $basic-shadow-light: 1px 1px 12px 1px rgba(235, 235, 235, 0.75);
+$transition: 0.5s;
+$transition-fast: 0.1s;
+$transition-slow: 2s;
 
 %fully-centered {
   position: absolute;
--- a/client/src/components/Sidebar.vue	Mon Aug 13 11:03:30 2018 +0200
+++ b/client/src/components/Sidebar.vue	Mon Aug 13 16:21:26 2018 +0200
@@ -92,9 +92,6 @@
 $sidebar-full-width: 210px;
 $collapser-left-offset: 170px;
 $sidebar-collapsed-width: 0px;
-$transition: 0.5s;
-$transition-fast: 0.1s;
-$transition-slow: 2s;
 
 .collapser {
   position: absolute;
--- a/client/src/components/Userdetail.vue	Mon Aug 13 11:03:30 2018 +0200
+++ b/client/src/components/Userdetail.vue	Mon Aug 13 16:21:26 2018 +0200
@@ -2,33 +2,55 @@
   <div class="userdetails shadow">
     <div class="card">
       <div class="card-header text-white bg-info mb-3">
-        {{ currentUser.user }}
+          {{ currentUser.user }}
+        <span @click="closeDetailview" class="pull-right"><i class="fa fa-close"></i></span>
       </div>
       <div class="card-body">
         <form @submit.prevent="save">
-          <div class="form-group row">
-            <label for="country">Country</label>
-            <select class="form-control form-control-sm" v-model="currentUser.country">
-              <option disabled value="">Please select one</option>
-              <option>AT</option>
-              <option>RO</option>
-              <option>BG</option>
-            </select>
+          <div class="formfields">
+            <div v-if="currentUser.isNew" class="form-group row">
+              <label for="user">Username</label>
+              <input type="user" class="form-control form-control-sm" id="user" aria-describedby="userHelp" v-model="currentUser.user">
+              <div v-show="errors.email" class="text-danger"><small><i class="fa fa-warning"></i> {{ errors.email }}</small></div>
+            </div>
+            <div class="form-group row">
+              <label for="country">Country</label>
+              <select class="form-control form-control-sm" v-on:change="validateCountry" v-model="currentUser.country">
+                <option disabled value="">Please select one</option>
+                <option>AT</option>
+                <option>RO</option>
+                <option>BG</option>
+              </select>
+              <div v-show="errors.country" class="text-danger"><small><i class="fa fa-warning"></i> {{ errors.country }}</small></div>
+            </div>
+            <div class="form-group row">
+              <label for="email">Email address</label>
+              <input type="email" v-on:change="validateEmailaddress" class="form-control form-control-sm" id="email" aria-describedby="emailHelp" v-model="currentUser.email">
+              <div v-show="errors.email" class="text-danger"><small><i class="fa fa-warning"></i> {{ errors.email }}</small></div>
+            </div>
+            <div class="form-group row">
+              <label for="role">Role</label>
+              <select class="form-control form-control-sm" v-model="currentUser.role">
+                <option disabled value="">Please select one</option>
+                <option value="sys_admin">Sysadmin</option>
+                <option value="waterway_admin">Waterway Admin</option>
+                <option value="waterway_user">Waterway User</option>
+              </select>
+            </div>
+            <div class="form-group row">
+              <label for="password">Password</label>
+              <input type="password" v-on:change="validatePassword" class="form-control form-control-sm" id="password" aria-describedby="passwordHelp" v-model="password">
+              <div v-show="errors.password" class="text-danger"><small><i class="fa fa-warning"></i> {{ errors.password }}</small></div>
+            </div>
+            <div class="form-group row">
+              <label for="passwordre">Retype Password</label>
+              <input type="password" v-on:change="validatePassword" class="form-control form-control-sm" id="passwordre" aria-describedby="passwordreHelp" v-model="passwordre">
+              <div v-show="errors.passwordre" class="text-danger"><small><i class="fa fa-warning"></i> {{ errors.passwordre }}</small></div>
+            </div>
           </div>
-          <div class="form-group row">
-            <label for="email">Email address</label>
-            <input type="email" class="form-control form-control-sm" id="email" aria-describedby="emailHelp" v-model="currentUser.email">
+          <div>
+            <button type="submit" :disabled="submitted" class="btn btn-info pull-right">Submit</button>
           </div>
-          <div class="form-group row">
-            <label for="role">Role</label>
-             <select class="form-control form-control-sm" v-model="currentUser.role">
-              <option disabled value="">Please select one</option>
-              <option value="sys_admin">Sysadmin</option>
-              <option value="waterway_admin">Waterway Admin</option>
-              <option value="waterway_user">Waterway User</option>
-            </select>
-          </div>
-          <button type="submit" class="btn btn-primary pull-right">Submit</button>
         </form>
     </div>
     </div>
@@ -38,16 +60,20 @@
 <style lang="scss">
 @import "../assets/application.scss";
 
+.formfields {
+  width: 10vw;
+}
+
 .userdetails {
   margin-top: $large-offset;
-  width: 30vw;
+  width: 53vw;
   margin-right: auto;
   height: 100%;
 }
 
 form {
-  width: 20vw;
-  margin: auto;
+  margin-left: $offset;
+  font-size: 0.9rem;
 }
 
 .shadow {
@@ -61,8 +87,17 @@
   name: "userdetail",
   data() {
     return {
+      password: "",
+      passwordre: "",
       currentUser: {},
-      path: null
+      path: null,
+      submitted: false,
+      errors: {
+        email: "",
+        country: "",
+        password: "",
+        passwordre: ""
+      }
     };
   },
   mounted() {
@@ -78,17 +113,71 @@
   computed: {
     user() {
       return this.$store.getters["usermanagement/currentUser"];
+    },
+    validationErrors() {
+      const errorMessages = this.errors;
+      return (
+        errorMessages.email ||
+        errorMessages.country ||
+        errorMessages.password ||
+        errorMessages.passwordre
+      );
     }
   },
   methods: {
+    closeDetailview() {
+      this.$store.commit("usermanagement/clearCurrentUser");
+      this.$store.commit("usermanagement/setUserDetailsInvisible");
+    },
+    validateCountry(event) {
+      if (event.target.value !== "") {
+        this.errors.country = "";
+      } else {
+        this.errors.country = "Please choose a valid country";
+      }
+    },
+    validatePassword() {
+      if (this.password !== this.passwordre) {
+        this.errors.passwordre = "Passwords do not match!";
+      } else {
+        this.errors.passwordre = "";
+      }
+      if (
+        // rules according to issue 70
+        this.password.length < 8 ||
+        /\W/.test(this.password) == false ||
+        /\d/.test(this.password) == false
+      ) {
+        this.errors.password =
+          "Password should at least be 8 char long including 1 digit and 1 special char like $";
+      } else {
+        this.errors.password = "";
+      }
+    },
+    validateEmailaddress(event) {
+      if (
+        /* cf. types.go */
+        // eslint-disable-next-line
+        /(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])/.test(
+          event.target.value
+        )
+      ) {
+        this.errors.email = "";
+      } else {
+        this.errors.email = "invalid email";
+      }
+    },
     save() {
+      if (this.validationErrors) return;
+      if (this.password) this.currentUser.password = this.password;
+      this.submitted = true;
       this.$store
         .dispatch("usermanagement/saveCurrentUser", {
           path: this.user.user,
           user: this.currentUser
         })
         .then(() => {
-          this.$store.commit("usermanagement/clearCurrentUser");
+          this.submitted = false;
           this.$store.dispatch("usermanagement/loadUsers").catch(error => {
             const { status, data } = error.response;
             app.$toast.error({
@@ -98,6 +187,7 @@
           });
         })
         .catch(error => {
+          this.submitted = false;
           const { status, data } = error.response;
           app.$toast.error({
             title: "Error while saving user",
--- a/client/src/stores/application.js	Mon Aug 13 11:03:30 2018 +0200
+++ b/client/src/stores/application.js	Mon Aug 13 16:21:26 2018 +0200
@@ -2,9 +2,15 @@
   namespaced: true,
   state: {
     appTitle: process.env.VUE_APP_TITLE,
-    secondaryLogo: process.env.VUE_APP_SECONDARY_LOGO_URL
+    secondaryLogo: process.env.VUE_APP_SECONDARY_LOGO_URL,
+    sidebar: {
+      iscollapsed: false
+    }
   },
   getters: {
+    sidebarCollapsed: state => {
+      return state.sidebar.iscollapsed;
+    },
     appTitle: state => {
       return state.appTitle;
     },
@@ -12,7 +18,11 @@
       return state.secondaryLogo;
     }
   },
-  mutations: {},
+  mutations: {
+    toggleSidebar: () => {
+      this.sidebar.iscollapsed = !this.sidebar.iscollapsed;
+    }
+  },
   actions: {}
 };
 
--- a/client/src/stores/usermanagement.js	Mon Aug 13 11:03:30 2018 +0200
+++ b/client/src/stores/usermanagement.js	Mon Aug 13 16:21:26 2018 +0200
@@ -2,11 +2,12 @@
 
 const newUser = () => {
   return {
-    user: "",
+    user: "N.N",
     email: "",
     country: null,
     role: null,
-    isNew: true
+    isNew: true,
+    password: ""
   };
 };
 
@@ -54,18 +55,35 @@
   actions: {
     saveCurrentUser({ commit }, data) {
       const { path, user } = data;
-      return new Promise((resolve, reject) => {
-        HTTP.put("/users/" + path, user, {
-          headers: { "X-Gemma-Auth": localStorage.getItem("token") }
-        })
-          .then(response => {
-            commit("setUserDetailsInvisible");
-            resolve(response);
+      if (user.isNew) {
+        return new Promise((resolve, reject) => {
+          HTTP.post("/users/", user, {
+            headers: { "X-Gemma-Auth": localStorage.getItem("token") }
           })
-          .catch(error => {
-            reject(error);
-          });
-      });
+            .then(response => {
+              commit("setUserDetailsInvisible");
+              commit("clearCurrentUser");
+              resolve(response);
+            })
+            .catch(error => {
+              reject(error);
+            });
+        });
+      } else {
+        return new Promise((resolve, reject) => {
+          HTTP.put("/users/" + path, user, {
+            headers: { "X-Gemma-Auth": localStorage.getItem("token") }
+          })
+            .then(response => {
+              commit("setUserDetailsInvisible");
+              commit("clearCurrentUser");
+              resolve(response);
+            })
+            .catch(error => {
+              reject(error);
+            });
+        });
+      }
     },
     loadUsers({ commit }) {
       return new Promise((resolve, reject) => {
--- a/client/src/views/Users.vue	Mon Aug 13 11:03:30 2018 +0200
+++ b/client/src/views/Users.vue	Mon Aug 13 16:21:26 2018 +0200
@@ -6,7 +6,7 @@
           <h1>User Management</h1>
         </div>
         <div class="d-flex flex-row">
-          <div class="userlist shadow">
+          <div :class="userlistStyle">
             <div class="card">
                 <div class="card-header text-white bg-info mb-3">
                   users
@@ -31,6 +31,9 @@
                     </tbody>
                 </table>
               </div>
+                <div class="adduser">
+                  <button @click="addUser" class="btn btn-info pull-right">Add User</button>
+                </div>
             </div>
           </div>
           <Userdetail v-if="isUserDetailsVisible"></Userdetail>
@@ -51,21 +54,37 @@
   margin-right: auto;
 }
 
+.adduser {
+  margin-right: $offset;
+  padding-bottom: $offset;
+}
+
 .userlist {
   margin-top: $large-offset;
-  width: 50vw;
-  margin-right: $large-offset;
+  margin-right: $offset;
+  height: 100%;
 }
+
+.userlistsmall {
+  width: 30vw;
+}
+
+.userlistextended {
+  width: 70vw;
+}
+
 .shadow {
   box-shadow: $basic-shadow-light !important;
 }
 
 .table th,
 td {
+  font-size: 0.9rem;
   border-top: 0px !important;
 }
 
 .table td {
+  font-size: 0.9rem;
   cursor: pointer;
 }
 </style>
@@ -87,9 +106,21 @@
     Userdetail
   },
   computed: {
-    ...mapGetters("usermanagement", ["users", "isUserDetailsVisible"])
+    ...mapGetters("usermanagement", ["users", "isUserDetailsVisible"]),
+    userlistStyle() {
+      return {
+        userlist: true,
+        shadow: true,
+        userlistsmall: this.isUserDetailsVisible,
+        userlistextended: !this.isUserDetailsVisible
+      };
+    }
   },
   methods: {
+    addUser() {
+      this.$store.commit("usermanagement/clearCurrentUser");
+      this.$store.commit("usermanagement/setUserDetailsVisible");
+    },
     selectUser(name) {
       const user = this.$store.getters["usermanagement/getUserByName"](name);
       this.$store.commit("usermanagement/setCurrentUser", user);
@@ -106,6 +137,11 @@
           message: `${status}: ${data}`
         });
       });
+  },
+  beforeRouteLeave(to, from, next) {
+    store.commit("usermanagement/clearCurrentUser");
+    store.commit("usermanagement/setUserDetailsInvisible");
+    next();
   }
 };
 </script>