Browse Source

feat(editor): add alert when data is not saved

Ahmad Kholid 3 years ago
parent
commit
1652dc5674

+ 6 - 1
src/components/block/BlockConditions.vue

@@ -85,6 +85,7 @@
 import { watch, toRaw } from 'vue';
 import { VRemixIcon as VRemixicon } from 'v-remixicon';
 import { nanoid } from 'nanoid';
+import emitter from 'tiny-emitter/instance';
 import { debounce } from '@/utils/helper';
 import { icons } from '@/lib/v-remixicon';
 import { useComponentId } from '@/composable/componentId';
@@ -128,12 +129,16 @@ function deleteComparison(index) {
 
 watch(
   () => block.data.conditions,
-  debounce((newValue) => {
+  debounce((newValue, oldValue) => {
     props.editor.updateNodeDataFromId(block.id, {
       conditions: toRaw(newValue),
     });
 
     props.editor.updateConnectionNodes(`node-${block.id}`);
+
+    if (oldValue) {
+      emitter.emit('editor:data-changed', block.id);
+    }
   }, 250),
   { deep: true }
 );

+ 2 - 0
src/components/block/BlockDelay.vue

@@ -32,6 +32,7 @@
 </template>
 <script setup>
 import { VRemixIcon as VRemixicon } from 'v-remixicon';
+import emitter from 'tiny-emitter/instance';
 import { icons } from '@/lib/v-remixicon';
 import { useComponentId } from '@/composable/componentId';
 import { useEditorBlock } from '@/composable/editorBlock';
@@ -54,5 +55,6 @@ function handleInput({ target }) {
   if (time < 0) return;
 
   props.editor.updateNodeDataFromId(block.id, { time });
+  emitter.emit('editor:data-changed', block.id);
 }
 </script>

+ 2 - 0
src/components/block/BlockElementExists.vue

@@ -31,6 +31,7 @@
 </template>
 <script setup>
 import { VRemixIcon as VRemixicon } from 'v-remixicon';
+import emitter from 'tiny-emitter/instance';
 import { icons } from '@/lib/v-remixicon';
 import { useComponentId } from '@/composable/componentId';
 import { useEditorBlock } from '@/composable/editorBlock';
@@ -49,6 +50,7 @@ function handleInput({ target }) {
   target.reportValidity();
 
   props.editor.updateNodeDataFromId(block.id, { selector: target.value });
+  emitter.emit('editor:data-changed', block.id);
 }
 </script>
 <style>

+ 2 - 0
src/components/block/BlockExportData.vue

@@ -33,6 +33,7 @@
 </template>
 <script setup>
 import { VRemixIcon as VRemixicon } from 'v-remixicon';
+import emitter from 'tiny-emitter/instance';
 import { icons } from '@/lib/v-remixicon';
 import { useComponentId } from '@/composable/componentId';
 import { useEditorBlock } from '@/composable/editorBlock';
@@ -55,5 +56,6 @@ const exportTypes = [
 
 function handleInput({ target }) {
   props.editor.updateNodeDataFromId(block.id, { type: target.value });
+  emitter.emit('editor:data-changed', block.id);
 }
 </script>

+ 2 - 0
src/components/block/BlockOpenWebsite.vue

@@ -31,6 +31,7 @@
 </template>
 <script setup>
 import { VRemixIcon as VRemixicon } from 'v-remixicon';
+import emitter from 'tiny-emitter/instance';
 import { icons } from '@/lib/v-remixicon';
 import { debounce } from '@/utils/helper';
 import { useComponentId } from '@/composable/componentId';
@@ -57,5 +58,6 @@ const handleInput = debounce(({ target }) => {
   if (!res) return;
 
   props.editor.updateNodeDataFromId(block.id, { url: res[0] });
+  emitter.emit('editor:data-changed', block.id);
 }, 250);
 </script>

+ 2 - 0
src/components/block/BlockRepeatTask.vue

@@ -45,6 +45,7 @@
 </template>
 <script setup>
 import { VRemixIcon as VRemixicon } from 'v-remixicon';
+import emitter from 'tiny-emitter/instance';
 import { icons } from '@/lib/v-remixicon';
 import { useComponentId } from '@/composable/componentId';
 import { useEditorBlock } from '@/composable/editorBlock';
@@ -67,6 +68,7 @@ function handleInput({ target }) {
   if (repeatFor < 0) return;
 
   props.editor.updateNodeDataFromId(block.id, { repeatFor });
+  emitter.emit('editor:data-changed', block.id);
 }
 </script>
 <style>

+ 0 - 12
src/components/block/BlockStart.vue

@@ -1,12 +0,0 @@
-<template>
-  <div class="flex items-center relative p-4 overflow-hidden rounded-lg">
-    <span class="inline-block p-2 mr-4 rounded-lg bg-yellow-200">
-      <v-remixicon :path="riFlagLine" />
-    </span>
-    <p class="font-semibold mr-4">Start</p>
-  </div>
-</template>
-<script setup>
-import { VRemixIcon as VRemixicon } from 'v-remixicon';
-import { riFlagLine } from 'v-remixicon/icons';
-</script>

+ 10 - 3
src/components/newtab/workflow/WorkflowBuilder.vue

@@ -24,6 +24,7 @@
 <script>
 /* eslint-disable camelcase */
 import { onMounted, shallowRef, getCurrentInstance } from 'vue';
+import emitter from 'tiny-emitter/instance';
 import { tasks } from '@/utils/shared';
 import drawflow from '@/lib/drawflow';
 
@@ -34,7 +35,7 @@ export default {
       default: null,
     },
   },
-  emits: ['addBlock', 'export', 'load', 'deleteBlock'],
+  emits: ['load', 'deleteBlock'],
   setup(props, { emit }) {
     const editor = shallowRef(null);
 
@@ -72,6 +73,7 @@ export default {
         block.component,
         'vue'
       );
+      emitter.emit('editor:data-changed');
     }
     function isInputAllowed(allowedInputs, input) {
       if (typeof allowedInputs === 'boolean') return allowedInputs;
@@ -118,12 +120,12 @@ export default {
       editor.value.on(
         'connectionCreated',
         ({ output_id, input_id, output_class, input_class }) => {
-          const { name: outputs } = editor.value.getNodeFromId(output_id);
+          const { outputs } = editor.value.getNodeFromId(output_id);
           const { name: inputName } = editor.value.getNodeFromId(input_id);
           const { allowedInputs, maxConnection } = tasks[inputName];
           const isAllowed = isInputAllowed(allowedInputs, inputName);
           const isMaxConnections =
-            outputs[output_class].connections.length > maxConnection;
+            outputs[output_class]?.connections.length > maxConnection;
 
           if (!isAllowed || isMaxConnections) {
             editor.value.removeSingleConnection(
@@ -133,8 +135,13 @@ export default {
               input_class
             );
           }
+
+          emitter.emit('editor:data-changed');
         }
       );
+      editor.value.on('connectionRemoved', () => {
+        emitter.emit('editor:data-changed');
+      });
     });
 
     return {

+ 1 - 1
src/components/newtab/workflow/WorkflowDetailsCard.vue

@@ -24,7 +24,7 @@
         </ui-button>
       </template>
       <ui-list class="w-36">
-        <ui-list-item>
+        <ui-list-item class="cursor-pointer" @click="$emit('execute')">
           <v-remixicon name="riPlayLine" class="mr-2 -ml-1" />
           <span>Execute</span>
         </ui-list-item>

+ 31 - 12
src/newtab/pages/workflows/[id].vue

@@ -22,9 +22,7 @@
       class="flex-1"
       :data="workflow.drawflow"
       @load="editor = $event"
-      @addBlock="addBlock"
       @deleteBlock="deleteBlock"
-      @export="updateWorkflow({ drawflow: $event })"
     />
   </div>
   <ui-modal v-model="state.showDataColumnsModal">
@@ -37,6 +35,7 @@
   </ui-modal>
 </template>
 <script setup>
+/* eslint-disable consistent-return */
 import {
   computed,
   reactive,
@@ -45,9 +44,8 @@ import {
   onMounted,
   onUnmounted,
 } from 'vue';
-import { useRoute, useRouter } from 'vue-router';
+import { useRoute, useRouter, onBeforeRouteLeave } from 'vue-router';
 import emitter from 'tiny-emitter/instance';
-import Task from '@/models/task';
 import Workflow from '@/models/workflow';
 import { debounce } from '@/utils/helper';
 import WorkflowBuilder from '@/components/newtab/workflow/WorkflowBuilder.vue';
@@ -64,12 +62,14 @@ const editor = shallowRef(null);
 const state = reactive({
   blockData: {},
   isEditBlock: false,
+  isDataChanged: false,
   showDataColumnsModal: false,
 });
 const workflow = computed(() => Workflow.find(workflowId) || {});
 
 const updateBlockData = debounce((data) => {
   state.blockData.data = data;
+  state.isDataChanged = true;
   editor.value.updateNodeDataFromId(state.blockData.blockId, data);
 
   const inputEl = document.querySelector(
@@ -78,27 +78,25 @@ const updateBlockData = debounce((data) => {
 
   if (inputEl) inputEl.dispatchEvent(new Event('change'));
 }, 250);
-function addBlock(data) {
-  Task.insert({
-    data: { ...data, workflowId },
-  });
-}
 function deleteBlock(id) {
   if (state.isEditBlock && state.blockData.blockId === id) {
     state.isEditBlock = false;
     state.blockData = {};
   }
+
+  state.isDataChanged = true;
 }
 function updateWorkflow(data) {
-  Workflow.update({
+  return Workflow.update({
     where: workflowId,
     data,
   });
 }
 function saveWorkflow() {
   const data = editor.value.export();
-  console.log(data);
-  updateWorkflow({ drawflow: JSON.stringify(data) });
+  updateWorkflow({ drawflow: JSON.stringify(data) }).then(() => {
+    state.isDataChanged = false;
+  });
 }
 function editBlock(data) {
   state.isEditBlock = true;
@@ -107,6 +105,9 @@ function editBlock(data) {
 function executeWorkflow() {
   console.log(editor.value);
 }
+function handleEditorDataChanged() {
+  state.isDataChanged = true;
+}
 
 provide('workflow', {
   data: workflow,
@@ -115,6 +116,15 @@ provide('workflow', {
   showDataColumnsModal: (show = true) => (state.showDataColumnsModal = show),
 });
 
+onBeforeRouteLeave(() => {
+  if (!state.isDataChanged) return;
+
+  const answer = window.confirm(
+    'Do you really want to leave? you have unsaved changes!'
+  );
+
+  if (!answer) return false;
+});
 onMounted(() => {
   const isWorkflowExists = Workflow.query().where('id', workflowId).exists();
 
@@ -122,10 +132,19 @@ onMounted(() => {
     router.push('/workflows');
   }
 
+  window.onbeforeunload = () => {
+    if (state.isDataChanged) {
+      return 'Changes you made may not be saved.';
+    }
+  };
+
   emitter.on('editor:edit-block', editBlock);
+  emitter.on('editor:data-changed', handleEditorDataChanged);
 });
 onUnmounted(() => {
+  window.onbeforeunload = null;
   emitter.off('editor:edit-block', editBlock);
+  emitter.off('editor:data-changed', handleEditorDataChanged);
 });
 </script>
 <style>