Mercurial > gemma
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>