Browse Source

feat: add "javascript code" block

Ahmad Kholid 3 years ago
parent
commit
36b6c5fe81

+ 13 - 0
src/assets/css/prism-editor.css

@@ -0,0 +1,13 @@
+.my-editor,
+.prism-editor-wrapper {
+  color: #ccc;
+  font-family: JetBrains Mono, Fira code, Fira Mono, Consolas, Menlo, Courier,
+    monospace;
+  font-size: 14px;
+  line-height: 1.5;
+  padding: 5px;
+  @apply bg-gray-900 rounded-lg;
+}
+.prism-editor__textarea:focus {
+  outline: none;
+}

+ 49 - 10
src/background/blocks-handler.js

@@ -1,6 +1,6 @@
 /* eslint-disable no-underscore-dangle */
 import browser from 'webextension-polyfill';
-import { objectHasKey, fileSaver } from '@/utils/helper';
+import { objectHasKey, fileSaver, isObject } from '@/utils/helper';
 import { tasks } from '@/utils/shared';
 import dataExporter from '@/utils/data-exporter';
 import compareBlockValue from '@/utils/compare-block-value';
@@ -26,8 +26,8 @@ function convertData(data, type) {
 
   return result;
 }
-function generateBlockError(block) {
-  const message = errorMessage('no-tab', tasks[block.name]);
+function generateBlockError(block, code) {
+  const message = errorMessage(code || 'no-tab', tasks[block.name]);
   const error = new Error(message);
   error.nextBlockId = getBlockConnection(block);
 
@@ -279,22 +279,61 @@ export function interactionHandler(block) {
       once: true,
       delay: block.name === 'link' ? 5000 : 0,
       callback: (data) => {
+        if (data?.isError) {
+          const error = new Error(data.message);
+          error.nextBlockId = nextBlockId;
+
+          reject(error);
+          return;
+        }
+
+        const getColumn = (name) =>
+          this.workflow.dataColumns.find((item) => item.name === name) || {
+            name: 'column',
+            type: 'text',
+          };
+        const pushData = (column, value) => {
+          this.data[column.name]?.push(convertData(value, column.type));
+        };
+
         if (objectHasKey(block.data, 'dataColumn')) {
-          const { name, type } = Object.values(this.workflow.dataColumns).find(
-            (item) => item.name === block.data.dataColumn
-          ) || { name: 'column', type: 'text' };
+          const column = getColumn(block.data.dataColumn);
 
           if (block.data.saveData) {
-            if (!objectHasKey(this.data, name)) this.data[name] = [];
-
             if (Array.isArray(data)) {
               data.forEach((item) => {
-                this.data[name].push(convertData(item, type));
+                pushData(column, item);
               });
             } else {
-              this.data[name].push(convertData(data, type));
+              pushData(column, data);
             }
           }
+        } else if (block.name === 'javascript-code') {
+          const memoColumn = {};
+          const pushObjectData = (obj) => {
+            Object.entries(obj).forEach(([key, value]) => {
+              let column;
+
+              if (memoColumn[key]) {
+                column = memoColumn[key];
+              } else {
+                const currentColumn = getColumn(key);
+
+                column = currentColumn;
+                memoColumn[key] = currentColumn;
+              }
+
+              pushData(column, value);
+            });
+          };
+
+          if (Array.isArray(data)) {
+            data.forEach((obj) => {
+              if (isObject(obj)) pushObjectData(obj);
+            });
+          } else if (isObject(data)) {
+            pushObjectData(data);
+          }
         }
 
         resolve({

+ 13 - 0
src/background/workflow-engine.js

@@ -118,8 +118,21 @@ class WorkflowEngine {
     browser.tabs.onUpdated.addListener(this.tabUpdatedHandler);
     browser.tabs.onRemoved.addListener(this.tabRemovedHandler);
 
+    const dataColumns = Array.isArray(this.workflow.dataColumns)
+      ? this.workflow.dataColumns
+      : Object.values(this.workflow.dataColumns);
+
     this.blocks = blocks;
     this.startedTimestamp = Date.now();
+    this.workflow.dataColumns = dataColumns;
+    this.data = dataColumns.reduce(
+      (acc, column) => {
+        acc[column.name] = [];
+
+        return acc;
+      },
+      { column: [] }
+    );
 
     workflowState
       .add(this.id, {

+ 2 - 19
src/components/newtab/logs/LogsDataViewer.vue

@@ -24,7 +24,7 @@
   </div>
   <prism-editor
     :model-value="dataStr"
-    :highlight="highlighter"
+    :highlight="highlighter('json')"
     :class="editorClass"
     readonly
     class="my-editor p-4 bg-gray-900 rounded-lg mt-4"
@@ -33,12 +33,9 @@
 <script setup>
 import { ref } from 'vue';
 import { PrismEditor } from 'vue-prism-editor';
-import { highlight, languages } from 'prismjs/components/prism-core';
+import { highlighter } from '@/lib/prism';
 import { dataExportTypes } from '@/utils/shared';
 import dataExporter, { generateJSON } from '@/utils/data-exporter';
-import 'vue-prism-editor/dist/prismeditor.min.css';
-import 'prismjs/components/prism-json';
-import 'prismjs/themes/prism-tomorrow.css';
 
 const props = defineProps({
   log: {
@@ -55,22 +52,8 @@ const data = generateJSON(Object.keys(props.log.data), props.log.data);
 const dataStr = JSON.stringify(data, null, 2);
 
 const fileName = ref(props.log.name);
-const highlighter = (code) => highlight(code, languages.json);
 
 function exportData(type) {
   dataExporter(data, { name: fileName.value, type }, true);
 }
 </script>
-<style scoped>
-.my-editor {
-  color: #ccc;
-  font-family: JetBrains Mono, Fira code, Fira Mono, Consolas, Menlo, Courier,
-    monospace;
-  font-size: 14px;
-  line-height: 1.5;
-  padding: 5px;
-}
-.prism-editor__textarea:focus {
-  outline: none;
-}
-</style>

+ 15 - 22
src/components/newtab/workflow/WorkflowEditBlock.vue

@@ -17,30 +17,23 @@
 </template>
 <script>
 import { computed } from 'vue';
-import EditForms from './edit/EditForms.vue';
-import EditTrigger from './edit/EditTrigger.vue';
-import EditGetText from './edit/EditGetText.vue';
-import EditCloseTab from './edit/EditCloseTab.vue';
-import EditTriggerEvent from './edit/EditTriggerEvent.vue';
-import EditElementExists from './edit/EditElementExists.vue';
-import EditScrollElement from './edit/EditScrollElement.vue';
-import EditAttributeValue from './edit/EditAttributeValue.vue';
-import EditTakeScreenshot from './edit/EditTakeScreenshot.vue';
-import EditInteractionBase from './edit/EditInteractionBase.vue';
+
+const editComponents = require.context(
+  './edit',
+  false,
+  /^(?:.*\/)?Edit[^/]*\.vue$/
+);
+const components = editComponents.keys().reduce((acc, key) => {
+  const name = key.replace(/(.\/)|\.vue$/g, '');
+  const componentObj = editComponents(key)?.default ?? {};
+
+  acc[name] = componentObj;
+
+  return acc;
+}, {});
 
 export default {
-  components: {
-    EditForms,
-    EditTrigger,
-    EditGetText,
-    EditCloseTab,
-    EditTriggerEvent,
-    EditElementExists,
-    EditScrollElement,
-    EditAttributeValue,
-    EditTakeScreenshot,
-    EditInteractionBase,
-  },
+  components,
 };
 </script>
 <script setup>

+ 91 - 0
src/components/newtab/workflow/edit/EditJavascriptCode.vue

@@ -0,0 +1,91 @@
+<template>
+  <div class="mb-2 mt-4">
+    <ui-textarea
+      :model-value="data.description"
+      autoresize
+      placeholder="Description"
+      class="w-full mb-2"
+      @change="updateData({ description: $event })"
+    />
+    <ui-input
+      type="number"
+      :model-value="data.timeout"
+      class="mb-2 w-full"
+      placeholder="Timeout"
+      title="Javascript code execution timeout"
+      @change="updateData({ timeout: +$event })"
+    />
+    <prism-editor
+      v-if="!showCodeModal"
+      :model-value="data.code"
+      :highlight="highlighter('javascript')"
+      readonly
+      class="p-4 max-h-80"
+      @click="showCodeModal = true"
+    />
+    <ui-modal
+      v-model="showCodeModal"
+      title="Javascript code"
+      content-class="max-w-3xl"
+    >
+      <prism-editor
+        v-model="code"
+        class="py-4"
+        :highlight="highlighter('javascript')"
+        line-numbers
+        style="height: calc(100vh - 18rem)"
+      />
+      <div>
+        Note:
+        <ol class="list-decimal pl-5">
+          <li>
+            To execute the next block, you can call the
+            <code>automaNextBlock</code> function. This function accepts one
+            parameter, which you can use to save data to the workflow. Data
+            format:
+            <ul class="list-disc space-y-2 mt-2 text-sm pl-5">
+              <li><code>{ key: value }</code></li>
+              <li>
+                <code>[{ key: value }, { key: value }]</code>
+              </li>
+            </ul>
+            You must use the column that you added as a key.
+          </li>
+          <li>
+            To reset the execution timeout of the code, you can call the
+            <code>automaResetTimeout</code> function.
+          </li>
+        </ol>
+      </div>
+    </ui-modal>
+  </div>
+</template>
+<script setup>
+import { ref, watch } from 'vue';
+import { PrismEditor } from 'vue-prism-editor';
+import { highlighter } from '@/lib/prism';
+
+const props = defineProps({
+  data: {
+    type: Object,
+    default: () => ({}),
+  },
+});
+const emit = defineEmits(['update:data']);
+
+const code = ref(props.data.code);
+const showCodeModal = ref(false);
+
+function updateData(value) {
+  emit('update:data', { ...props.data, ...value });
+}
+
+watch(code, (value) => {
+  updateData({ code: value });
+});
+</script>
+<style scoped>
+code {
+  @apply bg-gray-900 text-sm text-white p-1 rounded-md;
+}
+</style>

+ 5 - 1
src/components/ui/UiModal.vue

@@ -31,7 +31,7 @@
             <div class="mb-4">
               <div class="flex items-center justify-between">
                 <span class="content-header">
-                  <slot name="header"></slot>
+                  <slot name="header">{{ title }}</slot>
                 </span>
                 <v-remixicon
                   v-show="!persist"
@@ -66,6 +66,10 @@ export default {
       type: String,
       default: 'max-w-lg',
     },
+    title: {
+      type: String,
+      default: '',
+    },
     customContent: Boolean,
     persist: Boolean,
     blur: Boolean,

+ 42 - 0
src/content/blocks-handler.js

@@ -74,6 +74,48 @@ function incScrollPos(element, data, vertical = true) {
 
   return currentPos;
 }
+
+const automaScript = `
+function automaNextBlock(data) {
+  window.dispatchEvent(new CustomEvent('__automa-next-block__', { detail: data }));
+}
+function automaResetTimeout() {
+ window.dispatchEvent(new CustomEvent('__automa-reset-timeout__'));
+}
+`;
+
+export function javascriptCode(block) {
+  return new Promise((resolve) => {
+    const isScriptExists = document.getElementById('automa-custom-js');
+
+    if (isScriptExists) isScriptExists.remove();
+
+    const script = document.createElement('script');
+    let timeout;
+
+    script.id = 'automa-custom-js';
+    script.innerHTML = `${automaScript} ${block.data.code}`;
+
+    window.addEventListener('__automa-next-block__', ({ detail }) => {
+      clearTimeout(timeout);
+      resolve(detail || {});
+    });
+    window.addEventListener('__automa-reset-timeout__', () => {
+      clearTimeout(timeout);
+
+      timeout = setTimeout(() => {
+        resolve('');
+      }, block.data.timeout);
+    });
+
+    document.body.appendChild(script);
+
+    timeout = setTimeout(() => {
+      resolve('');
+    }, block.data.timeout);
+  });
+}
+
 export function elementScroll(block) {
   return new Promise((resolve) => {
     const { data } = block;

+ 10 - 3
src/content/index.js

@@ -9,9 +9,16 @@ function onConnectListener() {
       const handler = blocksHandler[toCamelCase(data.name)];
 
       if (handler) {
-        handler(data).then((result) => {
-          port.postMessage({ type: data.name, data: result });
-        });
+        handler(data)
+          .then((result) => {
+            port.postMessage({ type: data.name, data: result });
+          })
+          .catch((error) => {
+            port.postMessage({
+              isError: true,
+              message: error?.message || error,
+            });
+          });
       } else {
         console.error(`"${data.name}" doesn't have a handler`);
       }

+ 13 - 0
src/lib/prism.js

@@ -0,0 +1,13 @@
+import { highlight, languages } from 'prismjs/components/prism-core';
+import 'vue-prism-editor/dist/prismeditor.min.css';
+import 'prismjs/components/prism-clike';
+import 'prismjs/components/prism-javascript';
+import 'prismjs/components/prism-json';
+import 'prismjs/themes/prism-tomorrow.css';
+import '@/assets/css/prism-editor.css';
+
+export function highlighter(language) {
+  return function (code) {
+    return highlight(code, languages[language]);
+  };
+}

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

@@ -2,6 +2,7 @@ import vRemixicon from 'v-remixicon';
 import {
   riHome5Line,
   riGithubFill,
+  riCodeSSlashLine,
   riRecordCircleLine,
   riErrorWarningLine,
   riCalendarLine,
@@ -66,6 +67,7 @@ import {
 export const icons = {
   riHome5Line,
   riGithubFill,
+  riCodeSSlashLine,
   riRecordCircleLine,
   riErrorWarningLine,
   riCalendarLine,

+ 1 - 0
src/newtab/App.vue

@@ -6,6 +6,7 @@
   <ui-dialog />
 </template>
 <script setup>
+/* to-do add documentation of the extension */
 import { ref } from 'vue';
 import { useStore } from 'vuex';
 import browser from 'webextension-polyfill';

+ 1 - 1
src/utils/helper.js

@@ -26,7 +26,7 @@ export function toCamelCase(str) {
 }
 
 export function isObject(obj) {
-  return typeof obj === 'object' && object !== null;
+  return typeof obj === 'object' && obj !== null;
 }
 
 export function objectHasKey(obj, key) {

+ 19 - 18
src/utils/reference-data.js

@@ -1,5 +1,5 @@
 import objectPath from 'object-path';
-import { isObject } from '@/utils/helper';
+import { isObject, objectHasKey } from '@/utils/helper';
 
 function parseKey(key) {
   const [dataKey, path] = key.split('@');
@@ -31,24 +31,25 @@ export default function (block, data) {
   const replaceKeys = ['url', 'fileName', 'name', 'value'];
 
   replaceKeys.forEach((blockDataKey) => {
-    const blockDataValue = objectPath.get(block, `data.${blockDataKey}`);
-
-    if (!blockDataValue) return;
-
-    const newDataValue = blockDataValue.replace(/\[(.+?)]/g, (match) => {
-      const key = match.replace(/\[|]/g, '');
-      const { dataKey, path } = parseKey(key);
-
-      if (
-        dataKey === 'prevBlockData' &&
-        (!isObject(data.prevBlockData) || !Array.isArray(data.prevBlockData))
-      ) {
-        return data.prevBlockData;
+    if (!objectHasKey(block.data, blockDataKey)) return;
+
+    const newDataValue = block.data[blockDataKey].replace(
+      /\[(.+?)]/g,
+      (match) => {
+        const key = match.replace(/\[|]/g, '');
+        const { dataKey, path } = parseKey(key);
+
+        if (
+          dataKey === 'prevBlockData' &&
+          (!isObject(data.prevBlockData) || !Array.isArray(data.prevBlockData))
+        ) {
+          return data.prevBlockData;
+        }
+
+        return objectPath.get(data, `${dataKey}.${path}`) || match;
       }
+    );
 
-      return objectPath.get(data, `${dataKey}.${path}`) || match;
-    });
-
-    objectPath.set(block, `data.${blockDataKey}`, newDataValue);
+    block.data[blockDataKey] = newDataValue;
   });
 }

+ 17 - 0
src/utils/shared.js

@@ -293,6 +293,23 @@ export const tasks = {
   //   disableEdit: true,
   //   data: {},
   // },
+  'javascript-code': {
+    name: 'JavaScript code',
+    description: 'Execute your custom javascript code in a webpage',
+    icon: 'riCodeSSlashLine',
+    component: 'BlockBasic',
+    editComponent: 'EditJavascriptCode',
+    category: 'interaction',
+    inputs: 1,
+    outputs: 1,
+    allowedInputs: true,
+    maxConnection: 1,
+    data: {
+      description: '',
+      timeout: 10000,
+      code: 'console.log("Hello world!")',
+    },
+  },
   'trigger-event': {
     name: 'Trigger event',
     description: 'Trigger event',