Browse Source

feat: package editor

Ahmad Kholid 2 years ago
parent
commit
62fe9507f0

+ 51 - 0
src/background/workflowEngine/blocksHandler/handlerBlockPackage.js

@@ -0,0 +1,51 @@
+/* eslint-disable */
+export default async function (
+  { data, id },
+  { targetHandle, sourceHandle, prevBlockData }
+) {
+  const { 1: targetId } = targetHandle.split('input-');
+  const input = data.inputs.find((input) => input.id === targetId);
+  if (!input) {
+    throw new Error('Input not found');
+  }
+
+  let block = null;
+
+  data.data.nodes.find((node) => {
+    if (node.id === input.blockId) {
+      block = node;
+    }
+
+    this.engine.blocks[node.id] = { ...node };
+  });
+
+  if (!block) {
+    throw new Error(`Can't find block for this input`);
+  }
+
+  console.log(id, data);
+  const connections = {}
+
+  // const connections = data.data.edges.reduce(
+  //   (acc, { sourceHandle, target, targetHandle }) => {
+  //     // const outputId =
+
+  //     if (!acc[sourceHandle]) acc[sourceHandle] = [];
+  //     acc[sourceHandle].push({ id: target, targetHandle, sourceHandle });
+
+  //     return acc;
+  //   },
+  //   {
+  //     [targetId]: [
+  //       { id: block.id, sourceHandle, targetId: `${block.id}-input-1` },
+  //     ],
+  //   }
+  // );
+
+  // Object.assign(this.engine.connectionsMap, connections);
+  // console.log('MAP:\t', this.engine.connectionsMap);
+  return {
+    data: prevBlockData,
+    nextBlockId: [{ id: block.id }],
+  };
+}

+ 1 - 1
src/background/workflowEngine/blocksHandler/handlerBlocksGroup.js

@@ -40,7 +40,7 @@ function blocksGroup({ data, id }, { prevBlockData }) {
 
 
     resolve({
     resolve({
       data: prevBlockData,
       data: prevBlockData,
-      nextBlockId: [data.blocks[0].itemId],
+      nextBlockId: [{ id: data.blocks[0].itemId }],
     });
     });
   });
   });
 }
 }

+ 1 - 1
src/background/workflowEngine/blocksHandler/handlerLoopBreakpoint.js

@@ -18,7 +18,7 @@ function loopBreakpoint(block, { prevBlockData }) {
     ) {
     ) {
       resolve({
       resolve({
         data: '',
         data: '',
-        nextBlockId: [currentLoop.blockId],
+        nextBlockId: [{ id: currentLoop.blockId }],
       });
       });
     } else {
     } else {
       if (currentLoop.type === 'elements') {
       if (currentLoop.type === 'elements') {

+ 9 - 6
src/background/workflowEngine/engine.js

@@ -18,6 +18,7 @@ class WorkflowEngine {
     this.workerId = 0;
     this.workerId = 0;
     this.workers = new Map();
     this.workers = new Map();
 
 
+    this.packagesCache = {};
     this.extractedGroup = {};
     this.extractedGroup = {};
     this.connectionsMap = {};
     this.connectionsMap = {};
     this.waitConnections = {};
     this.waitConnections = {};
@@ -146,13 +147,15 @@ class WorkflowEngine {
 
 
         return acc;
         return acc;
       }, {});
       }, {});
-      this.connectionsMap = edges.reduce((acc, { sourceHandle, target }) => {
-        if (!acc[sourceHandle]) acc[sourceHandle] = [];
+      this.connectionsMap = edges.reduce(
+        (acc, { sourceHandle, target, targetHandle }) => {
+          if (!acc[sourceHandle]) acc[sourceHandle] = [];
+          acc[sourceHandle].push({ id: target, targetHandle, sourceHandle });
 
 
-        acc[sourceHandle].push(target);
-
-        return acc;
-      }, {});
+          return acc;
+        },
+        {}
+      );
 
 
       const workflowTable = this.workflow.table || this.workflow.dataColumns;
       const workflowTable = this.workflow.table || this.workflow.dataColumns;
       let columns = Array.isArray(workflowTable)
       let columns = Array.isArray(workflowTable)

+ 16 - 10
src/background/workflowEngine/worker.js

@@ -38,7 +38,7 @@ class Worker {
     };
     };
   }
   }
 
 
-  init({ blockId, prevBlockData, state }) {
+  init({ blockId, execParam, state }) {
     if (state) {
     if (state) {
       Object.keys(state).forEach((key) => {
       Object.keys(state).forEach((key) => {
         this[key] = state[key];
         this[key] = state[key];
@@ -46,7 +46,7 @@ class Worker {
     }
     }
 
 
     const block = this.engine.blocks[blockId];
     const block = this.engine.blocks[blockId];
-    this.executeBlock(block, prevBlockData);
+    this.executeBlock(block, execParam);
   }
   }
 
 
   addDataToColumn(key, value) {
   addDataToColumn(key, value) {
@@ -94,9 +94,14 @@ class Worker {
   }
   }
 
 
   executeNextBlocks(connections, prevBlockData) {
   executeNextBlocks(connections, prevBlockData) {
-    connections.forEach((nodeId, index) => {
+    connections.forEach(({ id, targetHandle, sourceHandle }, index) => {
+      const execParam = { prevBlockData, targetHandle, sourceHandle };
+
       if (index === 0) {
       if (index === 0) {
-        this.executeBlock(this.engine.blocks[nodeId], prevBlockData);
+        this.executeBlock(this.engine.blocks[id], {
+          prevBlockData,
+          ...execParam,
+        });
       } else {
       } else {
         const state = structuredClone({
         const state = structuredClone({
           windowId: this.windowId,
           windowId: this.windowId,
@@ -109,14 +114,14 @@ class Worker {
 
 
         this.engine.addWorker({
         this.engine.addWorker({
           state,
           state,
-          prevBlockData,
-          blockId: nodeId,
+          execParam,
+          blockId: id,
         });
         });
       }
       }
     });
     });
   }
   }
 
 
-  async executeBlock(block, prevBlockData, isRetry) {
+  async executeBlock(block, execParam = {}, isRetry = false) {
     const currentState = await this.engine.states.get(this.engine.id);
     const currentState = await this.engine.states.get(this.engine.id);
 
 
     if (!currentState || currentState.isDestroyed) {
     if (!currentState || currentState.isDestroyed) {
@@ -149,6 +154,7 @@ class Worker {
       return;
       return;
     }
     }
 
 
+    const { prevBlockData } = execParam;
     const refData = {
     const refData = {
       prevBlockData,
       prevBlockData,
       ...this.engine.referenceData,
       ...this.engine.referenceData,
@@ -190,7 +196,7 @@ class Worker {
         result = await handler.call(this, replacedBlock, {
         result = await handler.call(this, replacedBlock, {
           refData,
           refData,
           prevBlock,
           prevBlock,
-          prevBlockData,
+          ...(execParam || {}),
         });
         });
 
 
         if (result.replacedValue) {
         if (result.replacedValue) {
@@ -216,7 +222,7 @@ class Worker {
         if (blockOnError.retry && blockOnError.retryTimes) {
         if (blockOnError.retry && blockOnError.retryTimes) {
           await sleep(blockOnError.retryInterval * 1000);
           await sleep(blockOnError.retryInterval * 1000);
           blockOnError.retryTimes -= 1;
           blockOnError.retryTimes -= 1;
-          await this.executeBlock(replacedBlock, prevBlockData, true);
+          await this.executeBlock(replacedBlock, execParam, true);
 
 
           return;
           return;
         }
         }
@@ -276,7 +282,7 @@ class Worker {
         this.reset();
         this.reset();
 
 
         const triggerBlock = this.engine.blocks[this.engine.triggerBlockId];
         const triggerBlock = this.engine.blocks[this.engine.triggerBlockId];
-        this.executeBlock(triggerBlock);
+        this.executeBlock(triggerBlock, execParam);
 
 
         localStorage.setItem(restartKey, restartCount + 1);
         localStorage.setItem(restartKey, restartCount + 1);
       } else {
       } else {

+ 147 - 0
src/components/block/BlockPackage.vue

@@ -0,0 +1,147 @@
+<template>
+  <ui-card :id="componentId" class="p-4 w-64 block-package">
+    <div class="flex items-center">
+      <img
+        v-if="data.icon.startsWith('http')"
+        :src="data.icon"
+        width="38"
+        height="38"
+        class="mr-2 rounded-lg"
+      />
+      <div
+        :class="data.disableBlock ? 'bg-box-transparent' : block.category.color"
+        class="inline-block text-sm mr-4 p-2 rounded-lg dark:text-black overflow-hidden"
+      >
+        <v-remixicon
+          v-if="!data.icon.startsWith('http')"
+          :name="data.icon"
+          size="20"
+          class="inline-block mr-1"
+        />
+        <span class="text-overflow">{{ data.name || 'Unnamed package' }}</span>
+      </div>
+      <div class="flex-grow" />
+      <v-remixicon
+        v-if="!data.isExternal"
+        title="Update package"
+        name="riRefreshLine"
+        class="cursor-pointer"
+        @click="updatePackage"
+      />
+    </div>
+    <div class="grid grid-cols-2 mt-4 gap-x-2">
+      <ul class="pkg-handle-container">
+        <li
+          v-for="input in data.inputs"
+          :key="input.id"
+          :title="input.name"
+          class="relative target"
+        >
+          <Handle
+            :id="`${id}-input-${input.id}`"
+            type="target"
+            :position="Position.Left"
+          />
+          <p class="text-overflow">{{ input.name }}</p>
+        </li>
+      </ul>
+      <ul class="pkg-handle-container">
+        <li
+          v-for="output in data.outputs"
+          :key="output.id"
+          :title="output.name"
+          class="relative source"
+        >
+          <Handle
+            :id="`${id}-output-${output.id}`"
+            type="source"
+            :position="Position.Right"
+          />
+          <p class="text-overflow">{{ output.name }}</p>
+        </li>
+      </ul>
+    </div>
+  </ui-card>
+</template>
+<script setup>
+import cloneDeep from 'lodash.clonedeep';
+import { Handle, Position } from '@braks/vue-flow';
+import { usePackageStore } from '@/stores/package';
+import { useComponentId } from '@/composable/componentId';
+import { useEditorBlock } from '@/composable/editorBlock';
+
+const props = defineProps({
+  id: {
+    type: String,
+    default: '',
+  },
+  label: {
+    type: String,
+    default: '',
+  },
+  data: {
+    type: Object,
+    default: () => ({}),
+  },
+  editor: {
+    type: Object,
+    default: null,
+  },
+});
+const emit = defineEmits(['update', 'delete']);
+
+const packageStore = usePackageStore();
+const block = useEditorBlock(props.label);
+const componentId = useComponentId('block-package');
+
+function removeConnections(type, old, newEdges) {
+  const removedEdges = [];
+  old.forEach((edge) => {
+    const isNotDeleted = newEdges.find((item) => item.id === edge.id);
+    if (isNotDeleted) return;
+
+    const handleType = type.slice(0, -1);
+
+    removedEdges.push(`${props.id}-${handleType}-${edge.id}`);
+  });
+
+  const edgesToRemove = props.editor.getEdges.value.filter(
+    ({ sourceHandle, targetHandle }) => {
+      if (type === 'outputs') {
+        return removedEdges.includes(sourceHandle);
+      }
+
+      return removedEdges.includes(targetHandle);
+    }
+  );
+
+  props.editor.removeEdges(edgesToRemove);
+}
+function updatePackage() {
+  const pkg = packageStore.getById(props.data.id);
+  if (!pkg) return;
+
+  const currentInputs = [...props.data.inputs];
+  const currentOutputs = [...props.data.outputs];
+
+  removeConnections('inputs', currentInputs, pkg.inputs);
+  removeConnections('outputs', currentOutputs, pkg.outputs);
+
+  emit('update', cloneDeep(pkg));
+}
+</script>
+<style>
+.pkg-handle-container li {
+  @apply h-8 flex items-center text-sm;
+
+  &.target .vue-flow__handle {
+    margin-left: -33px;
+  }
+  &.source {
+    @apply justify-end;
+    .vue-flow__handle {
+      margin-right: -33px;
+    }
+  }
+}
+</style>

+ 152 - 0
src/components/newtab/package/PackageSettingIOSelect.vue

@@ -0,0 +1,152 @@
+<template>
+  <ui-popover>
+    <template #trigger>
+      <ui-button class="w-full">
+        Select block {{ props.data.blockId }}
+      </ui-button>
+    </template>
+    <div class="w-64">
+      <ui-input
+        v-if="state.selectType === 'nodes'"
+        v-model="state.query"
+        placeholder="Search..."
+        class="w-full mb-4"
+      />
+      <template v-else>
+        <div
+          class="flex items-center cursor-pointer"
+          @click="state.selectType = 'nodes'"
+        >
+          <v-remixicon
+            name="riArrowLeftSLine"
+            title="Go back"
+            class="mr-1 -ml-1"
+          />
+          <span class="flex-1 text-overflow">
+            {{ getBlockName(selectedNode) }}
+          </span>
+        </div>
+        <p class="mt-2 mb-4">Select {{ type }}</p>
+      </template>
+      <ui-list class="space-y-1">
+        <ui-list-item
+          v-for="(item, index) in items"
+          :key="item.id"
+          :active="isActive(item)"
+          class="cursor-pointer"
+          @click="selectItem(item)"
+        >
+          <p class="text-overflow">
+            {{
+              state.selectType === 'nodes'
+                ? getBlockName(item, state.selectType)
+                : `${type} ${index + 1}`
+            }}
+          </p>
+        </ui-list-item>
+      </ui-list>
+      {{ data }}
+    </div>
+  </ui-popover>
+</template>
+<script setup>
+/* eslint-disable */
+import { reactive, computed, onMounted } from 'vue';
+import { tasks } from '@/utils/shared';
+
+const props = defineProps({
+  nodes: {
+    type: Array,
+    default: () => [],
+  },
+  data: {
+    type: Object,
+    default: () => ({}),
+  },
+  type: {
+    type: String,
+    default: 'inputs',
+  },
+});
+const emit = defineEmits(['update']);
+
+const handleType = props.type === 'inputs' ? 'target' : 'source';
+
+const state = reactive({
+  query: '',
+  selectType: 'nodes',
+});
+
+const includeQuery = (str) =>
+  str.toLocaleLowerCase().includes(state.query.toLocaleLowerCase());
+
+const selectedNode = computed(() =>
+  props.nodes.find((node) => node.id === props.data.blockId)
+);
+const items = computed(() => {
+  const query = state.query.toLocaleLowerCase();
+
+  if (state.selectType === 'nodes') {
+    return props.nodes.filter(({ data, label }) => {
+      let additionalKey = false;
+
+      if (data.name) additionalKey = includeQuery(data.name);
+      else if (data.description) additionalKey = includeQuery(data.description);
+
+      return includeQuery(tasks[label]?.name || '') || additionalKey;
+    });
+  }
+
+  return selectedNode.value.handleBounds[handleType];
+});
+
+function updateData(data) {
+  console.log('saveToComputer', data);
+  emit('update', data);
+}
+function selectItem(item) {
+  if (state.selectType === 'nodes') {
+    const payload = { blockId: item.id };
+
+    if (props.data.blockId && props.data.blockId !== item.id) {
+      payload.handleId = '';
+    }
+
+    updateData(payload);
+    state.selectType = 'handle';
+  } else {
+    updateData({ handleId: item.id });
+  }
+
+  console.log(item, props.data);
+}
+function getBlockName(item, type) {
+  const { label, data } = item;
+  let name = tasks[label]?.name || '';
+
+  if (data.name) name += ` (${data.name})`;
+  else if (data.description) name += ` (${data.description})`;
+
+  return name;
+}
+function isActive(item) {
+  if (state.selectType === 'nodes') {
+    return item.id === props.data.blockId;
+  }
+
+  return item.id === props.data.handleId;
+}
+
+onMounted(() => {
+  if (props.data.blockId) {
+    const blockExists = props.nodes.some(
+      (node) => node.id === props.data.blockId
+    );
+    if (blockExists) {
+      state.selectType = 'handle';
+    } else {
+      emit('update', { blockId: '', handleId: '' });
+    }
+  }
+});
+</script>

+ 219 - 0
src/components/newtab/package/PackageSettings.vue

@@ -0,0 +1,219 @@
+<template>
+  <label class="inline-flex items-center">
+    <ui-switch v-model="packageState.asBlock" />
+    <span class="ml-4">
+      {{ $t('packages.settings.asBlock') }}
+    </span>
+  </label>
+  <div v-if="packageState.asBlock" class="mt-6 pb-8 flex space-x-6">
+    <div class="flex-1">
+      <p class="font-semibold">Block inputs</p>
+      <div class="mt-4">
+        <div
+          v-if="packageState.inputs.length > 0"
+          class="grid grid-cols-12 gap-x-4"
+        >
+          <div class="col-span-5 pl-1 text-sm">Input name</div>
+          <div class="col-span-6 pl-1 text-sm">Block</div>
+        </div>
+        <draggable
+          v-model="packageState.inputs"
+          group="inputs"
+          handle=".handle"
+          item-key="id"
+        >
+          <template #item="{ element, index }">
+            <div
+              class="grid grid-cols-12 mb-2 relative gap-x-4 items-center group"
+            >
+              <span
+                class="absolute left-0 handle -ml-6 cursor-move invisible group-hover:visible"
+              >
+                <v-remixicon name="mdiDrag" />
+              </span>
+              <ui-input
+                v-model="element.name"
+                class="col-span-5"
+                :placeholder="`Input ${index + 1}`"
+              />
+              <div class="flex items-center col-span-6">
+                <ui-button
+                  v-tooltip="'Go to block'"
+                  class="mr-2"
+                  icon
+                  @click="$emit('goBlock', element.blockId)"
+                >
+                  <v-remixicon name="riFocus3Line" />
+                </ui-button>
+                <p
+                  :title="getBlockIOName('inputs', element)"
+                  class="text-overflow flex-1"
+                >
+                  {{ getBlockIOName('inputs', element) }}
+                </p>
+              </div>
+              <div class="col-span-1 text-right">
+                <v-remixicon
+                  name="riDeleteBin7Line"
+                  class="cursor-pointer text-gray-600 dark:text-gray-200 inline-block"
+                  @click="deleteBlockIo('inputs', index)"
+                />
+              </div>
+            </div>
+          </template>
+        </draggable>
+      </div>
+    </div>
+    <hr class="border-r" />
+    <div class="flex-1">
+      <p class="font-semibold">Block outputs</p>
+      <div class="mt-4">
+        <div class="grid grid-cols-12 gap-x-4">
+          <div class="col-span-5 pl-1 text-sm">Output name</div>
+          <div class="col-span-6 pl-1 text-sm">Block</div>
+        </div>
+        <draggable
+          v-model="packageState.outputs"
+          group="outputs"
+          handle=".handle"
+          item-key="id"
+        >
+          <template #item="{ element, index }">
+            <div
+              class="grid grid-cols-12 mb-2 relative gap-x-4 items-center group"
+            >
+              <span
+                class="absolute left-0 handle -ml-6 cursor-move invisible group-hover:visible"
+              >
+                <v-remixicon name="mdiDrag" />
+              </span>
+              <ui-input
+                v-model="element.name"
+                class="col-span-5"
+                :placeholder="`Output ${index + 1}`"
+              />
+              <div class="flex items-center col-span-6">
+                <ui-button
+                  v-tooltip="'Go to block'"
+                  class="mr-2"
+                  icon
+                  @click="$emit('goBlock', element.blockId)"
+                >
+                  <v-remixicon name="riFocus3Line" />
+                </ui-button>
+                <p
+                  :title="getBlockIOName('outputs', element)"
+                  class="text-overflow flex-1"
+                >
+                  {{ getBlockIOName('outputs', element) }}
+                </p>
+              </div>
+              <div class="col-span-1 text-right">
+                <v-remixicon
+                  name="riDeleteBin7Line"
+                  class="cursor-pointer text-gray-600 dark:text-gray-200 inline-block"
+                  @click="deleteBlockIo('outputs', index)"
+                />
+              </div>
+            </div>
+          </template>
+        </draggable>
+      </div>
+    </div>
+  </div>
+</template>
+<script setup>
+import { reactive, watch, onMounted } from 'vue';
+import cloneDeep from 'lodash.clonedeep';
+import Draggable from 'vuedraggable';
+import { tasks } from '@/utils/shared';
+import { debounce } from '@/utils/helper';
+
+const props = defineProps({
+  data: {
+    type: Object,
+    default: () => ({}),
+  },
+  editor: {
+    type: Object,
+    default: () => ({}),
+  },
+});
+const emit = defineEmits(['update', 'goBlock']);
+
+const state = reactive({
+  retrieved: false,
+});
+const packageState = reactive({
+  inputs: [],
+  outputs: [],
+  asBlock: false,
+});
+
+function deleteBlockIo(type, index) {
+  packageState[type].splice(index, 1);
+}
+
+const cacheIOName = new Map();
+
+function getNodeName({ label, data }) {
+  let name = tasks[label]?.name || '';
+
+  if (data.name) name += ` (${data.name})`;
+  else if (data.description) name += ` (${data.description})`;
+
+  return name;
+}
+function getBlockIOName(type, data) {
+  if (!props.editor) return '';
+
+  const cacheId = `${data.blockId}-${data.handleId}`;
+  if (cacheIOName.has(cacheId)) return cacheIOName.get(cacheId);
+
+  let name = '';
+
+  const node = props.editor.getNode.value(data.blockId);
+  if (!node) {
+    name = 'Block not found';
+  } else {
+    const nodeName = getNodeName(node);
+    const handleType = type === 'outputs' ? 'source' : 'target';
+    const index = node.handleBounds[handleType].findIndex(
+      (item) => item.id === data.handleId
+    );
+    const handleName =
+      index === -1 ? 'Not found' : `${type.slice(0, -1)} ${index + 1}`;
+
+    name = `${nodeName} > ${handleName}`;
+  }
+
+  cacheIOName.set(cacheId, name);
+
+  return name;
+}
+
+watch(
+  packageState,
+  debounce(() => {
+    if (state.retrieved) {
+      emit('update', packageState);
+    }
+  }, 500),
+  { deep: true }
+);
+
+onMounted(() => {
+  Object.assign(
+    packageState,
+    cloneDeep({
+      inputs: props.data.inputs,
+      asBlock: props.data.asBlock,
+      outputs: props.data.outputs,
+    })
+  );
+
+  setTimeout(() => {
+    state.retrieved = true;
+  }, 1000);
+});
+</script>

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

@@ -126,7 +126,10 @@ const icons = [
   'riCommandLine',
   'riCommandLine',
 ];
 ];
 
 
-const blocksArr = Object.entries({ ...tasks, ...customBlocks }).map(
+const copyTasks = { ...tasks };
+delete copyTasks['block-package'];
+
+const blocksArr = Object.entries({ ...copyTasks, ...customBlocks }).map(
   ([key, block]) => {
   ([key, block]) => {
     const localeKey = `workflow.blocks.${key}.name`;
     const localeKey = `workflow.blocks.${key}.name`;
 
 

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

@@ -47,7 +47,10 @@
     <template v-for="(node, name) in nodeTypes" :key="name" #[name]="nodeProps">
     <template v-for="(node, name) in nodeTypes" :key="name" #[name]="nodeProps">
       <component
       <component
         :is="node"
         :is="node"
-        v-bind="nodeProps"
+        v-bind="{
+          ...nodeProps,
+          editor: name === 'node-BlockPackage' ? editor : null,
+        }"
         @delete="deleteBlock"
         @delete="deleteBlock"
         @edit="editBlock(nodeProps, $event)"
         @edit="editBlock(nodeProps, $event)"
         @update="updateBlockData(nodeProps.id, $event)"
         @update="updateBlockData(nodeProps.id, $event)"

+ 8 - 4
src/components/newtab/workflow/editor/EditorLocalActions.vue

@@ -642,17 +642,21 @@ function renameWorkflow() {
 }
 }
 function deleteWorkflow() {
 function deleteWorkflow() {
   dialog.confirm({
   dialog.confirm({
-    title: t('workflow.delete'),
+    title: props.isPackage ? t('common.delete') : t('workflow.delete'),
     okVariant: 'danger',
     okVariant: 'danger',
-    body: t('message.delete', { name: props.workflow.name }),
+    body: props.isPackage
+      ? `Are you sure want to delete "${props.workflow.name}" package?`
+      : t('message.delete', { name: props.workflow.name }),
     onConfirm: async () => {
     onConfirm: async () => {
-      if (props.isTeam) {
+      if (props.isPackage) {
+        await packageStore.delete(props.workflow.id);
+      } else if (props.isTeam) {
         await teamWorkflowStore.delete(teamId, props.workflow.id);
         await teamWorkflowStore.delete(teamId, props.workflow.id);
       } else {
       } else {
         await workflowStore.delete(props.workflow.id);
         await workflowStore.delete(props.workflow.id);
       }
       }
 
 
-      router.replace('/');
+      router.replace(props.isPackage ? '/packages' : '/');
     },
     },
   });
   });
 }
 }

+ 32 - 3
src/components/newtab/workflow/editor/EditorLocalCtxMenu.vue

@@ -37,15 +37,17 @@ const props = defineProps({
     type: Object,
     type: Object,
     default: () => ({}),
     default: () => ({}),
   },
   },
+  packageIo: Boolean,
   isPackage: Boolean,
   isPackage: Boolean,
 });
 });
 const emit = defineEmits([
 const emit = defineEmits([
   'copy',
   'copy',
   'paste',
   'paste',
-  'duplicate',
   'group',
   'group',
   'ungroup',
   'ungroup',
   'saveBlock',
   'saveBlock',
+  'duplicate',
+  'packageIo',
 ]);
 ]);
 
 
 const { t } = useI18n();
 const { t } = useI18n();
@@ -107,6 +109,16 @@ const menuItems = {
     event: () => emit('duplicate', ctxData),
     event: () => emit('duplicate', ctxData),
     shortcut: getShortcut('editor:duplicate-block').readable,
     shortcut: getShortcut('editor:duplicate-block').readable,
   },
   },
+  setAsInput: {
+    id: 'setAsInput',
+    name: 'Set as block input',
+    event: () => emit('packageIo', { type: 'inputs', ...ctxData }),
+  },
+  setAsOutput: {
+    id: 'setAsOutput',
+    name: 'Set as block output',
+    event: () => emit('packageIo', { type: 'outputs', ...ctxData }),
+  },
 };
 };
 
 
 /* eslint-disable-next-line */
 /* eslint-disable-next-line */
@@ -148,12 +160,29 @@ onMounted(() => {
       items.splice(3, 0, 'group');
       items.splice(3, 0, 'group');
     }
     }
 
 
-    showCtxMenu(items, event);
-    ctxData = {
+    const currCtxData = {
       edges: [],
       edges: [],
       nodes: [node],
       nodes: [node],
       position: { clientX: event.clientX, clientY: event.clientY },
       position: { clientX: event.clientX, clientY: event.clientY },
     };
     };
+
+    if (
+      props.isPackage &&
+      props.packageIo &&
+      event.target.closest('[data-handleid]')
+    ) {
+      const { handleid, nodeid } = event.target.dataset;
+
+      currCtxData.nodeId = nodeid;
+      currCtxData.handleId = handleid;
+
+      items.unshift(
+        event.target.classList.contains('source') ? 'setAsOutput' : 'setAsInput'
+      );
+    }
+
+    showCtxMenu(items, event);
+    ctxData = currCtxData;
   });
   });
   props.editor.onEdgeContextMenu(({ event, edge }) => {
   props.editor.onEdgeContextMenu(({ event, edge }) => {
     showCtxMenu(['delete'], event);
     showCtxMenu(['delete'], event);

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

@@ -15,6 +15,9 @@
     "open": "Open packages",
     "open": "Open packages",
     "new": "New package",
     "new": "New package",
     "set": "Set as a package",
     "set": "Set as a package",
+    "settings": {
+      "asBlock": "Set package as block"
+    },
     "categories": {
     "categories": {
       "my": "My Packages",
       "my": "My Packages",
       "installed": "Installed Packages",
       "installed": "Installed Packages",

+ 89 - 27
src/newtab/pages/workflows/[id].vue

@@ -70,7 +70,10 @@
             />
             />
           </button>
           </button>
           <ui-tab value="editor">{{ t('common.editor') }}</ui-tab>
           <ui-tab value="editor">{{ t('common.editor') }}</ui-tab>
-          <ui-tab v-if="!isPackage" value="logs" class="flex items-center">
+          <ui-tab v-if="isPackage" value="package-settings">
+            {{ t('common.settings') }}
+          </ui-tab>
+          <ui-tab v-else value="logs" class="flex items-center">
             {{ t('common.log', 2) }}
             {{ t('common.log', 2) }}
             <span
             <span
               v-if="workflowStates.length > 0"
               v-if="workflowStates.length > 0"
@@ -106,11 +109,24 @@
       </div>
       </div>
       <ui-tab-panels
       <ui-tab-panels
         v-model="state.activeTab"
         v-model="state.activeTab"
-        class="overflow-hidden h-full w-full"
+        :class="{ 'overflow-hidden': state.activeTab !== 'package-settings' }"
+        class="h-full w-full"
         @drop="onDropInEditor"
         @drop="onDropInEditor"
         @dragend="clearHighlightedElements"
         @dragend="clearHighlightedElements"
         @dragover.prevent="onDragoverEditor"
         @dragover.prevent="onDragoverEditor"
       >
       >
+        <ui-tab-panel
+          v-if="isPackage"
+          value="package-settings"
+          class="mt-24 container"
+        >
+          <package-settings
+            :data="workflow"
+            :editor="editor"
+            @update="updateWorkflow"
+            @goBlock="goToPkgBlock"
+          />
+        </ui-tab-panel>
         <ui-tab-panel cache value="editor" class="w-full">
         <ui-tab-panel cache value="editor" class="w-full">
           <workflow-editor
           <workflow-editor
             v-if="state.workflowConverted"
             v-if="state.workflowConverted"
@@ -178,12 +194,14 @@
             v-if="editor"
             v-if="editor"
             :editor="editor"
             :editor="editor"
             :is-package="isPackage"
             :is-package="isPackage"
+            :package-io="workflow.asBlock"
             @group="groupBlocks"
             @group="groupBlocks"
             @ungroup="ungroupBlocks"
             @ungroup="ungroupBlocks"
             @copy="copySelectedElements"
             @copy="copySelectedElements"
             @paste="pasteCopiedElements"
             @paste="pasteCopiedElements"
             @saveBlock="initBlockFolder"
             @saveBlock="initBlockFolder"
             @duplicate="duplicateElements"
             @duplicate="duplicateElements"
+            @packageIo="addPackageIO"
           />
           />
         </ui-tab-panel>
         </ui-tab-panel>
         <ui-tab-panel value="logs" class="mt-24 container">
         <ui-tab-panel value="logs" class="mt-24 container">
@@ -287,13 +305,14 @@ import WorkflowEditBlock from '@/components/newtab/workflow/WorkflowEditBlock.vu
 import WorkflowDataTable from '@/components/newtab/workflow/WorkflowDataTable.vue';
 import WorkflowDataTable from '@/components/newtab/workflow/WorkflowDataTable.vue';
 import WorkflowGlobalData from '@/components/newtab/workflow/WorkflowGlobalData.vue';
 import WorkflowGlobalData from '@/components/newtab/workflow/WorkflowGlobalData.vue';
 import WorkflowDetailsCard from '@/components/newtab/workflow/WorkflowDetailsCard.vue';
 import WorkflowDetailsCard from '@/components/newtab/workflow/WorkflowDetailsCard.vue';
-import EditorLogs from '@/components/newtab/workflow/editor/EditorLogs.vue';
 import SharedPermissionsModal from '@/components/newtab/shared/SharedPermissionsModal.vue';
 import SharedPermissionsModal from '@/components/newtab/shared/SharedPermissionsModal.vue';
+import EditorLogs from '@/components/newtab/workflow/editor/EditorLogs.vue';
 import EditorAddPackage from '@/components/newtab/workflow/editor/EditorAddPackage.vue';
 import EditorAddPackage from '@/components/newtab/workflow/editor/EditorAddPackage.vue';
 import EditorLocalCtxMenu from '@/components/newtab/workflow/editor/EditorLocalCtxMenu.vue';
 import EditorLocalCtxMenu from '@/components/newtab/workflow/editor/EditorLocalCtxMenu.vue';
 import EditorLocalActions from '@/components/newtab/workflow/editor/EditorLocalActions.vue';
 import EditorLocalActions from '@/components/newtab/workflow/editor/EditorLocalActions.vue';
 import EditorUsedCredentials from '@/components/newtab/workflow/editor/EditorUsedCredentials.vue';
 import EditorUsedCredentials from '@/components/newtab/workflow/editor/EditorUsedCredentials.vue';
 import EditorLocalSavedBlocks from '@/components/newtab/workflow/editor/EditorLocalSavedBlocks.vue';
 import EditorLocalSavedBlocks from '@/components/newtab/workflow/editor/EditorLocalSavedBlocks.vue';
+import PackageSettings from '@/components/newtab/package/PackageSettings.vue';
 
 
 const blocks = { ...tasks, ...customBlocks };
 const blocks = { ...tasks, ...customBlocks };
 
 
@@ -622,6 +641,49 @@ const onEdgesChange = debounce((changes) => {
   // if (command) commandManager.add(command);
   // if (command) commandManager.add(command);
 }, 250);
 }, 250);
 
 
+function goToBlock(blockId) {
+  if (!editor.value) return;
+
+  const block = editor.value.getNode.value(blockId);
+  if (!block) return;
+
+  editor.value.addSelectedNodes([block]);
+  setTimeout(() => {
+    const editorContainer = document.querySelector('.vue-flow');
+    const { height, width } = editorContainer.getBoundingClientRect();
+    const { x, y } = block.position;
+
+    editor.value.setTransform({
+      y: -(y - height / 2),
+      x: -(x - width / 2) - 200,
+      zoom: 1,
+    });
+  }, 200);
+}
+function goToPkgBlock(blockId) {
+  state.activeTab = 'editor';
+  goToBlock(blockId);
+}
+function addPackageIO({ type, handleId, nodeId }) {
+  const copyPkgIO = [...workflow.value[type]];
+  const itemExist = copyPkgIO.some(
+    (io) => io.blockId === nodeId && handleId === io.handleId
+  );
+  if (itemExist) {
+    toast.error(`You already add this as an ${type.slice(0, -1)}`);
+    return;
+  }
+
+  copyPkgIO.push({
+    handleId,
+    name: '',
+    id: nanoid(),
+    blockId: nodeId,
+  });
+
+  /* eslint-disable-next-line */
+  updateWorkflow({ [type]: copyPkgIO });
+}
 function initBlockFolder({ nodes }) {
 function initBlockFolder({ nodes }) {
   Object.assign(blockFolderModal, {
   Object.assign(blockFolderModal, {
     nodes,
     nodes,
@@ -955,7 +1017,7 @@ async function updateWorkflow(data) {
   try {
   try {
     if (isPackage) {
     if (isPackage) {
       delete data.drawflow;
       delete data.drawflow;
-
+      console.log('update.....', data);
       await packageStore.update({
       await packageStore.update({
         id: workflowId,
         id: workflowId,
         data,
         data,
@@ -1029,23 +1091,7 @@ function onEditorInit(instance) {
   }, 1000);
   }, 1000);
 
 
   const { blockId } = route.query;
   const { blockId } = route.query;
-  if (blockId) {
-    const block = instance.getNode.value(blockId);
-    if (!block) return;
-
-    instance.addSelectedNodes([block]);
-    setTimeout(() => {
-      const editorContainer = document.querySelector('.vue-flow');
-      const { height, width } = editorContainer.getBoundingClientRect();
-      const { x, y } = block.position;
-
-      instance.setTransform({
-        y: -(y - height / 2),
-        x: -(x - width / 2) - 200,
-        zoom: 1,
-      });
-    }, 200);
-  }
+  if (blockId) goToBlock(blockId);
 }
 }
 function clearHighlightedElements() {
 function clearHighlightedElements() {
   const elements = document.querySelectorAll(
   const elements = document.querySelectorAll(
@@ -1085,13 +1131,29 @@ function onDragoverEditor({ target }) {
 }
 }
 function onDropInEditor({ dataTransfer, clientX, clientY, target }) {
 function onDropInEditor({ dataTransfer, clientX, clientY, target }) {
   const savedBlocks = parseJSON(dataTransfer.getData('savedBlocks'), null);
   const savedBlocks = parseJSON(dataTransfer.getData('savedBlocks'), null);
-  if (savedBlocks) {
-    const { nodes, edges } = savedBlocks.data;
-    /* eslint-disable-next-line */
-    const newElements = copyElements(nodes, edges, { clientX, clientY });
+  if (savedBlocks && !isPackage) {
+    if (savedBlocks.asBlock) {
+      const position = editor.value.project({
+        x: clientX - 360,
+        y: clientY - 18,
+      });
+      editor.value.addNodes([
+        {
+          position,
+          id: nanoid(),
+          data: savedBlocks,
+          type: 'BlockPackage',
+          label: 'block-package',
+        },
+      ]);
+    } else {
+      const { nodes, edges } = savedBlocks.data;
+      /* eslint-disable-next-line */
+      const newElements = copyElements(nodes, edges, { clientX, clientY });
 
 
-    editor.value.addNodes(newElements.nodes);
-    editor.value.addEdges(newElements.edges);
+      editor.value.addNodes(newElements.nodes);
+      editor.value.addEdges(newElements.edges);
+    }
 
 
     state.dataChanged = true;
     state.dataChanged = true;
     return;
     return;

+ 18 - 0
src/utils/shared.js

@@ -1261,6 +1261,19 @@ export const tasks = {
       dataColumn: '',
       dataColumn: '',
     },
     },
   },
   },
+  'block-package': {
+    name: 'Block package',
+    description: 'Block package',
+    icon: 'riHtml5Line',
+    editComponent: 'EditPackage',
+    component: 'BlockPackage',
+    category: 'package',
+    inputs: 1,
+    outputs: 1,
+    allowedInputs: true,
+    maxConnection: 1,
+    data: {},
+  },
   ...customBlocks,
   ...customBlocks,
 };
 };
 
 
@@ -1297,6 +1310,11 @@ export const categories = {
     border: 'border-blue-200 dark:border-blue-300',
     border: 'border-blue-200 dark:border-blue-300',
     color: 'bg-blue-200 dark:bg-blue-300 fill-blue-200 dark:fill-blue-300',
     color: 'bg-blue-200 dark:bg-blue-300 fill-blue-200 dark:fill-blue-300',
   },
   },
+  package: {
+    name: 'Packages',
+    border: 'border-cyan-200 dark:border-cyan-300',
+    color: 'bg-cyan-200 dark:bg-cyan-300 fill-cyan-200 dark:fill-cyan-300',
+  },
 };
 };
 
 
 export const tagColors = {
 export const tagColors = {