Browse Source

feat: add condition builder

Ahmad Kholid 3 years ago
parent
commit
015de45556

+ 44 - 19
src/background/workflow-engine/blocks-handler/handler-conditions.js

@@ -1,22 +1,47 @@
 import compareBlockValue from '@/utils/compare-block-value';
 import mustacheReplacer from '@/utils/reference-data/mustache-replacer';
+import testConditions from '@/utils/test-conditions';
 import { getBlockConnection } from '../helper';
 
-function conditions({ data, outputs }, { prevBlockData, refData }) {
-  return new Promise((resolve, reject) => {
-    if (data.conditions.length === 0) {
-      reject(new Error('conditions-empty'));
-      return;
-    }
+async function conditions({ data, outputs }, { prevBlockData, refData }) {
+  if (data.conditions.length === 0) {
+    throw new Error('conditions-empty');
+  }
+
+  let resultData = '';
+  let isConditionMatch = false;
+  let outputIndex = data.conditions.length + 1;
+
+  const replacedValue = {};
+  const condition = data.conditions[0];
+  const prevData = Array.isArray(prevBlockData)
+    ? prevBlockData[0]
+    : prevBlockData;
+
+  if (condition && condition.conditions) {
+    const conditionPayload = {
+      refData,
+      activeTab: this.activeTab.id,
+      sendMessage: (payload) =>
+        this._sendMessageToTab({ ...payload, isBlock: false }),
+    };
+
+    for (let index = 0; index < data.conditions.length; index += 1) {
+      const result = await testConditions(
+        data.conditions[index].conditions,
+        conditionPayload
+      );
 
-    let resultData = '';
-    let isConditionMatch = false;
-    let outputIndex = data.conditions.length + 1;
-    const replacedValue = {};
-    const prevData = Array.isArray(prevBlockData)
-      ? prevBlockData[0]
-      : prevBlockData;
+      Object.assign(replacedValue, result?.replacedValue || {});
 
+      if (result.isMatch) {
+        isConditionMatch = true;
+        outputIndex = index + 1;
+
+        break;
+      }
+    }
+  } else {
     data.conditions.forEach(({ type, value, compareValue }, index) => {
       if (isConditionMatch) return;
 
@@ -37,13 +62,13 @@ function conditions({ data, outputs }, { prevBlockData, refData }) {
         isConditionMatch = true;
       }
     });
+  }
 
-    resolve({
-      replacedValue,
-      data: resultData,
-      nextBlockId: getBlockConnection({ outputs }, outputIndex),
-    });
-  });
+  return {
+    replacedValue,
+    data: resultData,
+    nextBlockId: getBlockConnection({ outputs }, outputIndex),
+  };
 }
 
 export default conditions;

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

@@ -20,18 +20,23 @@
         @click="editBlock"
       />
     </div>
-    <div
+    <ul
       v-if="block.data.conditions && block.data.conditions.length !== 0"
       class="mt-4 space-y-2"
     >
-      <div
+      <li
         v-for="item in block.data.conditions"
         :key="item.id"
-        class="flex items-center justify-end"
+        class="flex items-center flex-1 p-2 bg-box-transparent rounded-lg overflow-hidden w-44"
       >
-        <div
-          class="flex items-center flex-1 p-2 bg-box-transparent rounded-lg overflow-hidden w-44"
+        <p
+          v-if="item.name"
+          class="text-overflow w-full text-right"
+          :title="item.name"
         >
+          {{ item.name }}
+        </p>
+        <template v-else>
           <p class="w-5/12 text-overflow text-right">
             {{ item.compareValue || '_____' }}
           </p>
@@ -41,18 +46,16 @@
           <p class="w-5/12 text-overflow">
             {{ item.value || '_____' }}
           </p>
-        </div>
-      </div>
+        </template>
+      </li>
       <p
         v-if="block.data.conditions && block.data.conditions.length !== 0"
         class="text-right text-gray-600 dark:text-gray-200"
       >
-        <span :title="t('workflow.blocks.conditions.fallbackTitle')">
-          &#9432;
-        </span>
-        {{ t('common.fallback') }}
+        <span title="Fallback"> &#9432; </span>
+        Fallback
       </p>
-    </div>
+    </ul>
     <input class="trigger hidden" @change="onChange" />
   </div>
 </template>

+ 106 - 51
src/components/newtab/workflow/edit/EditConditions.vue

@@ -8,55 +8,65 @@
     >
       {{ t('workflow.blocks.conditions.add') }}
     </ui-button>
-    <ul class="space-y-2">
-      <li
-        v-for="(condition, index) in conditions"
-        :key="index"
-        class="relative rounded-lg bg-input transition-colors group"
+    <ui-list class="space-y-1">
+      <ui-list-item
+        v-for="(item, index) in conditions"
+        :key="item.id"
+        class="group"
       >
-        <input
-          v-model="condition.compareValue"
-          type="text"
-          placeholder="value"
-          class="py-2 px-4 w-full transition rounded-lg bg-transparent"
+        <v-remixicon name="riGuideLine" size="20" class="mr-2 -ml-1" />
+        <p class="flex-1 text-overflow" :title="item.name">
+          {{ item.name }}
+        </p>
+        <v-remixicon
+          class="cursor-pointer group-hover:visible invisible"
+          name="riPencilLine"
+          size="20"
+          @click="editCondition(index)"
         />
-        <button
-          class="bg-white dark:bg-gray-700 absolute top-1/2 right-4 p-2 rounded-lg -translate-y-1/2 group-hover:right-14"
+        <v-remixicon
+          name="riDeleteBin7Line"
+          size="20"
+          class="ml-2 -mr-1 cursor-pointer"
           @click="deleteCondition(index)"
-        >
-          <v-remixicon size="20" name="riDeleteBin7Line" />
-        </button>
-        <select
-          v-model="condition.type"
-          :title="getTitle(index)"
-          class="bg-white dark:bg-gray-700 absolute right-4 font-mono z-10 p-2 top-1/2 leading-tight -translate-y-1/2 text-center transition rounded-lg appearance-none"
-        >
-          <option
-            v-for="(name, type) in conditionTypes"
-            :key="type"
-            :value="type"
-          >
-            {{ type }}
-          </option>
-        </select>
-        <div
-          class="w-full bg-gray-300 dark:bg-gray-700 h-px mx-auto"
-          style="max-width: 89%"
-        ></div>
-        <input
-          v-model="condition.value"
-          type="text"
-          placeholder="value"
-          class="py-2 px-4 w-full transition rounded-lg bg-transparent"
         />
-      </li>
-    </ul>
+      </ui-list-item>
+    </ui-list>
+    <ui-modal v-model="state.showModal" custom-content>
+      <ui-card padding="p-0" class="w-full max-w-3xl">
+        <div class="px-4 pt-4 flex items-center">
+          <p class="flex-1">
+            {{ t('workflow.conditionBuilder.title') }}
+          </p>
+          <v-remixicon
+            name="riCloseLine"
+            class="cursor-pointer"
+            @click="state.showModal = false"
+          />
+        </div>
+        <div
+          class="overflow-auto p-4 mt-4 scroll"
+          style="height: calc(100vh - 8rem)"
+        >
+          <input
+            v-model="conditions[state.conditionsIndex].name"
+            class="text-xl font-semibold mb-4 bg-transparent focus:ring-0"
+          />
+          <shared-condition-builder
+            :model-value="conditions[state.conditionsIndex].conditions"
+            @change="conditions[state.conditionsIndex].conditions = $event"
+          />
+        </div>
+      </ui-card>
+    </ui-modal>
   </div>
 </template>
 <script setup>
-import { ref, watch } from 'vue';
+import { ref, watch, onMounted, shallowReactive } from 'vue';
 import { useI18n } from 'vue-i18n';
+import { nanoid } from 'nanoid';
 import emitter from '@/lib/mitt';
+import SharedConditionBuilder from '@/components/newtab/shared/SharedConditionBuilder/index.vue';
 
 const props = defineProps({
   data: {
@@ -71,22 +81,25 @@ const props = defineProps({
 const emit = defineEmits(['update:data']);
 
 const conditionTypes = {
-  '==': 'equals',
-  '!=': 'ne',
+  '==': 'eq',
+  '!=': 'nq',
   '>': 'gt',
   '>=': 'gte',
   '<': 'lt',
   '<=': 'lte',
-  '()': 'contains',
+  '()': 'cnt',
 };
 const { t } = useI18n();
 
 const conditions = ref(props.data.conditions);
+const state = shallowReactive({
+  showModal: false,
+  conditionsIndex: 0,
+});
 
-function getTitle(index) {
-  const type = conditionTypes[conditions.value[index]?.type] || 'equals';
-
-  return t(`workflow.blocks.conditions.${type}`);
+function editCondition(index) {
+  state.conditionsIndex = index;
+  state.showModal = true;
 }
 function addCondition() {
   if (conditions.value.length >= 10) return;
@@ -95,10 +108,15 @@ function addCondition() {
     id: props.blockId,
   });
 
-  conditions.value.unshift({
-    compareValue: '',
-    value: '',
-    type: '==',
+  conditions.value.push({
+    id: nanoid(),
+    name: `Path ${conditions.value.length + 1}`,
+    conditions: [
+      {
+        id: nanoid(),
+        conditions: [],
+      },
+    ],
   });
 }
 function deleteCondition(index) {
@@ -119,4 +137,41 @@ watch(
   },
   { deep: true }
 );
+
+onMounted(() => {
+  const condition = props.data.conditions[0];
+
+  if (condition && condition.conditions) return;
+
+  const generateConditionItem = (type, data) => {
+    if (type === 'value') {
+      return {
+        id: nanoid(),
+        type: 'value',
+        category: 'value',
+        data: { value: data },
+      };
+    }
+
+    return { id: nanoid(), category: 'compare', type: data };
+  };
+  conditions.value = conditions.value.map((item, index) => {
+    const items = [
+      generateConditionItem('value', item.compareValue),
+      generateConditionItem('compare', conditionTypes[item.type]),
+      generateConditionItem('value', item.value),
+    ];
+
+    return {
+      id: nanoid(),
+      name: `Path ${index + 1}`,
+      conditions: [
+        {
+          id: nanoid(),
+          conditions: [{ id: nanoid(), items }],
+        },
+      ],
+    };
+  });
+});
 </script>

+ 1 - 1
src/content/element-selector/App.vue

@@ -196,7 +196,7 @@ const getElementSelector = (element) =>
   state.selectorType === 'css'
     ? getCssSelector(element, {
         includeTag: true,
-        blacklist: ['[focused]', /focus/, /href/],
+        blacklist: ['[focused]', /focus/, /href/, /src/],
       })
     : generateXPath(element);
 

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

@@ -4,6 +4,7 @@ import {
   riH2,
   riLinkM,
   riTwitterLine,
+  riGuideLine,
   riChat3Line,
   riDiscordLine,
   riEarthLine,
@@ -109,6 +110,7 @@ export const icons = {
   riH2,
   riLinkM,
   riTwitterLine,
+  riGuideLine,
   riChat3Line,
   riDiscordLine,
   riEarthLine,

+ 13 - 6
src/utils/test-conditions.js

@@ -2,16 +2,23 @@
 import mustacheReplacer from './reference-data/mustache-replacer';
 import { conditionBuilder } from './shared';
 
+const isBoolStr = (str) => {
+  if (str === 'true') return true;
+  if (str === 'false') return false;
+
+  return str;
+};
+const isNumStr = (str) => (Number.isNaN(+str) ? str : +str);
 const comparisons = {
   eq: (a, b) => a === b,
   nq: (a, b) => a !== b,
-  gt: (a, b) => a > b,
-  gte: (a, b) => a >= b,
-  lt: (a, b) => a < b,
-  lte: (a, b) => a <= b,
+  gt: (a, b) => isNumStr(a) > isNumStr(b),
+  gte: (a, b) => isNumStr(a) >= isNumStr(b),
+  lt: (a, b) => isNumStr(a) < isNumStr(b),
+  lte: (a, b) => isNumStr(a) <= isNumStr(b),
   cnt: (a, b) => a?.includes(b) ?? false,
-  itr: (a) => Boolean(a),
-  ifl: (a) => !a,
+  itr: (a) => Boolean(isBoolStr(a)),
+  ifl: (a) => !isBoolStr(a),
 };
 
 export default async function (conditionsArr, workflowData) {