Browse Source

feat: add auto-align

Ahmad Kholid 3 years ago
parent
commit
d26c2b04b6

+ 2 - 0
package.json

@@ -49,6 +49,7 @@
     "compare-versions": "^4.1.2",
     "crypto-js": "^4.1.1",
     "css-selector-generator": "^3.6.1",
+    "dagre": "^0.8.5",
     "dayjs": "^1.10.7",
     "defu": "^6.0.0",
     "dexie": "^3.2.2",
@@ -112,6 +113,7 @@
     "tailwindcss": "^3.0.7",
     "terser-webpack-plugin": "^5.3.3",
     "vue-loader": "^17.0.0",
+    "web-worker": "^1.2.0",
     "webpack": "^5.73.0",
     "webpack-cli": "^4.10.0",
     "webpack-dev-server": "^4.9.2"

+ 7 - 1
src/components/newtab/workflow/WorkflowEditor.vue

@@ -13,9 +13,10 @@
       v-if="editorControls"
       class="flex items-end absolute p-4 left-0 bottom-0 z-10"
     >
+      <slot name="controls-prepend" />
       <button
         v-tooltip.group="t('workflow.editor.resetZoom')"
-        class="p-2 rounded-lg bg-white dark:bg-gray-800 mr-2"
+        class="control-button mr-2"
         @click="editor.fitView()"
       >
         <v-remixicon name="riFullscreenLine" />
@@ -38,6 +39,7 @@
         </button>
       </div>
       <editor-search-blocks :editor="editor" />
+      <slot name="controls-append" />
     </div>
     <template v-for="(node, name) in nodeTypes" :key="name" #[name]="nodeProps">
       <component
@@ -201,4 +203,8 @@ onBeforeUnmount(() => {
 <style>
 @import '@braks/vue-flow/dist/style.css';
 @import '@braks/vue-flow/dist/theme-default.css';
+
+.control-button {
+  @apply p-2 rounded-lg bg-white dark:bg-gray-800 transition-colors;
+}
 </style>

+ 1 - 1
src/components/newtab/workflow/edit/EditBlockSettings.vue

@@ -32,7 +32,7 @@
         </ui-tab-panel>
         <ui-tab-panel value="on-error">
           <block-setting-on-error
-            data="state.onError"
+            :data="state.onError"
             @change="onDataChange('onError', $event)"
           />
         </ui-tab-panel>

+ 1 - 1
src/components/newtab/workflows/WorkflowsLocal.vue

@@ -24,7 +24,7 @@
       >
         <template #header>
           <div class="flex items-center mb-4">
-            <template v-if="!workflow.isDisabled">
+            <template v-if="workflow && !workflow.isDisabled">
               <ui-img
                 v-if="workflow.icon.startsWith('http')"
                 :src="workflow.icon"

+ 1 - 1
src/components/ui/UiCard.vue

@@ -2,7 +2,7 @@
   <component
     :is="tag"
     v-bind="$attrs"
-    class="bg-white dark:bg-gray-800 transform rounded-lg transition-transform ui-card"
+    class="bg-white dark:bg-gray-800 transform rounded-lg ui-card"
     :class="[padding, { 'hover:shadow-xl hover:-translate-y-1': hover }]"
   >
     <slot></slot>

+ 9 - 15
src/content/services/webService.js

@@ -1,7 +1,6 @@
 import { openDB } from 'idb';
 import { nanoid } from 'nanoid';
 import browser from 'webextension-polyfill';
-import cloneDeep from 'lodash.clonedeep';
 import { objectHasKey } from '@/utils/helper';
 import { sendMessage } from '@/utils/message';
 
@@ -50,25 +49,20 @@ function initWebListener() {
         const { workflows: workflowsStorage } = await browser.storage.local.get(
           'workflows'
         );
-        const copyWorkflow = cloneDeep(workflow);
-
-        copyWorkflow.table = copyWorkflow.table || copyWorkflow.dataColumns;
-        copyWorkflow.dataColumns = [];
 
         const workflowId = nanoid();
+        const workflowData = {
+          ...workflow,
+          id: workflowId,
+          dataColumns: [],
+          createdAt: Date.now(),
+          table: workflow.table || workflow.dataColumns,
+        };
 
         if (Array.isArray(workflowsStorage)) {
-          workflowsStorage.push({
-            ...copyWorkflow,
-            id: workflowId,
-            createdAt: Date.now(),
-          });
+          workflowsStorage.push(workflowData);
         } else {
-          workflowsStorage[workflowId] = {
-            ...copyWorkflow,
-            id: workflowId,
-            createdAt: Date.now(),
-          };
+          workflowsStorage[workflowId] = workflowData;
         }
 
         await browser.storage.local.set({ workflows: workflowsStorage });

+ 2 - 0
src/lib/vRemixicon.js

@@ -39,6 +39,7 @@ import {
   riCloseLine,
   riCheckLine,
   riTimerLine,
+  riMagicLine,
   riToggleLine,
   riFolderLine,
   riGithubFill,
@@ -155,6 +156,7 @@ export const icons = {
   riCloseLine,
   riCheckLine,
   riTimerLine,
+  riMagicLine,
   riToggleLine,
   riFolderLine,
   riGithubFill,

+ 3 - 0
src/locales/en/newtab.json

@@ -139,6 +139,9 @@
     "clickToEnable": "Click to enable",
     "toggleSidebar": "Toggle sidebar",
     "cantEdit": "Can't edit shared workflow",
+    "autoAlign": {
+      "title": "Auto-align"
+    },
     "searchBlocks": {
       "title": "Search blocks in the editor"
     },

+ 68 - 7
src/newtab/pages/workflows/[id].vue

@@ -74,12 +74,23 @@
             v-if="state.workflowConverted"
             :id="route.params.id"
             :data="workflow.drawflow"
+            :class="{ 'animate-blocks': state.animateBlocks }"
             class="h-screen"
             @init="onEditorInit"
             @edit="initEditBlock"
             @update:node="state.dataChanged = true"
             @delete:node="state.dataChanged = true"
-          />
+          >
+            <template #controls-append>
+              <button
+                v-tooltip="t('workflow.autoAlign.title')"
+                class="control-button hoverable ml-2"
+                @click="autoAlign"
+              >
+                <v-remixicon name="riMagicLine" />
+              </button>
+            </template>
+          </workflow-editor>
           <editor-local-ctx-menu
             v-if="editor"
             :editor="editor"
@@ -137,6 +148,7 @@ import { useI18n } from 'vue-i18n';
 import { useRoute, useRouter, onBeforeRouteLeave } from 'vue-router';
 import { customAlphabet } from 'nanoid';
 import defu from 'defu';
+import dagre from 'dagre';
 import { useStore } from '@/stores/main';
 import { useUserStore } from '@/stores/user';
 import { useWorkflowStore } from '@/stores/workflow';
@@ -172,6 +184,7 @@ const editor = shallowRef(null);
 const state = reactive({
   showSidebar: true,
   dataChanged: false,
+  animateBlocks: false,
   workflowConverted: false,
   activeTab: route.query.tab || 'editor',
 });
@@ -347,7 +360,54 @@ const onNodesChange = debounce((changes) => {
     }
   });
 }, 250);
+const onEdgesChange = debounce((changes) => {
+  changes.forEach(({ type }) => {
+    if (state.dataChanged) return;
+    state.dataChanged = type !== 'select';
+  });
+}, 250);
+
+function autoAlign() {
+  state.animateBlocks = true;
+
+  const graph = new dagre.graphlib.Graph();
+  graph.setGraph({
+    rankdir: 'LR',
+    ranksep: 100,
+    ranker: 'tight-tree',
+  });
+  graph._isMultigraph = true;
+  graph.setDefaultEdgeLabel(() => ({}));
+  editor.value.getNodes.value.forEach(({ id, label, dimensions }) => {
+    graph.setNode(id, {
+      label,
+      width: dimensions.width,
+      height: dimensions.height,
+    });
+  });
+  editor.value.getEdges.value.forEach(({ source, target, id }) => {
+    graph.setEdge(source, target, { id });
+  });
 
+  dagre.layout(graph);
+  const nodeChanges = graph.nodes().map((nodeId) => {
+    const { x, y } = graph.node(nodeId);
+
+    return {
+      id: nodeId,
+      type: 'position',
+      dragging: false,
+      position: { x, y },
+    };
+  });
+
+  editor.value.applyNodeChanges(nodeChanges);
+  editor.value.fitView();
+
+  setTimeout(() => {
+    state.animateBlocks = false;
+  }, 500);
+}
 function toggleSidebar() {
   state.showSidebar = !state.showSidebar;
   localStorage.setItem('workflow:sidebar', state.showSidebar);
@@ -402,16 +462,11 @@ function onActionUpdated({ data, changedIndicator }) {
 }
 function onEditorInit(instance) {
   editor.value = instance;
-  instance.onEdgesChange((changes) => {
-    changes.forEach(({ type }) => {
-      if (state.dataChanged) return;
 
-      state.dataChanged = type !== 'select';
-    });
-  });
   instance.onEdgeDoubleClick(({ edge }) => {
     instance.removeEdges([edge]);
   });
+  instance.onEdgesChange(onEdgesChange);
   instance.onNodesChange(onNodesChange);
 
   const { blockId } = route.query;
@@ -695,4 +750,10 @@ onBeforeUnmount(() => {
 .dropable-area__handle {
   @apply ring-4;
 }
+.animate-blocks {
+  .vue-flow__transformationpane,
+  .vue-flow__node {
+    transition: transform 300ms ease;
+  }
+}
 </style>

+ 6 - 2
src/stores/workflow.js

@@ -103,13 +103,17 @@ export const useWorkflowStore = defineStore('workflow', {
         'isFirstTime',
       ]);
 
-      const localWorkflows = isFirstTime ? firstWorkflows : workflows;
-      this.workflows = convertWorkflowsToObject(localWorkflows);
+      let localWorkflows = workflows;
 
       if (isFirstTime) {
+        localWorkflows = firstWorkflows.map((workflow) =>
+          defaultWorkflow(workflow)
+        );
         await browser.storage.local.set({ isFirstTime: false });
       }
 
+      this.workflows = convertWorkflowsToObject(localWorkflows);
+
       const storedStates = localStorage.getItem('workflowState') || '{}';
       const states = parseJSON(storedStates, {});
       this.states = Object.values(states).filter(

+ 1 - 4
src/utils/convertWorkflowData.js

@@ -16,8 +16,6 @@ export default function (workflow) {
   const nodes = [];
   const edges = [];
 
-  let edgeId = 0;
-
   function extractBlock(blockId) {
     if (tracedBlocks.has(blockId)) return;
 
@@ -64,12 +62,11 @@ export default function (workflow) {
           targetHandle,
           source: block.id,
           target: outputId,
-          id: `edge-${edgeId}`,
+          id: `vueflow__edge-${sourceHandle}-${targetHandle}`,
           class: `source-${sourceHandle} target-${targetHandle}`,
         });
 
         nextBlockIds.push(outputId);
-        edgeId += 1;
       });
     });
 

File diff suppressed because it is too large
+ 215 - 2
src/utils/shared.js


+ 21 - 1
yarn.lock

@@ -2866,6 +2866,14 @@ d3-zoom@^3.0.0:
     d3-selection "2 - 3"
     d3-transition "2 - 3"
 
+dagre@^0.8.5:
+  version "0.8.5"
+  resolved "https://registry.yarnpkg.com/dagre/-/dagre-0.8.5.tgz#ba30b0055dac12b6c1fcc247817442777d06afee"
+  integrity sha512-/aTqmnRta7x7MCCpExk7HQL2O4owCT2h8NT//9I1OQ9vt29Pa0BzSAkR5lwFUcQ7491yVi/3CXU9jQ5o0Mn2Sw==
+  dependencies:
+    graphlib "^2.1.8"
+    lodash "^4.17.15"
+
 dayjs@^1.10.7:
   version "1.11.3"
   resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.3.tgz#4754eb694a624057b9ad2224b67b15d552589258"
@@ -3880,6 +3888,13 @@ graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4,
   resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c"
   integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==
 
+graphlib@^2.1.8:
+  version "2.1.8"
+  resolved "https://registry.yarnpkg.com/graphlib/-/graphlib-2.1.8.tgz#5761d414737870084c92ec7b5dbcb0592c9d35da"
+  integrity sha512-jcLLfkpoVGmH7/InMC/1hIvOPSUh38oJtGhvrOFGzioE1DZ+0YW16RgmOJhHiuWTvGiJQ9Z1Ik43JvkRPRvE+A==
+  dependencies:
+    lodash "^4.17.15"
+
 handle-thing@^2.0.0:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-2.0.1.tgz#857f79ce359580c340d43081cc648970d0bb234e"
@@ -4613,7 +4628,7 @@ lodash.union@^4.6.0:
   resolved "https://registry.yarnpkg.com/lodash.union/-/lodash.union-4.6.0.tgz#48bb5088409f16f1821666641c44dd1aaae3cd88"
   integrity sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==
 
-lodash@^4.17.20, lodash@^4.17.21:
+lodash@^4.17.15, lodash@^4.17.20, lodash@^4.17.21:
   version "4.17.21"
   resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
   integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
@@ -6461,6 +6476,11 @@ wbuf@^1.1.0, wbuf@^1.7.3:
   dependencies:
     minimalistic-assert "^1.0.0"
 
+web-worker@^1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/web-worker/-/web-worker-1.2.0.tgz#5d85a04a7fbc1e7db58f66595d7a3ac7c9c180da"
+  integrity sha512-PgF341avzqyx60neE9DD+XS26MMNMoUQRz9NOZwW32nPQrF6p77f1htcnjBSEV8BGMKZ16choqUG4hyI0Hx7mA==
+
 webextension-polyfill@^0.9.0:
   version "0.9.0"
   resolved "https://registry.yarnpkg.com/webextension-polyfill/-/webextension-polyfill-0.9.0.tgz#de6c1941d0ef1b0858b20e9c7b46bbc042c5a960"

Some files were not shown because too many files changed in this diff