airavata-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From machris...@apache.org
Subject [airavata-django-portal] 04/04: AIRAVATA-2876 Don't allow leaving page with unsaved changes
Date Wed, 19 Sep 2018 17:56:19 GMT
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 <machrist@iu.edu>
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 = {


Mime
View raw message