@@ -59,6 +60,10 @@ export default {
type: String,
default: '',
},
+ padding: {
+ type: String,
+ default: 'p-4',
+ },
customContent: Boolean,
persist: Boolean,
blur: Boolean,
diff --git a/src/content/element-selector/App.vue b/src/content/element-selector/App.vue
index 4fe751492..84a6b6574 100644
--- a/src/content/element-selector/App.vue
+++ b/src/content/element-selector/App.vue
@@ -196,7 +196,7 @@ const getElementSelector = (element) =>
state.selectorType === 'css'
? getCssSelector(element, {
includeTag: true,
- blacklist: ['[focused]', /focus/],
+ blacklist: ['[focused]', /focus/, /href/],
})
: generateXPath(element);
diff --git a/src/content/handle-selector.js b/src/content/handle-selector.js
index 7ef65b0c2..e953f2036 100644
--- a/src/content/handle-selector.js
+++ b/src/content/handle-selector.js
@@ -1,4 +1,5 @@
import FindElement from '@/utils/find-element';
+import { scrollIfNeeded } from '@/utils/helper';
/* eslint-disable consistent-return */
@@ -36,21 +37,6 @@ export function waitForSelector({
});
}
-function scrollIfNeeded(debugMode, element) {
- if (!debugMode) return;
-
- const { top, left, bottom, right } = element.getBoundingClientRect();
- const isInViewport =
- top >= 0 &&
- left >= 0 &&
- bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
- right <= (window.innerWidth || document.documentElement.clientWidth);
-
- if (!isInViewport) {
- element.scrollIntoView();
- }
-}
-
export default async function (
{ data, id, frameSelector, debugMode },
{ onSelected, onError, onSuccess, returnElement }
@@ -109,13 +95,13 @@ export default async function (
await Promise.allSettled(
Array.from(element).map((el) => {
markElement(el, { id, data });
- scrollIfNeeded(debugMode, el);
+ if (debugMode) scrollIfNeeded(el);
return onSelected(el);
})
);
} else if (element) {
markElement(element, { id, data });
- scrollIfNeeded(debugMode, element);
+ if (debugMode) scrollIfNeeded(element);
await onSelected(element);
}
diff --git a/src/content/index.js b/src/content/index.js
index dc87d2253..be6c9f3cc 100644
--- a/src/content/index.js
+++ b/src/content/index.js
@@ -4,6 +4,35 @@ import { toCamelCase } from '@/utils/helper';
import executedBlock from './executed-block';
import blocksHandler from './blocks-handler';
+const elementActions = {
+ text: (element) => element.innerText,
+ visible: (element) => {
+ const { visibility, display } = getComputedStyle(element);
+
+ return visibility !== 'hidden' || display !== 'none';
+ },
+ invisible: (element) => !elementActions.visible(element),
+ attribute: (element, { attrName }) => {
+ if (!element.hasAttribute(attrName)) return null;
+
+ return element.getAttribute(attrName);
+ },
+};
+function handleConditionBuilder({ data, type }) {
+ if (!type.startsWith('element')) return null;
+
+ const element = document.querySelector(data.selector);
+ const { 1: actionType } = type.split('#');
+
+ if (!element) {
+ if (actionType === 'visible' || actionType === 'invisible') return false;
+
+ return null;
+ }
+
+ return elementActions[actionType](element, data);
+}
+
(() => {
if (window.isAutomaInjected) return;
@@ -36,6 +65,9 @@ import blocksHandler from './blocks-handler';
}
switch (data.type) {
+ case 'condition-builder':
+ resolve(handleConditionBuilder(data.data));
+ break;
case 'content-script-exists':
resolve(true);
break;
diff --git a/src/lib/v-remixicon.js b/src/lib/v-remixicon.js
index 8d7ccb8b7..e86efc999 100644
--- a/src/lib/v-remixicon.js
+++ b/src/lib/v-remixicon.js
@@ -36,6 +36,7 @@ import {
riWindow2Line,
riArrowUpDownLine,
riRefreshLine,
+ riRefreshFill,
riBook3Line,
riGithubFill,
riCodeSSlashLine,
@@ -140,6 +141,7 @@ export const icons = {
riWindow2Line,
riArrowUpDownLine,
riRefreshLine,
+ riRefreshFill,
riBook3Line,
riGithubFill,
riCodeSSlashLine,
diff --git a/src/locales/en/blocks.json b/src/locales/en/blocks.json
index 863e3c421..e52e57c47 100644
--- a/src/locales/en/blocks.json
+++ b/src/locales/en/blocks.json
@@ -426,6 +426,12 @@
"response": "Response"
}
},
+ "while-loop": {
+ "name": "While loop",
+ "description": "Execute blocks while the condition is met",
+ "editCondition": "Edit condition",
+ "fallback": "Execute when the condition is false"
+ },
"loop-data": {
"name": "Loop data",
"description": "Iterate through table or your custom data",
diff --git a/src/locales/en/newtab.json b/src/locales/en/newtab.json
index 02f195770..fe6804b49 100644
--- a/src/locales/en/newtab.json
+++ b/src/locales/en/newtab.json
@@ -79,6 +79,12 @@
"clickToEnable": "Click to enable",
"toggleSidebar": "Toggle sidebar",
"cantEdit": "Can't edit shared workflow",
+ "conditionBuilder": {
+ "title": "Condition builder",
+ "add": "Add condition",
+ "and": "AND",
+ "or": "OR"
+ },
"host": {
"title": "Host workflow",
"set": "Set as host workflow",
diff --git a/src/utils/helper.js b/src/utils/helper.js
index 14d9bca96..9931c8a4c 100644
--- a/src/utils/helper.js
+++ b/src/utils/helper.js
@@ -1,3 +1,16 @@
+export function scrollIfNeeded(element) {
+ const { top, left, bottom, right } = element.getBoundingClientRect();
+ const isInViewport =
+ top >= 0 &&
+ left >= 0 &&
+ bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
+ right <= (window.innerWidth || document.documentElement.clientWidth);
+
+ if (!isInViewport) {
+ element.scrollIntoView();
+ }
+}
+
export function sleep(timeout = 500) {
return new Promise((resolve) => setTimeout(resolve, timeout));
}
diff --git a/src/utils/shared.js b/src/utils/shared.js
index 1e16f23a4..3ae87dc55 100644
--- a/src/utils/shared.js
+++ b/src/utils/shared.js
@@ -572,7 +572,7 @@ export const tasks = {
name: 'HTTP Request',
description: 'make an HTTP request',
icon: 'riEarthLine',
- component: 'BlockWebhook',
+ component: 'BlockBasicWithFallback',
editComponent: 'EditWebhook',
category: 'general',
inputs: 1,
@@ -596,6 +596,22 @@ export const tasks = {
responseType: 'json',
},
},
+ 'while-loop': {
+ name: 'While loop',
+ description: 'Execute blocks while the condition is met',
+ icon: 'riRefreshFill',
+ component: 'BlockBasicWithFallback',
+ editComponent: 'EditWhileLoop',
+ category: 'general',
+ inputs: 1,
+ outputs: 2,
+ allowedInputs: true,
+ maxConnection: 1,
+ data: {
+ description: '',
+ conditions: null,
+ },
+ },
'loop-data': {
name: 'Loop data',
icon: 'riRefreshLine',
@@ -880,3 +896,68 @@ export const supportLocales = [
{ id: 'vi', name: 'Tiếng Việt' },
{ id: 'fr', name: 'Français' },
];
+
+export const conditionBuilder = {
+ valueTypes: [
+ {
+ id: 'value',
+ category: 'value',
+ name: 'Value',
+ compareable: true,
+ data: { value: '' },
+ },
+ {
+ id: 'element#text',
+ category: 'element',
+ name: 'Element text',
+ compareable: true,
+ data: { selector: '' },
+ },
+ {
+ id: 'element#visible',
+ category: 'element',
+ name: 'Element visible',
+ compareable: false,
+ data: { selector: '' },
+ },
+ {
+ id: 'element#invisible',
+ category: 'element',
+ name: 'Element invisible',
+ compareable: false,
+ data: { selector: '' },
+ },
+ {
+ id: 'element#attribute',
+ category: 'element',
+ name: 'Element attribute value',
+ compareable: true,
+ data: { selector: '', attrName: '' },
+ },
+ ],
+ compareTypes: [
+ { id: 'eq', name: 'Equals', needValue: true },
+ { id: 'nq', name: 'Not equals', needValue: true },
+ { id: 'gt', name: 'Greater than', needValue: true },
+ { id: 'gte', name: 'Greater than or equal', needValue: true },
+ { id: 'lt', name: 'Less than', needValue: true },
+ { id: 'lte', name: 'Less than or equal', needValue: true },
+ { id: 'cnt', name: 'Contains', needValue: true },
+ { id: 'itr', name: 'Is truthy', needValue: false },
+ { id: 'ifl', name: 'Is falsy', needValue: false },
+ ],
+ inputTypes: {
+ selector: {
+ placeholder: '.class',
+ label: 'CSS selector',
+ },
+ value: {
+ label: 'Value',
+ placeholder: 'abc123',
+ },
+ attrName: {
+ label: 'Attribute name',
+ placeholder: 'name',
+ },
+ },
+};
diff --git a/src/utils/test-conditions.js b/src/utils/test-conditions.js
new file mode 100644
index 000000000..1dc6009f6
--- /dev/null
+++ b/src/utils/test-conditions.js
@@ -0,0 +1,113 @@
+/* eslint-disable no-restricted-syntax */
+import mustacheReplacer from './reference-data/mustache-replacer';
+import { conditionBuilder } from './shared';
+
+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,
+ cnt: (a, b) => a?.includes(b) ?? false,
+ itr: (a) => Boolean(a),
+ ifl: (a) => !a,
+};
+
+export default async function (conditionsArr, workflowData) {
+ const result = {
+ isMatch: false,
+ replacedValue: {},
+ };
+
+ async function getConditionItemValue({ type, data }) {
+ const copyData = JSON.parse(JSON.stringify(data));
+
+ Object.keys(data).forEach((key) => {
+ const { value, list } = mustacheReplacer(
+ copyData[key],
+ workflowData.refData
+ );
+
+ copyData[key] = value;
+ Object.assign(result.replacedValue, list);
+ });
+
+ if (type === 'value') return copyData.value;
+
+ if (type.startsWith('element')) {
+ const conditionValue = await workflowData.sendMessage({
+ type: 'condition-builder',
+ data: {
+ type,
+ data: copyData,
+ },
+ });
+
+ return conditionValue;
+ }
+
+ return '';
+ }
+ async function checkConditions(items) {
+ let conditionResult = true;
+ const condition = {
+ value: '',
+ operator: '',
+ };
+
+ for (const { category, data, type } of items) {
+ if (!conditionResult) return conditionResult;
+
+ if (category === 'compare') {
+ const isNeedValue = conditionBuilder.compareTypes.find(
+ ({ id }) => id === type
+ ).needValue;
+
+ if (!isNeedValue) {
+ conditionResult = comparisons[type](condition.value);
+
+ return conditionResult;
+ }
+
+ condition.operator = type;
+ } else if (category === 'value') {
+ const conditionValue = await getConditionItemValue({ data, type });
+ const isCompareable = conditionBuilder.valueTypes.find(
+ ({ id }) => id === type
+ ).compareable;
+
+ if (!isCompareable) {
+ conditionResult = conditionValue;
+ } else if (condition.operator) {
+ conditionResult = comparisons[condition.operator](
+ condition.value,
+ conditionValue
+ );
+
+ condition.operator = '';
+ }
+
+ condition.value = conditionValue;
+ }
+ }
+
+ return conditionResult;
+ }
+
+ for (const { conditions } of conditionsArr) {
+ if (result.isMatch) return result;
+
+ let isAllMatch = false;
+
+ for (const { items } of conditions) {
+ isAllMatch = await checkConditions(items, workflowData);
+
+ if (!isAllMatch) break;
+ }
+
+ result.isMatch = isAllMatch;
+ }
+
+ return result;
+}
From 015de455560d9bea428f8dba928084e7d59a0a04 Mon Sep 17 00:00:00 2001
From: Ahmad Kholid
Date: Mon, 28 Mar 2022 11:20:37 +0800
Subject: [PATCH 6/6] feat: add condition builder
---
.../blocks-handler/handler-conditions.js | 63 ++++---
src/components/block/BlockConditions.vue | 27 +--
.../newtab/workflow/edit/EditConditions.vue | 157 ++++++++++++------
src/content/element-selector/App.vue | 2 +-
src/lib/v-remixicon.js | 2 +
src/utils/test-conditions.js | 19 ++-
6 files changed, 181 insertions(+), 89 deletions(-)
diff --git a/src/background/workflow-engine/blocks-handler/handler-conditions.js b/src/background/workflow-engine/blocks-handler/handler-conditions.js
index 7624396c8..ed66b79eb 100644
--- a/src/background/workflow-engine/blocks-handler/handler-conditions.js
+++ b/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;
diff --git a/src/components/block/BlockConditions.vue b/src/components/block/BlockConditions.vue
index acccf2a6a..615acc9a9 100644
--- a/src/components/block/BlockConditions.vue
+++ b/src/components/block/BlockConditions.vue
@@ -20,18 +20,23 @@
@click="editBlock"
/>
-
-
-
+ {{ item.name }}
+
+
{{ item.compareValue || '_____' }}
@@ -41,18 +46,16 @@
{{ item.value || '_____' }}
-
-
+
+
-
- ⓘ
-
- {{ t('common.fallback') }}
+ ⓘ
+ Fallback
-
+