소스 검색

fix: recording flow generator

Ahmad Kholid 3 년 전
부모
커밋
6351345b27

+ 1 - 1
src/components/block/BlockGroup.vue

@@ -140,7 +140,7 @@ const state = reactive({
   retrieved: false,
 });
 
-const workflow = inject('workflow');
+const workflow = inject('workflow', {});
 
 function onDragStart(item, event) {
   event.dataTransfer.setData(

+ 14 - 2
src/components/newtab/workflow/WorkflowEditor.vue

@@ -1,8 +1,11 @@
 <template>
   <vue-flow :id="props.id" :class="{ disabled: options.disabled }">
     <Background />
-    <MiniMap :node-class-name="minimapNodeClassName" />
-    <div class="flex items-end absolute p-4 left-0 bottom-0 z-10">
+    <MiniMap v-if="minimap" :node-class-name="minimapNodeClassName" />
+    <div
+      v-if="editorControls"
+      class="flex items-end absolute p-4 left-0 bottom-0 z-10"
+    >
       <button
         v-tooltip.group="t('workflow.editor.resetZoom')"
         class="p-2 rounded-lg bg-white dark:bg-gray-800 mr-2"
@@ -67,6 +70,14 @@ const props = defineProps({
     type: Object,
     default: () => ({}),
   },
+  editorControls: {
+    type: Boolean,
+    default: true,
+  },
+  minimap: {
+    type: Boolean,
+    default: true,
+  },
 });
 const emit = defineEmits(['edit', 'init', 'update:node', 'delete:node']);
 
@@ -97,6 +108,7 @@ const { t } = useI18n();
 const editor = useVueFlow({
   id: props.id,
   minZoom: 0.4,
+  edgeUpdaterRadius: 20,
   deleteKeyCode: 'Delete',
   elevateEdgesOnSelect: true,
   defaultZoom: props.data?.zoom ?? 0.7,

+ 2 - 0
src/components/newtab/workflow/editor/EditorLocalActions.vue

@@ -174,6 +174,7 @@ import { reactive, computed } from 'vue';
 import { useI18n } from 'vue-i18n';
 import { useRouter } from 'vue-router';
 import { useToast } from 'vue-toastification';
+import browser from 'webextension-polyfill';
 import { sendMessage } from '@/utils/message';
 import { fetchApi } from '@/utils/api';
 import { useUserStore } from '@/stores/user';
@@ -388,6 +389,7 @@ async function saveWorkflow() {
       {
         drawflow: flow,
         trigger: triggerBlock.data,
+        version: browser.runtime.getManifest().version,
       },
       false
     );

+ 63 - 94
src/components/popup/home/HomeSelectBlock.vue

@@ -14,10 +14,13 @@
     <p class="mt-2">
       {{ t('home.record.selectBlock') }}
     </p>
-    <div
-      ref="editorContainer"
-      class="parent-drawflow h-56 min-h w-full rounded-lg bg-box-transparent"
-    ></div>
+    <workflow-editor
+      :minimap="false"
+      :editor-controls="false"
+      :options="editorOptions"
+      class="h-56 w-full rounded-lg bg-box-transparent"
+      @init="onEditorInit"
+    />
     <ui-button
       :disabled="!state.activeBlock"
       variant="accent"
@@ -29,16 +32,10 @@
   </div>
 </template>
 <script setup>
-import {
-  shallowReactive,
-  ref,
-  getCurrentInstance,
-  shallowRef,
-  onMounted,
-} from 'vue';
+import { onMounted, onBeforeUnmount, shallowReactive } from 'vue';
 import { useI18n } from 'vue-i18n';
-import { findTriggerBlock } from '@/utils/helper';
-import drawflow from '@/lib/drawflow';
+import convertWorkflowData from '@/utils/convertWorkflowData';
+import WorkflowEditor from '@/components/newtab/workflow/WorkflowEditor.vue';
 
 const props = defineProps({
   workflow: {
@@ -46,69 +43,68 @@ const props = defineProps({
     default: () => ({}),
   },
 });
-const emit = defineEmits(['goBack', 'record']);
+const emit = defineEmits(['goBack', 'record', 'update']);
 
 const { t } = useI18n();
-const context = getCurrentInstance().appContext.app._context;
 
-const editor = shallowRef(null);
-const editorContainer = ref(null);
+const editorOptions = {
+  disabled: true,
+  fitViewOnInit: true,
+  nodesDraggable: false,
+  edgesUpdateable: false,
+  nodesConnectable: false,
+};
+
 const state = shallowReactive({
+  retrieved: false,
   activeBlock: null,
-  blockOutput: 'output_1',
+  blockOutput: null,
 });
 
-function onEditorClick(event) {
-  const [target] = event.composedPath();
-  const nodeEl = target.closest('.drawflow-node');
-
-  if (nodeEl) {
-    const prevActiveEl = editorContainer.value.querySelector(
-      '.drawflow-node.selected'
-    );
-    if (prevActiveEl) {
-      prevActiveEl.classList.remove('selected');
-
-      const outputEl = prevActiveEl.querySelector('.output.active');
-      outputEl.classList.remove('active');
-    }
-
-    const nodeId = nodeEl.id.slice(5);
-    const node = editor.value.getNodeFromId(nodeId);
-    const outputs = Object.keys(node.outputs);
-
-    if (outputs.length === 0) {
-      alert(t('home.record.anotherBlock'));
-      state.activeBlock = null;
-      state.blockOutput = null;
-      return;
-    }
-
-    let outputEl = target.closest('.output');
+function onEditorInit(editor) {
+  const convertedData = convertWorkflowData(props.workflow);
+  emit('update', { drawflow: convertedData.drawflow });
 
-    if (outputEl) {
-      /* eslint-disable-next-line */
-      state.blockOutput = outputEl.classList[1];
-      outputEl.classList.add('active');
-    } else {
-      const firstOutput = outputs[0];
+  editor.setNodes(convertedData.drawflow.nodes);
+  editor.setEdges(convertedData.drawflow.edges);
+}
+function clearSelectedHandle() {
+  document.querySelectorAll('.selected-handle').forEach((el) => {
+    el.classList.remove('selected-handle');
+  });
+}
+function onClick({ target }) {
+  let selectedHandle = null;
+
+  const handleEl = target.closest('.vue-flow__handle.source');
+  if (handleEl) {
+    clearSelectedHandle();
+    handleEl.classList.add('selected-handle');
+    selectedHandle = handleEl;
+  }
 
-      state.blockOutput = firstOutput || '';
-      outputEl = nodeEl.querySelector(`.${firstOutput}`);
+  if (!handleEl) {
+    const nodeEl = target.closest('.vue-flow__node');
+    if (nodeEl) {
+      clearSelectedHandle();
+      const handle = nodeEl.querySelector('.vue-flow__handle.source');
+      handle.classList.add('selected-handle');
+      selectedHandle = handle;
     }
+  }
 
-    if (outputEl) outputEl.classList.add('active');
+  if (!selectedHandle) return;
 
-    nodeEl.classList.add('selected');
-    state.activeBlock = node;
-  }
+  const { handleid, nodeid } = selectedHandle.dataset;
+  state.activeBlock = nodeid;
+  state.blockOutput = handleid;
 }
 function startRecording() {
   const options = {
     name: props.workflow.name,
     workflowId: props.workflow.id,
     connectFrom: {
-      id: state.activeBlock.id,
+      id: state.activeBlock,
       output: state.blockOutput,
     },
   };
@@ -117,45 +113,18 @@ function startRecording() {
 }
 
 onMounted(() => {
-  const flowData = props.workflow.drawflow;
-  const flow = typeof flowData === 'string' ? JSON.parse(flowData) : flowData;
-  const triggerBlock = findTriggerBlock(flow);
-
-  const editorInstance = drawflow(editorContainer.value, {
-    context,
-    options: {
-      zoom: 0.5,
-      zoom_min: 0.1,
-      zoom_max: 0.8,
-      minimap: true,
-      editor_mode: 'fixed',
-    },
-  });
-
-  editorInstance.start();
-  editorInstance.import(flow);
-
-  if (triggerBlock) {
-    const getCoordinate = (pos) => {
-      const num = Math.abs(pos);
-
-      if (pos > 0) return -num;
-
-      return num;
-    };
-
-    editorInstance.translate_to(
-      getCoordinate(triggerBlock.pos_x),
-      getCoordinate(triggerBlock.pos_y)
-    );
-  }
-
-  editor.value = editorInstance;
-  editorContainer.value.addEventListener('click', onEditorClick);
+  window.addEventListener('click', onClick);
+});
+onBeforeUnmount(() => {
+  window.removeEventListener('click', onClick);
 });
 </script>
 <style>
-.output.active {
+.selected-handle {
   @apply ring-4;
 }
+
+.vue-flow__handle.source {
+  pointer-events: auto !important;
+}
 </style>

+ 8 - 0
src/components/popup/home/HomeStartRecording.vue

@@ -28,6 +28,7 @@
       <home-select-block
         v-if="activeWorkflow"
         :workflow="activeWorkflow"
+        @update="updateWorkflow"
         @record="$emit('record', $event)"
         @goBack="state.activeWorkflow = ''"
       />
@@ -107,4 +108,11 @@ const workflows = computed(() =>
     )
     .sort((a, b) => (a.createdAt > b.createdAt ? 1 : -1))
 );
+
+function updateWorkflow(data) {
+  workflowStore.update({
+    data,
+    id: state.activeWorkflow,
+  });
+}
 </script>

+ 0 - 1
src/newtab/App.vue

@@ -199,7 +199,6 @@ window.addEventListener('storage', ({ key, newValue }) => {
     await dataMigration();
 
     retrieved.value = true;
-    workflowStore.retrieved = true;
 
     await userStore.loadUser();
     if (userStore.user) {

+ 39 - 10
src/newtab/pages/workflows/[id].vue

@@ -351,16 +351,44 @@ function initEditBlock(data) {
   const { editComponent, data: blockDefData } = tasks[data.id];
   const blockData = defu(data.data, blockDefData);
 
-  editState.editing = true;
   editState.blockData = { ...data, editComponent, data: blockData };
+
+  if (data.id === 'wait-connections') {
+    const connections = editor.value.getEdges.value.reduce(
+      (acc, { target, sourceNode, source }) => {
+        if (target !== data.blockId) return acc;
+
+        let name = t(`workflow.blocks.${sourceNode.label}.name`);
+
+        const { description } = sourceNode.data;
+        if (description) name += ` (${description})`;
+
+        acc.push({
+          name,
+          id: source,
+        });
+
+        return acc;
+      },
+      []
+    );
+
+    editState.blockData.connections = connections;
+  }
+
+  editState.editing = true;
 }
-function updateWorkflow(data) {
-  workflowStore.update({
-    data,
-    id: route.params.id,
-  });
-  workflowPayload.data = { ...workflowPayload.data, ...data };
-  updateHostedWorkflow();
+async function updateWorkflow(data) {
+  try {
+    await workflowStore.update({
+      data,
+      id: route.params.id,
+    });
+    workflowPayload.data = { ...workflowPayload.data, ...data };
+    await updateHostedWorkflow();
+  } catch (error) {
+    console.error(error);
+  }
 }
 function onActionUpdated({ data, changedIndicator }) {
   state.dataChanged = changedIndicator;
@@ -594,8 +622,9 @@ onMounted(() => {
     JSON.parse(localStorage.getItem('workflow:sidebar')) ?? true;
 
   const convertedData = convertWorkflowData(workflow.value);
-  updateWorkflow({ drawflow: convertedData.drawflow });
-  state.workflowConverted = true;
+  updateWorkflow({ drawflow: convertedData.drawflow }).then(() => {
+    state.workflowConverted = true;
+  });
 
   window.onbeforeunload = () => {
     updateHostedWorkflow();

+ 1 - 0
src/popup/index.js

@@ -7,6 +7,7 @@ import vueI18n from '../lib/vueI18n';
 import vRemixicon, { icons } from '../lib/vRemixicon';
 import '../assets/css/tailwind.css';
 import '../assets/css/fonts.css';
+import '../assets/css/flow.css';
 
 createApp(App)
   .use(router)

+ 151 - 159
src/popup/pages/Recording.vue

@@ -50,18 +50,18 @@
   </div>
 </template>
 <script setup>
-/* eslint-disable */
 import { onMounted, reactive, toRaw } from 'vue';
 import { useI18n } from 'vue-i18n';
-// import { useRouter } from 'vue-router';
+import { useRouter } from 'vue-router';
 import { nanoid } from 'nanoid';
 import defu from 'defu';
 import browser from 'webextension-polyfill';
 import { tasks } from '@/utils/shared';
-// import Workflow from '@/models/workflow';
+import { useWorkflowStore } from '@/stores/workflow';
 
 const { t } = useI18n();
-// const router = useRouter();
+const router = useRouter();
+const workflowStore = useWorkflowStore();
 
 const state = reactive({
   name: '',
@@ -70,161 +70,153 @@ const state = reactive({
   isGenerating: false,
 });
 
-// function generateDrawflow(startBlock, startBlockData) {
-//   let nextNodeId = nanoid();
-//   const triggerId = startBlock?.id || nanoid();
-//   let prevNodeId = startBlock?.id || triggerId;
-
-//   const nodes = {
-//     [triggerId]: {
-//       pos_x: 50,
-//       pos_y: 300,
-//       inputs: {},
-//       outputs: {
-//         output_1: {
-//           connections: [{ node: nextNodeId, output: 'input_1' }],
-//         },
-//       },
-//       id: triggerId,
-//       typenode: 'vue',
-//       name: 'trigger',
-//       class: 'trigger',
-//       html: 'BlockBasic',
-//       data: tasks.trigger.data,
-//       ...startBlockData,
-//     },
-//   };
-
-//   if (startBlock) {
-//     nodes[triggerId].outputs[startBlock.output]?.connections.push({
-//       node: nextNodeId,
-//       output: 'input_1',
-//     });
-//   }
-
-//   const position = {
-//     y: startBlockData ? startBlockData.pos_y + 50 : 300,
-//     x: startBlockData ? startBlockData.pos_x + 120 : 260,
-//   };
-//   const groups = {};
-
-//   state.flows.forEach((block, index) => {
-//     if (block.groupId) {
-//       if (!groups[block.groupId]) groups[block.groupId] = [];
-
-//       groups[block.groupId].push({
-//         id: block.id,
-//         itemId: nanoid(),
-//         data: defu(block.data, tasks[block.id].data),
-//       });
-
-//       const nextNodeInGroup = state.flows[index + 1]?.groupId;
-//       if (nextNodeInGroup) return;
-
-//       block.id = 'blocks-group';
-//       block.data = { blocks: groups[block.groupId] };
-
-//       delete groups[block.groupId];
-//     }
-
-//     const node = {
-//       id: nextNodeId,
-//       name: block.id,
-//       class: block.id,
-//       typenode: 'vue',
-//       pos_x: position.x,
-//       pos_y: position.y,
-//       inputs: { input_1: { connections: [] } },
-//       outputs: { output_1: { connections: [] } },
-//       html: tasks[block.id].component,
-//       data: defu(block.data, tasks[block.id].data),
-//     };
-
-//     node.inputs.input_1.connections.push({
-//       node: prevNodeId,
-//       input: index === 0 && startBlock ? startBlock.output : 'output_1',
-//     });
-
-//     const isLastIndex = index === state.flows.length - 1;
-
-//     prevNodeId = nextNodeId;
-//     nextNodeId = nanoid();
-
-//     if (!isLastIndex) {
-//       node.outputs.output_1.connections.push({
-//         node: nextNodeId,
-//         output: 'input_1',
-//       });
-//     }
-
-//     const inNewRow = (index + 1) % 5 === 0;
-//     const blockNameLen = tasks[block.id].name.length * 14 + 120;
-//     position.x = inNewRow ? 50 : position.x + blockNameLen;
-//     position.y = inNewRow ? position.y + 150 : position.y;
-
-//     nodes[node.id] = node;
-//   });
-
-//   if (startBlock) return nodes;
-
-//   return { drawflow: { Home: { data: nodes } } };
-// }
-// async function stopRecording() {
-//   if (state.isGenerating) return;
-
-//   try {
-//     state.isGenerating = true;
-
-//     if (state.flows.length !== 0) {
-//       if (state.workflowId) {
-//         const workflow = Workflow.find(state.workflowId);
-//         const drawflow =
-//           typeof workflow.drawflow === 'string'
-//             ? JSON.parse(workflow.drawflow)
-//             : workflow.drawflow;
-//         const node = drawflow.drawflow.Home.data[state.connectFrom.id];
-//         const updatedDrawflow = generateDrawflow(state.connectFrom, node);
-
-//         Object.assign(drawflow.drawflow.Home.data, updatedDrawflow);
-
-//         await Workflow.update({
-//           where: state.workflowId,
-//           data: {
-//             drawflow: JSON.stringify(drawflow),
-//           },
-//         });
-//       } else {
-//         const drawflow = generateDrawflow();
-
-//         await Workflow.insert({
-//           data: {
-//             name: state.name,
-//             drawflow: JSON.stringify(drawflow),
-//           },
-//         });
-//       }
-//     }
-
-//     await browser.storage.local.remove(['isRecording', 'recording']);
-//     await browser.browserAction.setBadgeText({ text: '' });
-
-//     const tabs = (await browser.tabs.query({})).filter((tab) =>
-//       tab.url.startsWith('http')
-//     );
-//     Promise.allSettled(
-//       tabs.map(({ id }) =>
-//         browser.tabs.sendMessage(id, { type: 'recording:stop' })
-//       )
-//     );
-
-//     state.isGenerating = false;
-
-//     router.push('/');
-//   } catch (error) {
-//     state.isGenerating = false;
-//     console.error(error);
-//   }
-// }
+function generateDrawflow(startBlock, startBlockData) {
+  let nextNodeId = nanoid();
+  const triggerId = startBlock?.id || nanoid();
+  let prevNodeId = startBlock?.id || triggerId;
+
+  const nodes = [];
+  const edges = [];
+
+  const addEdge = (data = {}) => {
+    edges.push({
+      ...data,
+      id: nanoid(),
+      class: `source-${data.sourceHandle} targte-${data.targetHandle}`,
+    });
+  };
+  addEdge({
+    source: prevNodeId,
+    target: nextNodeId,
+    targetHandle: `${nextNodeId}-input-1`,
+    sourceHandle: startBlock?.output || `${prevNodeId}-output-1`,
+  });
+
+  if (!startBlock) {
+    nodes.push({
+      position: {
+        x: 50,
+        y: 300,
+      },
+      id: triggerId,
+      label: 'trigger',
+      type: 'BlockBasic',
+      data: tasks.trigger.data,
+    });
+  }
+
+  const position = {
+    y: startBlockData ? startBlockData.position.y + 120 : 300,
+    x: startBlockData ? startBlockData.position.x + 280 : 320,
+  };
+  const groups = {};
+
+  state.flows.forEach((block, index) => {
+    if (block.groupId) {
+      if (!groups[block.groupId]) groups[block.groupId] = [];
+
+      groups[block.groupId].push({
+        id: block.id,
+        itemId: nanoid(),
+        data: defu(block.data, tasks[block.id].data),
+      });
+
+      const nextNodeInGroup = state.flows[index + 1]?.groupId;
+      if (nextNodeInGroup) return;
+
+      block.id = 'blocks-group';
+      block.data = { blocks: groups[block.groupId] };
+
+      delete groups[block.groupId];
+    }
+
+    const node = {
+      id: nextNodeId,
+      label: block.id,
+      type: tasks[block.id].component,
+      data: defu(block.data, tasks[block.id].data),
+      position: JSON.parse(JSON.stringify(position)),
+    };
+
+    prevNodeId = nextNodeId;
+    nextNodeId = nanoid();
+
+    if (index !== state.flows.length - 1) {
+      addEdge({
+        target: nextNodeId,
+        source: prevNodeId,
+        targetHandle: `${nextNodeId}-input-1`,
+        sourceHandle: `${prevNodeId}-output-1`,
+      });
+    }
+
+    const inNewRow = (index + 1) % 5 === 0;
+
+    position.x = inNewRow ? 50 : position.x + 280;
+    position.y = inNewRow ? position.y + 150 : position.y;
+
+    nodes.push(node);
+  });
+
+  return {
+    edges,
+    nodes,
+  };
+}
+async function stopRecording() {
+  if (state.isGenerating) return;
+
+  try {
+    state.isGenerating = true;
+
+    if (state.flows.length !== 0) {
+      if (state.workflowId) {
+        const workflow = workflowStore.getById(state.workflowId);
+        const startBlock = workflow.drawflow.nodes.find(
+          (node) => node.id === state.connectFrom.id
+        );
+        const updatedDrawflow = generateDrawflow(state.connectFrom, startBlock);
+
+        const drawflow = {
+          ...workflow.drawflow,
+          nodes: [...workflow.drawflow.nodes, ...updatedDrawflow.nodes],
+          edges: [...workflow.drawflow.edges, ...updatedDrawflow.edges],
+        };
+
+        await workflowStore.update({
+          id: state.workflowId,
+          data: { drawflow },
+        });
+      } else {
+        const drawflow = generateDrawflow();
+
+        await workflowStore.insert({
+          drawflow,
+          name: state.name,
+        });
+      }
+    }
+
+    await browser.storage.local.remove(['isRecording', 'recording']);
+    await browser.browserAction.setBadgeText({ text: '' });
+
+    const tabs = (await browser.tabs.query({})).filter((tab) =>
+      tab.url.startsWith('http')
+    );
+    Promise.allSettled(
+      tabs.map(({ id }) =>
+        browser.tabs.sendMessage(id, { type: 'recording:stop' })
+      )
+    );
+
+    state.isGenerating = false;
+
+    router.push('/');
+  } catch (error) {
+    state.isGenerating = false;
+    console.error(error);
+  }
+}
 function removeBlock(index) {
   state.flows.splice(index, 1);
 

+ 2 - 0
src/stores/main.js

@@ -23,11 +23,13 @@ export const useStore = defineStore('main', {
         reroute_curvature_start_end: 0.5,
       },
     },
+    retrieved: true,
   }),
   actions: {
     loadSettings() {
       return browser.storage.local.get('settings').then(({ settings }) => {
         this.settings = defu(settings || {}, this.settings);
+        this.retrieved = true;
       });
     },
     updateSettings(settings = {}) {

+ 10 - 2
src/stores/workflow.js

@@ -38,7 +38,6 @@ const defaultWorkflow = (data = null) => {
     dataColumns: [],
     description: '',
     trigger: null,
-    version: '',
     createdAt: Date.now(),
     isDisabled: false,
     settings: {
@@ -55,10 +54,17 @@ const defaultWorkflow = (data = null) => {
       insertDefaultColumn: true,
       defaultColumnName: 'column',
     },
+    version: browser.runtime.getManifest().version,
     globalData: '{\n\t"key": "value"\n}',
   };
 
-  if (data) workflowData = defu(data, workflowData);
+  if (data) {
+    if (data.drawflow?.nodes?.length > 0) {
+      workflowData.drawflow.nodes = [];
+    }
+
+    workflowData = defu(data, workflowData);
+  }
 
   return workflowData;
 };
@@ -107,6 +113,8 @@ export const useWorkflowStore = defineStore('workflow', {
       this.states = Object.values(states).filter(
         ({ isDestroyed }) => !isDestroyed
       );
+
+      this.retrieved = true;
     },
     async insert(data = {}) {
       const insertedWorkflows = {};