Sfoglia il codice sorgente

feat(newtab): add flow builder

Ahmad Kholid 3 anni fa
parent
commit
6f2bc3f461

+ 2 - 0
.eslintrc.js

@@ -29,6 +29,7 @@ module.exports = {
   },
   // add your custom rules here
   rules: {
+    'no-undef': 'off',
     'func-names': 'off',
     'import/extensions': [
       'error',
@@ -47,6 +48,7 @@ module.exports = {
           'state', // for vuex state
           'acc', // for reduce accumulators
           'e', // for e.returnvalue
+          'arr',
         ],
       },
     ],

+ 2 - 1
package.json

@@ -23,10 +23,11 @@
     "@medv/finder": "^2.1.0",
     "@vuex-orm/core": "^0.36.4",
     "dayjs": "^1.10.7",
+    "drawflow": "^0.0.49",
     "nanoid": "^3.1.25",
     "tiny-emitter": "^2.1.0",
     "tippy.js": "^6.3.1",
-    "v-remixicon": "^0.1.0",
+    "v-remixicon": "^0.1.1",
     "vue": "^3.2.11",
     "vue-router": "^4.0.11",
     "vuedraggable": "^4.1.0",

+ 173 - 0
src/assets/css/drawflow.css

@@ -0,0 +1,173 @@
+.drawflow-node.selected .menu {
+  @apply translate-y-16;
+}
+
+.drawflow .drawflow-node.option .outputs {
+  top: 72px;
+  transform: none;
+}
+.drawflow .drawflow-node.option .output {
+  margin-bottom: 34px;
+}
+
+.drawflow,
+.drawflow .parent-node {
+  position: relative
+}
+
+.parent-drawflow {
+  display: flex;
+  overflow: hidden;
+  touch-action: none;
+  outline: 0
+}
+
+.drawflow {
+  width: 100%;
+  height: 100%;
+  user-select: none
+}
+
+.drawflow .drawflow-node {
+  position: absolute;
+  background: white;
+  min-width: 100px;
+  min-height: 40px;
+  z-index: 2;
+  cursor: move;
+  @apply rounded-lg transition ring-2 ring-transparent duration-200 shadow-lg;
+}
+
+.drawflow .drawflow-node.selected {
+  @apply ring-accent;
+}
+
+.drawflow .drawflow-node .inputs,
+.drawflow .drawflow-node .outputs {
+  z-index: 20;
+  position: absolute;
+  top: 50%;
+  transform: translateY(-50%);
+}
+
+.drawflow .drawflow-node .inputs {
+  left: -22px;
+}
+.drawflow .drawflow-node .outputs {
+  right: -22px;
+}
+
+.drawflow .drawflow-node .drawflow_content_node {
+  width: 100%;
+  display: block
+}
+
+.drawflow .drawflow-node .input,
+.drawflow .drawflow-node .output {
+  position: relative;
+  width: 18px;
+  height: 18px;
+  background: #fff;
+  border-radius: 50%;
+  cursor: crosshair;
+  z-index: 1;
+  margin-bottom: 5px;
+  border-color: theme('colors.accent');
+  border-width: 3px;
+}
+
+.drawflow .drawflow-node .input {
+  @apply bg-accent;
+}
+
+.drawflow .icon-ui {
+  position: relative;
+}
+
+.drawflow svg:not(.v-remixicon) {
+  z-index: 0;
+  position: absolute;
+  overflow: visible !important
+}
+
+.drawflow .connection {
+  position: absolute;
+  transform: translate(9999px, 9999px)
+}
+
+.drawflow .connection .main-path {
+  fill: none;
+  stroke-width: 5px;
+  stroke: theme('colors.accent');
+  transform: translate(-9999px, -9999px)
+}
+
+.drawflow .connection .main-path:hover {
+  stroke: theme('colors.yellow.300');
+  cursor: pointer
+}
+
+.drawflow .connection .main-path.selected {
+  stroke: theme('colors.green.300');
+}
+
+.drawflow .connection .point {
+  cursor: move;
+  stroke: #000;
+  stroke-width: 2;
+  fill: #fff;
+  transform: translate(-9999px, -9999px)
+}
+
+.drawflow .connection .point.selected,
+.drawflow .connection .point:hover {
+  fill: theme('colors.blue.600');
+}
+
+.drawflow .main-path {
+  fill: none;
+  stroke-width: 5px;
+  stroke: theme('colors.accent');
+}
+
+.drawflow .selectbox {
+  z-index: 3;
+  position: absolute;
+  transform: translate(9999px, 9999px)
+}
+
+.drawflow .selectbox rect {
+  fill: #00f;
+  opacity: .5;
+  stroke: #ff0;
+  stroke-width: 5;
+  stroke-opacity: .5;
+  transform: translate(-9999px, -9999px)
+}
+
+.drawflow-delete {
+  position: absolute;
+  display: block;
+  width: 20px;
+  height: 20px;
+  @apply bg-red-500;
+  color: #fff;
+  z-index: 4;
+  line-height: 20px;
+  font-weight: 700;
+  text-align: center;
+  border-radius: 50%;
+  font-family: monospace;
+  cursor: pointer;
+  text-transform: uppercase;
+}
+
+.drawflow>.drawflow-delete {
+  margin-left: -15px;
+  margin-top: 15px
+}
+
+.parent-node .drawflow-delete {
+  right: -15px;
+  top: -15px
+}

BIN
src/assets/images/tile.png


+ 1 - 1
src/components/newtab/app/AppSidebar.vue

@@ -1,5 +1,5 @@
 <template>
-  <aside class="fixed h-screen left-0 top-0 w-16 py-5 bg-white">
+  <aside class="fixed h-screen left-0 top-0 w-16 py-5 bg-white z-50">
     <div
       class="space-y-2 relative text-center"
       @mouseleave="showHoverIndicator = false"

+ 63 - 0
src/components/newtab/workflow/WorkflowBuilder.vue

@@ -0,0 +1,63 @@
+<template>
+  <div id="drawflow" @drop="dropHandler" @dragover.prevent></div>
+</template>
+<script>
+import { onMounted, shallowRef } from 'vue';
+import drawflow from '@/lib/drawflow';
+
+export default {
+  setup() {
+    const editor = shallowRef(null);
+
+    function dropHandler({ dataTransfer, clientX, clientY }) {
+      const block = JSON.parse(dataTransfer.getData('block') || null);
+
+      if (!block) return;
+      console.log(block);
+
+      const xPosition =
+        clientX *
+          (editor.value.precanvas.clientWidth /
+            (editor.value.precanvas.clientWidth * editor.value.zoom)) -
+        editor.value.precanvas.getBoundingClientRect().x *
+          (editor.value.precanvas.clientWidth /
+            (editor.value.precanvas.clientWidth * editor.value.zoom));
+      const yPosition =
+        clientY *
+          (editor.value.precanvas.clientHeight /
+            (editor.value.precanvas.clientHeight * editor.value.zoom)) -
+        editor.value.precanvas.getBoundingClientRect().y *
+          (editor.value.precanvas.clientHeight /
+            (editor.value.precanvas.clientHeight * editor.value.zoom));
+
+      editor.value.addNode(
+        block.id,
+        1,
+        1,
+        xPosition,
+        yPosition,
+        block.id,
+        { id: block.id, name: block.name },
+        block.component,
+        'vue'
+      );
+    }
+
+    onMounted(() => {
+      const element = document.querySelector('#drawflow');
+
+      editor.value = drawflow(element);
+    });
+
+    return {
+      dropHandler,
+    };
+  },
+};
+</script>
+<style>
+#drawflow {
+  background-image: url('@/assets/images/tile.png');
+  background-size: 35px;
+}
+</style>

+ 3 - 3
src/components/newtab/workflow/WorkflowCard.vue

@@ -8,7 +8,7 @@
       <button>
         <v-remixicon name="riPlayLine" />
       </button>
-      <ui-popover v-if="showDetails" class="ml-3 h-6">
+      <ui-popover v-if="showDetails" class="ml-2 h-6">
         <template #trigger>
           <button>
             <v-remixicon name="riMoreLine" />
@@ -20,7 +20,7 @@
             class="cursor-pointer"
             @click="$emit('rename', workflow)"
           >
-            <v-remixicon name="riPencilLine" class="mr-3 -ml-1" />
+            <v-remixicon name="riPencilLine" class="mr-2 -ml-1" />
             <span>Rename</span>
           </ui-list-item>
           <ui-list-item
@@ -28,7 +28,7 @@
             class="text-red-500 cursor-pointer"
             @click="$emit('delete', workflow)"
           >
-            <v-remixicon name="riDeleteBin7Line" class="mr-3 -ml-1" />
+            <v-remixicon name="riDeleteBin7Line" class="mr-2 -ml-1" />
             <span>Delete</span>
           </ui-list-item>
         </ui-list>

+ 67 - 48
src/components/newtab/workflow/WorkflowDetailsCard.vue

@@ -1,57 +1,73 @@
 <template>
-  <ui-card class="w-80 h-full sticky top-[10px]" padding="p-0">
-    <div class="mb-4 px-4 pt-4">
+  <div
+    class="w-80 bg-white py-4 relative border-l border-gray-100 flex flex-col"
+    padding="p-0"
+  >
+    <div class="px-4 mb-2">
       <span
-        class="p-2 inline-block align-middle rounded-lg bg-box-transparent mr-2"
+        class="
+          p-2
+          inline-block
+          rounded-lg
+          bg-accent
+          text-white
+          mr-2
+          align-middle
+        "
       >
-        <v-remixicon name="riGlobalLine" />
+        <v-remixicon :name="workflow.icon" />
       </span>
-      <p class="font-semibold text-lg inline-block align-middle">
-        Workflow name
+      <p class="font-semibold inline-block text-lg flex-1 mr-4 align-middle">
+        {{ workflow.name }}
       </p>
     </div>
-    <div class="flex items-center px-4">
-      <ui-button variant="accent" class="flex-1 mr-2">
-        <v-remixicon class="-ml-1 mr-1" name="riPlayLine" />
-        Execute
-      </ui-button>
-      <ui-button icon>
-        <v-remixicon name="riDeleteBin7Line" />
-      </ui-button>
-    </div>
-    <ui-tabs v-model="state.activeTab" fill class="m-4">
-      <ui-tab value="tasks">Tasks</ui-tab>
+    <ui-tabs v-model="state.activeTab" fill class="mx-4 mb-4">
+      <ui-tab value="blocks">Blocks</ui-tab>
       <ui-tab value="data-schema">Data Columns</ui-tab>
     </ui-tabs>
+    <!-- <div class="px-4 mb-2">
+      <ui-input prepend-icon="riSearch2Line" class="w-full" placeholder="Search..." />
+    </div> -->
     <ui-tab-panels
       v-model="state.activeTab"
-      class="scroll bg-scroll overflow-auto pb-4 px-4"
-      style="max-height: calc(100vh - 240px); overflow: overlay"
+      class="scroll bg-scroll overflow-auto px-4 flex-1"
+      style="overflow: overlay"
     >
-      <ui-tab-panel value="tasks">
-        <draggable
-          :list="taskList"
-          :sort="false"
-          :group="{ name: 'tasks', pull: 'clone', put: false }"
-          item-key="id"
-          ghost-class="ghost"
-          class="grid grid-cols-2 gap-2"
-          @start="$emit('dragstart')"
-          @end="$emit('dragend')"
-        >
-          <template #item="{ element }">
+      <ui-tab-panel value="blocks">
+        <template v-for="(items, catId) in taskList" :key="catId">
+          <div class="flex items-center top-0 space-x-2 mb-2">
+            <span
+              :class="categories[catId].color"
+              class="h-3 w-3 rounded-full"
+            ></span>
+            <p class="capitalize text-gray-600">{{ categories[catId].name }}</p>
+          </div>
+          <div class="grid grid-cols-2 gap-2 mb-4">
             <div
-              :title="element.name"
-              style="cursor: grab"
-              class="select-none group p-4 rounded-lg bg-input transition"
+              v-for="task in items"
+              :key="task.id"
+              :title="task.name"
+              draggable="true"
+              class="
+                select-none
+                cursor-move
+                relative
+                p-4
+                rounded-lg
+                bg-input
+                transition
+              "
+              @dragstart="
+                $event.dataTransfer.setData('block', JSON.stringify(task))
+              "
             >
-              <v-remixicon :name="element.icon" size="24" class="mb-3" />
+              <v-remixicon :name="task.icon" size="24" class="mb-2" />
               <p class="leading-tight text-overflow">
-                {{ element.name }}
+                {{ task.name }}
               </p>
             </div>
-          </template>
-        </draggable>
+          </div>
+        </template>
       </ui-tab-panel>
       <ui-tab-panel value="data-schema" class="pt-1">
         <div class="mb-4 space-y-2">
@@ -63,7 +79,7 @@
             >
               <ui-input
                 v-model="state.dataSchema[index]"
-                class="mr-4"
+                class="mr-2"
                 placeholder="Column name"
               />
               <button @click="state.dataSchema.splice(index, 1)">
@@ -77,13 +93,11 @@
         </ui-button>
       </ui-tab-panel>
     </ui-tab-panels>
-  </ui-card>
+  </div>
 </template>
 <script setup>
-/* eslint-disable no-undef */
 import { reactive, watch } from 'vue';
-import Draggable from 'vuedraggable';
-import { tasks } from '@/utils/shared';
+import { tasks, categories } from '@/utils/shared';
 import { debounce } from '@/utils/helper';
 
 const props = defineProps({
@@ -92,15 +106,20 @@ const props = defineProps({
     default: () => ({}),
   },
 });
-const emit = defineEmits(['dragstart', 'dragend', 'update-workflow']);
+const emit = defineEmits(['update-workflow']);
+
+const taskList = Object.keys(tasks).reduce((arr, key) => {
+  const task = tasks[key];
+
+  (arr[task.category] = arr[task.category] || []).push({ id: key, ...task });
 
-const taskList = Object.keys(tasks)
-  .map((id) => ({ id, isNewTask: true, ...tasks[id] }))
-  .sort((a, b) => (a.name > b.name ? 1 : -1));
+  return arr;
+}, {});
 
 const state = reactive({
+  show: false,
   dataSchema: [],
-  activeTab: 'tasks',
+  activeTab: 'blocks',
 });
 
 watch(

+ 25 - 7
src/components/newtab/workflow/WorkflowTask.vue

@@ -39,25 +39,43 @@
       />
     </div>
     <transition-expand>
-      <template v-if="show">
-        <div class="pb-2 pr-4 pl-12 max-w-lg">
+      <div v-if="show" class="py-2 pr-4 pl-12 max-w-xl">
+        <div class="flex items-center mb-2">
           <ui-input
             :model-value="task.name"
             placeholder="Task name"
-            class="w-full"
+            class="flex-1"
             @change="updateTask({ name: $event || currentTask.task })"
           />
+          <ui-input
+            v-if="currentTask.needWebsite"
+            placeholder="Website"
+            class="flex-1 ml-2"
+          />
         </div>
-      </template>
+        <div v-if="currentTask.needSelector" class="flex items-center">
+          <ui-button icon class="mr-2">
+            <v-remixicon name="riFocus3Line" />
+          </ui-button>
+          <ui-input placeholder="Element selector" class="mr-4 flex-1" />
+          <ui-checkbox>Multiple</ui-checkbox>
+        </div>
+        <!-- <component is="TaskClickElement" /> -->
+      </div>
     </transition-expand>
   </div>
 </template>
-<script setup>
-/* eslint-disable no-undef */
-
+<script>
 import { ref, computed } from 'vue';
+import TaskClickElement from './task/TaskClickElement.vue';
+</script>
+<script setup>
 import { tasks } from '@/utils/shared';
 
+export default {
+  components: { TaskClickElement },
+};
+
 const props = defineProps({
   task: {
     type: Object,

+ 9 - 0
src/components/newtab/workflow/task/TaskClickElement.vue

@@ -0,0 +1,9 @@
+<template>
+  <div class="flex items-center">
+    <ui-button icon class="mr-2">
+      <v-remixicon name="riFocus3Line" />
+    </ui-button>
+    <ui-input placeholder="Element selector" class="mr-4 flex-1" />
+    <ui-checkbox>Multiple</ui-checkbox>
+  </div>
+</template>

+ 69 - 0
src/components/node/NodeBase.vue

@@ -0,0 +1,69 @@
+<template>
+  <div ref="rootRef" class="group relative">
+    <div
+      class="
+        z-10
+        flex
+        items-center
+        bg-white
+        relative
+        rounded-lg
+        overflow-hidden
+        p-4
+      "
+    >
+      <span class="inline-block p-2 mr-2 rounded-lg bg-green-200">
+        <v-remixicon :path="riCursorLine" />
+      </span>
+      <p>Click element</p>
+    </div>
+    <div
+      class="
+        absolute
+        top-0
+        transition-transform
+        duration-300
+        group-hover:translate-y-16
+        pt-4
+        menu
+      "
+    >
+      <div class="bg-accent px-4 py-2 text-white rounded-lg flex items-center">
+        <button class="-ml-1">
+          <v-remixicon size="20" :path="riPencilLine" />
+        </button>
+        <hr class="border-r border-gray-600 h-5 mx-3" />
+        <button class="-mr-1">
+          <v-remixicon size="20" :path="riDeleteBin7Line" />
+        </button>
+      </div>
+    </div>
+  </div>
+</template>
+<script setup>
+import { ref, nextTick } from 'vue';
+import { VRemixIcon as VRemixicon } from 'v-remixicon';
+import {
+  riCursorLine,
+  riDeleteBin7Line,
+  riPencilLine,
+} from 'v-remixicon/icons';
+
+const props = defineProps({
+  editor: {
+    type: Object,
+    default: () => ({}),
+  },
+});
+
+const rootRef = ref(null);
+const nodeId = ref('');
+
+nextTick(() => {
+  nodeId.value = rootRef.value?.parentElement.parentElement.id;
+  console.log(rootRef.value?.parentElement.parentElement);
+  if (nodeId.value) {
+    console.log(props.editor);
+  }
+});
+</script>

+ 13 - 0
src/components/node/NodeStart.vue

@@ -0,0 +1,13 @@
+<template>
+  <div class="flex items-center relative p-4 overflow-hidden rounded-lg">
+    <div class="absolute top-0 left-0 w-full h-2 bg-yellow-200"></div>
+    <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>

+ 0 - 0
src/components/node/index.js


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

@@ -55,7 +55,7 @@ export default {
       primary:
         'bg-primary text-white dark:bg-secondary dark:hover:bg-primary hover:bg-secondary',
       danger:
-        'bg-red-500 text-white dark:bg-red-600 dark:hover:bg-red-500 hover:bg-red-600',
+        'bg-red-100 text-red-500 dark:bg-red-600 dark:hover:bg-red-500 hover:bg-red-200',
     };
 
     return {

+ 89 - 0
src/components/ui/uiCheckbox.vue

@@ -0,0 +1,89 @@
+<template>
+  <label class="checkbox-ui inline-flex items-center">
+    <div
+      class="
+        relative
+        h-5
+        w-5
+        inline-block
+        focus-within:ring-2 focus-within:ring-accent
+        rounded
+      "
+    >
+      <input
+        type="checkbox"
+        class="opacity-0 checkbox-ui__input"
+        :value="modelValue"
+        v-bind="{ checked: modelValue }"
+        @change="changeHandler"
+      />
+      <div
+        class="
+          border
+          rounded
+          absolute
+          top-0
+          left-0
+          bg-input
+          checkbox-ui__mark
+          cursor-pointer
+        "
+      >
+        <v-remixicon
+          name="riCheckLine"
+          size="20"
+          class="text-white"
+        ></v-remixicon>
+      </div>
+    </div>
+    <span v-if="$slots.default" class="ml-2 inline-block">
+      <slot></slot>
+    </span>
+  </label>
+</template>
+<script>
+export default {
+  props: {
+    modelValue: {
+      type: Boolean,
+      default: false,
+    },
+  },
+  emits: ['update:modelValue', 'change'],
+  setup(props, { emit }) {
+    function changeHandler({ target: { checked } }) {
+      emit('update:modelValue', checked);
+      emit('change', checked);
+    }
+
+    return {
+      changeHandler,
+    };
+  },
+};
+</script>
+<style scoped>
+.checkbox-ui__input:checked ~ .checkbox-ui__mark .v-remixicon {
+  transform: scale(1) !important;
+}
+.checkbox-ui .v-remixicon {
+  transform: scale(0);
+}
+.checkbox-ui__input:checked ~ .checkbox-ui__mark {
+  @apply bg-accent border-accent bg-opacity-100;
+}
+.checkbox-ui__mark {
+  width: 100%;
+  height: 100%;
+  transition-property: background-color, border-color;
+  transition-timing-function: ease;
+  transition-duration: 200ms;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+.checkbox-ui__mark .v-remixicon {
+  transform: scale(0) !important;
+  transition: transform 200ms ease;
+}
+</style>

+ 47 - 0
src/lib/drawflow.js

@@ -0,0 +1,47 @@
+import { createApp, h } from 'vue';
+import Drawflow from 'drawflow';
+import '@/assets/css/drawflow.css';
+
+const nodeComponents = require.context('../components/node', false, /\.vue$/);
+
+export default function (element, ctx) {
+  const editor = new Drawflow(element, { createApp, version: 3, h }, ctx);
+
+  editor.useuuid = true;
+  editor.reroute = true;
+
+  nodeComponents.keys().forEach((key) => {
+    const name = key.replace(/(.\/)|\.vue$/g, '');
+
+    editor.registerNode(name, nodeComponents(key).default, { editor }, {});
+  });
+
+  editor.start();
+  editor.addNode(
+    'NodeStart',
+    0,
+    1,
+    150,
+    300,
+    'node-start',
+    {},
+    'NodeStart',
+    'vue'
+  );
+  editor.on('nodeCreated', (id) => {
+    const { name } = editor.getNodeFromId(id);
+
+    console.log(name, id, editor.getNodeFromId(id));
+
+    // Node.insert({
+    //   data: {
+    //     id,
+    //     data: nodeData,
+    //     storyId,
+    //     type: name,
+    //   },
+    // });
+  });
+
+  return editor;
+}

+ 10 - 0
src/lib/v-remixicon.js

@@ -32,6 +32,11 @@ import {
   riImageLine,
   riCloseLine,
   riDragDropLine,
+  riCheckLine,
+  riFocus3Line,
+  riTimerLine,
+  riLightbulbFlashLine,
+  riFlashlightLine,
 } from 'v-remixicon/icons';
 
 export const icons = {
@@ -67,6 +72,11 @@ export const icons = {
   riImageLine,
   riCloseLine,
   riDragDropLine,
+  riCheckLine,
+  riFocus3Line,
+  riTimerLine,
+  riLightbulbFlashLine,
+  riFlashlightLine,
   mdiDrag:
     'M7,19V17H9V19H7M11,19V17H13V19H11M15,19V17H17V19H15M7,15V13H9V15H7M11,15V13H13V15H11M15,15V13H17V15H15M7,11V9H9V11H7M11,11V9H13V11H11M15,11V9H17V11H15M7,7V5H9V7H7M11,7V5H13V7H11M15,7V5H17V7H15Z',
 };

+ 1 - 0
src/models/task.js

@@ -8,6 +8,7 @@ class Task extends Model {
     return {
       id: this.uid(() => nanoid()),
       name: this.string(''),
+      description: this.string(''),
       type: this.string(''),
       createdAt: this.number(),
       data: this.attr(null),

+ 1 - 1
src/newtab/App.vue

@@ -1,6 +1,6 @@
 <template>
   <app-sidebar />
-  <main class="pl-16 container mx-auto pr-2 pt-6 pb-4">
+  <main class="pl-16">
     <router-view v-if="retrieved" />
   </main>
   <ui-dialog />

+ 67 - 62
src/newtab/pages/Home.vue

@@ -1,69 +1,74 @@
 <template>
-  <h1 class="text-2xl font-semibold mb-8">Dashboard</h1>
-  <div class="flex items-start">
-    <div class="w-7/12 mr-8">
-      <div class="grid gap-3 mb-8 grid-cols-3">
-        <workflow-card
-          v-for="workflow in workflows"
-          :key="workflow.id"
-          v-bind="{ workflow }"
-          :show-details="false"
-        />
-      </div>
-      <div>
-        <div class="mb-2 flex items-center justify-between">
-          <p class="font-semibold inline-block text-lg">Logs</p>
-          <router-link to="/logs" class="text-gray-600 dark:text-gray-200">
-            View all
-          </router-link>
+  <div class="container pt-8 pb-4">
+    <h1 class="text-2xl font-semibold mb-8">Dashboard</h1>
+    <div class="flex items-start">
+      <div class="w-7/12 mr-8">
+        <div class="grid gap-4 mb-8 grid-cols-3">
+          <workflow-card
+            v-for="workflow in workflows"
+            :key="workflow.id"
+            v-bind="{ workflow }"
+            :show-details="false"
+          />
+        </div>
+        <div>
+          <div class="mb-2 flex items-center justify-between">
+            <p class="font-semibold inline-block">Logs</p>
+            <router-link
+              to="/logs"
+              class="text-gray-600 text-sm dark:text-gray-200"
+            >
+              View all
+            </router-link>
+          </div>
+          <table class="w-full table-fixed">
+            <tbody class="divide-y">
+              <tr v-for="i in 10" :key="i" class="hoverable">
+                <td class="p-2 w-6/12 text-overflow">
+                  Lorem ipsum dolor sit amet
+                </td>
+                <td class="p-2 text-gray-600 dark:text-gray-200">
+                  {{ i + 1 }} Days ago
+                </td>
+                <td class="p-2 text-right">
+                  <span
+                    class="
+                      inline-block
+                      py-1
+                      px-2
+                      text-sm text-green-700
+                      bg-green-500/10
+                      rounded-lg
+                    "
+                  >
+                    Success
+                  </span>
+                </td>
+              </tr>
+            </tbody>
+          </table>
         </div>
-        <table class="w-full table-fixed">
-          <tbody class="divide-y">
-            <tr v-for="i in 10" :key="i" class="hoverable">
-              <td class="p-2 w-6/12 text-overflow">
-                Lorem ipsum dolor sit amet
-              </td>
-              <td class="p-2 text-gray-600 dark:text-gray-200">
-                {{ i + 1 }} Days ago
-              </td>
-              <td class="p-2 text-right">
-                <span
-                  class="
-                    inline-block
-                    py-1
-                    px-2
-                    text-sm text-green-700
-                    bg-green-500/10
-                    rounded-lg
-                  "
-                >
-                  Success
-                </span>
-              </td>
-            </tr>
-          </tbody>
-        </table>
       </div>
+      <ui-card class="flex-1">
+        <!-- <p class="mb-4">Running workflow</p> -->
+        <div class="flex items-center mb-4">
+          <span class="p-2 rounded-full bg-accent text-white inline-block">
+            <v-remixicon name="riGlobalLine" />
+          </span>
+          <div class="flex-grow"></div>
+          <ui-button class="mr-3">
+            <v-remixicon class="-ml-1 mr-1" name="riPauseLine" />
+            <span>Pause</span>
+          </ui-button>
+          <ui-button variant="accent">
+            <v-remixicon class="-ml-1 mr-1" name="riStopLine" />
+            <span>Stop</span>
+          </ui-button>
+        </div>
+        <p class="mb-2 text-lg font-semibold">Workflow name</p>
+        <shared-task-list class="bg-gray-100 rounded-lg p-2" :tasks="tasks" />
+      </ui-card>
     </div>
-    <ui-card class="flex-1">
-      <!-- <p class="mb-4">Running workflow</p> -->
-      <div class="flex items-center mb-4">
-        <span class="p-2 rounded-full bg-accent text-white inline-block">
-          <v-remixicon name="riGlobalLine" />
-        </span>
-        <div class="flex-grow"></div>
-        <ui-button class="mr-3">
-          <v-remixicon class="-ml-1 mr-1" name="riPauseLine" />
-          <span>Pause</span>
-        </ui-button>
-        <ui-button variant="accent">
-          <v-remixicon class="-ml-1 mr-1" name="riStopLine" />
-          <span>Stop</span>
-        </ui-button>
-      </div>
-      <p class="mb-2 text-lg font-semibold">Workflow name</p>
-      <shared-task-list class="bg-gray-100 rounded-lg p-2" :tasks="tasks" />
-    </ui-card>
   </div>
 </template>
 <script setup>

+ 46 - 40
src/newtab/pages/Workflows.vue

@@ -1,47 +1,53 @@
 <template>
-  <h1 class="text-2xl font-semibold mb-8">Workflows</h1>
-  <div class="flex items-center mb-6 space-x-4">
-    <ui-input
-      v-model="state.query"
-      prepend-icon="riSearch2Line"
-      placeholder="Search..."
-      class="flex-1"
-    />
-    <div class="flex items-center workflow-sort">
-      <ui-button
-        icon
-        class="rounded-r-none border-gray-300 border-r"
-        @click="state.sortOrder = state.sortOrder === 'asc' ? 'desc' : 'asc'"
-      >
-        <v-remixicon
-          :name="state.sortOrder === 'asc' ? 'riSortAsc' : 'riSortDesc'"
-        />
+  <div class="container pt-8 pb-4">
+    <h1 class="text-2xl font-semibold mb-8">Workflows</h1>
+    <div class="flex items-center mb-6 space-x-4">
+      <ui-input
+        v-model="state.query"
+        prepend-icon="riSearch2Line"
+        placeholder="Search..."
+        class="flex-1"
+      />
+      <div class="flex items-center workflow-sort">
+        <ui-button
+          icon
+          class="rounded-r-none border-gray-300 border-r"
+          @click="state.sortOrder = state.sortOrder === 'asc' ? 'desc' : 'asc'"
+        >
+          <v-remixicon
+            :name="state.sortOrder === 'asc' ? 'riSortAsc' : 'riSortDesc'"
+          />
+        </ui-button>
+        <ui-select v-model="state.sortBy" placeholder="Sort by">
+          <option v-for="sort in sorts" :key="sort.id" :value="sort.id">
+            {{ sort.name }}
+          </option>
+        </ui-select>
+      </div>
+      <ui-button variant="accent" @click="newWorkflow">
+        New workflow
       </ui-button>
-      <ui-select v-model="state.sortBy" placeholder="Sort by">
-        <option v-for="sort in sorts" :key="sort.id" :value="sort.id">
-          {{ sort.name }}
-        </option>
-      </ui-select>
     </div>
-    <ui-button variant="accent" @click="newWorkflow"> New workflow </ui-button>
-  </div>
-  <div v-if="Workflow.all().length === 0" class="py-12 flex items-center">
-    <img src="@/assets/svg/alien.svg" class="w-96" />
-    <div class="ml-4">
-      <h1 class="text-2xl font-semibold max-w-md mb-6">
-        Oppss... It's looks like you don't have any workflows.
-      </h1>
-      <ui-button variant="accent" @click="newWorkflow">New workflow</ui-button>
+    <div v-if="Workflow.all().length === 0" class="py-12 flex items-center">
+      <img src="@/assets/svg/alien.svg" class="w-96" />
+      <div class="ml-4">
+        <h1 class="text-2xl font-semibold max-w-md mb-6">
+          Oppss... It's looks like you don't have any workflows.
+        </h1>
+        <ui-button variant="accent" @click="newWorkflow"
+          >New workflow</ui-button
+        >
+      </div>
+    </div>
+    <div v-else class="grid gap-4 grid-cols-5">
+      <workflow-card
+        v-for="workflow in workflows"
+        :key="workflow.id"
+        v-bind="{ workflow }"
+        @delete="deleteWorkflow"
+        @rename="renameWorkflow"
+      />
     </div>
-  </div>
-  <div v-else class="grid gap-4 grid-cols-5">
-    <workflow-card
-      v-for="workflow in workflows"
-      :key="workflow.id"
-      v-bind="{ workflow }"
-      @delete="deleteWorkflow"
-      @rename="renameWorkflow"
-    />
   </div>
 </template>
 <script setup>

+ 3 - 1
src/newtab/pages/logs.vue

@@ -1,3 +1,5 @@
 <template>
-  <p>logs.......</p>
+  <div class="container pt-8 pb-4">
+    <p>logs.......</p>
+  </div>
 </template>

+ 6 - 92
src/newtab/pages/workflows/[id].vue

@@ -1,106 +1,20 @@
 <template>
-  <div class="flex items-start">
-    <workflow-details-card
-      class="mr-6"
-      :workflow="workflow"
-      @dragstart="showEmptyState = false"
-      @dragend="showEmptyState = true"
-      @update-workflow="updateWorkflowSchema"
-    />
-    <div class="flex-1 relative">
-      <h1 class="font-semibold text-xl mb-4">Tasks</h1>
-      <div
-        v-if="tasks.length === 0 && showEmptyState"
-        class="text-center absolute w-full mt-20"
-      >
-        <div class="inline-block p-6 rounded-lg mb-4 bg-box-transparent">
-          <v-remixicon name="riDragDropLine" size="36" />
-        </div>
-        <p class="text-lg">Drag and drop tasks to here</p>
-      </div>
-      <div class="space-y-1 z-20 relative" style="min-height: 400px">
-        <draggable
-          v-model="tasks"
-          :component-data="{ name: 'list' }"
-          handle="#drag-handler"
-          item-key="id"
-          group="tasks"
-          ghost-class="ghost-task"
-          tag="transition-group"
-        >
-          <template #item="{ element }">
-            <workflow-task
-              :task="element"
-              class="list-item-transition"
-              @delete="deleteTask"
-              @update="updateTask"
-            />
-          </template>
-        </draggable>
-      </div>
-    </div>
+  <div class="flex h-screen">
+    <workflow-details-card :workflow="workflow" />
+    <workflow-builder class="flex-1" />
   </div>
 </template>
 <script setup>
-import { computed, onMounted, ref } from 'vue';
+import { computed, onMounted } from 'vue';
 import { useRoute, useRouter } from 'vue-router';
-import Draggable from 'vuedraggable';
 import Workflow from '@/models/workflow';
-import Task from '@/models/task';
+import WorkflowBuilder from '@/components/newtab/workflow/WorkflowBuilder.vue';
 import WorkflowDetailsCard from '@/components/newtab/workflow/WorkflowDetailsCard.vue';
-import WorkflowTask from '@/components/newtab/workflow/WorkflowTask.vue';
 
 const route = useRoute();
 const router = useRouter();
 
-const showEmptyState = ref(true);
-
-const workflow = computed(() => Workflow.find(route.params.id));
-const tasks = computed({
-  set(value) {
-    const newTasks = value.map((item, index) => {
-      let task = item;
-
-      if (item.isNewTask) {
-        task = {
-          name: '',
-          type: item.id,
-          createdAt: Date.now(),
-          workflowId: route.params.id,
-        };
-      }
-
-      task.order = index;
-
-      return task;
-    });
-
-    Task.insertOrUpdate({ data: newTasks });
-  },
-  get() {
-    return Task.query()
-      .where('workflowId', route.params.id)
-      .orderBy('order')
-      .get();
-  },
-});
-
-function deleteTask({ id }) {
-  Task.delete(id);
-}
-function updateTask(id, data) {
-  Task.update({
-    where: id,
-    data,
-  });
-}
-function updateWorkflowSchema(data) {
-  console.log(data, route.params.id);
-  Workflow.update({
-    where: route.params.id,
-    data: { dataSchema: data },
-  });
-}
+const workflow = computed(() => Workflow.find(route.params.id) || {});
 
 onMounted(() => {
   const isWorkflowExists = Workflow.query()

+ 52 - 10
src/utils/shared.js

@@ -1,42 +1,84 @@
 export const tasks = {
+  trigger: {
+    name: 'Trigger',
+    icon: 'riFlashlightLine',
+    component: 'task-',
+    category: 'general',
+  },
   'event-click': {
     name: 'Click element',
     icon: 'riCursorLine',
+    component: 'NodeBase',
+    category: 'interaction',
+  },
+  delay: {
+    name: 'Delay',
+    icon: 'riTimerLine',
+    component: 'NodeBase',
+    category: 'general',
   },
   'get-text': {
     name: 'Get text',
     icon: 'riParagraph',
-  },
-  'save-assets': {
-    name: 'Save assets',
-    icon: 'riImageLine',
+    component: 'NodeBase',
+    category: 'interaction',
   },
   'export-data': {
     name: 'Export data',
     icon: 'riDownloadLine',
+    component: 'task-',
+    category: 'general',
   },
   'element-scroll': {
     name: 'Scroll element',
     icon: 'riMouseLine',
+    component: 'NodeBase',
+    category: 'interaction',
+  },
+  'get-attribute': {
+    name: 'Get attribute',
+    icon: 'riBracketsLine',
+    component: 'task-',
+    category: 'interaction',
   },
   'open-website': {
     name: 'Open website',
     icon: 'riGlobalLine',
+    component: 'task-',
+    category: 'general',
   },
   'text-input': {
     name: 'Text input',
     icon: 'riInputCursorMove',
+    component: 'task-',
+    category: 'interaction',
   },
   'repeat-task': {
     name: 'Repeat tasks',
     icon: 'riRepeat2Line',
+    component: 'task-',
+    category: 'general',
   },
-  'get-attribute': {
-    name: 'Get attribute',
-    icon: 'riBracketsLine',
+  'trigger-element-events': {
+    name: 'Trigger element events',
+    icon: 'riLightbulbFlashLine',
+    component: 'task-',
+    category: 'interaction',
   },
-  'trigger-events': {
-    name: 'Trigger events',
-    icon: 'riEqualizerLine',
+};
+
+export const categories = {
+  interaction: {
+    name: 'Web interaction',
+    color: 'bg-green-200',
+  },
+  general: {
+    name: 'General',
+    color: 'bg-yellow-200',
   },
 };
+
+export const conditions = {
+  /* has attribute or attribute value/key equel to */
+  attribute: {},
+};

+ 7 - 0
tailwind.config.js

@@ -16,6 +16,13 @@ module.exports = {
         sans: ['Poppins', 'sans-serif'],
         mono: ['JetBrains Mono', 'monospace'],
       },
+      container: {
+        center: true,
+        padding: {
+          DEFAULT: '1rem',
+          sm: '2rem',
+        },
+      },
     },
   },
   variants: {

+ 9 - 4
yarn.lock

@@ -2472,6 +2472,11 @@ dot-case@^3.0.4:
     no-case "^3.0.4"
     tslib "^2.0.3"
 
+drawflow@^0.0.49:
+  version "0.0.49"
+  resolved "https://registry.yarnpkg.com/drawflow/-/drawflow-0.0.49.tgz#0dfc86d1b83757a3c6625caaa74fd4a2e399fdf0"
+  integrity sha512-oNTJ2BqUAGaM6KILWD4blpOs7q6CwFId8ZZXRqSCidyaKgDnhdkDREaPOfPaRa05TAOnGjYGde4ijZ0DpyYcIA==
+
 ee-first@1.1.1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
@@ -6524,10 +6529,10 @@ uuid@^3.3.2, uuid@^3.4.0:
   resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
   integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==
 
-v-remixicon@^0.1.0:
-  version "0.1.0"
-  resolved "https://registry.yarnpkg.com/v-remixicon/-/v-remixicon-0.1.0.tgz#3d3f4ea261138cd75d6250b30c9ae42830ee99c9"
-  integrity sha512-5tGnFPRmipEYdLY0JMRiXKE527jA3YCHfNBuHNPtPueWfMQcjqZN/AsxCONj+QlTQirQ5rfGnWKiX8sNT75PYg==
+v-remixicon@^0.1.1:
+  version "0.1.1"
+  resolved "https://registry.yarnpkg.com/v-remixicon/-/v-remixicon-0.1.1.tgz#60f58e205991371a9386ea750aa0054b6cf43df6"
+  integrity sha512-xnIZWajndAQ1WBtIFKgLYB/BKx+Rmo9304zp67q3WQMx9LNROg/k+N9gkU7r/VdNVOTHNQcs+pxmbfugUqQrPA==
 
 v8-compile-cache@^2.0.3, v8-compile-cache@^2.2.0:
   version "2.3.0"