This is an automated email from the ASF dual-hosted git repository. machristie pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/airavata-django-portal.git
commit e62902ce22fcfd14dd2493d8bb6724143bc69415 Author: Marcus Christie <[email protected]> AuthorDate: Tue Sep 18 14:54:50 2018 -0400 AIRAVATA-2876 Don't allow leaving page with unsaved changes --- .../applications/ApplicationDeploymentEditor.vue | 30 +++++++++++- .../applications/ApplicationEditorContainer.vue | 54 +++++++++++++++++++--- .../common/js/components/UnsavedChangesGuard.vue | 29 ++++++++++++ django_airavata/static/common/js/index.js | 4 +- 4 files changed, 107 insertions(+), 10 deletions(-) diff --git a/django_airavata/apps/admin/static/django_airavata_admin/src/components/applications/ApplicationDeploymentEditor.vue b/django_airavata/apps/admin/static/django_airavata_admin/src/components/applications/ApplicationDeploymentEditor.vue index 7091db8..407d3a3 100644 --- a/django_airavata/apps/admin/static/django_airavata_admin/src/components/applications/ApplicationDeploymentEditor.vue +++ b/django_airavata/apps/admin/static/django_airavata_admin/src/components/applications/ApplicationDeploymentEditor.vue @@ -1,5 +1,8 @@ <template> <div> + <confirmation-dialog ref="unsavedChangesDialog" title="You have unsaved changes"> + You have unsaved changes. Are you sure you want to leave this page? + </confirmation-dialog> <div class="row"> <div class="col"> <h1 class="h4 mb-4"> @@ -89,14 +92,24 @@ export default { components: { CommandObjectsEditor, SetEnvPathsEditor, - "share-button": components.ShareButton + "share-button": components.ShareButton, + "confirmation-dialog": components.ConfirmationDialog }, data() { return { computeResource: null, - localSharedEntity: this.sharedEntity ? this.sharedEntity.clone() : null + localSharedEntity: this.sharedEntity ? this.sharedEntity.clone() : null, + dirty: false }; }, + mounted() { + this.$on("input", () => { + this.dirty = true; + }); + }, + destroyed() { + this.$off("input"); + }, computed: { name() { if (this.computeResource) { @@ -161,9 +174,13 @@ export default { }, methods: { save() { + // FIXME: if the save operation fails then this form should still be + // dirty. But this editor doesn't know if the save fails. + this.dirty = false; this.$emit("save"); }, cancel() { + this.dirty = false; this.$emit("cancel"); }, defaultQueueChanged(queueName) { @@ -175,6 +192,7 @@ export default { this.data.defaultWalltime = queue.defaultWalltime; }, sharingChanged(newSharedEntity) { + this.dirty = true; this.$emit("sharing-changed", newSharedEntity); } }, @@ -182,6 +200,14 @@ export default { sharedEntity(newValue, oldValue) { this.localSharedEntity = newValue.clone(); } + }, + beforeRouteLeave(to, from, next) { + if (this.dirty) { + this.$refs.unsavedChangesDialog.show(); + this.$refs.unsavedChangesDialog.$on("ok", next); + } else { + next(); + } } }; </script> diff --git a/django_airavata/apps/admin/static/django_airavata_admin/src/components/applications/ApplicationEditorContainer.vue b/django_airavata/apps/admin/static/django_airavata_admin/src/components/applications/ApplicationEditorContainer.vue index 1e0667c..58bb9b5 100644 --- a/django_airavata/apps/admin/static/django_airavata_admin/src/components/applications/ApplicationEditorContainer.vue +++ b/django_airavata/apps/admin/static/django_airavata_admin/src/components/applications/ApplicationEditorContainer.vue @@ -1,5 +1,9 @@ <template> <div> + <unsaved-changes-guard :dirty="isDirty" /> + <confirmation-dialog ref="unsavedChangesDialog" title="You have unsaved changes"> + You have unsaved changes. Are you sure you want to leave this page? + </confirmation-dialog> <div class="row"> <div class="col"> <h1 class="h4 mb-4"> @@ -15,13 +19,13 @@ <b-nav-item active-class="active" :to="{name: 'application_deployments', params: {id: id}}" :disabled="!id">Deployments</b-nav-item> </b-nav> <router-view name="module" v-if="module" v-model="module" @save="saveModule" @cancel="cancelModule" @delete="deleteApplication" - :readonly="!module.userHasWriteAccess" /> + @input="moduleIsDirty = true" :readonly="!module.userHasWriteAccess" /> <router-view name="interface" v-if="appInterface" v-model="appInterface" @save="saveInterface" @cancel="cancelInterface" - :readonly="!appInterface.userHasWriteAccess" /> + @input="interfaceIsDirty = true" :readonly="!appInterface.userHasWriteAccess" /> <router-view name="deployments" v-if="deployments" :deployments="deployments" @new="createNewDeployment" @delete="deleteDeployment" /> <router-view name="deployment" v-if="deployment" v-model="deployment" :shared-entity="deploymentSharedEntity" @sharing-changed="deploymentSharingChanged" - @save="saveDeployment" @cancel="cancelDeployment" /> + @input="deploymentIsDirty = true" @save="saveDeployment" @cancel="cancelDeployment" /> </div> </div> </div> @@ -30,7 +34,7 @@ <script> import { mapActions, mapState } from "vuex"; import { models, services } from "django-airavata-api"; -import { notifications } from "django-airavata-common-ui"; +import { components, notifications } from "django-airavata-common-ui"; export default { name: "application-editor-container", @@ -39,12 +43,19 @@ export default { deployment_id: String, hostId: String }, + components: { + "unsaved-changes-guard": components.UnsavedChangesGuard, + "confirmation-dialog": components.ConfirmationDialog + }, data: function() { return { module: null, appInterface: null, deployment: null, - deploymentSharedEntity: null + deploymentSharedEntity: null, + moduleIsDirty: false, + interfaceIsDirty: false, + deploymentIsDirty: false }; }, computed: { @@ -57,6 +68,11 @@ export default { return "Create a New Application"; } }, + isDirty() { + return ( + this.moduleIsDirty || this.interfaceIsDirty || this.deploymentIsDirty + ); + }, ...mapState("applications/modules", ["currentModule"]), ...mapState("applications/interfaces", ["currentInterface"]), ...mapState("applications/deployments", [ @@ -116,10 +132,12 @@ export default { saveModule() { if (this.id) { this.updateApplicationModule(this.module).then(() => { + this.moduleIsDirty = false; this.$router.push({ path: "/applications" }); }); } else { this.createApplicationModule(this.module).then(appModule => { + this.moduleIsDirty = false; this.$router.push({ name: "application_module", params: { id: appModule.appModuleId } @@ -137,6 +155,7 @@ export default { if (this.appInterface.applicationInterfaceId) { return this.updateApplicationInterface(this.appInterface).then( () => { + this.interfaceIsDirty = false; this.$router.push({ path: "/applications" }); } ); @@ -144,6 +163,7 @@ export default { this.appInterface.applicationModules = [this.id]; return this.createApplicationInterface(this.appInterface).then( () => { + this.interfaceIsDirty = false; this.$router.push({ path: "/applications" }); } ); @@ -180,6 +200,7 @@ export default { : this.createApplicationModule(this.module); return moduleSave .then(appModule => { + this.moduleIsDirty = false; this.appInterface.applicationName = appModule.appModuleName; this.appInterface.applicationDescription = appModule.appModuleDescription; @@ -192,6 +213,7 @@ export default { } }) .then(appInterface => { + this.interfaceIsDirty = false; if (this.deployment) { if (this.deployment.appDeploymentId) { return this.updateApplicationDeployment(this.deployment); @@ -208,15 +230,19 @@ export default { } else { return Promise.resolve(null); } - }); + }) + .then(() => (this.deploymentIsDirty = false)); }, cancelModule() { + this.moduleIsDirty = false; this.$router.push({ path: "/applications" }); }, cancelInterface() { + this.interfaceIsDirty = false; this.$router.push({ path: "/applications" }); }, cancelDeployment() { + this.deploymentIsDirty = false; this.$router.push({ name: "application_deployments", params: { id: this.id } @@ -224,7 +250,10 @@ export default { }, deleteDeployment(deployment) { return this.deleteApplicationDeployment(deployment) - .then(() => this.loadApplicationDeployments(this.id)) + .then(() => { + this.deploymentIsDirty = false; + return this.loadApplicationDeployments(this.id); + }) .then(() => { this.$router.push({ name: "application_deployments", @@ -243,6 +272,7 @@ export default { ); return Promise.all(deleteAllDeployments) .then(() => { + this.deploymentIsDirty = false; if (this.appInterface && this.appInterface.applicationInterfaceId) { return services.ApplicationInterfaceService.delete({ lookup: this.appInterface.applicationInterfaceId @@ -250,9 +280,11 @@ export default { } }) .then(() => { + this.interfaceIsDirty = false; return services.ApplicationModuleService.delete({ lookup: this.id }); }) .then(() => { + this.deploymentIsDirty = false; this.$router.push({ path: "/applications" }); }); } @@ -279,6 +311,14 @@ export default { currentDeployment: function(newDeployment) { this.deployment = newDeployment ? newDeployment.clone() : null; } + }, + beforeRouteLeave(to, from, next) { + if (this.isDirty) { + this.$refs.unsavedChangesDialog.show(); + this.$refs.unsavedChangesDialog.$on("ok", next); + } else { + next(); + } } }; </script> diff --git a/django_airavata/static/common/js/components/UnsavedChangesGuard.vue b/django_airavata/static/common/js/components/UnsavedChangesGuard.vue new file mode 100644 index 0000000..028d170 --- /dev/null +++ b/django_airavata/static/common/js/components/UnsavedChangesGuard.vue @@ -0,0 +1,29 @@ +<template> +</template> + +<script> +export default { + name: "unsaved-changes-guard", + props: { + dirty: { + type: Boolean, + default: false + } + }, + mounted() { + window.addEventListener("beforeunload", this.onBeforeUnload); + }, + destroyed() { + window.removeEventListener("beforeunload", this.onBeforeUnload); + }, + methods: { + onBeforeUnload(event) { + if (this.dirty) { + event.preventDefault(); + return "You have unsaved changes. Are you sure that you want to leave this page?"; + } + } + } +}; +</script> + diff --git a/django_airavata/static/common/js/index.js b/django_airavata/static/common/js/index.js index b103f91..c39dbd6 100644 --- a/django_airavata/static/common/js/index.js +++ b/django_airavata/static/common/js/index.js @@ -7,6 +7,7 @@ import DeleteLink from "./components/DeleteLink.vue"; import NotificationsDisplay from "./components/NotificationsDisplay.vue"; import Pager from "./components/Pager.vue"; import ShareButton from "./components/ShareButton.vue"; +import UnsavedChangesGuard from "./components/UnsavedChangesGuard.vue"; import GlobalErrorHandler from "./errors/GlobalErrorHandler"; @@ -26,7 +27,8 @@ exports.components = { DeleteButton, DeleteLink, NotificationsDisplay, - ShareButton + ShareButton, + UnsavedChangesGuard }; exports.errors = {
