ソースを参照

feat: add `code` in conditions builder

Ahmad Kholid 3 年 前
コミット
d0c5f14aaf

+ 2 - 2
src/background/workflowEngine/worker.js

@@ -218,7 +218,7 @@ class Worker {
           block,
           blockOnError.toDo === 'continue' ? 1 : 2
         );
-        if (blockOnError.toDo !== 'error' && nextBlocks.connections) {
+        if (blockOnError.toDo !== 'error' && nextBlocks?.connections) {
           addBlockLog('error', {
             message: error.message,
             ...(error.data || {}),
@@ -236,7 +236,7 @@ class Worker {
       });
 
       const { onError } = this.settings;
-      const nodeConnections = error.nextBlockId.connections;
+      const nodeConnections = error.nextBlockId?.connections;
 
       if (onError === 'keep-running' && nodeConnections) {
         setTimeout(() => {

+ 1 - 1
src/components/content/selector/SelectorElementsDetail.vue

@@ -107,5 +107,5 @@ defineProps({
     default: '',
   },
 });
-defineEmits(['update:activeTab', 'execute', 'highlight']);
+defineEmits(['update:activeTab', 'execute', 'highlight', 'update']);
 </script>

+ 42 - 15
src/components/newtab/shared/SharedConditionBuilder/ConditionBuilderInputs.vue

@@ -2,7 +2,7 @@
   <div
     v-for="(item, index) in inputsData"
     :key="item.id"
-    class="condition-input"
+    class="condition-input scroll"
   >
     <div
       v-if="item.category === 'value'"
@@ -10,6 +10,7 @@
     >
       <ui-select
         :model-value="item.type"
+        class="flex-shrink-0"
         @change="updateValueType($event, index)"
       >
         <optgroup
@@ -22,19 +23,33 @@
           </option>
         </optgroup>
       </ui-select>
-      <edit-autocomplete
-        v-for="(_, name) in item.data"
-        :key="item.id + name + index"
-        class="flex-1"
-      >
-        <ui-input
-          v-model="inputsData[index].data[name]"
-          :title="conditionBuilder.inputTypes[name].label"
-          :placeholder="conditionBuilder.inputTypes[name].label"
-          autocomplete="off"
-          class="w-full"
+      <template v-for="(_, name) in item.data" :key="item.id + name + index">
+        <v-remixicon
+          v-if="name === 'code'"
+          :title="t('workflow.conditionBuilder.topAwait')"
+          name="riInformationLine"
         />
-      </edit-autocomplete>
+        <edit-autocomplete
+          :disabled="name === 'code'"
+          :class="[name === 'code' ? 'w-full' : 'flex-1']"
+          :style="{ marginLeft: name === 'code' ? 0 : null }"
+        >
+          <shared-codemirror
+            v-if="name === 'code'"
+            v-model="inputsData[index].data[name]"
+            class="code-condition mt-2"
+            style="margin-left: 0"
+          />
+          <ui-input
+            v-else
+            v-model="inputsData[index].data[name]"
+            :title="conditionBuilder.inputTypes[name].label"
+            :placeholder="conditionBuilder.inputTypes[name].label"
+            autocomplete="off"
+            class="w-full"
+          />
+        </edit-autocomplete>
+      </template>
     </div>
     <ui-select
       v-else-if="item.category === 'compare'"
@@ -52,11 +67,17 @@
   </div>
 </template>
 <script setup>
-import { ref, watch } from 'vue';
+import { ref, watch, defineAsyncComponent } from 'vue';
 import { nanoid } from 'nanoid';
+import { useI18n } from 'vue-i18n';
+import cloneDeep from 'lodash.clonedeep';
 import { conditionBuilder } from '@/utils/shared';
 import EditAutocomplete from '../../workflow/edit/EditAutocomplete.vue';
 
+const SharedCodemirror = defineAsyncComponent(() =>
+  import('../SharedCodemirror.vue')
+);
+
 const props = defineProps({
   data: {
     type: Array,
@@ -69,7 +90,8 @@ const props = defineProps({
 });
 const emit = defineEmits(['update']);
 
-const inputsData = ref(JSON.parse(JSON.stringify(props.data)));
+const { t } = useI18n();
+const inputsData = ref(cloneDeep(props.data));
 
 function getDefaultValues(items) {
   const defaultValues = {
@@ -131,3 +153,8 @@ watch(
   { deep: true }
 );
 </script>
+<style>
+.code-condition .cm-content {
+  white-space: pre-wrap;
+}
+</style>

+ 2 - 0
src/components/newtab/shared/SharedConditionBuilder/index.vue

@@ -132,6 +132,8 @@ function getConditionText({ category, type, data }) {
 
   if (type === 'value') {
     text = data.value || 'Empty';
+  } else if (type === 'code') {
+    text = 'JS Code';
   } else if (type.startsWith('element')) {
     text = type;
 

+ 1 - 1
src/components/newtab/workflow/WorkflowSettings.vue

@@ -156,7 +156,7 @@ const props = defineProps({
     default: () => ({}),
   },
 });
-const emit = defineEmits(['update']);
+const emit = defineEmits(['update', 'close']);
 
 const { t } = useI18n();
 const toast = useToast();

+ 5 - 0
src/components/newtab/workflow/edit/EditAutocomplete.vue

@@ -1,5 +1,6 @@
 <template>
   <ui-autocomplete
+    :disabled="disabled"
     :items="autocompleteList"
     :trigger-char="['{{', '}}']"
     :custom-filter="autocompleteFilter"
@@ -14,6 +15,10 @@
 import { inject, shallowReactive, computed } from 'vue';
 import objectPath from 'object-path';
 
+defineProps({
+  disabled: Boolean,
+});
+
 const autocompleteData = inject('autocompleteData', {});
 const state = shallowReactive({
   path: '',

+ 2 - 30
src/content/blocksHandler/handlerJavascriptCode.js

@@ -1,4 +1,5 @@
 import { sendMessage } from '@/utils/message';
+import { automaRefDataStr } from '../utils';
 
 function getAutomaScript(blockId, everyNewTab) {
   const str = `
@@ -16,36 +17,7 @@ function automaNextBlock(data, insert = true) {
 function automaResetTimeout() {
  window.dispatchEvent(new CustomEvent('__automa-reset-timeout__'));
 }
-function findData(obj, path) {
-  const paths = path.split('.');
-  const isWhitespace = paths.length === 1 && !/\\S/.test(paths[0]);
-
-  if (path.startsWith('$last') && Array.isArray(obj)) {
-    paths[0] = obj.length - 1;
-  }
-
-  if (paths.length === 0 || isWhitespace) return obj;
-  else if (paths.length === 1) return obj[paths[0]];
-
-  let result = obj;
-
-  for (let i = 0; i < paths.length; i++) {
-    if (result[paths[i]] == undefined) {
-      return undefined;
-    } else {
-      result = result[paths[i]];
-    }
-  }
-
-  return result;
-}
-function automaRefData(keyword, path = '') {
-  const data = JSON.parse(sessionStorage.getItem('automa--${blockId}')) || null;
-
-  if (data === null) return null;
-
-  return findData(data[keyword], path);
-}
+${automaRefDataStr(blockId)}
   `;
 
   if (everyNewTab) return '';

+ 11 - 15
src/content/handleSelector.js

@@ -77,19 +77,17 @@ export default async function (
       return null;
     }
 
-    if (data.multiple) {
-      await Promise.allSettled(
-        Array.from(elements).map((el) => {
-          markElement(el, { id, data });
-          if (debugMode) scrollIfNeeded(el);
-          return onSelected(el);
-        })
-      );
-    } else if (elements) {
-      markElement(elements, { id, data });
-      if (debugMode) scrollIfNeeded(elements);
-      await onSelected(elements);
-    }
+    const elementsArr = data.multiple ? Array.from(elements) : [elements];
+
+    await Promise.allSettled(
+      elementsArr.map(async (el) => {
+        markElement(el, { id, data });
+
+        if (debugMode) scrollIfNeeded(el);
+
+        if (onSelected) await onSelected(el);
+      })
+    );
 
     if (onSuccess) onSuccess();
 
@@ -97,6 +95,4 @@ export default async function (
   } catch (error) {
     console.error(error);
   }
-
-  return elements;
 }

+ 94 - 0
src/content/handleTestCondition.js

@@ -0,0 +1,94 @@
+import { nanoid } from 'nanoid';
+import FindElement from '@/utils/FindElement';
+import { automaRefDataStr } from './utils';
+
+function handleConditionElement({ data, type }) {
+  const selectorType = data.selector.startsWith('/') ? 'xpath' : 'cssSelector';
+
+  const element = FindElement[selectorType](data);
+  const { 1: actionType } = type.split('#');
+
+  if (!element) {
+    if (actionType === 'visible' || actionType === 'invisible') return false;
+
+    return null;
+  }
+
+  const elementActions = {
+    text: () => element.innerText,
+    visible: () => {
+      const { visibility, display } = getComputedStyle(element);
+
+      return visibility !== 'hidden' && display !== 'none';
+    },
+    invisible: () => {
+      const { visibility, display } = getComputedStyle(element);
+
+      return visibility === 'hidden' || display === 'none';
+    },
+    attribute: ({ attrName }) => {
+      if (!element.hasAttribute(attrName)) return null;
+
+      return element.getAttribute(attrName);
+    },
+  };
+
+  return elementActions[actionType](data);
+}
+function injectJsCode({ data, refData }) {
+  return new Promise((resolve, reject) => {
+    const stateId = nanoid();
+
+    sessionStorage.setItem(`automa--${stateId}`, JSON.stringify(refData));
+
+    const scriptEl = document.createElement('script');
+    scriptEl.textContent = `
+      (async () => {
+        ${automaRefDataStr(stateId)}
+        try {
+          ${data.code}
+        } catch (error) {
+          return {
+            $isError: true,
+            message: error.message,
+          }
+        }
+      })()
+        .then((detail) => {
+          window.dispatchEvent(new CustomEvent('__automa-condition-code__', { detail }));
+        });
+    `;
+
+    document.body.appendChild(scriptEl);
+
+    const handleAutomaEvent = ({ detail }) => {
+      scriptEl.remove();
+      window.removeEventListener(
+        '__automa-condition-code__',
+        handleAutomaEvent
+      );
+
+      if (detail.$isError) {
+        reject(new Error(detail.message));
+        return;
+      }
+
+      resolve(detail);
+    };
+
+    window.addEventListener('__automa-condition-code__', handleAutomaEvent);
+  });
+}
+
+export default async function (data) {
+  let result = null;
+
+  if (data.type.startsWith('element')) {
+    result = await handleConditionElement(data);
+  }
+  if (data.type === 'code') {
+    result = await injectJsCode(data);
+  }
+
+  return result;
+}

+ 4 - 43
src/content/index.js

@@ -5,42 +5,7 @@ import FindElement from '@/utils/FindElement';
 import { getDocumentCtx } from './handleSelector';
 import executedBlock from './executedBlock';
 import blocksHandler from './blocksHandler';
-
-function handleConditionBuilder({ data, type }) {
-  if (!type.startsWith('element')) return null;
-
-  const selectorType = data.selector.startsWith('/') ? 'xpath' : 'cssSelector';
-
-  const element = FindElement[selectorType](data);
-  const { 1: actionType } = type.split('#');
-
-  if (!element) {
-    if (actionType === 'visible' || actionType === 'invisible') return false;
-
-    return null;
-  }
-
-  const elementActions = {
-    text: () => element.innerText,
-    visible: () => {
-      const { visibility, display } = getComputedStyle(element);
-
-      return visibility !== 'hidden' && display !== 'none';
-    },
-    invisible: () => {
-      const { visibility, display } = getComputedStyle(element);
-
-      return visibility === 'hidden' || display === 'none';
-    },
-    attribute: ({ attrName }) => {
-      if (!element.hasAttribute(attrName)) return null;
-
-      return element.getAttribute(attrName);
-    },
-  };
-
-  return elementActions[actionType](data);
-}
+import handleTestCondition from './handleTestCondition';
 
 (() => {
   if (window.isAutomaInjected) return;
@@ -75,17 +40,13 @@ function handleConditionBuilder({ data, type }) {
 
       switch (data.type) {
         case 'condition-builder':
-          resolve(handleConditionBuilder(data.data));
+          handleTestCondition(data.data)
+            .then((result) => resolve(result))
+            .catch((error) => reject(error));
           break;
         case 'content-script-exists':
           resolve(true);
           break;
-        case 'give-me-the-frame-id':
-          browser.runtime.sendMessage({
-            type: 'this-is-the-frame-id',
-          });
-          resolve();
-          break;
         case 'loop-elements': {
           const selectors = [];
           const attrId = nanoid(5);

+ 34 - 0
src/content/utils.js

@@ -0,0 +1,34 @@
+export function automaRefDataStr(stateId) {
+  return `
+function findData(obj, path) {
+  const paths = path.split('.');
+  const isWhitespace = paths.length === 1 && !/\\S/.test(paths[0]);
+
+  if (path.startsWith('$last') && Array.isArray(obj)) {
+    paths[0] = obj.length - 1;
+  }
+
+  if (paths.length === 0 || isWhitespace) return obj;
+  else if (paths.length === 1) return obj[paths[0]];
+
+  let result = obj;
+
+  for (let i = 0; i < paths.length; i++) {
+    if (result[paths[i]] == undefined) {
+      return undefined;
+    } else {
+      result = result[paths[i]];
+    }
+  }
+
+  return result;
+}
+function automaRefData(keyword, path = '') {
+  const data = JSON.parse(sessionStorage.getItem('automa--${stateId}')) || null;
+
+  if (data === null) return null;
+
+  return findData(data[keyword], path);
+}
+  `;
+}

+ 2 - 1
src/locales/en/newtab.json

@@ -109,7 +109,8 @@
       "title": "Condition builder",
       "add": "Add condition",
       "and": "AND",
-      "or": "OR"
+      "or": "OR",
+      "topAwait": "Support top-level await"
     },
     "host": {
       "title": "Host workflow",

+ 7 - 0
src/utils/shared.js

@@ -1064,6 +1064,13 @@ export const conditionBuilder = {
       compareable: true,
       data: { value: '' },
     },
+    {
+      id: 'code',
+      category: 'value',
+      name: 'Code',
+      compareable: true,
+      data: { code: '\nreturn true;' },
+    },
     {
       id: 'element#text',
       category: 'element',

+ 4 - 2
src/utils/testConditions.js

@@ -1,3 +1,4 @@
+import cloneDeep from 'lodash.clonedeep';
 import mustacheReplacer from './referenceData/mustacheReplacer';
 import { conditionBuilder } from './shared';
 
@@ -27,7 +28,7 @@ export default async function (conditionsArr, workflowData) {
   };
 
   async function getConditionItemValue({ type, data }) {
-    const copyData = JSON.parse(JSON.stringify(data));
+    const copyData = cloneDeep(data);
 
     Object.keys(data).forEach((key) => {
       const { value, list } = mustacheReplacer(
@@ -41,12 +42,13 @@ export default async function (conditionsArr, workflowData) {
 
     if (type === 'value') return copyData.value;
 
-    if (type.startsWith('element')) {
+    if (type.startsWith('element') || type === 'code') {
       const conditionValue = await workflowData.sendMessage({
         type: 'condition-builder',
         data: {
           type,
           data: copyData,
+          refData: workflowData.refData,
         },
       });