Browse Source

feat: workflow editor in element selector

Ahmad Kholid 3 years ago
parent
commit
e7d4eade3f

+ 2 - 2
package.json

@@ -1,6 +1,6 @@
 {
   "name": "automa",
-  "version": "1.9.5",
+  "version": "1.9.6",
   "description": "An extension for automating your browser by connecting blocks",
   "license": "MIT",
   "repository": {
@@ -48,7 +48,7 @@
     "css-selector-generator": "^3.6.0",
     "dayjs": "^1.10.7",
     "defu": "^6.0.0",
-    "drawflow": "^0.0.51",
+    "drawflow": "^0.0.58",
     "idb": "^7.0.0",
     "lodash.clonedeep": "^4.5.0",
     "mitt": "^3.0.0",

+ 5 - 0
src/components/block/BlockBase.vue

@@ -9,6 +9,7 @@
     </div>
     <slot name="append" />
     <div
+      v-if="!minimap"
       class="absolute bottom-1 transition-transform duration-300 pt-4 ml-1 menu"
     >
       <div
@@ -35,6 +36,10 @@ defineProps({
     type: Boolean,
     default: false,
   },
+  minimap: {
+    type: Boolean,
+    default: false,
+  },
   hideEdit: {
     type: Boolean,
     default: false,

+ 2 - 1
src/components/block/BlockBasic.vue

@@ -3,6 +3,7 @@
     :id="componentId"
     :hide-edit="block.details.disableEdit"
     :hide-delete="block.details.disableDelete"
+    :minimap="editor.minimap"
     class="block-basic"
     @edit="editBlock"
     @delete="editor.removeNodeId(`node-${block.id}`)"
@@ -53,7 +54,7 @@
     <slot :block="block"></slot>
     <template #prepend>
       <div
-        v-if="block.details.id !== 'trigger'"
+        v-if="!editor.minimap && block.details.id !== 'trigger'"
         :title="t('workflow.blocks.base.moveToGroup')"
         draggable="true"
         class="bg-white dark:bg-gray-700 invisible move-to-group z-50 absolute -top-2 -right-2 rounded-md p-1 shadow-md"

+ 12 - 10
src/components/block/BlockConditions.vue

@@ -11,16 +11,18 @@
         <span>{{ t('workflow.blocks.conditions.name') }}</span>
       </div>
       <div class="flex-grow"></div>
-      <v-remixicon
-        name="riDeleteBin7Line"
-        class="cursor-pointer mr-2"
-        @click="editor.removeNodeId(`node-${block.id}`)"
-      />
-      <v-remixicon
-        name="riPencilLine"
-        class="inline-block cursor-pointer"
-        @click="editBlock"
-      />
+      <template v-if="!editor.minimap">
+        <v-remixicon
+          name="riDeleteBin7Line"
+          class="cursor-pointer mr-2"
+          @click="editor.removeNodeId(`node-${block.id}`)"
+        />
+        <v-remixicon
+          name="riPencilLine"
+          class="inline-block cursor-pointer"
+          @click="editBlock"
+        />
+      </template>
     </div>
     <ul
       v-if="block.data.conditions && block.data.conditions.length !== 0"

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

@@ -10,6 +10,7 @@
       </div>
       <div class="flex-grow"></div>
       <v-remixicon
+        v-if="!editor.minimap"
         name="riDeleteBin7Line"
         class="cursor-pointer"
         @click="editor.removeNodeId(`node-${block.id}`)"

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

@@ -1,6 +1,7 @@
 <template>
   <block-base
     :id="componentId"
+    :minimap="editor.minimap"
     class="element-exists"
     @edit="editBlock"
     @delete="editor.removeNodeId(`node-${block.id}`)"

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

@@ -58,7 +58,7 @@
               {{ element.data.description }}
             </p>
           </div>
-          <div class="invisible group-hover:visible">
+          <div v-if="!editor.minimap" class="invisible group-hover:visible">
             <v-remixicon
               name="riPencilLine"
               size="20"

+ 1 - 0
src/components/block/BlockLoopBreakpoint.vue

@@ -10,6 +10,7 @@
       </div>
       <div class="flex-grow"></div>
       <v-remixicon
+        v-if="!editor.minimap"
         name="riDeleteBin7Line"
         class="cursor-pointer"
         @click="editor.removeNodeId(`node-${block.id}`)"

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

@@ -10,6 +10,7 @@
       </div>
       <div class="flex-grow"></div>
       <v-remixicon
+        v-if="!editor.minimap"
         name="riDeleteBin7Line"
         class="cursor-pointer"
         @click="editor.removeNodeId(`node-${block.id}`)"

+ 5 - 0
src/components/ui/UiTab.vue

@@ -4,6 +4,7 @@
     :class="[
       uiTabs.type.value,
       {
+        'pointer-events-none opacity-75': disabled,
         small: uiTabs.small.value,
         'flex-1': uiTabs.fill.value,
         'is-active': uiTabs.modelValue.value === value,
@@ -23,6 +24,10 @@ import { inject } from 'vue';
 
 /* eslint-disable-next-line */
 const props = defineProps({
+  disabled: {
+    type: Boolean,
+    default: false,
+  },
   value: {
     type: [String, Number],
     default: '',

+ 24 - 9
src/components/ui/UiTabPanel.vue

@@ -1,17 +1,11 @@
 <template>
-  <div
-    v-if="value === uiTabPanels.modelValue.value"
-    class="ui-tab-panel"
-    :class="activeClass"
-  >
-    <slot></slot>
-  </div>
+  <render />
 </template>
 <script setup>
-import { inject } from 'vue';
+import { inject, h, useSlots } from 'vue';
 
 /* eslint-disable-next-line */
-defineProps({
+const props = defineProps({
   value: {
     type: [String, Number],
     default: '',
@@ -20,7 +14,28 @@ defineProps({
     type: String,
     default: 'ui-tab-panel--active',
   },
+  cache: Boolean,
 });
 
+const slots = useSlots();
 const uiTabPanels = inject('ui-tab-panels', {});
+
+const render = () => {
+  const isActive = props.value === uiTabPanels.modelValue.value;
+  const cache = props.cache || uiTabPanels.cache.value;
+  const component = h(
+    'div',
+    {
+      class: [props.activeClass, 'ui-tab-panel'],
+      style: {
+        display: cache && !isActive ? 'none' : null,
+      },
+    },
+    slots
+  );
+
+  if (props.cache || isActive) return component;
+
+  return null;
+};
 </script>

+ 4 - 0
src/components/ui/UiTabPanels.vue

@@ -12,6 +12,10 @@ const props = defineProps({
     type: [String, Number],
     default: '',
   },
+  cache: {
+    type: Boolean,
+    default: false,
+  },
 });
 
 provide('ui-tab-panels', toRefs(props));

+ 2 - 1
src/composable/editorBlock.js

@@ -12,7 +12,8 @@ export function useEditorBlock(selector, editor) {
   });
 
   nextTick(() => {
-    const element = document.querySelector(selector);
+    const rootElement = editor.rootElement || document;
+    const element = rootElement.querySelector(selector);
 
     if (block.id || !element) return;
 

+ 71 - 36
src/content/elementSelector/App.vue

@@ -9,8 +9,8 @@
     <div
       ref="cardEl"
       :style="{ transform: `translate(${cardRect.x}px, ${cardRect.y}px)` }"
-      style="width: 320px"
-      class="absolute root-card bg-white shadow-xl z-50 p-4 pointer-events-auto rounded-lg"
+      style="width: 320px; min-height: 175px"
+      class="absolute root-card bg-white shadow-xl z-50 pointer-events-auto rounded-lg"
     >
       <div
         class="absolute p-2 drag-button shadow-xl bg-white p-1 cursor-move rounded-lg"
@@ -21,22 +21,35 @@
           @mousedown="state.isDragging = true"
         />
       </div>
-      <div class="flex items-center">
-        <p class="ml-1 text-lg font-semibold">Automa</p>
+      <div class="flex px-4 pt-4 items-center">
+        <ui-tabs
+          v-if="false"
+          v-model="mainActiveTab"
+          type="fill"
+          class="main-tab"
+        >
+          <ui-tab value="selector"> Selector </ui-tab>
+          <ui-tab value="workflow"> Workflow </ui-tab>
+        </ui-tabs>
+        <p class="text-lg font-semibold">Automa</p>
         <div class="flex-grow"></div>
-        <ui-button icon class="mr-2" @click="state.hide = !state.hide">
+        <button
+          class="mr-1 hoverable p-1 rounded-md transition"
+          size="20"
+          @click="state.hide = !state.hide"
+        >
           <v-remixicon :name="state.hide ? 'riEyeOffLine' : 'riEyeLine'" />
-        </ui-button>
-        <ui-button icon @click="destroy">
+        </button>
+        <button
+          class="hoverable p-1 rounded-md transition"
+          size="20"
+          @click="destroy"
+        >
           <v-remixicon name="riCloseLine" />
-        </ui-button>
+        </button>
       </div>
-      <ui-tabs v-model="mainActiveTab" fill class="mt-2">
-        <ui-tab value="selector"> Selector </ui-tab>
-        <ui-tab value="workflow"> Workflow </ui-tab>
-      </ui-tabs>
       <ui-tab-panels :model-value="mainActiveTab">
-        <ui-tab-panel value="selector">
+        <ui-tab-panel value="selector" class="p-4">
           <app-selector
             v-model:selectorType="state.selectorType"
             v-model:selectList="state.selectList"
@@ -54,7 +67,6 @@
               selectElements: state.selectElements,
               selectedElements: state.selectedElements,
             }"
-            @update="updateCardSize"
             @highlight="toggleHighlightElement"
             @execute="state.isExecuting = $event"
           />
@@ -81,7 +93,7 @@
   </div>
 </template>
 <script setup>
-import { reactive, ref, watch, inject, nextTick } from 'vue';
+import { reactive, ref, watch, inject, onMounted, onBeforeUnmount } from 'vue';
 import { getCssSelector } from 'css-selector-generator';
 import { debounce } from '@/utils/helper';
 import { finder } from '@medv/finder';
@@ -104,7 +116,6 @@ const rootElement = inject('rootElement');
 const cardEl = ref('cardEl');
 const mainActiveTab = ref('selector');
 const state = reactive({
-  activeTab: '',
   elSelector: '',
   listSelector: '',
   isDragging: false,
@@ -114,6 +125,7 @@ const state = reactive({
   hoveredElements: [],
   selectorType: 'css',
   selectedElements: [],
+  activeTab: 'attributes',
   hide: window.self !== window.top,
 });
 const cardRect = reactive({
@@ -123,6 +135,13 @@ const cardRect = reactive({
   width: 0,
 });
 
+const cardElementObserver = new ResizeObserver(([entry]) => {
+  const { height, width } = entry.contentRect;
+
+  cardRect.width = width;
+  cardRect.height = height;
+});
+
 /* eslint-disable  no-use-before-define */
 const getElementSelector = (element, options = {}) => {
   if (state.selectorType === 'css') {
@@ -265,19 +284,15 @@ function getElementList(target) {
 }
 let prevHoverElement = null;
 function handleMouseMove({ clientX, clientY, target }) {
-  if (prevHoverElement === target) return;
-
-  prevHoverElement = target;
-
   if (state.isDragging) {
     const height = window.innerHeight;
     const width = document.documentElement.clientWidth;
 
-    if (clientY < 10) clientY = 0;
+    if (clientY < 10) clientY = 10;
     else if (cardRect.height + clientY > height)
       clientY = height - cardRect.height;
 
-    if (clientX < 10) clientX = 0;
+    if (clientX < 10) clientX = 10;
     else if (cardRect.width + clientX > width) clientX = width - cardRect.width;
 
     cardRect.x = clientX;
@@ -286,6 +301,9 @@ function handleMouseMove({ clientX, clientY, target }) {
     return;
   }
 
+  if (prevHoverElement === target) return;
+  prevHoverElement = target;
+
   if (state.hide || rootElement === target) return;
 
   let elementsRect = [];
@@ -427,11 +445,6 @@ function selectParentElement() {
 function handleMouseUp() {
   if (state.isDragging) state.isDragging = false;
 }
-function updateCardSize() {
-  setTimeout(() => {
-    cardRect.height = cardEl.value.getBoundingClientRect().height;
-  }, 250);
-}
 const handleScroll = debounce(() => {
   if (state.hide) return;
 
@@ -470,11 +483,22 @@ function destroy() {
 
   document.documentElement.style.fontSize = originalFontSize;
 }
+function attachListeners() {
+  cardElementObserver.observe(cardEl.value);
 
-window.addEventListener('scroll', handleScroll);
-window.addEventListener('mouseup', handleMouseUp);
-window.addEventListener('mousemove', handleMouseMove);
-document.addEventListener('click', handleClick, true);
+  window.addEventListener('scroll', handleScroll);
+  window.addEventListener('mouseup', handleMouseUp);
+  window.addEventListener('mousemove', handleMouseMove);
+  document.addEventListener('click', handleClick, true);
+}
+function detachListeners() {
+  cardElementObserver.disconnect();
+
+  window.removeEventListener('scroll', handleScroll);
+  window.removeEventListener('mouseup', handleMouseUp);
+  window.removeEventListener('mousemove', handleMouseMove);
+  document.removeEventListener('click', handleClick, true);
+}
 
 watch(
   () => state.isDragging,
@@ -482,7 +506,6 @@ watch(
     document.body.toggleAttribute('automa-isDragging', value);
   }
 );
-watch(() => [state.elSelector, state.activeTab, state.hide], updateCardSize);
 watch(
   () => state.selectList,
   (value) => {
@@ -497,7 +520,7 @@ watch(
   }
 );
 
-nextTick(() => {
+onMounted(() => {
   setTimeout(() => {
     const { height, width } = cardEl.value.getBoundingClientRect();
 
@@ -511,7 +534,12 @@ nextTick(() => {
       '16px',
       'important'
     );
-  }, 250);
+  }, 500);
+
+  attachListeners();
+});
+onBeforeUnmount(() => {
+  detachListeners();
 });
 </script>
 <style>
@@ -522,11 +550,18 @@ nextTick(() => {
   font-family: 'Inter var', sans-serif;
   font-feature-settings: 'cv02', 'cv03', 'cv04', 'cv11';
 }
+.root-card:hover .drag-button {
+  transform: scale(1);
+}
 .drag-button {
   transform: scale(0);
   transition: transform 200ms ease-in-out;
 }
-.root-card:hover .drag-button {
-  transform: scale(1);
+.main-tab {
+  background-color: transparent !important;
+  padding: 0 !important;
+}
+.main-tab .ui-tab.is-active.fill {
+  @apply bg-accent text-white !important;
 }
 </style>

+ 111 - 0
src/content/elementSelector/AppElementsDetail.vue

@@ -0,0 +1,111 @@
+<template>
+  <ui-tabs
+    :model-value="activeTab"
+    class="mt-2"
+    fill
+    @change="$emit('update:activeTab', $event)"
+  >
+    <ui-tab value="attributes"> Attributes </ui-tab>
+    <ui-tab v-if="selectElements.length > 0" value="options"> Options </ui-tab>
+    <ui-tab value="blocks"> Blocks </ui-tab>
+  </ui-tabs>
+  <ui-tab-panels
+    :model-value="activeTab"
+    class="overflow-y-auto scroll"
+    style="max-height: calc(100vh - 17rem)"
+  >
+    <ui-tab-panel value="attributes">
+      <app-element-list
+        :elements="selectedElements"
+        @highlight="$emit('highlight', $event)"
+      >
+        <template #item="{ element }">
+          <div
+            v-for="(value, name) in element.attributes"
+            :key="name"
+            class="bg-box-transparent mb-1 rounded-lg py-2 px-3"
+          >
+            <p
+              class="text-sm text-overflow leading-tight text-gray-600"
+              title="Attribute name"
+            >
+              {{ name }}
+            </p>
+            <input
+              :value="value"
+              readonly
+              title="Attribute value"
+              class="bg-transparent w-full"
+            />
+          </div>
+        </template>
+      </app-element-list>
+    </ui-tab-panel>
+    <ui-tab-panel value="options">
+      <app-element-list
+        :elements="selectElements"
+        element-name="Select element options"
+        @highlight="
+          $emit('highlight', {
+            index: $event.element.index,
+            highlight: $event.highlight,
+          })
+        "
+      >
+        <template #item="{ element }">
+          <div
+            v-for="option in element.options"
+            :key="option.name"
+            class="bg-box-transparent mb-1 rounded-lg py-2 px-3"
+          >
+            <p
+              class="text-sm text-overflow leading-tight text-gray-600"
+              title="Option name"
+            >
+              {{ option.name }}
+            </p>
+            <input
+              :value="option.value"
+              title="Option value"
+              class="text-overflow focus:ring-0 w-full bg-transparent"
+              readonly
+              @click="$event.target.select()"
+            />
+          </div>
+        </template>
+      </app-element-list>
+    </ui-tab-panel>
+    <ui-tab-panel value="blocks">
+      <app-blocks
+        :elements="selectedElements"
+        :selector="elSelector"
+        @execute="$emit('execute', $event)"
+        @update="$emit('update')"
+      />
+    </ui-tab-panel>
+  </ui-tab-panels>
+</template>
+<script setup>
+import AppBlocks from './AppBlocks.vue';
+import AppElementList from './AppElementList.vue';
+
+defineProps({
+  activeTab: {
+    type: String,
+    default: '',
+  },
+  selectElements: {
+    type: Array,
+    default: () => [],
+  },
+  selectedElements: {
+    type: Array,
+    default: () => [],
+  },
+  elSelector: {
+    type: String,
+    default: '',
+  },
+});
+defineEmits(['update:activeTab', 'execute', 'highlight']);
+</script>

+ 2 - 2
src/content/elementSelector/AppSelector.vue

@@ -1,5 +1,5 @@
 <template>
-  <div class="mt-4">
+  <div>
     <div class="flex items-center">
       <ui-select
         :model-value="selectorType"
@@ -36,7 +36,7 @@
       </ui-input>
       <template v-if="selectedCount === 1">
         <button
-          class="mr-2 ml-4"
+          class="mr-1 ml-2"
           title="Parent element"
           @click="$emit('parent')"
         >

+ 4 - 0
src/content/elementSelector/compsUi.js

@@ -1,10 +1,12 @@
 import VAutofocus from '@/directives/VAutofocus';
 import UiTab from '@/components/ui/UiTab.vue';
+import UiList from '@/components/ui/UiList.vue';
 import UiTabs from '@/components/ui/UiTabs.vue';
 import UiInput from '@/components/ui/UiInput.vue';
 import UiButton from '@/components/ui/UiButton.vue';
 import UiSelect from '@/components/ui/UiSelect.vue';
 import UiExpand from '@/components/ui/UiExpand.vue';
+import UiListItem from '@/components/ui/UiListItem.vue';
 import UiTextarea from '@/components/ui/UiTextarea.vue';
 import UiCheckbox from '@/components/ui/UiCheckbox.vue';
 import UiTabPanel from '@/components/ui/UiTabPanel.vue';
@@ -14,10 +16,12 @@ import TransitionExpand from '@/components/transitions/TransitionExpand.vue';
 export default function (app) {
   app.component('UiTab', UiTab);
   app.component('UiTabs', UiTabs);
+  app.component('UiList', UiList);
   app.component('UiInput', UiInput);
   app.component('UiButton', UiButton);
   app.component('UiSelect', UiSelect);
   app.component('UiExpand', UiExpand);
+  app.component('UiListItem', UiListItem);
   app.component('UiTextarea', UiTextarea);
   app.component('UiCheckbox', UiCheckbox);
   app.component('UiTabPanel', UiTabPanel);

+ 98 - 0
src/content/elementSelector/icons.js

@@ -1,27 +1,125 @@
 import {
   riEyeLine,
+  riAB,
+  riLink,
+  riStopLine,
+  riFlowChart,
+  riParagraph,
+  riMouseLine,
+  riEarthLine,
+  riImageLine,
+  riChat3Line,
   riCheckLine,
   riCloseLine,
+  riTimerLine,
+  riWindowLine,
+  riFocus3Line,
+  riGithubFill,
   riEyeOffLine,
+  riGlobalLine,
+  riCursorLine,
+  riWindow2Line,
+  riRepeat2Line,
+  riRefreshFill,
+  riRefreshLine,
+  riRestartLine,
+  riTwitterLine,
+  riDiscordLine,
+  riCommandLine,
+  riSearch2Line,
+  riBracketsLine,
   riFileCopyLine,
   riDragMoveLine,
+  riFileTextLine,
+  riCalendarLine,
+  riDownloadLine,
+  riLightbulbLine,
+  riFolderZipLine,
+  riClipboardLine,
+  riEqualizerLine,
+  riDatabase2Line,
   riListUnordered,
   riArrowLeftLine,
+  riCodeSSlashLine,
+  riFileUploadLine,
+  riDeleteBin7Line,
+  riTimerFlashLine,
+  riFlashlightLine,
   riArrowLeftSLine,
+  riArrowGoBackLine,
+  riCloseCircleLine,
+  riInputCursorMove,
+  riArrowUpDownLine,
   riInformationLine,
+  riFileDownloadLine,
+  riShieldKeyholeLine,
   riArrowDropDownLine,
+  riArrowLeftRightLine,
+  riArrowGoForwardLine,
+  riLightbulbFlashLine,
 } from 'v-remixicon/icons';
 
 export default {
+  riAB,
+  riLink,
   riEyeLine,
+  riStopLine,
+  riFlowChart,
+  riParagraph,
+  riMouseLine,
+  riEarthLine,
+  riImageLine,
+  riChat3Line,
   riCheckLine,
   riCloseLine,
+  riTimerLine,
+  riFocus3Line,
+  riGithubFill,
   riEyeOffLine,
+  riGlobalLine,
+  riWindowLine,
+  riCursorLine,
+  riWindow2Line,
+  riRepeat2Line,
+  riRefreshFill,
+  riRefreshLine,
+  riRestartLine,
+  riTwitterLine,
+  riDiscordLine,
+  riCommandLine,
+  riSearch2Line,
+  riBracketsLine,
   riFileCopyLine,
   riDragMoveLine,
+  riFileTextLine,
+  riCalendarLine,
+  riDownloadLine,
+  riLightbulbLine,
+  riFolderZipLine,
+  riClipboardLine,
+  riEqualizerLine,
+  riDatabase2Line,
   riListUnordered,
   riArrowLeftLine,
+  riCodeSSlashLine,
+  riFileUploadLine,
+  riDeleteBin7Line,
+  riTimerFlashLine,
+  riFlashlightLine,
   riArrowLeftSLine,
+  riArrowGoBackLine,
+  riCloseCircleLine,
+  riInputCursorMove,
+  riArrowUpDownLine,
   riInformationLine,
+  riFileDownloadLine,
+  riShieldKeyholeLine,
   riArrowDropDownLine,
+  riArrowLeftRightLine,
+  riArrowGoForwardLine,
+  riLightbulbFlashLine,
+  mdiGoogleSheet:
+    'M19,11V9H11V5H9V9H5V11H9V19H11V11H19M19,3C19.5,3 20,3.2 20.39,3.61C20.8,4 21,4.5 21,5V19C21,19.5 20.8,20 20.39,20.39C20,20.8 19.5,21 19,21H5C4.5,21 4,20.8 3.61,20.39C3.2,20 3,19.5 3,19V5C3,4.5 3.2,4 3.61,3.61C4,3.2 4.5,3 5,3H19Z',
+  mdiCursorDefaultClickOutline:
+    'M11.5,11L17.88,16.37L17,16.55L16.36,16.67C15.73,16.8 15.37,17.5 15.65,18.07L15.92,18.65L17.28,21.59L15.86,22.25L14.5,19.32L14.24,18.74C13.97,18.15 13.22,17.97 12.72,18.38L12.21,18.78L11.5,19.35V11M10.76,8.69A0.76,0.76 0 0,0 10,9.45V20.9C10,21.32 10.34,21.66 10.76,21.66C10.95,21.66 11.11,21.6 11.24,21.5L13.15,19.95L14.81,23.57C14.94,23.84 15.21,24 15.5,24C15.61,24 15.72,24 15.83,23.92L18.59,22.64C18.97,22.46 19.15,22 18.95,21.63L17.28,18L19.69,17.55C19.85,17.5 20,17.43 20.12,17.29C20.39,16.97 20.35,16.5 20,16.21L11.26,8.86L11.25,8.87C11.12,8.76 10.95,8.69 10.76,8.69M15,10V8H20V10H15M13.83,4.76L16.66,1.93L18.07,3.34L15.24,6.17L13.83,4.76M10,0H12V5H10V0M3.93,14.66L6.76,11.83L8.17,13.24L5.34,16.07L3.93,14.66M3.93,3.34L5.34,1.93L8.17,4.76L6.76,6.17L3.93,3.34M7,10H2V8H7V10',
 };

+ 3 - 0
src/content/elementSelector/workflow/WorkflowAddBlock.vue

@@ -0,0 +1,3 @@
+<template>
+  <ui-select class="w-full" placeholder="Select a block to add"> </ui-select>
+</template>

+ 148 - 0
src/content/elementSelector/workflow/WorkflowEditor.vue

@@ -0,0 +1,148 @@
+<template>
+  <workflow-list
+    v-if="!activeWorkflow"
+    :workflows="state.workflows"
+    @select="state.activeWorkflow = $event"
+  />
+  <div v-else class="mt-4 px-4 pb-4">
+    <div class="flex items-center">
+      <button class="group" @click="state.activeWorkflow = ''">
+        <v-remixicon
+          name="riArrowLeftLine"
+          class="group-hover:-translate-x-1 -ml-1 transition-transform align-bottom inline-block"
+        />
+      </button>
+      <p class="flex-1 text-overflow font-semibold ml-1">
+        {{ activeWorkflow.name }}
+      </p>
+    </div>
+    <p class="mt-2">Select a block output to start</p>
+    <div
+      ref="editorContainer"
+      class="parent-drawflow h-40 min-h w-full rounded-lg bg-box-transparent"
+    ></div>
+    <workflow-add-block v-if="activeBlock" />
+  </div>
+</template>
+<script setup>
+import {
+  shallowReactive,
+  computed,
+  watch,
+  ref,
+  getCurrentInstance,
+  shallowRef,
+  inject,
+} from 'vue';
+import browser from 'webextension-polyfill';
+import { findTriggerBlock } from '@/utils/helper';
+import drawflow from '@/lib/drawflow';
+import WorkflowList from './WorkflowList.vue';
+import WorkflowAddBlock from './WorkflowAddBlock.vue';
+
+const rootElement = inject('rootElement');
+const context = getCurrentInstance().appContext.app._context;
+
+const editor = shallowRef(null);
+const editorContainer = ref(null);
+const activeBlock = shallowRef(null);
+const state = shallowReactive({
+  workflows: [],
+  activeWorkflow: '',
+  blockOutput: 'output_1',
+});
+
+const activeWorkflow = computed(() =>
+  state.workflows.find(({ id }) => id === state.activeWorkflow)
+);
+
+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);
+    let outputEl = target.closest('.output');
+
+    if (outputEl) {
+      /* eslint-disable-next-line */
+      state.blockOutput = outputEl.classList[1];
+      outputEl.classList.add('active');
+    } else {
+      const firstOutput = Object.keys(node.outputs)[0];
+
+      state.blockOutput = firstOutput || '';
+      outputEl = nodeEl.querySelector(`.${firstOutput}`);
+    }
+
+    console.log(outputEl);
+    if (outputEl) outputEl.classList.add('active');
+
+    nodeEl.classList.add('selected');
+    activeBlock.value = node;
+    console.log(activeBlock.value);
+  }
+}
+
+watch(editorContainer, (element) => {
+  if (!activeWorkflow.value) return;
+
+  const flowData = activeWorkflow.value.drawflow;
+  const flow = typeof flowData === 'string' ? JSON.parse(flowData) : flowData;
+  const triggerBlock = findTriggerBlock(flow);
+
+  const editorInstance = drawflow(element, {
+    context,
+    options: {
+      zoom: 0.5,
+      zoom_min: 0.1,
+      zoom_max: 0.8,
+      minimap: true,
+      editor_mode: 'fixed',
+      rootElement: rootElement.shadowRoot,
+    },
+  });
+
+  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;
+  element.addEventListener('click', onEditorClick);
+});
+
+(async () => {
+  const { workflows } = await browser.storage.local.get('workflows');
+  state.workflows = (workflows || []).reverse();
+})();
+</script>
+<style>
+.output.active {
+  @apply ring-4;
+}
+</style>

+ 69 - 0
src/content/elementSelector/workflow/WorkflowList.vue

@@ -0,0 +1,69 @@
+<template>
+  <div class="pb-4 mt-4">
+    <div class="px-4">
+      <p>Select a workflow</p>
+      <ui-input
+        v-model="query"
+        prepend-icon="riSearch2Line"
+        class="w-full"
+        autocomplete="off"
+        placeholder="Search..."
+      />
+    </div>
+    <ui-list
+      class="overflow-y-auto scroll px-4 mt-4"
+      style="max-height: calc(100vh - 14rem)"
+    >
+      <ui-list-item
+        v-for="workflow in filteredWorkflows"
+        :key="workflow.id"
+        small
+        class="cursor-pointer"
+        @click="$emit('select', workflow.id)"
+      >
+        <img
+          v-if="workflow.icon?.startsWith('http')"
+          :src="workflow.icon"
+          class="overflow-hidden rounded-lg"
+          style="height: 32px; width: 32px"
+          alt="Can not display"
+        />
+        <span v-else class="p-2 rounded-lg bg-box-transparent">
+          <v-remixicon :name="workflow.icon" size="20" />
+        </span>
+        <div class="ml-2 overflow-hidden flex-1">
+          <p :title="workflow.name" class="text-overflow leading-tight">
+            {{ workflow.name }}
+          </p>
+          <p
+            :title="workflow.description"
+            class="text-overflow text-gray-600 leading-tight text-sm"
+          >
+            {{ workflow.description }}
+          </p>
+        </div>
+      </ui-list-item>
+    </ui-list>
+  </div>
+</template>
+<script setup>
+import { shallowRef, computed } from 'vue';
+
+const props = defineProps({
+  workflows: {
+    type: Array,
+    default: () => [],
+  },
+});
+defineEmits(['select']);
+
+const query = shallowRef('');
+
+const filteredWorkflows = computed(() => {
+  const search = query.value.toLocaleLowerCase();
+
+  return props.workflows.filter((workflow) =>
+    workflow.name.toLocaleLowerCase().includes(search)
+  );
+});
+</script>

+ 12 - 0
src/lib/drawflow.js

@@ -8,6 +8,18 @@ export default function (element, { context, options = {} }) {
   const editor = new Drawflow(element, { render, version: 3, h }, context);
 
   editor.useuuid = true;
+  editor.translate_to = function (x, y) {
+    this.canvas_x = x;
+    this.canvas_y = y;
+
+    const storedZoom = this.zoom;
+
+    this.zoom = 1;
+    this.precanvas.style.transform = `translate("${this.canvas_x}"px, "${this.canvas_y}"px) scale("${this.zoom}")`;
+    this.zoom = storedZoom;
+    this.zoom_last_value = 1;
+    this.zoom_refresh();
+  };
   editor.createCurvature = (
     startPosX,
     startPosY,

+ 1 - 1
src/locales/en/common.json

@@ -60,5 +60,5 @@
     "stopped": "stopped",
     "error": "error",
     "success": "success"
-  },
+  }
 }

+ 4 - 4
yarn.lock

@@ -3226,10 +3226,10 @@ dot-case@^3.0.4:
     no-case "^3.0.4"
     tslib "^2.0.3"
 
-drawflow@^0.0.51:
-  version "0.0.51"
-  resolved "https://registry.yarnpkg.com/drawflow/-/drawflow-0.0.51.tgz#094b2b679a6d7487cec1d3b404bbbcc65e5049bb"
-  integrity sha512-1MjueSSy3fYKYJVKO/lc6BJCfAMS8E3ul44QrCzw1onl2MmfgCu528mHYfAmz6gEp5FMvwBNMjOyjTXgrTT2aw==
+drawflow@^0.0.58:
+  version "0.0.58"
+  resolved "https://registry.yarnpkg.com/drawflow/-/drawflow-0.0.58.tgz#bdbb4919825d22287046f1b0437fa4847aaa96f2"
+  integrity sha512-vvtDTuSPEFf3CPk86Z89YFXNwxALfio50FM8wDL328hK7sBha1V+eQfCFn+pV2m/f90XNg6rgKJYdOQ4QlQisA==
 
 ee-first@1.1.1:
   version "1.1.1"