Mercurial > gemma
changeset 2384:c06b001dc26b
client: improved popup implementation
For deleting users and templates there was a more or less quick n' dirty implementation of
a confirmation dialog/popup. Since we need this kind of dialog in several more places I
generalized the implementation a bit and made it more robust.
author | Markus Kottlaender <markus@intevation.de> |
---|---|
date | Mon, 25 Feb 2019 13:11:30 +0100 |
parents | 8d025f85a3fe |
children | 279334be495c |
files | client/src/assets/application.scss client/src/components/App.vue client/src/components/ImportStretches.vue client/src/components/Popup.vue client/src/components/importschedule/Importschedule.vue client/src/components/systemconfiguration/PDFTemplates.vue client/src/components/usermanagement/Usermanagement.vue client/src/store/application.js |
diffstat | 8 files changed, 389 insertions(+), 224 deletions(-) [+] |
line wrap: on
line diff
--- a/client/src/assets/application.scss Mon Feb 25 08:47:59 2019 +0100 +++ b/client/src/assets/application.scss Mon Feb 25 13:11:30 2019 +0100 @@ -99,17 +99,6 @@ margin: 0 0.5rem 1rem 0.5rem; } -.popup { - width: 300px; - max-width: 300px; - @extend %fully-centered; -} - -.popup.show { - margin: 0.5rem 0 0 0; - max-height: 999px; -} - // needed to fix the whitespace problem of // https://github.com/Polyconseil/vue-gettext/issues/80; // use like @@ -131,10 +120,10 @@ font-weight: bold; } -.list-fade-enter-active, .list-fade-leave-active { +.fade-enter-active, .fade-leave-active { transition: opacity .3s; } -.list-fade-enter, .list-fade-leave-to { +.fade-enter, .fade-leave-to { opacity: 0; }
--- a/client/src/components/App.vue Mon Feb 25 08:47:59 2019 +0100 +++ b/client/src/components/App.vue Mon Feb 25 13:11:30 2019 +0100 @@ -29,6 +29,7 @@ </div> <div class="d-flex flex-column"><router-view /></div> <vue-snotify></vue-snotify> + <Popup /> </div> </template> @@ -114,7 +115,8 @@ Sidebar: () => import("./Sidebar"), Search: () => import("./Search"), Contextbox: () => import("./Contextbox"), - Toolbar: () => import("./toolbar/Toolbar") + Toolbar: () => import("./toolbar/Toolbar"), + Popup: () => import("./Popup") } }; </script>
--- a/client/src/components/ImportStretches.vue Mon Feb 25 08:47:59 2019 +0100 +++ b/client/src/components/ImportStretches.vue Mon Feb 25 13:11:30 2019 +0100 @@ -11,8 +11,7 @@ <th class="header"><translate>Name</translate></th> <th class="header"><translate>Datum</translate></th> <th class="header"><translate>Source organization</translate></th> - <th class="tools"> </th> - <th class="tools"> </th> + <th></th> </tr> </thead> <tbody> @@ -39,21 +38,19 @@ {{ formatSurveyDate(stretch.properties["date_info"]) }} </td> <td>{{ stretch.properties["source_organization"] }}</td> - <td> - <font-awesome-icon - class="pointer" + <td class="text-right"> + <button + class="btn btn-sm btn-dark mr-1" @click="editStretch(index)" - icon="pencil-alt" - fixed-width - ></font-awesome-icon> - </td> - <td> - <font-awesome-icon - class="pointer" - @click="deleteStretch(index)" - icon="trash" - fixed-width - ></font-awesome-icon> + > + <font-awesome-icon icon="pencil-alt" fixed-width /> + </button> + <button + class="btn btn-sm btn-dark" + @click="deleteStretch(stretch)" + > + <font-awesome-icon icon="trash" fixed-width /> + </button> </td> </tr> </tbody> @@ -352,10 +349,30 @@ this.endrhm = this.sanitizeRHM(properties.upper); this.idEditable = false; }, - deleteStretch(index) { - displayInfo({ - title: this.$gettext("Not implemented"), - message: this.$gettext("Deleting " + this.stretches[index].id) + deleteStretch(stretch) { + this.$store.commit("application/popup", { + icon: "trash", + title: this.$gettext("Delete Stretch"), + content: + this.$gettext("Do you really want to delete this stretch:") + + `<br> + <b>${stretch.properties.name}, ${ + stretch.properties.source_organization + } (${stretch.properties.countries})</b>`, + confirm: { + label: this.$gettext("Delete"), + icon: "trash", + callback: () => { + displayInfo({ + title: this.$gettext("Not implemented"), + message: this.$gettext("Deleting " + stretch.id) + }); + } + }, + cancel: { + label: this.$gettext("Cancel"), + icon: "times" + } }); }, moveMapToStretch(index) {
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/src/components/Popup.vue Mon Feb 25 13:11:30 2019 +0100 @@ -0,0 +1,137 @@ +<template> + <transition name="fade"> + <div + class="overlay d-flex justify-content-center align-items-center" + v-if="popup" + > + <div class="popup"> + <h6 class="popup-header"> + <span class="popup-title"> + <font-awesome-icon :icon="popup.icon" class="popup-icon" /> + {{ popup.title }} + </span> + <span class="popup-close" @click="close()"> + <font-awesome-icon icon="times" /> + </span> + </h6> + <div class="popup-content" v-html="popup.content"></div> + <div class="popup-footer" v-if="popup.cancel || popup.confirm"> + <button + class="btn btn-sm btn-warning" + @click="cancel" + v-if="popup.cancel" + > + <font-awesome-icon + :icon="popup.cancel.icon" + class="fa-fw" + v-if="popup.cancel.icon" + /> + {{ popup.cancel.label || $gettext("Cancel") }} + </button> + <span /> + <button + class="btn btn-sm btn-info" + @click="confirm" + v-if="popup.confirm" + > + <font-awesome-icon + :icon="popup.confirm.icon" + class="fa-fw" + v-if="popup.confirm.icon" + /> + {{ popup.confirm.label || $gettext("Confirm") }} + </button> + </div> + </div> + </div> + </transition> +</template> + +<style lang="sass" scoped> +.overlay + position: fixed + z-index: 9 + top: 0 + right: 0 + bottom: 0 + left: 0 + background: rgba(0, 0, 0, .3) + .popup + display: flex + flex-direction: column + box-shadow: 0 .5rem 1rem rgba(0,0,0,.15) + background-color: #fff + border-radius: 0.25rem + max-width: 320px + .popup-header + display: flex + justify-content: space-between + align-items: center + padding-left: .5rem + border-bottom: 1px solid #dee2e6 + color: $color-info + margin-bottom: 0 + padding: 0.25rem + font-weight: bold + .popup-title + padding-left: 0.25rem + .popup-icon + margin-right: 0.25rem + .popup-close + color: #aaa + padding: 3px 5px + border-radius: 0.25rem + cursor: pointer + transition: background-color 0.3s, color 0.3s + &:hover + color: #888 + background-color: #eee + .popup-content + padding: 1rem + .popup-footer + display: flex + justify-content: space-between + align-items: center + border-top: 1px solid #dee2e6 + padding: 0.25rem +</style> + +<script> +/* This is Free Software under GNU Affero General Public License v >= 3.0 + * without warranty, see README.md and license for details. + * + * SPDX-License-Identifier: AGPL-3.0-or-later + * License-Filename: LICENSES/AGPL-3.0.txt + * + * Copyright (C) 2018 by via donau + * – Österreichische Wasserstraßen-Gesellschaft mbH + * Software engineering by Intevation GmbH + * + * Author(s): + * Markus Kottländer <markus.kottlaender@intevation.de> + */ + +import { mapState } from "vuex"; + +export default { + name: "popup", + computed: { + ...mapState("application", ["popup"]) + }, + methods: { + confirm() { + if (this.popup.confirm && this.popup.confirm.callback) + this.popup.confirm.callback(); + this.close(); + }, + cancel() { + if (this.popup.cancel && this.popup.cancel.callback) + this.popup.cancel.callback(); + this.close(); + }, + close() { + this.$store.commit("application/popup", null); + } + } +}; +</script>
--- a/client/src/components/importschedule/Importschedule.vue Mon Feb 25 08:47:59 2019 +0100 +++ b/client/src/components/importschedule/Importschedule.vue Mon Feb 25 13:11:30 2019 +0100 @@ -28,69 +28,71 @@ /> </div> </div> - <table v-if="schedules.length" class="table table-hover"> - <thead> - <tr> - <th><translate>ID</translate></th> - <th><translate>Type</translate></th> - <th><translate>Author</translate></th> - <th><translate>Schedule</translate></th> - <th><translate>Email</translate></th> - <th style="width: 140px"></th> - </tr> - </thead> - <tbody> - <tr v-for="schedule in schedules" :key="schedule.id"> - <td>{{ schedule.id }}</td> - <td>{{ schedule.kind.toUpperCase() }}</td> - <td>{{ schedule.user }}</td> - <td>{{ schedule.config.cron }}</td> - <td> - <font-awesome-icon - v-if="schedule.config['send-email']" - class="fa-fw mr-2" - fixed-width - icon="check" - ></font-awesome-icon> - </td> - <td class="text-right"> - <button - @click="editSchedule(schedule.id)" - class="btn btn-sm btn-dark mr-1" - :disabled="importScheduleDetailVisible" - > + <transition name="fade"> + <table v-if="schedules.length" class="table table-hover"> + <thead> + <tr> + <th><translate>ID</translate></th> + <th><translate>Type</translate></th> + <th><translate>Author</translate></th> + <th><translate>Schedule</translate></th> + <th><translate>Email</translate></th> + <th style="width: 140px"></th> + </tr> + </thead> + <transition-group name="fade" tag="tbody"> + <tr v-for="schedule in schedules" :key="schedule.id"> + <td>{{ schedule.id }}</td> + <td>{{ schedule.kind.toUpperCase() }}</td> + <td>{{ schedule.user }}</td> + <td>{{ schedule.config.cron }}</td> + <td> <font-awesome-icon - icon="pencil-alt" + v-if="schedule.config['send-email']" + class="fa-fw mr-2" fixed-width - ></font-awesome-icon> - </button> - <button - @click="deleteSchedule(schedule.id)" - class="btn btn-sm btn-dark mr-1" - :disabled="importScheduleDetailVisible" - > - <font-awesome-icon - icon="trash" - fixed-width + icon="check" ></font-awesome-icon> - </button> - <button - @click="triggerManualImport(schedule.id)" - class="btn btn-sm btn-dark" - :disabled="importScheduleDetailVisible" - > - <font-awesome-icon - icon="play" - fixed-width - ></font-awesome-icon> - </button> - </td> - </tr> - </tbody> - </table> - <div v-else class="mt-4 small text-center py-3"> - <translate>No scheduled imports</translate> - </div> + </td> + <td class="text-right"> + <button + @click="editSchedule(schedule.id)" + class="btn btn-sm btn-dark mr-1" + :disabled="importScheduleDetailVisible" + > + <font-awesome-icon + icon="pencil-alt" + fixed-width + ></font-awesome-icon> + </button> + <button + @click="deleteSchedule(schedule)" + class="btn btn-sm btn-dark mr-1" + :disabled="importScheduleDetailVisible" + > + <font-awesome-icon + icon="trash" + fixed-width + ></font-awesome-icon> + </button> + <button + @click="triggerManualImport(schedule.id)" + class="btn btn-sm btn-dark" + :disabled="importScheduleDetailVisible" + > + <font-awesome-icon + icon="play" + fixed-width + ></font-awesome-icon> + </button> + </td> + </tr> + </transition-group> + </table> + <div v-else class="mt-4 small text-center py-3"> + <translate>No scheduled imports</translate> + </div> + </transition> <div class="text-right"> <button :disabled="importScheduleDetailVisible" @@ -121,6 +123,7 @@ * * Author(s): * Thomas Junk <thomas.junk@intevation.de> + * Markus Kottländer <markus.kottlaender@intevation.de> */ import { mapState } from "vuex"; @@ -187,24 +190,43 @@ newImport() { this.$store.commit("importschedule/setImportScheduleDetailVisible"); }, - deleteSchedule(index) { - if (this.importScheduleDetailVisible) return; - this.$store - .dispatch("importschedule/deleteSchedule", index) - .then(() => { - this.getSchedules(); - displayInfo({ - title: this.$gettext("Imports"), - message: this.$gettext("Deleted import: #") + index - }); - }) - .catch(error => { - const { status, data } = error.response; - displayError({ - title: this.$gettext("Backend Error"), - message: `${status}: ${data.message || data}` - }); - }); + deleteSchedule(schedule) { + console.log(schedule); + this.$store.commit("application/popup", { + icon: "trash", + title: this.$gettext("Delete Import"), + content: this.$gettext( + `Do you really want to delete the import with ID <b>${ + schedule.id + }</b> of type <b>${schedule.kind.toUpperCase()}</b>?` + ), + confirm: { + label: this.$gettext("Delete"), + icon: "trash", + callback: () => { + this.$store + .dispatch("importschedule/deleteSchedule", schedule.id) + .then(() => { + this.getSchedules(); + displayInfo({ + title: this.$gettext("Imports"), + message: this.$gettext("Deleted import: #") + schedule.id + }); + }) + .catch(error => { + const { status, data } = error.response; + displayError({ + title: this.$gettext("Backend Error"), + message: `${status}: ${data.message || data}` + }); + }); + } + }, + cancel: { + label: this.$gettext("Cancel"), + icon: "times" + } + }); } }, computed: {
--- a/client/src/components/systemconfiguration/PDFTemplates.vue Mon Feb 25 08:47:59 2019 +0100 +++ b/client/src/components/systemconfiguration/PDFTemplates.vue Mon Feb 25 13:11:30 2019 +0100 @@ -3,7 +3,7 @@ <div class="d-flex flex-row justify-content-between"> <h5><translate>PDF-Templates</translate></h5> <input - @change="upload" + @change="uploadTemplate" id="uploadTemplate" ref="uploadTemplate" type="file" @@ -11,35 +11,34 @@ /> </div> <div class="mt-1 border-bottom pb-4"> - <table class="table table-sm table-hover" v-if="templates.length"> - <thead> - <tr> - <th><translate>Name</translate></th> - <th><translate>Date</translate></th> - <th><translate>Country</translate></th> - <th></th> - </tr> - </thead> - <transition-group name="list-fade" tag="tbody"> - <tr v-for="template in templates" :key="template.name"> - <td>{{ template.name }}</td> - <td>{{ template.time }}</td> - <td v-if="template.country">{{ template.country }}</td> - <td v-else><i>global</i></td> - <td class="text-right"> - <button - class="btn btn-sm btn-dark" - @click=" - showDeleteTemplatePrompt = true; - templateToDelete = template; - " - > - <font-awesome-icon icon="trash" /> - </button> - </td> - </tr> - </transition-group> - </table> + <transition name="fade"> + <table class="table table-sm table-hover" v-if="templates.length"> + <thead> + <tr> + <th><translate>Name</translate></th> + <th><translate>Date</translate></th> + <th><translate>Country</translate></th> + <th></th> + </tr> + </thead> + <transition-group name="fade" tag="tbody"> + <tr v-for="template in templates" :key="template.name"> + <td>{{ template.name }}</td> + <td>{{ template.time }}</td> + <td v-if="template.country">{{ template.country }}</td> + <td v-else><i>global</i></td> + <td class="text-right"> + <button + class="btn btn-sm btn-dark" + @click="deleteTemplate(template)" + > + <font-awesome-icon icon="trash" /> + </button> + </td> + </tr> + </transition-group> + </table> + </transition> <button class="btn btn-info mt-2" @click="$refs.uploadTemplate.click()"> <font-awesome-icon icon="spinner" @@ -50,44 +49,6 @@ <translate>Upload new template</translate> </button> </div> - - <div - :class="[ - 'box popup ui-element rounded bg-white', - { show: showDeleteTemplatePrompt } - ]" - > - <div> - <h6 class="mb-0 py-2 px-3 border-bottom d-flex align-items-center"> - <font-awesome-icon icon="trash" class="mr-2"></font-awesome-icon> - <translate>Delete PDF Template</translate> - <font-awesome-icon - icon="times" - class="ml-auto text-muted" - @click="showDeleteTemplatePrompt = false" - ></font-awesome-icon> - </h6> - <div class="p-3 text-left"> - <translate class="text-center d-block"> - Do you really want to delete the following template: - </translate> - <h5 class="mt-3 text-center">{{ templateToDelete.name }}</h5> - </div> - <div - class="py-2 px-3 border-top d-flex align-items-center justify-content-between" - > - <button - class="btn btn-sm btn-warning" - @click="showDeleteTemplatePrompt = false" - > - no - </button> - <button class="btn btn-sm btn-info" @click="remove(templateToDelete)"> - yes - </button> - </div> - </div> - </div> </div> </template> @@ -123,13 +84,11 @@ data() { return { templates: [], - uploading: false, - templateToDelete: "", - showDeleteTemplatePrompt: false + uploading: false }; }, methods: { - upload() { + uploadTemplate() { const reader = new FileReader(); reader.onload = event => { let template; @@ -201,19 +160,38 @@ }); }); }, - remove(template) { - this.showDeleteTemplatePrompt = false; - HTTP.delete("/templates/print/" + template.name, { - headers: { - "X-Gemma-Auth": localStorage.getItem("token"), - "Content-type": "text/xml; charset=UTF-8" - } - }).then(() => { - let removeIndex = this.templates.findIndex( - t => t.name === template.name - ); - if (removeIndex !== -1) { - this.templates.splice(removeIndex, 1); + deleteTemplate(template) { + this.$store.commit("application/popup", { + icon: "trash", + title: this.$gettext("Delete Template"), + content: + this.$gettext( + "Do you really want to delete the following template:" + ) + + `<br> + <b>${template.name}</b>`, + confirm: { + label: this.$gettext("Delete"), + icon: "trash", + callback: () => { + HTTP.delete("/templates/print/" + template.name, { + headers: { + "X-Gemma-Auth": localStorage.getItem("token"), + "Content-type": "text/xml; charset=UTF-8" + } + }).then(() => { + let removeIndex = this.templates.findIndex( + t => t.name === template.name + ); + if (removeIndex !== -1) { + this.templates.splice(removeIndex, 1); + } + }); + } + }, + cancel: { + label: this.$gettext("Cancel"), + icon: "times" } }); }
--- a/client/src/components/usermanagement/Usermanagement.vue Mon Feb 25 08:47:59 2019 +0100 +++ b/client/src/components/usermanagement/Usermanagement.vue Mon Feb 25 13:11:30 2019 +0100 @@ -56,7 +56,7 @@ <th scope="col"></th> </tr> </thead> - <tbody> + <transition-group name="fade" tag="tbody"> <tr v-for="user in users" :key="user.user"> <td @click="selectUser(user.user)"> <font-awesome-icon @@ -78,10 +78,7 @@ <font-awesome-icon icon="paper-plane"></font-awesome-icon> </button> <button - @click=" - showDeleteUserPrompt = true; - userToDelete = user.user; - " + @click="deleteUser(user.user)" class="btn btn-sm btn-dark" v-tooltip="$gettext('Delete user')" > @@ -89,7 +86,7 @@ </button> </td> </tr> - </tbody> + </transition-group> </table> </div> <div class="d-flex mx-auto align-items-center"> @@ -324,7 +321,6 @@ }) .catch(error => { this.loginFailed = true; - this.submitted = false; const { status, data } = error.response; displayError({ title: this.$gettext("Backend Error"), @@ -357,26 +353,46 @@ this.sortCriterion = criterion; }, deleteUser(name) { - this.$store - .dispatch("usermanagement/deleteUser", { name: name }) - .then(() => { - this.showDeleteUserPrompt = false; - this.submitted = false; - this.$store.dispatch("usermanagement/loadUsers").catch(error => { - const { status, data } = error.response; - displayError({ - title: this.$gettext("Backend Error"), - message: `${status}: ${data.message || data}` - }); - }); - }) - .catch(error => { - const { status, data } = error.response; - displayError({ - title: this.$gettext("Backend Error"), - message: `${status}: ${data.message || data}` - }); - }); + this.$store.commit("application/popup", { + icon: "trash", + title: this.$gettext("Delete User"), + content: + this.$gettext( + "Do you really want to delete the following user account:" + ) + + `<br> + <b>${name}</b>`, + confirm: { + label: this.$gettext("Delete"), + icon: "trash", + callback: () => { + this.$store + .dispatch("usermanagement/deleteUser", { name }) + .then(() => { + this.$store + .dispatch("usermanagement/loadUsers") + .catch(error => { + const { status, data } = error.response; + displayError({ + title: this.$gettext("Backend Error"), + message: `${status}: ${data.message || data}` + }); + }); + }) + .catch(error => { + const { status, data } = error.response; + displayError({ + title: this.$gettext("Backend Error"), + message: `${status}: ${data.message || data}` + }); + }); + } + }, + cancel: { + label: this.$gettext("Cancel"), + icon: "times" + } + }); }, addUser() { this.$store.commit("usermanagement/clearCurrentUser");
--- a/client/src/store/application.js Mon Feb 25 08:47:59 2019 +0100 +++ b/client/src/store/application.js Mon Feb 25 13:11:30 2019 +0100 @@ -22,6 +22,7 @@ appTitle: process.env.VUE_APP_TITLE, secondaryLogo: process.env.VUE_APP_SECONDARY_LOGO_URL, logoForPDF: process.env.VUE_APP_LOGO_FOR_PDF_URL, + popup: null, showSidebar: false, showUsermenu: false, showSplitscreen: false, @@ -65,6 +66,9 @@ } }, mutations: { + popup: (state, popup) => { + state.popup = popup; + }, showSidebar: (state, show) => { state.showSidebar = show; },