Ahmad Kholid 2 年之前
父節點
當前提交
0f0398e074

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "automa",
-  "version": "1.23.0",
+  "version": "1.23.3",
   "description": "An extension for automating your browser by connecting blocks",
   "repository": {
     "type": "git",

+ 1 - 0
src/background/index.js

@@ -116,6 +116,7 @@ message.on('workflow:execute', async (workflowData, sender) => {
   if (!isMV2 && (!context || context === 'popup')) {
     await BackgroundUtils.openDashboard('', false);
     await sleep(1000);
+    console.log('halo', workflowData);
     await BackgroundUtils.sendMessageToDashboard('workflow:execute', {
       data: workflowData,
       options: workflowData.option,

+ 15 - 0
src/components/block/BlockBasic.vue

@@ -28,6 +28,16 @@
         />
       </span>
       <div class="overflow-hidden flex-1">
+        <span
+          v-if="blockErrors"
+          v-tooltip="{
+            allowHTML: true,
+            content: blockErrors,
+          }"
+          class="absolute top-2 right-2 text-red-500 dark:text-red-400"
+        >
+          <v-remixicon name="riAlertLine" size="20" />
+        </span>
         <p
           v-if="block.details.id"
           class="font-semibold leading-tight text-overflow whitespace-nowrap"
@@ -78,6 +88,7 @@
 </template>
 <script setup>
 import { computed, shallowReactive } from 'vue';
+import { useBlockValidation } from '@/composable/blockValidation';
 import { Handle, Position } from '@vue-flow/core';
 import { useI18n } from 'vue-i18n';
 import { useEditorBlock } from '@/composable/editorBlock';
@@ -117,6 +128,10 @@ const loopBlocks = ['loop-data', 'loop-elements'];
 const { t, te } = useI18n();
 const block = useEditorBlock(props.label);
 const componentId = useComponentId('block-base');
+const { errors: blockErrors } = useBlockValidation(
+  props.label,
+  () => props.data
+);
 
 const state = shallowReactive({
   isCopied: false,

+ 15 - 0
src/components/block/BlockBasicWithFallback.vue

@@ -30,6 +30,16 @@
         </p>
       </div>
     </div>
+    <span
+      v-if="blockErrors"
+      v-tooltip="{
+        allowHTML: true,
+        content: blockErrors,
+      }"
+      class="absolute top-2 right-2 text-red-500 dark:text-red-400"
+    >
+      <v-remixicon name="riAlertLine" size="20" />
+    </span>
     <slot :block="block"></slot>
     <div class="fallback flex items-center justify-end">
       <v-remixicon
@@ -54,6 +64,7 @@
 <script setup>
 import { Handle, Position } from '@vue-flow/core';
 import { useI18n } from 'vue-i18n';
+import { useBlockValidation } from '@/composable/blockValidation';
 import { useEditorBlock } from '@/composable/editorBlock';
 import { useComponentId } from '@/composable/componentId';
 import BlockBase from './BlockBase.vue';
@@ -77,4 +88,8 @@ defineEmits(['delete', 'edit', 'update', 'settings']);
 const { t } = useI18n();
 const block = useEditorBlock(props.label);
 const componentId = useComponentId('block-base');
+const { errors: blockErrors } = useBlockValidation(
+  props.label,
+  () => props.data
+);
 </script>

+ 92 - 83
src/components/newtab/workflow/edit/EditExportData.vue

@@ -6,93 +6,102 @@
       :placeholder="t('common.description')"
       @change="updateData({ description: $event })"
     />
-    <ui-select
-      :model-value="data.dataToExport"
-      :label="t('workflow.blocks.export-data.dataToExport.placeholder')"
-      class="w-full mt-2"
-      @change="updateData({ dataToExport: $event })"
-    >
-      <option v-for="option in dataToExport" :key="option" :value="option">
-        {{ t(`workflow.blocks.export-data.dataToExport.options.${option}`) }}
-      </option>
-    </ui-select>
-    <ui-input
-      v-if="data.dataToExport === 'google-sheets'"
-      :model-value="data.refKey"
-      :title="t('workflow.blocks.export-data.refKey')"
-      :placeholder="t('workflow.blocks.export-data.refKey')"
-      class="w-full mt-2"
-      @change="updateData({ refKey: $event })"
-    />
-    <ui-input
-      v-if="data.dataToExport === 'variable'"
-      :model-value="data.variableName"
-      :title="t('workflow.variables.name')"
-      :placeholder="t('workflow.variables.name')"
-      class="w-full mt-2"
-      @change="updateData({ variableName: $event })"
-    />
-    <edit-autocomplete class="mt-2">
+    <template v-if="!permission.has.downloads">
+      <p class="mt-4">
+        {{ t('workflow.blocks.handle-download.noPermission') }}
+      </p>
+      <ui-button variant="accent" class="mt-2" @click="permission.request">
+        {{ t('workflow.blocks.clipboard.grantPermission') }}
+      </ui-button>
+    </template>
+    <template v-else>
+      <ui-select
+        :model-value="data.dataToExport"
+        :label="t('workflow.blocks.export-data.dataToExport.placeholder')"
+        class="w-full mt-2"
+        @change="updateData({ dataToExport: $event })"
+      >
+        <option v-for="option in dataToExport" :key="option" :value="option">
+          {{ t(`workflow.blocks.export-data.dataToExport.options.${option}`) }}
+        </option>
+      </ui-select>
       <ui-input
-        :model-value="data.name"
-        autocomplete="off"
-        label="File name"
-        class="w-full"
-        placeholder="unnamed"
-        @change="updateData({ name: $event })"
+        v-if="data.dataToExport === 'google-sheets'"
+        :model-value="data.refKey"
+        :title="t('workflow.blocks.export-data.refKey')"
+        :placeholder="t('workflow.blocks.export-data.refKey')"
+        class="w-full mt-2"
+        @change="updateData({ refKey: $event })"
       />
-    </edit-autocomplete>
-    <ui-select
-      v-if="permission.has.downloads"
-      :model-value="data.onConflict"
-      :label="t('workflow.blocks.handle-download.onConflict')"
-      class="mt-2 w-full"
-      @change="updateData({ onConflict: $event })"
-    >
-      <option v-for="item in onConflict" :key="item" :value="item">
-        {{ t(`workflow.blocks.base.downloads.onConflict.${item}`) }}
-      </option>
-    </ui-select>
-    <ui-select
-      :model-value="data.type"
-      :label="t('workflow.blocks.export-data.exportAs')"
-      class="w-full mt-2"
-      @change="updateData({ type: $event })"
-    >
-      <option v-for="type in dataExportTypes" :key="type.id" :value="type.id">
-        {{ type.name }}
-      </option>
-    </ui-select>
-    <ui-expand
-      v-if="data.type === 'csv'"
-      hide-header-icon
-      header-class="flex items-center focus:ring-0 w-full"
-    >
-      <template #header="{ show }">
-        <v-remixicon
-          :rotate="show ? 270 : 180"
-          name="riArrowLeftSLine"
-          class="transition-transform text-gray-600 dark:text-gray-300"
-        />
-        {{ t('common.options') }}
-      </template>
-      <div class="pl-6 mt-1">
-        <ui-checkbox
-          v-if="data.type === 'csv'"
-          :model-value="data.addBOMHeader"
-          @change="updateData({ addBOMHeader: $event })"
-        >
-          {{ t('workflow.blocks.export-data.bomHeader') }}
-        </ui-checkbox>
+      <ui-input
+        v-if="data.dataToExport === 'variable'"
+        :model-value="data.variableName"
+        :title="t('workflow.variables.name')"
+        :placeholder="t('workflow.variables.name')"
+        class="w-full mt-2"
+        @change="updateData({ variableName: $event })"
+      />
+      <edit-autocomplete class="mt-2">
         <ui-input
-          :model-value="data.csvDelimiter"
-          label="Delimiter"
-          class="mt-1"
-          placeholder=","
-          @change="updateData({ csvDelimiter: $event })"
+          :model-value="data.name"
+          autocomplete="off"
+          label="File name"
+          class="w-full"
+          placeholder="unnamed"
+          @change="updateData({ name: $event })"
         />
-      </div>
-    </ui-expand>
+      </edit-autocomplete>
+      <ui-select
+        :model-value="data.onConflict"
+        :label="t('workflow.blocks.handle-download.onConflict')"
+        class="mt-2 w-full"
+        @change="updateData({ onConflict: $event })"
+      >
+        <option v-for="item in onConflict" :key="item" :value="item">
+          {{ t(`workflow.blocks.base.downloads.onConflict.${item}`) }}
+        </option>
+      </ui-select>
+      <ui-select
+        :model-value="data.type"
+        :label="t('workflow.blocks.export-data.exportAs')"
+        class="w-full mt-2"
+        @change="updateData({ type: $event })"
+      >
+        <option v-for="type in dataExportTypes" :key="type.id" :value="type.id">
+          {{ type.name }}
+        </option>
+      </ui-select>
+      <ui-expand
+        v-if="data.type === 'csv'"
+        hide-header-icon
+        header-class="flex items-center focus:ring-0 w-full"
+      >
+        <template #header="{ show }">
+          <v-remixicon
+            :rotate="show ? 270 : 180"
+            name="riArrowLeftSLine"
+            class="transition-transform text-gray-600 dark:text-gray-300"
+          />
+          {{ t('common.options') }}
+        </template>
+        <div class="pl-6 mt-1">
+          <ui-checkbox
+            v-if="data.type === 'csv'"
+            :model-value="data.addBOMHeader"
+            @change="updateData({ addBOMHeader: $event })"
+          >
+            {{ t('workflow.blocks.export-data.bomHeader') }}
+          </ui-checkbox>
+          <ui-input
+            :model-value="data.csvDelimiter"
+            label="Delimiter"
+            class="mt-1"
+            placeholder=","
+            @change="updateData({ csvDelimiter: $event })"
+          />
+        </div>
+      </ui-expand>
+    </template>
   </div>
 </template>
 <script setup>

+ 2 - 1
src/components/newtab/workflow/edit/EditWebhook.vue

@@ -172,10 +172,11 @@ const { t } = useI18n();
 
 const methods = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'];
 const notHaveBody = ['GET', 'DELETE'];
+const copyHeaders = JSON.parse(JSON.stringify(props.data.headers));
 
 const activeTab = ref('headers');
 const showBodyModal = ref(false);
-const headers = ref(JSON.parse(JSON.stringify(props.data.headers)));
+const headers = ref(Array.isArray(copyHeaders) ? copyHeaders : []);
 
 function updateData(value) {
   emit('update:data', { ...props.data, ...value });

+ 40 - 0
src/composable/blockValidation.js

@@ -0,0 +1,40 @@
+import { onMounted, watch, shallowRef } from 'vue';
+import blocksValidation from '@/newtab/utils/blocksValidation';
+
+export function useBlockValidation(blockId, data) {
+  const errors = shallowRef('');
+
+  onMounted(() => {
+    const blockValidation = blocksValidation[blockId];
+    if (!blockValidation) return;
+
+    const unwatch = watch(
+      data,
+      (newData) => {
+        blockValidation
+          .func(newData)
+          .then((blockErrors) => {
+            let errorsStr = '';
+            blockErrors.forEach((error) => {
+              errorsStr += `<li>${error}</li>\n`;
+            });
+
+            errors.value =
+              errorsStr.trim() &&
+              `Issues: <ol class='list-disc list-inside'>${errorsStr}</ol>`;
+          })
+          .catch((error) => {
+            console.error(error);
+          })
+          .finally(() => {
+            if (blockValidation.once) {
+              unwatch();
+            }
+          });
+      },
+      { deep: true, immediate: true }
+    );
+  });
+
+  return { errors };
+}

+ 2 - 0
src/lib/vRemixicon.js

@@ -49,6 +49,7 @@ import {
   riToggleFill,
   riToggleLine,
   riFolderLine,
+  riAlertLine,
   riGithubFill,
   riEyeOffLine,
   riWindowLine,
@@ -184,6 +185,7 @@ export const icons = {
   riToggleFill,
   riToggleLine,
   riFolderLine,
+  riAlertLine,
   riGithubFill,
   riEyeOffLine,
   riWindowLine,

+ 442 - 10
src/locales/vi/blocks.json

@@ -10,7 +10,67 @@
   "workflow": {
     "blocks": {
       "base": {
+        "title": "Khối",
+        "moveToGroup": "Di chuyển khối sang nhóm khối",
         "selector": "Bộ chọn phần tử",
+        "selectorOptions": "Tùy chọn bộ chọn",
+        "timeout": "Thời gian chờ (mili giây)",
+        "noPermission": "Automa không có đủ quyền để thực hiện hành động này",
+        "grantPermission": "Cấp phép",
+        "action": "Hoạt động",
+        "element": {
+          "select": "Chọn một phần tử",
+          "verify": "Xác minh bộ chọn"
+        },
+        "settings": {
+          "title": "Cài đặt chặn",
+          "line": {
+            "title": "Dòng",
+            "label": "Nhãn dòng",
+            "animated": "Hoạt hình",
+            "select": "Chọn dòng",
+            "to": "Dòng tới khối {name}",
+            "lineColor": "Màu đường kẻ"
+          }
+        },
+        "toggle": {
+          "enable": "Bật khối",
+          "disable": "Tắt khối"
+        },
+        "onError": {
+          "info": "Các quy tắc này sẽ được áp dụng khi có lỗi xảy ra trên khối",
+          "button": "Có lỗi",
+          "title": "Khi xảy ra lỗi",
+          "retry": "Thử lại hành động",
+          "fallbackTitle": "Sẽ thực thi khi có lỗi xảy ra trong khối",
+          "times": {
+            "name": "Thời gian",
+            "description": "Số lần thử lại hành động"
+          },
+          "interval": {
+            "name": "Khoảng thời gian",
+            "description": "Khoảng thời gian chờ giữa mỗi lần thử",
+            "second": "thứ hai"
+          },
+          "toDo": {
+            "error": "Ném lỗi",
+            "continue": "Tiếp tục dòng chảy",
+            "fallback": "Thực thi dự phòng",
+            "restart": "Khởi động lại quy trình"
+          },
+          "insertData": {
+            "name": "Chèn dữ liệu"
+          }
+        },
+        "table": {
+          "checkbox": "Chèn vào bảng",
+          "select": "Chọn cột",
+          "extraRow": {
+            "checkbox": "Thêm hàng bổ sung",
+            "placeholder": "Giá trị",
+            "title": "Giá trị của hàng phụ"
+          }
+        },
         "findElement": {
           "placeholder": "Tìm phần tử bằng",
           "options": {
@@ -25,11 +85,223 @@
         "multiple": {
           "title": "Chọn nhiều phần tử",
           "text": "Nhiều"
+        },
+        "waitSelector": {
+          "title": "Chờ bộ chọn",
+          "timeout": "Thời gian chờ của bộ chọn (mili giây)"
+        },
+        "downloads": {
+          "onConflict": {
+            "uniquify": "Thống nhất",
+            "overwrite": "Ghi đè",
+            "prompt": "Lời nhắc"
+          }
+        }
+      },
+      "wait-connections": {
+        "name": "Chờ kết nối",
+        "description": "Chờ tất cả các kết nối trước khi tiếp tục khối tiếp theo",
+        "specificFlow": "Chỉ tiếp tục một quy trình cụ thể",
+        "selectFlow": "Chọn luồng"
+      },
+      "cookie": {
+        "name": "Cookie",
+        "description": "Nhận, đặt hoặc xóa cookie",
+        "types": {
+          "get": "Nhận cookie",
+          "set": "Đặt cookie",
+          "remove": "Xóa cookie",
+          "getAll": "Nhận tất cả cookie"
+        }
+      },
+      "note": {
+        "name": "Ghi chú"
+      },
+      "slice-variable": {
+        "name": "Biến Slice",
+        "description": "Trích xuất một phần của một giá trị biến",
+        "start": "Bắt đầu lập chỉ mục",
+        "end": "Chỉ mục kết thúc"
+      },
+      "workflow-state": {
+        "name": "Trạng thái quy trình làm việc",
+        "description": "Quản lý trạng thái quy trình công việc",
+        "actions": {
+          "stop": "Dừng quy trình công việc"
+        }
+      },
+      "regex-variable": {
+        "name": "Biến RegEx",
+        "description": "Khớp một giá trị biến với một biểu thức chính quy"
+      },
+      "data-mapping": {
+        "source": "Nguồn",
+        "destination": "Điểm đến",
+        "name": "Ánh xạ dữ liệu",
+        "edit": "Chỉnh sửa bản đồ dữ liệu",
+        "dataSource": "Nguồn dữ liệu",
+        "description": "Ánh xạ dữ liệu của một biến hoặc bảng",
+        "addSource": "Thêm nguồn",
+        "addDestination": "Thêm điểm đến"
+      },
+      "sort-data": {
+        "name": "Sắp xếp dữ liệu",
+        "description": "Sắp xếp các mục dữ liệu",
+        "property": "Sắp xếp theo thuộc tính của mặt hàng",
+        "addProperty": "Thêm tài sản"
+      },
+      "increase-variable": {
+        "name": "Tăng biến",
+        "description": "Tăng giá trị của một biến bằng số tiền cụ thể",
+        "increase": "Tăng bởi"
+      },
+      "notification": {
+        "name": "thông báo",
+        "description": "Hiển thị thông báo",
+        "title": "Tiêu đề",
+        "message": "Thông điệp",
+        "imageUrl": "URL hình ảnh (tùy chọn)",
+        "iconUrl": "URL biểu tượng (tùy chọn)"
+      },
+      "delete-data": {
+        "name": "Xóa dữ liệu",
+        "description": "Xóa bảng hoặc dữ liệu biến",
+        "from": "Dữ liệu từ",
+        "allColumns": "[Tất cả các cột]"
+      },
+      "log-data": {
+        "name": "Get log data",
+        "description": "Nhận dữ liệu nhật ký mới nhất của quy trình làm việc",
+        "data": "Ghi dữ liệu"
+      },
+      "tab-url": {
+        "name": "Nhận URL tab",
+        "description": "Lấy URL của tab",
+        "select": "Chọn tab",
+        "types": {
+          "active-tab": "Tab hoạt động",
+          "all": "Tất cả các tab"
+        }
+      },
+      "reload-tab": {
+        "name": "Tải lại tab",
+        "description": "Tải lại tab đang hoạt động"
+      },
+      "press-key": {
+        "name": "Nhấn phím",
+        "description": "Nhấn một phím hoặc một tổ hợp",
+        "target": "Yếu tố mục tiêu (tùy chọn)",
+        "key": "Chìa khóa",
+        "detect": "Phát hiện khóa",
+        "actions": {
+          "press-key": "Nhấn một phím",
+          "multiple-keys": "Nhấn nhiều phím"
+        }
+      },
+      "save-assets": {
+        "name": "Tiết kiệm tài sản",
+        "description": "Lưu nội dung (hình ảnh, video, âm thanh hoặc tệp) từ một phần tử hoặc URL",
+        "filename": "Tên tệp (tùy chọn)",
+        "contentTypes": {
+          "title": "Loại hình",
+          "element": "Phần tử phương tiện (hình ảnh, âm thanh hoặc video)",
+          "url": "URL"
+        }
+      },
+      "handle-dialog": {
+        "name": "Xử lý hộp thoại",
+        "description": "Chấp nhận hoặc loại bỏ hộp thoại khởi tạo JavaScript (cảnh báo, xác nhận, lời nhắc hoặc tải lên trên).",
+        "accept": "Hộp thoại chấp nhận",
+        "promptText": {
+          "label": "Văn bản nhắc nhở (tùy chọn)",
+          "description": "Văn bản cần nhập vào lời nhắc hộp thoại trước khi chấp nhận"
+        }
+      },
+      "handle-download": {
+        "name": "Xử lý tải xuống",
+        "description": "Xử lý tệp đã tải xuống",
+        "timeout": "Thời gian chờ (mili giây)",
+        "noPermission": "Không có quyền truy cập các bản tải xuống",
+        "onConflict": "Xung đột",
+        "waitFile": "Chờ tệp được tải xuống"
+      },
+      "insert-data": {
+        "name": "Chèn dữ liệu",
+        "description": "Chèn dữ liệu vào bảng hoặc biến"
+      },
+      "clipboard": {
+        "name": "Bộ nhớ tạm",
+        "description": "Lấy văn bản đã sao chép từ khay nhớ tạm",
+        "data": "Dữ liệu bảng tạm",
+        "noPermission": "Không có quyền truy cập khay nhớ tạm",
+        "grantPermission": "Cấp phép",
+        "copySelection": "Sao chép văn bản đã chọn trên trang",
+        "types": {
+          "get": "Nhận dữ liệu khay nhớ tạm",
+          "insert": "Chèn văn bản vào khay nhớ tạm"
         }
       },
+      "hover-element": {
+        "name": "Hover element",
+        "description": "Hover over an element"
+      },
+      "create-element": {
+        "name": "Tạo phần tử",
+        "description": "Tạo một phần tử và chèn nó vào trang",
+        "edit": "Chỉnh sửa phần tử",
+        "wrap": "Bọc phần tử bên trong",
+        "insertEl": {
+          "title": "Chèn phần tử",
+          "items": {
+            "before": "Là đứa con đầu lòng",
+            "after": "Là đứa con cuối cùng",
+            "next-sibling": "Là anh chị em tiếp theo",
+            "prev-sibling": "Là anh chị em trước",
+            "replace": "Thay thế phần tử mục tiêu"
+          }
+        }
+      },
+      "upload-file": {
+        "name": "Cập nhật dử liệu",
+        "description": "Tải tệp lên phần tử <input type=\"file\">",
+        "filePath": "URL hoặc đường dẫn tệp",
+        "addFile": "Thêm tập tin",
+        "onlyURL": "Chỉ hỗ trợ tải tệp lên từ một URL trong trình duyệt Firefox",
+        "requirement": "Xem yêu cầu trước khi sử dụng khối này",
+        "noFileAccess": "Automa không có quyền truy cập tệp"
+      },
+      "browser-event": {
+        "name": "Sự kiện trình duyệt",
+        "description": "Thực thi khối tiếp theo khi sự kiện được kích hoạt",
+        "events": "Sự kiện",
+        "timeout": "Thời gian chờ (mili giây)",
+        "activeTabLoaded": "Tab hoạt động",
+        "setAsActiveTab": "Đặt làm tab hoạt động"
+      },
+      "blocks-group-2": {
+        "name": "@:workflow.blocks.blocks-group.name 2",
+        "description": "@:workflow.blocks.blocks-group.description"
+      },
+      "blocks-group": {
+        "name": "Nhóm khối",
+        "groupName": "Tên nhóm",
+        "description": "Nhóm các khối",
+        "dropText": "Kéo và thả một khối ở đây",
+        "cantAdd": "Không thể thêm khối \"{blockName}\" vào nhóm."
+      },
       "trigger": {
         "name": "Kích hoạt",
         "description": "Quy trình bắt đầu được thực thi ở đây",
+        "addTime": "Thêm thời gian",
+        "selectDay": "Chọn ngày",
+        "timeExist": "Bạn đã thêm {time} vào {day}",
+        "fixedDelay": "Cố định độ trễ",
+        "contextMenus": {
+          "noPermission": "Trình kích hoạt này yêu cầu quyền \"contextMenus\" để hoạt động",
+          "grantPermission": "Cấp phép",
+          "appearIn": "Sẽ xuất hiện trong",
+          "contextName": "Tên quy trình làm việc trong menu ngữ cảnh"
+        },
         "days": [
           "Chủ nhật",
           "Thứ hai",
@@ -42,6 +314,7 @@
         "useRegex": "Dùng regex",
         "shortcut": {
           "tooltip": "Ghi lại lối tắt",
+          "stopRecord": "Dừng ghi",
           "checkboxTitle": "Execute shortcut even when you're in an input element",
           "checkbox": "Hoạt động khi nhập liệu",
           "note": "Lưu ý: phím tắt chỉ hoạt động khi bạn đang truy cập một trang web"
@@ -53,14 +326,49 @@
           "date": "Ngày",
           "time": "Giờ",
           "url": "URL hoặc Regex",
-          "shortcut": "Phim tắt"
+          "shortcut": "Phim tắt",
+          "cron-expression": "Biểu thức cron"
+        },
+        "element-change": {
+          "target": "Mục tiêu yếu tố để quan sát",
+          "optionsInfo": "Đột biến phần tử nào sẽ kích hoạt quy trình làm việc",
+          "targetWebsite": "Mẫu Đối sánh của trang web có phần tử mục tiêu (nhấp để xem thêm các ví dụ về Mẫu Đối sánh)",
+          "baseEl": {
+            "title": "Phần tử cơ sở (tùy chọn)",
+            "description": "Automa sẽ bắt đầu lại việc quan sát phần tử mục tiêu khi phần tử này thay đổi"
+          },
+          "subtree": {
+            "title": "Bao gồm cây con",
+            "description": "Mở rộng giám sát cho toàn bộ cây con của phần tử mục tiêu"
+          },
+          "childList": {
+            "title": "Danh sách con",
+            "description": "Giám sát việc thêm các phần tử con mới hoặc xóa các phần tử con hiện có"
+          },
+          "attributes": {
+            "title": "Thuộc tính",
+            "description": "Theo dõi các thay đổi đối với giá trị của các thuộc tính trên phần tử mục tiêu"
+          },
+          "attributeFilter": {
+            "title": "Bộ lọc thuộc tính",
+            "separate": "Sử dụng dấu phẩy (,) để phân tách tên thuộc tính",
+            "description": "Chỉ theo dõi các thuộc tính cụ thể (để trống để theo dõi tất cả)"
+          },
+          "characterData": {
+            "title": "Dữ liệu ký tự",
+            "description": "Theo dõi các thay đổi đối với dữ liệu / văn bản ký tự trong phần tử đích"
+          }
         },
         "items": {
           "manual": "Thủ công",
           "interval": "Chu kỳ",
+          "cron-job": "Lập lịch công việc",
           "date": "Vào một ngày cụ thể",
+          "context-menu": "Danh mục",
+          "element-change": "Khi phần tử thay đổi",
           "specific-day": "Vào một ngày cụ thể",
           "visit-web": "Khi truy cập một trang web",
+          "on-startup": "Khi khởi động trình duyệt",
           "keyboard-shortcut": "Phim tắt"
         }
       },
@@ -68,7 +376,48 @@
         "name": "Thực thi quy trình",
         "overwriteNote": "Thao tác này sẽ ghi đè lên dữ liệu chung của quy trình đã chọn",
         "select": "Chọn quy trình",
-        "description": ""
+        "executeId": "Thực thi ID (tùy chọn)",
+        "description": "",
+        "insertAllVars": "Chèn tất cả các biến quy trình làm việc hiện tại",
+        "insertVars": "Chèn các biến quy trình công việc hiện tại",
+        "useCommas": "Sử dụng dấu phẩy để phân tách tên biến"
+      },
+       "google-sheets": {
+        "name": "Google sheets",
+        "description": "Đọc hoặc cập nhật dữ liệu Google Trang tính",
+        "previewData": "Xem trước dữ liệu",
+        "firstRow": "Sử dụng hàng đầu tiên làm khóa",
+        "keysAsFirstRow": "Sử dụng các phím làm hàng đầu tiên",
+        "insertData": "Chèn dữ liệu",
+        "valueInputOption": "Tùy chọn nhập giá trị",
+        "insertDataOption": "Chèn tùy chọn dữ liệu",
+        "rangeToSearch": "Phạm vi bắt đầu tìm kiếm",
+        "dataFrom": {
+          "label": "Dữ liệu từ",
+          "options": {
+            "data-columns": "Bảng",
+            "custom": "Tùy chỉnh"
+          }
+        },
+        "refKey": {
+          "label": "Khóa tham chiếu (tùy chọn)",
+          "placeholder": "Tên khóa"
+        },
+        "spreadsheetId": {
+          "label": "Id bảng tính",
+          "link": "Xem cách lấy ID bảng tính"
+        },
+        "range": {
+          "label": "Phạm vi",
+          "link": "Bấm để xem thêm ví dụ"
+        },
+        "select": {
+          "get": "Nhận giá trị ô bảng tính",
+          "getRange": "Nhận phạm vi bảng tính",
+          "update": "Cập nhật giá trị ô bảng tính",
+          "append": "Nối các giá trị ô bảng tính",
+          "clear": "Xóa giá trị ô bảng tính"
+        }
       },
       "active-tab": {
         "name": "Tab hoạt động",
@@ -86,6 +435,13 @@
       "new-window": {
         "name": "Cửa sổ mới",
         "description": "Tạo cửa sổ mới",
+        "top": "Trên",
+        "left": "Trái",
+        "height": "Chiều cao",
+        "width": "Chiều rộng",
+        "note": "Lưu ý: sử dụng số 0 để tắt",
+        "position": "Vị trí cửa sổ",
+        "size": "Kích thước cửa sổ",
         "windowState": {
           "placeholder": "Trạng thái cửa sổ",
           "options": {
@@ -111,8 +467,9 @@
       "close-tab": {
         "name": "Đóng tab",
         "description": "",
+        "url": "URL hoặc match pattern",
         "activeTab": "Đóng activeTab",
-        "url": "URL hoặc match pattern"
+        "allWindows": "Đóng tất cả các cửa sổ"
       },
       "event-click": {
         "name": "Nhấp vào phần tử",
@@ -126,9 +483,14 @@
           "placeholder": "(mili giây)"
         }
       },
+      "parameter-prompt": {
+        "name": "Nhắc tham số"
+      },
       "get-text": {
         "name": "Trích văn bản",
         "description": "Trích văn bản từ một phần tử",
+        "checkbox": "Chèn vào bảng",
+        "includeTags": "Bao gồm các thẻ HTML",
         "prefixText": {
           "placeholder": "Tiền tố văn bản",
           "title": "Thêm tiền tố vào văn bản"
@@ -140,7 +502,18 @@
       },
       "export-data": {
         "name": "Xuất dữ liệu",
-        "description": "Xuất dữ liệu của quy trình"
+        "description": "Xuất dữ liệu của quy trình",
+        "exportAs": "Xuất file tại",
+        "refKey": "Khóa liên kết",
+        "bomHeader": "Thêm UTF-8 BOM",
+        "dataToExport": {
+          "placeholder": "Dữ liệu để xuất",
+          "options": {
+            "data-columns": "Bảng",
+            "google-sheets": "Google sheets",
+            "variable": "Biến"
+          }
+        }
       },
       "element-scroll": {
         "name": "Cuộn",
@@ -155,8 +528,11 @@
       "new-tab": {
         "name": "Tab mới",
         "description": "",
+        "url": "URL tab mới",
+        "customUserAgent": "Sử dụng tác nhân người dùng tùy chỉnh",
         "activeTab": "Đặt làm tab hoạt động",
         "tabToGroup": "Thêm tab vào nhóm",
+        "waitTabLoaded": "Chờ cho đến khi tab được tải",
         "updatePrevTab": {
           "title": "Sử dụng tab mới đã mở trước đó thay vì tạo tab mới",
           "text": "Cập nhật tab đã mở trước đó"
@@ -172,7 +548,12 @@
         "forms": {
           "name": "Tên thuộc tính",
           "checkbox": "Lưu dữ liệu",
-          "column": "Chọn cột"
+          "column": "Chọn cột",
+          "extraRow": {
+            "checkbox": "Thêm hàng bổ sung",
+            "placeholder": "Giá trị",
+            "title": "Giá trị của hàng phụ"
+          }
         }
       },
       "forms": {
@@ -205,6 +586,14 @@
         "description": "Thực thi code Javascript trong trang web",
         "availabeFuncs": "Các hàm có sẵn:",
         "removeAfterExec": "Xóa sau khi khối được thực thi",
+        "everyNewTab": "Thực thi mọi tab mới",
+        "context": {
+          "name": "Bối cảnh thực thi",
+          "items": {
+            "website": "Tab hoạt động",
+            "background": "Nền"
+          }
+        },
         "modal": {
           "tabs": {
             "code": "JavaScript code",
@@ -223,13 +612,17 @@
       },
       "conditions": {
         "name": "Điều kiện",
+        "add": "Thêm điều kiện",
         "description": "Khối có điều kiện",
+        "retryConditions": "Thử lại nếu tất cả các điều kiện không được đáp ứng",
+        "refresh": "Làm mới các kết nối điều kiện",
         "fallbackTitle": "Thực thi khi tất cả các phép so sánh không đáp ứng yêu cầu",
         "equals": "Ngang bằng",
         "gt": "Lớn hơn",
         "gte": "Lớn hơn hoặc ngang bằng",
         "lt": "Nhỏ hơn",
         "lte": "Nhỏ hơn hoặc ngang bằng",
+        "ne": "Không bằng",
         "contains": "Bao hàm"
       },
       "element-exists": {
@@ -237,6 +630,7 @@
         "description": "Kiểm tra xem một phần tử có tồn tại không",
         "selector": "Bộ chọn phần tử",
         "fallbackTitle": "Thực thi khi phần tử không tồn tại",
+        "throwError": "Ném lỗi nếu không tồn tại",
         "tryFor": {
           "title": "Cố gắng kiểm tra xem phần tử có tồn tại không",
           "label": "Số lần thử"
@@ -251,6 +645,8 @@
         "description": "Webhook cho phép dịch vụ bên ngoài được thông báo",
         "url": "The Post receive URL",
         "contentType": "Chọn một loại nội dung",
+        "method": "Yêu cầu phương thức",
+        "fallback": "Thực thi khi không thành công hoặc lỗi khi thực hiện một yêu cầu HTTP",
         "buttons": {
           "header": "Thêm header"
         },
@@ -260,13 +656,36 @@
         },
         "tabs": {
           "headers": "Headers",
-          "body": "Nội dung"
+          "body": "Nội dung",
+          "response": "Phản hồi"
+        }
+      },
+      "while-loop": {
+        "name": "Trong khi lặp lại",
+        "description": "Thực thi các khối khi điều kiện được đáp ứng",
+        "editCondition": "Chỉnh sửa điều kiện",
+        "fallback": "Thực thi khi điều kiện sai"
+      },
+      "loop-elements": {
+        "name": "Yếu tố vòng lặp",
+        "description": "Lặp lại qua các phần tử",
+        "loadMore": "Tải thêm các phần tử",
+        "scrollToBottom": "Cuộn xuống dưới cùng",
+        "actions": {
+          "none": "Không có",
+          "click-element": "Nhấp vào một phần tử để tải thêm",
+          "scroll": "Cuộn xuống để tải thêm",
+          "click-link": "Nhấp vào liên kết để tải thêm"
         }
       },
       "loop-data": {
-        "name": "Loop data",
+        "name": "Dữ liệu vòng lặp",
         "description": "Lặp lại qua các cột dữ liệu hoặc dữ liệu tùy chỉnh của bạn",
-        "loopId": "Loop ID",
+        "loopId": "ID Vòng lặp",
+         "refKey": "Khóa liên kết",
+        "startIndex": "Bắt đầu từ chỉ mục",
+        "resumeLastWorkflow": "Tiếp tục quy trình làm việc cuối cùng",
+        "reverse": "Thứ tự vòng lặp đảo ngược",
         "modal": {
           "fileTooLarge": "Tệp quá lớn để chỉnh sửa",
           "maxFile": "Kích thước tệp tối đa là 1MB",
@@ -289,8 +708,12 @@
           "toNumber": "Đến số",
           "options": {
             "numbers": "Số liệu",
+            "variable": "Biến",
             "data-columns": "Cột dữ liệu",
-            "custom-data": "Dữ liệu tùy chỉnh"
+            "table": "Bảng",
+            "custom-data": "Dữ liệu tùy chỉnh",
+            "google-sheets": "Google sheets",
+            "elements": "Các yếu tố"
           }
         }
       },
@@ -300,8 +723,17 @@
       },
       "take-screenshot": {
         "name": "Chụp màn hình",
+        "fullPage": "Chụp ảnh màn hình toàn trang",
         "description": "Chụp màn hình của tab đang hoạt động",
-        "imageQuality": "Chất lượng hình ảnh"
+        "imageQuality": "Chất lượng hình ảnh",
+        "saveToColumn": "Chèn ảnh chụp màn hình vào bảng",
+        "saveToComputer": "Lưu ảnh chụp màn hình vào máy tính",
+        "types": {
+          "title": "Chụp ảnh màn hình của",
+          "page": "Trang",
+          "fullpage": "Toàn trang",
+          "element": "Một yếu tố"
+        }
       },
       "switch-to": {
         "name": "Chuyển đổi frame",

+ 21 - 7
src/locales/vi/common.json

@@ -1,12 +1,16 @@
 {
   "common": {
     "dashboard": "Bảng điều khiển",
-    "workflow": "Quy trình | Các quy trình",
-    "collection": "Bộ sưu tập | Các bộ sưu tập",
-    "log": "Nhật ký | Nhật ký",
-    "block": "Khối | Khối",
+    "workflow": "Quy trình | Danh sách quy trình",
+    "collection": "Bộ sưu tập | Danh sách bộ sưu tập",
+    "log": "Nhật ký | Danh sách nhật ký",
+    "block": "Khối | Danh sách khối",
+    "schedule": "Lịch trình",
+    "folder": "Thư mục | Danh sách thư mục",
+    "new": "Mới",
     "docs": "Tài liệu",
     "search": "Tìm kiếm",
+    "example": "Ví dụ | Danh sách ví dụ",
     "import": "Nhập",
     "export": "Xuất",
     "rename": "Đổi tên",
@@ -20,8 +24,11 @@
     "all": "Tất cả",
     "add": "Thêm",
     "save": "Lưu",
-    "data": "dữ liệu",
+    "data": "Dữ liệu",
     "stop": "Dừng lại",
+    "action": "Hành động | Danh sách jành động",
+    "packages": "Gói",
+    "storage": "Kho",
     "editor": "Trình biên tập",
     "running": "Đang chạy",
     "globalData": "Dữ liệu chung",
@@ -31,7 +38,12 @@
     "disabled": "Đã vô hiệu hóa",
     "enable": "Kích hoạt",
     "fallback": "Dự phòng",
-    "update": "Cập nhật"
+    "update": "Cập nhật",
+    "feature": "Tính năng",
+    "duplicate": "Nhân bản",
+    "password": "Mật khẩu",
+    "category": "Loại",
+    "optional": "Không bắt buộc"
   },
   "message": {
     "noBlock": "Không có khối",
@@ -40,8 +52,10 @@
     "useDynamicData": "Tìm hiểu cách thêm dữ liệu động",
     "delete": "Bạn có chắc chắn muốn xóa \"{name}\"?",
     "empty": "Mục này của bạn có vẻ đang bị trống",
-    "notSaved": "Bạn thực sự muốn thoát? Bạn có một số thay đổi chưa được lưu!",
     "maxSizeExceeded": "Kích thước tệp vượt quá mức tối đa cho phép",
+    "notSaved": "Bạn thực sự muốn thoát? Bạn có một số thay đổi chưa được lưu!",
+    "somethingWrong": "Đã xảy ra sự cố",
+    "limitExceeded": "Bạn đã vượt quá giới hạn"
   },
   "sort": {
     "sortBy": "Sắp xếp theo",

+ 346 - 12
src/locales/vi/newtab.json

@@ -1,52 +1,365 @@
 {
   "home": {
-    "viewAll": "Tất cả"
+    "viewAll": "Tất cả",
+    "communities": "Cộng đồng"
+  },
+  "welcome": {
+    "title": "Chào mừng bạn đến với Automa! 🎉",
+    "text": "Bắt đầu bằng cách đọc tài liệu hoặc duyệt quy trình công việc trong Automa Marketplace.",
+    "marketplace": "Cửa hàng"
+  },
+  "packages": {
+    "name": "Gói hàng | Danh sách gói",
+    "add": "Thêm gói",
+    "icon": "Biểu tượng gói",
+    "open": "Mở gói",
+    "new": "Gói mới",
+    "set": "Đặt dưới dạng một gói",
+    "settings": {
+      "asBlock": "Đặt gói dưới dạng khối"
+    },
+    "categories": {
+      "my": "Gói của tôi",
+      "installed": "Các gói đã cài đặt"
+    }
+  },
+  "scheduledWorkflow": {
+    "title": "Quy trình làm việc đã lên lịch",
+    "nextRun": "Lần chạy tiếp theo",
+    "active": "Kích hoạt",
+    "refresh": "Tải lại",
+    "schedule":{
+      "title": "Lịch trình",
+      "types": {
+        "everyDay": "Hằng ngày",
+        "general": "Mỗi {time}",
+        "interval": "Cứ {time} phút một lần"
+      }
+    }
+  },
+  "storage": {
+    "title": "Kho",
+    "table": {
+      "add": "Thêm bảng",
+      "createdAt": "Được tạo lúc",
+      "modifiedAt": "Đã sửa đổi lúc",
+      "rowsCount": "Số hàng",
+      "delete": "Xóa bảng"
+    }
+  },
+  "credential": {
+    "title": "Thông tin xác thực | Danh sách thông tin xác thực",
+    "add": "Thêm thông tin đăng nhập",
+    "use": {
+      "title": "Thông tin đăng nhập đã sử dụng",
+      "description": "Quy trình làm việc này sử dụng các thông tin xác thực này"
+    }
+  },
+  "workflowPermissions": {
+    "title": "Quyền quy trình làm việc",
+    "description": "Dòng công việc này yêu cầu các quyền này chạy đúng cách",
+    "contextMenus": {
+      "title": "Danh mục",
+      "description": "Để thực thi quy trình làm việc qua menu ngữ cảnh"
+    },
+    "clipboardRead": {
+      "title": "Bảng tạm",
+      "description": "Để truy cập dữ liệu khay nhớ tạm"
+    },
+    "notifications": {
+      "title": "Thông báo",
+      "description": "Để hiển thị một thông báo"
+    },
+    "downloads": {
+      "title": "Tải xuống",
+      "description": "Lưu nội dung trang và đổi tên tệp đã tải xuống"
+    },
+    "cookies": {
+      "title": "Cookies",
+      "description": "Đọc, đặt hoặc xóa cookie"
+    }
+  },
+  "updateMessage": {
+    "text1": "Automa đã được cập nhật lên v{version},",
+    "text2": "xem có gì mới."
+  },
+  "workflows": {
+    "folder": {
+      "new": "Thư mục mới",
+      "name": "Tên thư mục",
+      "delete": "Xóa thư mục",
+      "rename": "Đổi tên thư mục"
+    }
+  },
+  "auth": {
+    "title": "Xác thực",
+    "signIn": "Đăng nhập",
+    "username": "Bạn cần đặt tên người dùng của mình trước",
+    "clickHere": "Bấm vào đây",
+    "text": "Bạn cần phải đăng nhập trước khi có thể làm điều đó"
+  },
+  "running": {
+    "start": "Bắt đầu vào {date}",
+    "message": "Điều này chỉ hiển thị 5 bản ghi cuối cùng"
   },
   "settings": {
+    "theme": "Chủ đề",
+    "shortcuts": {
+      "duplicate": "Lối tắt đã được sử dụng bởi \"{name}\""
+    },
+     "editor": {
+      "title": "Tiêu đề",
+      "curvature": {
+        "title": "Đường cong",
+        "line": "Hàng",
+        "reroute": "Định tuyến",
+        "rerouteFirstLast": "Định tuyến lại điểm đầu tiên và điểm cuối cùng"
+      },
+      "arrow": {
+        "title": "Mũi tên dòng",
+        "description": "Thêm mũi tên vào cuối dòng"
+      },
+      "snapGrid": {
+        "title": "Bám vào lưới",
+        "description": "Bám vào lưới khi di chuyển một khối"
+      }
+    },
+    "deleteLog": {
+      "title": "Tự động xóa nhật ký quy trình làm việc",
+      "after": "Xóa sau",
+      "deleteAfter": {
+        "never": "Không bao giờ",
+        "days": "{day} ngày"
+      }
+    },
     "language": {
       "label": "Ngôn ngữ",
       "helpTranslate": "Không tìm thấy ngôn ngữ của bạn? Hãy đóng góp bản dịch với chúng tôi.",
       "reloadPage": "Tải lại trang để hoàn tất thao tác"
+    },
+    "menu": {
+      "backup": "Sao lưu quy trình làm việc",
+      "editor": "Biên tập viên",
+      "general": "Chung",
+      "shortcuts": "Các phím tắt",
+      "about": "Thông tin"
+    },
+    "backupWorkflows": {
+      "title": "Sao lưu cục bộ",
+      "invalidPassword": "Mật khẩu không hợp lệ",
+      "workflowsAdded": "{count} quy trình công việc đã được thêm vào",
+      "name": "Sao lưu quy trình công việc",
+      "needSignin": "Trước tiên bạn cần đăng nhập vào tài khoản của mình",
+      "backup": {
+        "button": "Sao lưu",
+        "encrypt": "Mã hóa bằng mật khẩu"
+      },
+      "restore": {
+        "title": "Khôi phục quy trình làm việc",
+        "button": "Khôi phục",
+        "update": "Cập nhật nếu quy trình làm việc tồn tại"
+      },
+      "cloud": {
+        "buttons": {
+          "local": "Cục bộ",
+          "cloud": "Đám mây"
+        },
+        "location": "Địa điểm",
+        "delete": "Xóa bản sao lưu",
+        "title": "Sao lưu dữ liệu đám mây",
+        "sync": "Đồng bộ",
+        "lastSync": "Lần đồng bộ cuối cùng",
+        "lastBackup": "Sao lưu cuối cùng",
+        "select": "Chọn quy trình làm việc",
+        "storedWorkflows": "Quy trình làm việc được lưu trữ trên đám mây",
+        "selected": "Đã chọn",
+        "selectText": "Chọn quy trình công việc mà bạn muốn sao lưu",
+        "selectAll": "Chọn tất cả",
+        "deselectAll": "Bỏ chọn tất cả",
+        "needSelectWorkflow": "Bạn cần chọn quy trình công việc mà bạn muốn sao lưu"
+      }
     }
   },
   "workflow": {
+    "previewMode": {
+      "title": "Chế độ xem trước",
+      "description": "Bạn đang ở chế độ xem trước, những thay đổi bạn đã thực hiện sẽ không được lưu"
+    },
+    "pinWorkflow": {
+      "pin": "Ghim quy trình làm việc",
+      "unpin": "Bỏ ghim quy trình làm việc",
+      "pinned": "Quy trình công việc được ghim"
+    },
+    "my": "Quy trình làm việc của tôi",
     "import": "Nhập quy trình",
     "new": "Tạo quy trình mới",
     "delete": "Xóa quy trình",
+    "browse": "Duyệt quy trình công việc",
     "name": "Tên quy trình",
     "rename": "Sửa tên quy trình",
+    "backupCloud": "Sao lưu quy trình công việc lên đám mây",
     "add": "Thêm quy trình",
     "clickToEnable": "Nhấn để kích hoạt",
+    "toggleSidebar": "Chuyển đổi thanh bên",
+    "cantEdit": "Không thể chỉnh sửa quy trình làm việc được chia sẻ",
+    "undo": "Hoàn tác",
+    "redo": "Làm lại",
+    "autoAlign": {
+      "title": "Tự động căn chỉnh"
+    },
+    "blocksFolder": {
+      "title": "Thư mục khối",
+      "add": "Thêm khối vào thư mục",
+      "save": "Lưu vào thư mục"
+    },
+    "searchBlocks": {
+      "title": "Các khối tìm kiếm trong trình chỉnh sửa"
+    },
+    "conditionBuilder": {
+      "title": "Trình tạo điều kiện",
+      "add": "Add Thêm điều kiện",
+      "and": "Và",
+      "or": "Hoặc",
+      "topAwait": "Hỗ trợ chức năng chờ đợi cấp cao nhất và \"automaRefData \""
+    },
+    "host": {
+      "title": "Lưu trữ quy trình làm việc",
+      "set": "Đặt làm quy trình làm việc trên máy chủ lưu trữ",
+      "id": "ID Máy chủ",
+      "add": "Thêm quy trình làm việc được lưu trữ",
+      "sync": {
+        "title": "Đồng bộ",
+        "description": "Đồng bộ hóa với quy trình làm việc trên máy chủ"
+      },
+      "messages": {
+        "hostExist": "Bạn đã thêm máy chủ này",
+        "notFound": "Không thể tìm thấy quy trình làm việc được lưu trữ với id \"{id} \""
+      }
+    },
+    "type": {
+      "local": "Cục bộ",
+      "shared": "Được chia sẻ",
+      "host": "Máy chủ"
+    },
+    "unpublish": {
+      "title": "Hủy xuất bản quy trình làm việc",
+      "button": "Hủy xuất bản",
+      "body": "Bạn có chắc chắn muốn hủy xuất bản quy trình làm việc \"{name} \" không?"
+    },
+    "share": {
+      "url": "Chia sẻ URL",
+      "publish": "Xuất bản",
+      "sharedAs": "Được chia sẻ với tên \"{name} \"",
+      "title": "Chia sẻ quy trình làm việc",
+      "download": "Thêm quy trình làm việc vào cục bộ",
+      "edit": "Chỉnh sửa Mô tả",
+      "fetchLocal": "Tìm nạp quy trình làm việc cục bộ",
+      "update": "Cập nhật",
+      "unpublish": "Hủy xuất bản"
+    },
+    "variables": {
+      "title": "Biến | Danh sách biến",
+      "name": "Tên biến",
+      "assign": "Gán cho biến"
+    },
+    "protect": {
+      "title": "Bảo vệ quy trình làm việc ",
+      "remove": "Loại bỏ bảo vệ",
+      "button": "Bảo vệ",
+      "note": "Lưu ý: bạn phải nhớ mật khẩu này, mật khẩu này sẽ được yêu cầu để chỉnh sửa và xóa quy trình làm việc sau này."
+    },
+    "locked": {
+      "title": "Dòng công việc này được bảo vệ",
+      "body": "Nhập mật khẩu để mở khóa",
+      "unlock": "Mở khóa",
+      "messages": {
+        "incorrect-password": "Mật khẩu không đúng"
+      }
+    },
     "state": {
       "executeBy": "Thực hiện bởi: \"{name}\""
     },
-    "dataColumns": {
-      "title": "Cơ sở dữ liệu Cột",
+    "table": {
+      "title": "Bảng | Danh sách bảng",
       "placeholder": "Tìm kiếm hoặc thêm cột",
+      "select": "Chọn cột",
       "column": {
-        "name": "Tên cột",
-        "type": "Kiểu dữ liệu"
+        "name": "Tên cột dọc",
+        "type": "Loại dữ liệu"
       }
     },
     "sidebar": {
-      "workflowIcon": "Icon"
+      "workflowIcon": "Biểu tượng quy trình làm việc"
     },
     "editor": {
       "zoomIn": "Phóng to",
       "zoomOut": "Thu nhỏ",
       "resetZoom": "Về mặc định",
-      "duplicate": "Nhân bản"
+      "duplicate": "Nhân bản",
+      "copy": "Sao chép",
+      "paste": "Dán",
+      "group": "Nhóm khối",
+      "ungroup": "Bỏ nhóm các khối"
     },
     "settings": {
+      "saveLog": "Lưu nhật ký quy trình làm việc",
+      "executedBlockOnWeb": "Hiển thị khối đã thực thi trên trang web",
+      "notification": {
+        "title": "Thông báo quy trình làm việc",
+        "description": "Hiển thị trạng thái dòng công việc (thành công hay không thành công) sau khi nó được thực thi",
+        "noPermission": "Automa yêu cầu quyền \"thông báo \" để làm cho điều này hoạt động"
+      },
+      "publicId": {
+        "title": "ID công khai quy trình làm việc",
+        "description": "Sử dụng id công khai này để thực thi quy trình công việc bằng sự kiện tùy chỉnh JS"
+      },
+      "defaultColumn": {
+        "title": "Chèn vào cột mặc định",
+        "description": "Chèn dữ liệu vào cột mặc định nếu không có cột nào được chọn trong khối",
+        "name": "Tên cột mặc định"
+      },
+      "autocomplete": {
+        "title": "Tự động hoàn thành",
+        "description": "Bật tự động hoàn thành trong khối đầu vào (tắt nếu nó làm cho Automa không ổn định)"
+      },
+      "clearCache": {
+        "title": "Xóa bộ nhớ cache",
+        "description": "Xóa bộ nhớ cache (chỉ mục trạng thái và vòng lặp) của quy trình làm việc",
+        "info": "Xóa thành công bộ nhớ cache của quy trình làm việc",
+        "btn": "Xóa"
+      },
+      "reuseLastState": {
+        "title": "Sử dụng lại trạng thái quy trình làm việc cuối cùng",
+        "description": "Sử dụng dữ liệu trạng thái (bảng, biến và dữ liệu toàn cục) từ quy trình làm việc được thực thi gần đây nhất "
+      },
+      "debugMode": {
+        "title": "Chế độ kiểm tra sửa lỗi",
+        "description": "Thực thi quy trình làm việc bằng Giao thức Chrome DevTools"
+      },
+      "restartWorkflow": {
+        "for": "Khởi động lại cho",
+        "times": "Times",
+        "description": "Tối đa bao nhiêu lần quy trình làm việc sẽ khởi động lại"
+      },
       "onError": {
         "title": "Khi quy trình gặp lỗi",
+        "description": "Đặt những việc cần làm khi xảy ra lỗi trong quy trình làm việc",
         "items": {
           "keepRunning": "Tiếp tục chạy",
-          "stopWorkflow": "Dừng quy trình"
+          "stopWorkflow": "Dừng quy trình",
+          "restartWorkflow": "Khởi động lại quy trình làm việc"
         }
       },
       "timeout": {
         "title": "Thời lượng thực thi tối đa (Mili giây)"
+      },
+      "blockDelay": {
+        "title": "Chậm trễ khối (mili giây)",
+        "description": "Thêm độ trễ trước khi thực hiện từng khối"
+      },
+      "tabLoadTimeout": {
+        "title": "Thời gian chờ tải tab",
+        "description": "Thời gian tối đa để tải tab tính bằng mili giây, vượt qua 0 để tắt thời gian chờ."
       }
     }
   },
@@ -69,7 +382,9 @@
     }
   },
   "log": {
+    "flowId": "ID Luồng",
     "goBack": "Trở lại nhật ký \"{name}\"",
+    "goWorkflow": "Đi tới quy trình làm việc",
     "startedDate": "Ngày bắt đầu",
     "duration": "Thời lượng",
     "selectAll": "Chọn tất cả",
@@ -80,14 +395,33 @@
       "finish": "Hoàn thành"
     },
     "messages": {
+      "url-empty": "URL trống",
+      "invalid-url": "URL không hợp lệ",
+      "conditions-empty": "Điều kiện trống",
       "workflow-disabled": "Quy trình đã được vô hiệu hóa",
+      "selector-empty": "Bộ chọn phần tử trống",
+      "invalid-body": "Nội dung không phải là JSON hợp lệ",
+      "invalid-active-tab": "\"{url} \" là URL không hợp lệ",
+      "empty-spreadsheet-id": "Id bảng tính trống",
+      "invalid-loop-data": "Dữ liệu không hợp lệ để lặp lại",
+      "empty-workflow": "Đầu tiên, bạn phải chọn một quy trình",
+      "active-tab-removed": "Tab hoạt động của quy trình làm việc bị xóa",
+      "empty-spreadsheet-range": "Phạm vi bảng tính trống",
       "stop-timeout": "Quy trình đã bị dừng vì hết thời lượng thực thi",
       "invalid-proxy-host": "Máy chủ proxy không hợp lệ",
-      "no-iframe-id": "Không tìm thấy Frame ID cho iframe element với bộ chọn \"{selector}\"",
-      "no-tab": "Không thể kết nối với một tab, dùng \"New tab\" hoặc khối \"Active tab\" trước khi dùng khối \"{name}\".",
-      "empty-workflow": "Đầu tiên, bạn phải chọn một quy trình",
+      "no-file-access": "Automa không có quyền truy cập vào tệp ",
       "no-workflow": "Không tìm thấy quy trình với ID \"{workflowId}\"",
-      "workflow-infinite-loop": "Quy trình không được thực thi để ngăn vòng lặp vô hạn"
+      "no-match-tab": "Không thể tìm thấy tab có các mẫu \"{pattern} \"",
+      "no-clipboard-acces": "Không có quyền truy cập khay nhớ tạm",
+      "browser-not-supported": "Tính năng này không được hỗ trợ trong trình duyệt {browser}",
+      "element-not-found": "Không thể tìm thấy phần tử có bộ chọn \"{selector} \".",
+      "no-permission": "Không có quyền \"{allow} \" để thực hiện tác vụ này",
+      "not-iframe": "Phần tử có bộ chọn \"{selector} \" không phải là phần tử Iframe",
+      "iframe-not-found": "Không thể tìm thấy phần tử Iframe bằng bộ chọn \"{selector} \".",
+      "workflow-infinite-loop": "Quy trình không được thực thi để ngăn vòng lặp vô hạn",
+      "not-debug-mode": "Dòng công việc phải chạy ở chế độ gỡ lỗi để khối này hoạt động bình thường",
+      "no-iframe-id": "Không tìm thấy Frame ID cho iframe element với bộ chọn \"{selector}\"",
+      "no-tab": "Không thể kết nối với một tab, dùng \"New tab\" hoặc khối \"Active tab\" trước khi dùng khối \"{name}\"."
     },
     "description": {
       "text": "{status} vào {date} trong {duration}",

+ 21 - 2
src/locales/vi/popup.json

@@ -1,5 +1,20 @@
 {
+  "recording": {
+    "stop": "Dừng ghi",
+    "title": "Ghi âm"
+  },
   "home": {
+    "record": {
+      "title": "Ghi lại quy trình làm việc",
+      "button": "Ghi lại",
+      "name": "Tên quy trình làm việc",
+      "selectBlock": "Chọn một khối để bắt đầu",
+      "anotherBlock": "Không thể bắt đầu từ khối này",
+      "tabs": {
+        "new": "Quy trình làm việc mới",
+        "existing": "Quy trình làm việc hiện tại"
+      }
+    },
     "elementSelector": {
       "name": "Bộ chọn phần tử",
       "noAccess": "Không có quyền truy cập vào trang web này"
@@ -7,7 +22,11 @@
     "workflow": {
       "new": "Tạo quy trình mới",
       "rename": "Đổi tên quy trình",
-      "delete": "Xóa quy trình"
-    },
+      "delete": "Xóa quy trình",
+      "type": {
+        "host": "Máy chủ",
+        "local": "Cục bộ",
+      }
+    }
   }
 }

+ 2 - 2
src/newtab/App.vue

@@ -230,8 +230,8 @@ const messageEvents = {
         });
     }
   },
-  'workflow:execute': function ({ data, options = {} }) {
-    startWorkflowExec(data, options);
+  'workflow:execute': function ({ data }) {
+    startWorkflowExec(data, data?.options ?? {});
   },
   'recording:stop': stopRecording,
   'background--recording:stop': stopRecording,

+ 391 - 0
src/newtab/utils/blocksValidation.js

@@ -0,0 +1,391 @@
+import browser from 'webextension-polyfill';
+
+const checkPermissions = (permissions) =>
+  browser.permissions.contains({ permissions });
+const isEmptyStr = (str) => !str.trim();
+const isFirefox = BROWSER_TYPE === 'firefox';
+const defaultOptions = {
+  once: false,
+};
+
+export async function validateTrigger(data) {
+  const errors = [];
+  const checkValue = (value, { name, location }) => {
+    if (value && value.trim()) return;
+
+    errors.push(`"${name}" is empty in the ${location}`);
+  };
+  const triggersValidation = {
+    'cron-job': (triggerData) => {
+      checkValue(triggerData.expression, {
+        name: 'Expression',
+        location: 'Cron job trigger',
+      });
+    },
+    'context-menu': async (triggerData) => {
+      const permission = isFirefox ? 'menus' : 'contextMenus';
+      const hasPermission = await checkPermissions([permission]);
+
+      if (!hasPermission) {
+        errors.push(
+          "Doesn't have permission for the Context menu trigger (ignore if you already grant the permissions)"
+        );
+      } else {
+        checkValue(triggerData.contextMenuName, {
+          name: 'Context menu name',
+          location: 'Context menu trigger',
+        });
+      }
+    },
+    date: (triggerData) => {
+      checkValue(triggerData.date, {
+        name: 'Date',
+        location: 'On a specific date tigger',
+      });
+    },
+    'visit-web': (triggerData) => {
+      checkValue(triggerData.url, {
+        name: 'URL',
+        location: 'Visit web trigger',
+      });
+    },
+    'keyboard-shortcut': (triggerData) => {
+      checkValue(triggerData.shortcut, {
+        name: 'Shortcut',
+        location: 'Shortcut trigger',
+      });
+    },
+  };
+
+  if (data.triggers) {
+    for (const trigger of data.triggers) {
+      const validate = triggersValidation[trigger.type];
+      if (validate) await validate(trigger.data);
+    }
+  } else {
+    const validate = triggersValidation[data.type];
+    if (validate) await validate(data);
+  }
+
+  return errors;
+}
+
+export async function validateExecuteWorkflow(data) {
+  if (isEmptyStr(data.workflowId)) return ['No workflow selected'];
+
+  return [];
+}
+
+export async function validateNewTab(data) {
+  if (isEmptyStr(data.url)) return ['URL is empty'];
+
+  return [];
+}
+
+export async function validateSwitchTab(data) {
+  const errors = [];
+  const validateItems = {
+    'match-patterns': () => {
+      if (isEmptyStr(data.matchPattern))
+        errors.push('The Match patterns is empty');
+    },
+    'tab-title': () => {
+      if (isEmptyStr(data.tabTitle)) errors.push('The Tab title is empty');
+    },
+  };
+
+  if (validateItems[data.findTabBy]) validateItems[data.findTabBy]();
+
+  return errors;
+}
+
+export async function validateProxy(data) {
+  if (isEmptyStr(data.host)) return ['The Host is empty'];
+
+  return [];
+}
+
+export async function validateCloseTab(data) {
+  if (data.closeType === 'tab' && !data.activeTab && isEmptyStr(data.url)) {
+    return ['The Match patterns is empty'];
+  }
+
+  return [];
+}
+
+export async function validateTakeScreenshot(data) {
+  if (data.type === 'element' && isEmptyStr(data.selector)) {
+    return ['The CSS selector is empty'];
+  }
+
+  return [];
+}
+
+export async function validateInteractionBasic(data) {
+  if (isEmptyStr(data.selector)) return ['The Selector is empty'];
+
+  return [];
+}
+
+export async function validateExportData(data) {
+  const errors = [];
+
+  const hasPermission = await checkPermissions(['downloads']);
+  if (!hasPermission)
+    errors.push(
+      "Don't have download permission (ignore if you already grant the permissions)"
+    );
+
+  if (data.dataToExport === 'variable' && isEmptyStr(data.variableName)) {
+    errors.push('The Variable name is empty');
+  } else if (data.dataToExport === 'google-sheets' && isEmptyStr(data.refKey)) {
+    errors.push('The Reference key is empty');
+  }
+
+  return errors;
+}
+
+export async function validateAttributeValue(data) {
+  const errors = [];
+
+  if (isEmptyStr(data.selector)) errors.push('The Selector is empty');
+  if (isEmptyStr(data.attributeName))
+    errors.push('The Attribute name is empty');
+
+  return errors;
+}
+
+export async function validateGoogleSheets(data) {
+  const errors = [];
+
+  if (isEmptyStr(data.spreadsheetId))
+    errors.push('The Spreadsheet Id is empty');
+  if (isEmptyStr(data.range)) errors.push('The Range is empty');
+
+  return errors;
+}
+
+export async function validateWebhook(data) {
+  if (isEmptyStr(data.url)) return ['The URL is empty'];
+
+  return [];
+}
+
+export async function validateLoopData(data) {
+  const errors = [];
+  if (isEmptyStr(data.loopId)) errors.push('The Loop id is empty');
+
+  const loopThroughItems = {
+    'google-sheets': () => {
+      if (isEmptyStr(data.referenceKey))
+        errors.push('The Reference key is empty');
+    },
+    variable: () => {
+      if (isEmptyStr(data.variableName))
+        errors.push('The Variable name is empty');
+    },
+  };
+  const validateItem = loopThroughItems[data.loopThrough];
+  if (validateItem) validateItem();
+
+  return errors;
+}
+
+export async function validateLoopElements(data) {
+  const errors = [];
+  if (isEmptyStr(data.loopId)) errors.push('The Loop id is empty');
+  if (isEmptyStr(data.selector)) errors.push('The Selector is empty');
+
+  if (
+    ['click-element', 'click-link'].includes(data.loadMoreAction) &&
+    isEmptyStr(data.actionElSelector)
+  ) {
+    errors.push('The Selector for loading more elements is empty');
+  }
+
+  return errors;
+}
+
+export async function validateClipboard() {
+  const permissions = isFirefox
+    ? ['clipboardRead', 'clipboardWrite']
+    : ['clipboardRead'];
+  const hasPermission = await checkPermissions(permissions);
+
+  if (!hasPermission)
+    return [
+      "Don't have permission to access the clipboard (ignore if you already grant the permissions)",
+    ];
+
+  return [];
+}
+
+export async function validateSwitchTo(data) {
+  if (data.windowType === 'iframe' && isEmptyStr(data.selector)) {
+    return ['The Selector for Iframe is empty'];
+  }
+
+  return [];
+}
+
+export async function validateUploadFile(data) {
+  const errors = [];
+
+  if (isEmptyStr(data.selector)) errors.push('The Selector is empty');
+
+  const someInputsEmpty = data.filePaths.some((path) => isEmptyStr(path));
+  if (someInputsEmpty) errors.push('Some of the file paths is empty');
+
+  return errors;
+}
+
+export async function validateSaveAssets(data) {
+  const errors = [];
+
+  const hasPermission = await checkPermissions(['downloads']);
+  if (!hasPermission)
+    errors.push(
+      "Don't have download permission (ignore if you already grant the permissions)"
+    );
+  else if (isEmptyStr(data.selector)) errors.push('The Selector is empty');
+
+  return errors;
+}
+
+export async function validatePressKey(data) {
+  const errors = [];
+
+  if (isEmptyStr(data.selector)) errors.push('The Selector is empty');
+
+  const isKeyEmpty =
+    !data.action || (data.action === 'press-key' && isEmptyStr(data.keys));
+  const isMultipleKeysEmpty =
+    data.action === 'multiple-keys' && isEmptyStr(data.keysToPress);
+  if (isKeyEmpty || isMultipleKeysEmpty)
+    errors.push('The Keys to press is empty');
+
+  return errors;
+}
+
+export async function validateNotification() {
+  const hasPermission = await checkPermissions(['notifications']);
+  if (!hasPermission) return ["Don't have notifications permissions"];
+
+  return [];
+}
+
+export default {
+  trigger: {
+    ...defaultOptions,
+    func: validateTrigger,
+  },
+  'execute-workflow': {
+    ...defaultOptions,
+    func: validateExecuteWorkflow,
+  },
+  'new-tab': {
+    ...defaultOptions,
+    func: validateNewTab,
+  },
+  'switch-tab': {
+    ...defaultOptions,
+    func: validateSwitchTab,
+  },
+  proxy: {
+    ...defaultOptions,
+    func: validateProxy,
+  },
+  'close-tab': {
+    ...defaultOptions,
+    func: validateCloseTab,
+  },
+  'take-screenshot': {
+    ...defaultOptions,
+    func: validateTakeScreenshot,
+  },
+  'event-click': {
+    ...defaultOptions,
+    func: validateInteractionBasic,
+  },
+  'get-text': {
+    ...defaultOptions,
+    func: validateInteractionBasic,
+  },
+  'export-data': {
+    ...defaultOptions,
+    func: validateExportData,
+  },
+  'element-scroll': {
+    ...defaultOptions,
+    func: validateInteractionBasic,
+  },
+  link: {
+    ...defaultOptions,
+    func: validateInteractionBasic,
+  },
+  'attribute-value': {
+    ...defaultOptions,
+    func: validateAttributeValue,
+  },
+  forms: {
+    ...defaultOptions,
+    func: validateInteractionBasic,
+  },
+  'trigger-event': {
+    ...defaultOptions,
+    func: validateInteractionBasic,
+  },
+  'google-sheets': {
+    ...defaultOptions,
+    func: validateGoogleSheets,
+  },
+  'element-exists': {
+    ...defaultOptions,
+    func: validateInteractionBasic,
+  },
+  webhook: {
+    ...defaultOptions,
+    func: validateWebhook,
+  },
+  'loop-data': {
+    ...defaultOptions,
+    func: validateLoopData,
+  },
+  'loop-elements': {
+    ...defaultOptions,
+    func: validateLoopElements,
+  },
+  clipboard: {
+    ...defaultOptions,
+    once: true,
+    func: validateClipboard,
+  },
+  'switch-to': {
+    ...defaultOptions,
+    func: validateSwitchTo,
+  },
+  'upload-file': {
+    ...defaultOptions,
+    func: validateUploadFile,
+  },
+  'hover-element': {
+    ...defaultOptions,
+    func: validateInteractionBasic,
+  },
+  'save-assets': {
+    ...defaultOptions,
+    func: validateSaveAssets,
+  },
+  'press-key': {
+    ...defaultOptions,
+    func: validatePressKey,
+  },
+  notification: {
+    ...defaultOptions,
+    func: validateNotification,
+  },
+  'create-element': {
+    ...defaultOptions,
+    func: validateInteractionBasic,
+  },
+};

+ 8 - 2
src/popup/pages/Home.vue

@@ -291,8 +291,14 @@ function openDashboard(url) {
     window.close();
   });
 }
-function initElementSelector() {
-  initElementSelectorFunc().then(() => {
+async function initElementSelector() {
+  const [tab] = await browser.tabs.query({
+    url: '*://*/*',
+    active: true,
+    currentWindow: true,
+  });
+  if (!tab) return;
+  initElementSelectorFunc(tab).then(() => {
     window.close();
   });
 }

+ 1 - 1
src/sandbox/utils/handleConditionCode.js

@@ -39,5 +39,5 @@ export default function (data) {
     },
   };
 
-  document.body.appendChild(script);
+  (document.body || document.documentElement).appendChild(script);
 }

+ 2 - 2
src/sandbox/utils/handleJavascriptBlock.js

@@ -27,7 +27,7 @@ export default function (data) {
     const scriptEl = document.createElement('script');
     scriptEl.textContent = item.script;
 
-    document.body.appendChild(scriptEl);
+    (document.body || document.documentElement).appendChild(scriptEl);
 
     return scriptEl;
   });
@@ -131,5 +131,5 @@ export default function (data) {
   };
 
   timeout = setTimeout(cleanUp, data.blockData.timeout);
-  document.body.appendChild(script);
+  (document.body || document.documentElement).appendChild(script);
 }

+ 5 - 2
src/utils/dataExporter.js

@@ -38,7 +38,7 @@ export function generateJSON(keys, data) {
 
 export default function (
   data,
-  { name, type, addBOMHeader, csvOptions, returnUrl },
+  { name, type, addBOMHeader, csvOptions, returnUrl, returnBlob },
   converted
 ) {
   let result = data;
@@ -71,7 +71,10 @@ export default function (
   }
 
   const { mime, ext } = files[type];
-  const blobUrl = URL.createObjectURL(new Blob(payload, { type: mime }));
+  const blob = new Blob(payload, { type: mime });
+  if (returnBlob) return blob;
+
+  const blobUrl = URL.createObjectURL(blob);
 
   if (!returnUrl) fileSaver(`${name || 'unnamed'}${ext}`, blobUrl);
 

+ 5 - 4
src/utils/getFile.js

@@ -55,14 +55,15 @@ function getLocalFile(path, options) {
         .then((response) => {
           if (!response.ok) throw new Error(response.statusText);
 
-          if (options.returnValue) {
-            resolve(response);
-            return Promise.resolve(null);
-          }
+          if (options.returnValue) return response.text();
 
           return response.blob();
         })
         .then((blob) => {
+          if (options.returnValue) {
+            resolve(blob);
+            return;
+          }
           if (!blob) return;
 
           if (URL.createObjectURL) {

+ 2 - 2
src/workflowEngine/WorkflowEngine.js

@@ -143,7 +143,7 @@ class WorkflowEngine {
           browser.windows.create({
             type: 'popup',
             width: 480,
-            height: window.screen.availHeight,
+            height: 650,
             url: browser.runtime.getURL(
               `/params.html?workflowId=${this.workflow.id}`
             ),
@@ -271,7 +271,7 @@ class WorkflowEngine {
   addLogHistory(detail) {
     if (detail.name === 'blocks-group') return;
 
-    const isLimit = this.history.length >= this.logsLimit;
+    const isLimit = this.history?.length >= this.logsLimit;
     const notErrorLog = detail.type !== 'error';
 
     if ((isLimit || !this.saveLog) && notErrorLog) return;

+ 2 - 104
src/workflowEngine/blocksHandler/handlerConditions.js

@@ -1,11 +1,9 @@
-import { customAlphabet } from 'nanoid/non-secure';
 import browser from 'webextension-polyfill';
 import compareBlockValue from '@/utils/compareBlockValue';
-import testConditions from '@/utils/testConditions';
+import testConditions from '../utils/testConditions';
 import renderString from '../templating/renderString';
-import { automaRefDataStr, messageSandbox, checkCSPAndInject } from '../helper';
+import checkCodeCondition from '../utils/conditionCode';
 
-const nanoid = customAlphabet('1234567890abcdef', 5);
 const isMV2 = browser.runtime.getManifest().manifest_version === 2;
 
 function checkConditions(data, conditionOptions) {
@@ -46,106 +44,6 @@ function checkConditions(data, conditionOptions) {
     testAllConditions();
   });
 }
-async function checkCodeCondition(activeTab, payload) {
-  const variableId = `automa${nanoid()}`;
-
-  if (
-    !payload.data.context ||
-    payload.data.context === 'website' ||
-    !payload.isPopup
-  ) {
-    if (!activeTab.id) throw new Error('no-tab');
-
-    const refDataScriptStr = automaRefDataStr(variableId);
-
-    if (!isMV2) {
-      const result = await checkCSPAndInject(
-        {
-          target: { tabId: activeTab.id },
-          debugMode: payload.debugMode,
-        },
-        () => {
-          return `
-          (async () => {
-            const ${variableId} = ${JSON.stringify(payload.refData)};
-            ${refDataScriptStr}
-            try {
-              ${payload.data.code}
-            } catch (error) {
-              return {
-                $isError: true,
-                message: error.message,
-              }
-            }
-          })();
-        `;
-        }
-      );
-
-      if (result.isBlocked) return result.value;
-    }
-
-    const [{ result }] = await browser.scripting.executeScript({
-      world: 'MAIN',
-      args: [payload, variableId, refDataScriptStr],
-      target: {
-        tabId: activeTab.id,
-        frameIds: [activeTab.frameId || 0],
-      },
-      func: ({ data, refData }, varId, refDataScript) => {
-        return new Promise((resolve, reject) => {
-          const varName = varId;
-
-          const scriptEl = document.createElement('script');
-          scriptEl.textContent = `
-            (async () => {
-              const ${varName} = ${JSON.stringify(refData)};
-              ${refDataScript}
-              try {
-                ${data.code}
-              } catch (error) {
-                return {
-                  $isError: true,
-                  message: error.message,
-                }
-              }
-            })()
-              .then((detail) => {
-                window.dispatchEvent(new CustomEvent('__automa-condition-code__', { detail }));
-              });
-          `;
-
-          document.documentElement.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
-          );
-        });
-      },
-    });
-    return result;
-  }
-  const result = await messageSandbox('conditionCode', payload);
-  if (result && result.$isError) throw new Error(result.message);
-
-  return result;
-}
 
 async function conditions({ data, id }, { prevBlockData, refData }) {
   if (data.conditions.length === 0) {

+ 16 - 1
src/workflowEngine/blocksHandler/handlerExportData.js

@@ -1,6 +1,14 @@
 import browser from 'webextension-polyfill';
 import { default as dataExporter, files } from '@/utils/dataExporter';
 
+function blobToBase64(blob) {
+  return new Promise((resolve) => {
+    const reader = new FileReader();
+    reader.onloadend = () => resolve(reader.result);
+    reader.readAsDataURL(blob);
+  });
+}
+
 async function exportData({ data, id }, { refData }) {
   const dataToExport = data.dataToExport || 'data-columns';
   let payload = refData.table;
@@ -21,14 +29,21 @@ async function exportData({ data, id }, { refData }) {
   const hasDownloadAccess = await browser.permissions.contains({
     permissions: ['downloads'],
   });
-  const blobUrl = dataExporter(payload, {
+  let blobUrl = dataExporter(payload, {
     ...data,
     csvOptions: {
       delimiter: data.csvDelimiter || ',',
     },
     returnUrl: hasDownloadAccess,
+    returnBlob: !this.engine.isPopup,
   });
 
+  if (!this.engine.isPopup && !hasDownloadAccess) {
+    throw new Error("Don't have download permission");
+  } else if (!this.engine.isPopup) {
+    blobUrl = await blobToBase64(blobUrl);
+  }
+
   if (hasDownloadAccess) {
     const filename = `${data.name || 'unnamed'}${files[data.type].ext}`;
     const options = {

+ 2 - 8
src/workflowEngine/blocksHandler/handlerSwitchTab.js

@@ -22,18 +22,12 @@ export default async function ({ data, id }) {
     throw new Error('no-tab');
   }
 
-  const currentWindow = await browser.windows.getCurrent();
-  if (currentWindow.focused)
-    await browser.windows.update(currentWindow.id, { focused: false });
-
   const isTabsQuery = ['match-patterns', 'tab-title'];
   const tabs =
-    findTabBy !== 'match-patterns'
-      ? await browser.tabs.query({ lastFocusedWindow: true })
-      : [];
+    findTabBy !== 'match-patterns' ? await browser.tabs.query({}) : [];
 
   if (isTabsQuery.includes(findTabBy)) {
-    const query = { lastFocusedWindow: true };
+    const query = {};
 
     if (data.findTabBy === 'match-patterns') query.url = data.matchPattern;
     else if (data.findTabBy === 'tab-title') query.title = data.tabTitle;

+ 1 - 1
src/workflowEngine/blocksHandler/handlerWebhook.js

@@ -1,6 +1,6 @@
 import objectPath from 'object-path';
 import { isWhitespace } from '@/utils/helper';
-import { executeWebhook } from '@/utils/webhookUtil';
+import { executeWebhook } from '../utils/webhookUtil';
 import renderString from '../templating/renderString';
 
 function fileReader(blob) {

+ 7 - 1
src/workflowEngine/blocksHandler/handlerWhileLoop.js

@@ -1,11 +1,17 @@
-import testConditions from '@/utils/testConditions';
+import testConditions from '../utils/testConditions';
+import checkCodeCondition from '../utils/conditionCode';
 
 async function whileLoop({ data, id }, { refData }) {
+  const { debugMode } = this.engine.workflow?.settings || {};
   const conditionPayload = {
     refData,
     isMV2: this.engine.isMV2,
     isPopup: this.engine.isPopup,
     activeTab: this.activeTab.id,
+    checkCodeCondition: (payload) => {
+      payload.debugMode = debugMode;
+      return checkCodeCondition(this.activeTab, payload);
+    },
     sendMessage: (payload) =>
       this._sendMessageToTab({ ...payload.data, label: 'conditions', id }),
   };

+ 107 - 0
src/workflowEngine/utils/conditionCode.js

@@ -0,0 +1,107 @@
+import { customAlphabet } from 'nanoid/non-secure';
+import browser from 'webextension-polyfill';
+import { automaRefDataStr, messageSandbox, checkCSPAndInject } from '../helper';
+
+const nanoid = customAlphabet('1234567890abcdef', 5);
+const isMV2 = browser.runtime.getManifest().manifest_version === 2;
+
+export default async function (activeTab, payload) {
+  const variableId = `automa${nanoid()}`;
+
+  if (
+    !payload.data.context ||
+    payload.data.context === 'website' ||
+    !payload.isPopup
+  ) {
+    if (!activeTab.id) throw new Error('no-tab');
+
+    const refDataScriptStr = automaRefDataStr(variableId);
+
+    if (!isMV2) {
+      const result = await checkCSPAndInject(
+        {
+          target: { tabId: activeTab.id },
+          debugMode: payload.debugMode,
+        },
+        () => {
+          return `
+          (async () => {
+            const ${variableId} = ${JSON.stringify(payload.refData)};
+            ${refDataScriptStr}
+            try {
+              ${payload.data.code}
+            } catch (error) {
+              return {
+                $isError: true,
+                message: error.message,
+              }
+            }
+          })();
+        `;
+        }
+      );
+
+      if (result.isBlocked) return result.value;
+    }
+
+    const [{ result }] = await browser.scripting.executeScript({
+      world: 'MAIN',
+      args: [payload, variableId, refDataScriptStr],
+      target: {
+        tabId: activeTab.id,
+        frameIds: [activeTab.frameId || 0],
+      },
+      func: ({ data, refData }, varId, refDataScript) => {
+        return new Promise((resolve, reject) => {
+          const varName = varId;
+
+          const scriptEl = document.createElement('script');
+          scriptEl.textContent = `
+            (async () => {
+              const ${varName} = ${JSON.stringify(refData)};
+              ${refDataScript}
+              try {
+                ${data.code}
+              } catch (error) {
+                return {
+                  $isError: true,
+                  message: error.message,
+                }
+              }
+            })()
+              .then((detail) => {
+                window.dispatchEvent(new CustomEvent('__automa-condition-code__', { detail }));
+              });
+          `;
+
+          document.documentElement.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
+          );
+        });
+      },
+    });
+    return result;
+  }
+  const result = await messageSandbox('conditionCode', payload);
+  if (result && result.$isError) throw new Error(result.message);
+
+  return result;
+}

+ 2 - 2
src/utils/testConditions.js → src/workflowEngine/utils/testConditions.js

@@ -1,7 +1,7 @@
 import cloneDeep from 'lodash.clonedeep';
 import objectPath from 'object-path';
-import renderString from '@/workflowEngine/templating/renderString';
-import { conditionBuilder } from './shared';
+import { conditionBuilder } from '@/utils/shared';
+import renderString from '../templating/renderString';
 
 const isBoolStr = (str) => {
   if (str === 'true') return true;

+ 2 - 2
src/utils/webhookUtil.js → src/workflowEngine/utils/webhookUtil.js

@@ -1,4 +1,4 @@
-import { parseJSON, isWhitespace } from './helper';
+import { parseJSON, isWhitespace } from '@/utils/helper';
 
 const renderContent = (content, contentType) => {
   if (contentType === 'text/plain') return content;
@@ -29,7 +29,7 @@ const renderContent = (content, contentType) => {
 const filterHeaders = (headers) => {
   const filteredHeaders = {};
 
-  if (!headers) return filteredHeaders;
+  if (!headers || !Array.isArray(headers)) return filteredHeaders;
 
   headers.forEach((item) => {
     if (item.name && item.value) {