[
  {
    "path": ".babelrc",
    "content": "{\n  \"plugins\": [],\n  \"presets\": [\n    [\"@babel/preset-env\", {\n      \"useBuiltIns\": \"usage\",\n      \"corejs\": 3,\n      \"targets\": {\n        \"browsers\": \"last 2 Chrome versions\"\n      }\n    }]\n  ]\n}\n"
  },
  {
    "path": ".editorconfig",
    "content": "# EditorConfig is awesome: http://EditorConfig.org\n\n# https://github.com/jokeyrhyme/standard-editorconfig\n\n# top-most EditorConfig file\nroot = true\n\n# defaults\n[*]\ncharset = utf-8\nend_of_line = lf\ninsert_final_newline = true\ntrim_trailing_whitespace = true\nindent_size = 2\nindent_style = space\n\n[*.md]\ntrim_trailing_whitespace = false\n"
  },
  {
    "path": ".eslintrc.js",
    "content": "// https://eslint.org/docs/user-guide/configuring\n// File taken from https://github.com/vuejs-templates/webpack/blob/1.3.1/template/.eslintrc.js, thanks.\n\nmodule.exports = {\n  root: true,\n  parserOptions: {\n    parser: '@babel/eslint-parser',\n  },\n  env: {\n    browser: true,\n    webextensions: true,\n  },\n  ignorePatterns: ['src/lib/google-*'],\n  // https://github.com/vuejs/eslint-plugin-vue#priority-a-essential-error-prevention\n  // consider switching to `plugin:vue/strongly-recommended` or `plugin:vue/recommended` for stricter rules.\n  extends: [\n    'plugin:vue/vue3-recommended',\n    'airbnb-base',\n    'plugin:prettier/recommended',\n  ],\n  // required to lint *.vue files\n  plugins: ['vue'],\n  // check if imports actually resolve\n  settings: {\n    'import/resolver': {\n      webpack: {\n        config: './webpack.config.js',\n      },\n    },\n  },\n  // add your custom rules here\n  globals: {\n    BROWSER_TYPE: true,\n  },\n  rules: {\n    camelcase: 'off',\n    'no-await-in-loop': 'off',\n    'no-alert': 'off',\n    'import/no-import-module-exports': 'off',\n    'no-console': ['warn', { allow: ['warn', 'error'] }],\n    'no-underscore-dangle': 'off',\n    'func-names': 'off',\n    'vue/v-on-event-hyphenation': 'off',\n    'import/no-named-default': 'off',\n    'no-restricted-syntax': 'off',\n    'vue/multi-word-component-names': 'off',\n    'prettier/prettier': [\n      'error',\n      {\n        endOfLine: 'auto',\n      },\n    ],\n    'import/extensions': [\n      'error',\n      'always',\n      {\n        js: 'never',\n      },\n    ],\n    // disallow reassignment of function parameters\n    // disallow parameter object manipulation except for specific exclusions\n    'no-param-reassign': 'off',\n    'import/no-extraneous-dependencies': 'off',\n    // disallow default export over named export\n    'import/prefer-default-export': 'off',\n    // allow debugger during development\n    'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',\n  },\n};\n"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "# These are supported funding model platforms\n\ngithub: AutomaApp\n\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "content": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n**Describe the bug**\nA clear and concise description of what the bug is.\n\n**To Reproduce**\nSteps to reproduce the behavior:\n1. Go to '...'\n2. Click on '....'\n3. Scroll down to '....'\n4. See error\n\n**Expected behavior**\nA clear and concise description of what you expected to happen.\n\n**Screenshots**\nIf applicable, add screenshots to help explain your problem.\n\n**Desktop (please complete the following information):**\n - OS: [e.g. Windows]\n - Browser: [e.g. Google Chrome]\n - Extension Version: [e.g. v0.12.0]\n\n**Additional context**\nAdd any other context about the problem here.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "content": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n**Is your feature request related to a problem? Please describe.**\nA clear and concise description of what the problem is. Ex. I'm always frustrated when [...]\n\n**Describe the solution you'd like**\nA clear and concise description of what you want to happen.\n\n**Describe alternatives you've considered**\nA clear and concise description of any alternative solutions or features you've considered.\n\n**Additional context**\nAdd any other context or screenshots about the feature request here.\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "version: 2\nupdates:\n  - package-ecosystem: \"npm\"\n    directory: \"/\"\n    target-branch: \"dev\"\n    schedule:\n      interval: \"daily\"\n      \n"
  },
  {
    "path": ".gitignore",
    "content": "# See https://help.github.com/ignore-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n\n# testing\n/coverage\n\n# production\n/build\n/build-zip\ndist\n\n# misc\n.DS_Store\n.eslintcache\n.env.local\n.env.development.local\n.env.test.local\n.env.production.local\n.history\n*.log\n\n# secrets\nsecrets.production.js\nsecrets.development.js\nget-pass-key.js\ngetPassKey.js\n\n/business/prod\n/business/test\n\n.idea\n"
  },
  {
    "path": ".prettierrc",
    "content": "{\n  \"singleQuote\": true,\n  \"trailingComma\": \"es5\",\n  \"arrowParens\": \"always\"\n}\n"
  },
  {
    "path": ".vscode/settings.json",
    "content": "{\n  \"i18n-ally.localesPaths\": [\n    \"src/locales\"\n  ],\n  \"i18n-ally.keystyle\": \"nested\"\n}\n"
  },
  {
    "path": "LICENSE.txt",
    "content": "Source code in this repository is variously licensed under the GNU Affero General Public License (AGPL), or the Automa Commercial License (https://extension.automa.site/license/commercial/).\n\n* Outside of the top-level \"business\" directory, source code in a given file is licensed under the AGPL.\n\n* Within the the top-level \"business\" directory, source code in a given file is licensed under the Automa Commercial License, unless otherwise noted.\n\nWhen built, binary files are generated for the AGPL source code and the Automa Commercial License source code. Binaries located at business.automa.site are released under the Automa Commercial License. Binaries located at all non-business paths are released under the AGPL.\n"
  },
  {
    "path": "README.md",
    "content": "<img src=\"src/assets/images/icon-128.png\" width=\"64\"/>\n\n# Automa\n<p>\n  <img alt=\"Automa latest version\" src=\"https://img.shields.io/github/package-json/v/AutomaApp/automa\" />\n  <a href=\"https://twitter.com/AutomaApp\">\n    <img alt=\"Follow Us on Twitter\" src=\"https://img.shields.io/twitter/follow/AutomaApp?style=social\" />\n  </a>\n  <a href=\"https://discord.gg/C6khwwTE84\">\n    <img alt=\"Chat with us on Discord\" src=\"https://img.shields.io/discord/942211415517835354?label=join%20discord&logo=Discord&logoColor=white\" />\n  </a>\n</p>\n\nAn extension for automating your browser by connecting blocks. <br />\nAuto-fill forms, do a repetitive task, take a screenshot, or scrape website data — the choice is yours. You can even schedule when the automation will execute!\n\n## Downloads\n<table cellspacing=\"0\" cellpadding=\"0\">\n  <tr>\n    <td valign=\"center\">\n      <a align=\"center\" href=\"https://chrome.google.com/webstore/detail/automa/infppggnoaenmfagbfknfkancpbljcca\">\n        <img src=\"https://user-images.githubusercontent.com/22908993/166417152-f870bfbd-1770-4c28-b69d-a7303aebc9a6.png\" alt=\"Chrome web store\" />\n        <p align=\"center\">Chrome Web Store</p>\n      </a>\n    </td>\n    <td valign=\"center\">\n      <a href=\"https://addons.mozilla.org/en-US/firefox/addon/automa/\">\n        <img src=\"https://user-images.githubusercontent.com/22908993/166417727-3481fef4-00e5-4cf0-bb03-27fb880d993c.png\" alt=\"Firefox add-ons\" />\n        <p align=\"center\">Firefox Add-ons</p>\n      </a>\n    </td>\n  </tr>\n</table>\n\n## Marketplace\nBrowse the Automa marketplace where you can share and download workflows with others. [Go to the marketplace &#187;](https://extension.automa.site/marketplace)\n\n## Automa Chrome Extension Builder\nAutoma Chrome Extension Builder (Automa CEB for short) allows you to generate a standalone chrome extension based on Automa workflows. [Go to the documentation &#187;](https://docs.extension.automa.site/extension-builder)\n\n\n## Project setup\nBefore running the `yarn dev` or `yarn build` script, you need to create the `getPassKey.js` file in the `src/utils` directory.  Inside the file write\n\n```js\nexport default function() {\n  return 'anything-you-want';\n}\n```\n\n```bash\n# Install dependencies\npnpm install\n\n# Compiles and hot-reloads for development for the chrome browser\npnpm dev\n\n# Compiles and minifies for production for the chrome browser\npnpm build\n\n# Create a zip file from the build folder\npnpm build:zip\n\n# Compiles and hot-reloads for development for the firefox browser\npnpm dev:firefox\n\n# Compiles and minifies for production for the firefox browser\npnpm build:firefox\n\n# Lints and fixes files\npnpm lint\n```\n\n### Icon Preview\nv-remixicon/icons: https://preview-v-remixicon.vercel.app/\n\n### Install Locally\n#### Chrome\n1. Open chrome and navigate to extensions page using this URL: chrome://extensions.\n2. Enable the \"Developer mode\".\n3. Click \"Load unpacked extension\" button, browse the `automa/build` directory and select it.\n\n![Install in chrome](https://user-images.githubusercontent.com/22908993/166417152-f870bfbd-1770-4c28-b69d-a7303aebc9a6.png)\n\n### Firefox\n1. Open firefox and navigate to `about:debugging#/runtime/this-firefox`.\n2. Click the \"Load Temporary Add-on\" button.\n3. Browse the `automa/build` directory and select the `manifest.json` file.\n\n![Install in firefox](https://user-images.githubusercontent.com/22908993/166417727-3481fef4-00e5-4cf0-bb03-27fb880d993c.png)\n\n## Contributors\nThanks to everyone who has submitted issues, made suggestions, and generally helped make this a better project.\n\n<a href=\"https://github.com/AutomaApp/automa/graphs/contributors\">\n  <img src=\"https://contrib.rocks/image?repo=AutomaApp/automa\" />\n</a>\n\n## License\nSource code in this repository is variously licensed under the GNU Affero General Public License (AGPL), or the [Automa Commercial License](https://extension.automa.site/license/commercial/).\n\nSee [LICENSE.txt](./LICENSE.txt) for details.\n"
  },
  {
    "path": "business/dev/blocks/backgroundHandler/index.js",
    "content": "export default function () {\n  return {};\n}\n"
  },
  {
    "path": "business/dev/blocks/contentHandler/index.js",
    "content": "export default function () {\n  return {};\n}\n"
  },
  {
    "path": "business/dev/blocks/editComponents/index.js",
    "content": "export default function () {\n  return {};\n}\n"
  },
  {
    "path": "business/dev/blocks/index.js",
    "content": "export default function () {\n  return {};\n}\n"
  },
  {
    "path": "business/dev/index.js",
    "content": "export default function () {}\n"
  },
  {
    "path": "business/dev/parameters/index.js",
    "content": "export default function () {\n  return {};\n}\n"
  },
  {
    "path": "jsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"baseUrl\": \"./\",\n    \"paths\": {\n      \"@/*\": [\"src/*\"],\n      \"@business\": [\"business/dev/*\"]\n    },\n    \"lib\": [\"ESNext\", \"DOM\"],\n    \"module\": \"ESNext\",\n    \"target\": \"ES2020\"\n  },\n  \"include\": [\"src/**/*\", \"utils/**/*\"]\n}\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"automa\",\n  \"version\": \"1.30.00\",\n  \"description\": \"An extension for automating your browser by connecting blocks\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/AutomaApp/automa.git\"\n  },\n  \"scripts\": {\n    \"build\": \"node utils/build.js\",\n    \"build:firefox\": \"cross-env BROWSER=firefox npm run build\",\n    \"build:zip\": \"node utils/build-zip.js\",\n    \"build:prod\": \"npm run build:prod-chrome && npm run build:prod-firefox\",\n    \"build:prod-chrome\": \"node utils/clean-build-cache.js && npm run build && npm run build:zip\",\n    \"build:prod-firefox\": \"npm run build:firefox && cross-env BROWSER=firefox npm run build:zip\",\n    \"dev\": \"node utils/webserver.js\",\n    \"dev:firefox\": \"cross-env BROWSER=firefox npm run dev\",\n    \"prettier\": \"prettier --write '**/*.{js,jsx,css,html}'\",\n    \"lint\": \"eslint --ext .js,.vue --ignore-path .gitignore .\"\n  },\n  \"engines\": {\n    \"node\": \">=14.18.1\"\n  },\n  \"simple-git-hooks\": {\n    \"pre-commit\": \"npx lint-staged\"\n  },\n  \"lint-staged\": {\n    \"*.{js,ts,vue}\": \"eslint --fix\"\n  },\n  \"dependencies\": {\n    \"@codemirror/autocomplete\": \"^6.9.0\",\n    \"@codemirror/commands\": \"^6.6.1\",\n    \"@codemirror/lang-css\": \"^6.2.1\",\n    \"@codemirror/lang-html\": \"^6.4.6\",\n    \"@codemirror/lang-javascript\": \"^6.2.1\",\n    \"@codemirror/lang-json\": \"^6.0.1\",\n    \"@codemirror/language\": \"^6.9.0\",\n    \"@codemirror/state\": \"^6.4.1\",\n    \"@codemirror/theme-one-dark\": \"^6.1.2\",\n    \"@codemirror/view\": \"^6.33.0\",\n    \"@medv/finder\": \"^3.1.0\",\n    \"@n8n_io/riot-tmpl\": \"^2.0.0\",\n    \"@tiptap/core\": \"^2.0.4\",\n    \"@tiptap/extension-character-count\": \"^2.0.4\",\n    \"@tiptap/extension-history\": \"^2.0.4\",\n    \"@tiptap/extension-image\": \"^2.0.4\",\n    \"@tiptap/extension-link\": \"^2.0.4\",\n    \"@tiptap/extension-placeholder\": \"^2.0.4\",\n    \"@tiptap/pm\": \"^2.0.4\",\n    \"@tiptap/starter-kit\": \"^2.0.4\",\n    \"@tiptap/vue-3\": \"^2.0.4\",\n    \"@viselect/vanilla\": \"^3.5.0\",\n    \"@vue-flow/background\": \"^1.2.0\",\n    \"@vue-flow/core\": \"^1.23.0\",\n    \"@vue-flow/minimap\": \"^1.2.0\",\n    \"@vueuse/head\": \"^1.3.1\",\n    \"@vueuse/rxjs\": \"^9.12.0\",\n    \"@vuex-orm/core\": \"^0.36.4\",\n    \"codemirror\": \"^6.0.1\",\n    \"compare-versions\": \"^6.0.0-rc.1\",\n    \"cron-parser\": \"^4.6.0\",\n    \"cronstrue\": \"^2.21.0\",\n    \"crypto-js\": \"4.2.0\",\n    \"css-selector-generator\": \"^3.6.4\",\n    \"dagre\": \"^0.8.5\",\n    \"dayjs\": \"^1.11.6\",\n    \"defu\": \"^6.1.2\",\n    \"dexie\": \"^3.2.3\",\n    \"html2canvas\": \"^1.4.1\",\n    \"idb\": \"^7.0.2\",\n    \"js-base64\": \"^3.7.5\",\n    \"json5\": \"^2.2.3\",\n    \"jsonpath\": \"^1.1.1\",\n    \"jspdf\": \"^2.5.1\",\n    \"loader-utils\": \"^3.2.1\",\n    \"lodash.clonedeep\": \"^4.5.0\",\n    \"lodash.merge\": \"^4.6.2\",\n    \"mitt\": \"^3.0.0\",\n    \"mousetrap\": \"^1.6.5\",\n    \"nanoid\": \"^4.0.0\",\n    \"object-path\": \"^0.11.8\",\n    \"papaparse\": \"^5.3.1\",\n    \"pinia\": \"^2.0.29\",\n    \"prosemirror-commands\": \"^1.5.0\",\n    \"prosemirror-dropcursor\": \"^1.6.1\",\n    \"prosemirror-gapcursor\": \"^1.3.1\",\n    \"prosemirror-history\": \"^1.3.0\",\n    \"prosemirror-keymap\": \"^1.2.0\",\n    \"prosemirror-schema-list\": \"^1.2.2\",\n    \"rxjs\": \"^7.8.0\",\n    \"sizzle\": \"^2.3.8\",\n    \"tippy.js\": \"^6.3.1\",\n    \"v-remixicon\": \"^0.1.1\",\n    \"vue\": \"3.4.38\",\n    \"vue-i18n\": \"^9.14.5\",\n    \"vue-imask\": \"^6.4.2\",\n    \"vue-router\": \"^4.2.4\",\n    \"vue-slider-component\": \"^4.1.0-beta.7\",\n    \"vue-toastification\": \"^2.0.0-rc.5\",\n    \"vuedraggable\": \"^4.1.0\",\n    \"vuex\": \"^4.0.2\",\n    \"webextension-polyfill\": \"^0.12.0\",\n    \"xlsx\": \"https://cdn.sheetjs.com/xlsx-0.19.1/xlsx-0.19.1.tgz\"\n  },\n  \"devDependencies\": {\n    \"@babel/core\": \"^7.20.7\",\n    \"@babel/eslint-parser\": \"^7.18.2\",\n    \"@babel/preset-env\": \"^7.20.2\",\n    \"@intlify/vue-i18n-loader\": \"^4.2.0\",\n    \"@tailwindcss/typography\": \"^0.5.1\",\n    \"@types/chrome\": \"^0.0.267\",\n    \"@vue/compiler-sfc\": \"^3.3.4\",\n    \"archiver\": \"^5.3.1\",\n    \"autoprefixer\": \"^10.4.12\",\n    \"babel-loader\": \"^9.1.2\",\n    \"clean-webpack-plugin\": \"4.0.0\",\n    \"copy-webpack-plugin\": \"^11.0.0\",\n    \"core-js\": \"^3.27.2\",\n    \"cross-env\": \"^7.0.3\",\n    \"css-loader\": \"^6.7.3\",\n    \"eslint\": \"^8.34.0\",\n    \"eslint-config-airbnb-base\": \"^15.0.0\",\n    \"eslint-config-prettier\": \"^8.6.0\",\n    \"eslint-friendly-formatter\": \"^4.0.1\",\n    \"eslint-import-resolver-webpack\": \"^0.13.2\",\n    \"eslint-plugin-import\": \"^2.27.5\",\n    \"eslint-plugin-prettier\": \"^4.0.0\",\n    \"eslint-plugin-vue\": \"^9.4.0\",\n    \"file-loader\": \"^6.2.0\",\n    \"fs-extra\": \"^11.1.0\",\n    \"html-loader\": \"^4.2.0\",\n    \"html-webpack-plugin\": \"^5.5.0\",\n    \"lint-staged\": \"^13.0.2\",\n    \"mini-css-extract-plugin\": \"^2.3.0\",\n    \"postcss\": \"^8.4.21\",\n    \"postcss-loader\": \"^7.0.0\",\n    \"prettier\": \"^2.8.2\",\n    \"simple-git-hooks\": \"^2.8.1\",\n    \"source-map-loader\": \"^4.0.0\",\n    \"tailwindcss\": \"^3.2.1\",\n    \"terser-webpack-plugin\": \"^5.3.6\",\n    \"vue-loader\": \"^17.2.2\",\n    \"web-worker\": \"^1.2.0\",\n    \"webpack\": \"5.76.0\",\n    \"webpack-cli\": \"^5.0.1\",\n    \"webpack-dev-server\": \"^4.11.1\"\n  },\n  \"volta\": {\n    \"node\": \"20.11.1\"\n  }\n}\n"
  },
  {
    "path": "postcss.config.js",
    "content": "module.exports = {\n  plugins: {\n    'tailwindcss/nesting': {},\n    tailwindcss: {},\n    autoprefixer: {},\n  },\n};\n"
  },
  {
    "path": "secrets.blank.js",
    "content": "export default {\n  baseApiUrl: '',\n};\n"
  },
  {
    "path": "src/assets/css/drawflow.css",
    "content": ".drawflow-node.selected-list .menu,\n.drawflow-node.selected .menu,\n.drawflow-node .block-base:hover .menu {\n  @apply translate-y-11;\n}\n\n.drawflow,\n.drawflow .parent-node {\n  position: relative\n}\n\n.parent-drawflow {\n  display: flex;\n  overflow: hidden;\n  touch-action: none;\n  outline: none;\n}\n\n.drawflow {\n  width: 100%;\n  height: 100%;\n  user-select: none;\n  perspective: 0;\n}\n\n.drawflow .drawflow-node {\n  position: absolute;\n  align-items: center;\n  background: white;\n  min-width: 150px;\n  min-height: 40px;\n  z-index: 2;\n  cursor: move;\n  white-space: nowrap;\n  @apply rounded-lg transition ring-2 ring-transparent duration-200 shadow-lg;\n}\n\n.drawflow .drawflow-node.selected,\n.drawflow .drawflow-node.selected-list {\n  @apply ring-accent;\n}\n\n.drawflow .drawflow-node .inputs,\n.drawflow .drawflow-node .outputs {\n  z-index: 20;\n  position: absolute;\n  top: 50%;\n  transform: translateY(-50%);\n}\n\n.drawflow .drawflow-node .inputs {\n  left: -22px;\n}\n.drawflow .drawflow-node .outputs {\n  right: -22px;\n}\n\n.drawflow .drawflow-node .drawflow_content_node {\n  width: 100%;\n  display: block\n}\n\n.drawflow .drawflow-node .input,\n.drawflow .drawflow-node .output {\n  position: relative;\n  width: 18px;\n  height: 18px;\n  border-radius: 50%;\n  cursor: crosshair;\n  z-index: 1;\n  margin-bottom: 5px;\n  border-width: 3px;\n  @apply border-accent bg-white dark:bg-gray-900;\n}\n\n.drawflow .drawflow-node .input {\n  @apply bg-accent !important;\n}\n\n.drawflow .icon-ui {\n  position: relative;\n}\n\n.drawflow svg:not(.v-remixicon) {\n  z-index: 0;\n  position: absolute;\n  overflow: visible !important\n}\n\n.drawflow .connection {\n  position: absolute;\n}\n\n.drawflow .connection .main-path {\n  fill: none;\n  stroke-width: 5px;\n  stroke: theme('colors.accent');\n  transition: stroke 100ms ease-in-out;\n}\n\n.drawflow .connection .main-path:hover {\n  stroke: theme('colors.yellow.300');\n  cursor: pointer\n}\n\n.drawflow .connection .main-path.selected {\n  stroke: theme('colors.green.300');\n}\n\n.drawflow .connection .point {\n  cursor: move;\n  stroke: #000;\n  stroke-width: 2;\n  fill: #fff;\n}\n\n.drawflow .connection .point.selected,\n.drawflow .connection .point:hover {\n  fill: theme('colors.blue.600');\n}\n\n.drawflow .main-path {\n  fill: none;\n  stroke-width: 5px;\n  stroke: theme('colors.accent');\n}\n\n.drawflow-node .drawflow-delete {\n  display: none !important;\n}\n\n.drawflow-delete {\n  position: absolute;\n  display: block;\n  width: 20px;\n  height: 20px;\n  @apply bg-red-500;\n  color: #fff;\n  z-index: 4;\n  line-height: 20px;\n  font-weight: 700;\n  text-align: center;\n  border-radius: 50%;\n  font-family: monospace;\n  cursor: pointer;\n  text-transform: uppercase;\n}\n\n.drawflow>.drawflow-delete {\n  margin-left: -15px;\n  margin-top: 15px\n}\n\n.parent-node .drawflow-delete {\n  right: -15px;\n  top: -15px\n}\n"
  },
  {
    "path": "src/assets/css/flow.css",
    "content": ".vue-flow__minimap {\n\t@apply rounded-lg dark:bg-gray-800;\n}\n\n.vue-flow__node {\n\t& > div {\n\t\t@apply rounded-lg transition;\n\t}\n\t&.selected .block-base__content {\n\t\t@apply ring-2 ring-accent;\n\t}\n\t&:hover {\n\t\t.block-menu-container {\n\t\t\tdisplay: block;\n\t\t}\n\t}\n\n\t&.vue-flow__node-BlockGroup2 {\n\t\tz-index: 0 !important;\n\t}\n\n\t.vue-flow__handle {\n\t\t@apply h-4 w-4 rounded-full border-0;\n\t\t&.target {\n\t\t\t@apply bg-accent -ml-4;\n\t\t}\n\t\t&.source {\n\t\t\tborder-width: 3px;\n\t\t\t@apply border-accent -mr-4 bg-white dark:bg-black;\n\t\t}\n\t}\n}\n\n.vue-flow {\n\t&.disabled {\n\t\t.vue-flow__handle {\n\t\t\tpointer-events: none;\n\t\t}\n\t}\n\tsvg g.connected-edges path {\n\t\tstroke:  theme('colors.primary');\n\t}\n}\n\n.vue-flow__edge {\n\tcursor: pointer;\n\t&.selected .vue-flow__edge-path {\n\t\tstroke: theme('colors.green.300');\n\t}\n}\n\n.dark .vue-flow__edge-path:hover {\n\tstroke: theme('colors.yellow.400');\n}\n.vue-flow__edge-path {\n  stroke: theme('colors.accent');\n  stroke-width: 4;\n  transition: stroke 100ms ease;\n  &:hover {\n\t\tstroke: theme('colors.yellow.500');\n  }\n}"
  },
  {
    "path": "src/assets/css/fonts.css",
    "content": "@font-face {\n  font-family: Inter var;\n  font-weight: 100 900;\n  font-display: swap;\n  font-style: normal;\n  font-named-instance: \"Regular\";\n  src: url('../fonts/Inter-roman-latin.var.woff2') format(\"woff2\");\n}\n\n/* source-code-pro-regular - latin */\n@font-face {\n  font-family: 'Source Code Pro';\n  font-style: normal;\n  font-weight: 400;\n  src: local(''),\n       url('../fonts/source-code-pro-v21-latin-regular.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */\n       url('../fonts/source-code-pro-v21-latin-regular.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */\n}\n/* source-code-pro-600 - latin */\n@font-face {\n  font-family: 'Source Code Pro';\n  font-style: normal;\n  font-weight: 600;\n  src: local(''),\n       url('../fonts/source-code-pro-v21-latin-600.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */\n       url('../fonts/source-code-pro-v21-latin-600.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */\n}"
  },
  {
    "path": "src/assets/css/style.css",
    "content": ".list-item-transition {\n  transition: all 0.4s ease;\n}\n\n.list-leave-active {\n  position: absolute;\n  width: 100%;\n}\n\n.list-enter-from,\n.list-leave-to {\n  opacity: 0;\n}\n\n.list-enter-from,\n.list-enter-from {\n  transform: translateY(30px);\n}\n"
  },
  {
    "path": "src/assets/css/tailwind.css",
    "content": "@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\n@layer utilities {\n  .hoverable {\n    @apply hover:bg-gray-800 hover:bg-opacity-5 dark:hover:bg-gray-200 dark:hover:bg-opacity-5;\n  }\n  .bg-input {\n    @apply bg-black bg-opacity-5 hover:bg-opacity-10 dark:bg-gray-200 dark:bg-opacity-5 dark:hover:bg-opacity-10;\n  }\n  .bg-box-transparent {\n    @apply bg-black bg-opacity-5 dark:bg-gray-200 dark:bg-opacity-5;\n  }\n  .bg-box-transparent-2 {\n    @apply bg-black bg-opacity-10 dark:bg-gray-200 dark:bg-opacity-10;\n  }\n}\n\n:host, :root {\n  --color-primary: 59 130 246;\n  --color-secondary: 96 165 250;\n  --color-accent: 24 24 27;\n}\n.dark {\n  --color-primary: 96 165 250;\n  --color-secondary: 59 130 246;\n  --color-accent: 244 244 245;\n}\n\n* {\n  @apply dark:border-gray-700;\n}\n\nhtml.dark {\n  @apply bg-gray-900;\n}\n\nbody, :host {\n  font-family: 'Inter var', sans-serif !important;\n  font-size: 16px !important;\n  font-feature-settings: 'cv02', 'cv03', 'cv04', 'cv11';\n  @apply bg-gray-50 dark:bg-gray-900 leading-normal;\n}\ntable th,\ntable td {\n  @apply py-2 px-4;\n}\ninput:focus,\nbutton:focus,\ntextarea:focus,\nselect:focus,\n[role='button']:focus {\n  outline: none;\n  @apply ring-2 ring-accent dark:ring-gray-200;\n}\n\n.text-overflow {\n  overflow: hidden;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n}\n.line-clamp {\n  display: -webkit-box;\n  -webkit-line-clamp: 2;\n  -webkit-box-orient: vertical;\n  overflow: hidden;\n  text-overflow: ellipsis;\n}\n\n\n.custom-table thead {\n  @apply bg-box-transparent;\n}\n.custom-table thead th {\n  @apply font-semibold;\n}\n.custom-table thead th:first-child {\n  @apply rounded-l-lg;\n}\n.custom-table thead th:last-child {\n  @apply rounded-r-lg;\n}\n.custom-table tbody {\n  @apply divide-y;\n}\n\n\n\npre {\n  font-size: 15px;\n}\n\n.scroll, \n.scroll .cm-scroller {\n  &::-webkit-scrollbar {\n    width: 7px;\n    height: 9px;\n  }\n  &::-webkit-scrollbar-thumb {\n    @apply bg-gray-300 dark:bg-gray-700;\n    border-radius: 8px;\n  }\n  &::-webkit-scrollbar-track {\n    background: transparent;\n  }\n\n  &.scroll-xs::-webkit-scrollbar {\n    width: 5px;\n    height: 5px;\n  }\n}\n\n\n.tippy-box[data-theme~='tooltip-theme'] {\n  @apply px-2 py-1 bg-gray-900 dark:bg-gray-200 dark:text-black text-sm text-gray-200 rounded-md;\n}\n.Vue-Toastification__toast {\n  font-family: inherit !important;\n}\n.ProseMirror > * + * {\n  margin-top: 0.75em;\n}\n.ProseMirror img {\n  max-width: 100%;\n  height: auto;\n}\n.ProseMirror img.ProseMirror-selectednode {\n  outline: 3px solid #68CEF8;\n}\n\n.input-label {\n  @apply ml-1 text-sm text-gray-600 dark:text-gray-200;\n}\n"
  },
  {
    "path": "src/background/BackgroundEventsListeners.js",
    "content": "import browser from 'webextension-polyfill';\nimport { initElementSelector } from '@/newtab/utils/elementSelector';\nimport dayjs from 'dayjs';\nimport dbStorage from '@/db/storage';\nimport cronParser from 'cron-parser';\nimport BackgroundUtils from './BackgroundUtils';\nimport BackgroundWorkflowTriggers from './BackgroundWorkflowTriggers';\n\nasync function handleScheduleBackup() {\n  try {\n    const { localBackupSettings, workflows } = await browser.storage.local.get([\n      'localBackupSettings',\n      'workflows',\n    ]);\n    if (!localBackupSettings) return;\n\n    const workflowsData = Object.values(workflows || []).reduce(\n      (acc, workflow) => {\n        if (workflow.isProtected) return acc;\n\n        delete workflow.$id;\n        delete workflow.createdAt;\n        delete workflow.data;\n        delete workflow.isDisabled;\n        delete workflow.isProtected;\n\n        acc.push(workflow);\n\n        return acc;\n      },\n      []\n    );\n\n    const payload = {\n      workflows: JSON.stringify(workflowsData),\n    };\n\n    if (localBackupSettings.includedItems.includes('storage:table')) {\n      const tables = await dbStorage.tablesItems.toArray();\n      payload.storageTables = JSON.stringify(tables);\n    }\n    if (localBackupSettings.includedItems.includes('storage:variables')) {\n      const variables = await dbStorage.variables.toArray();\n      payload.storageVariables = JSON.stringify(variables);\n    }\n\n    const base64 = btoa(encodeURIComponent(JSON.stringify(payload)));\n    const filename = `${\n      localBackupSettings.folderName ? `${localBackupSettings.folderName}/` : ''\n    }${dayjs().format('DD-MMM-YYYY--HH-mm')}.json`;\n\n    await browser.downloads.download({\n      filename,\n      url: `data:application/json;base64,${base64}`,\n    });\n    await browser.storage.local.set({\n      localBackupSettings: {\n        ...localBackupSettings,\n        lastBackup: Date.now(),\n      },\n    });\n\n    const expression =\n      localBackupSettings.schedule === 'custom'\n        ? localBackupSettings.customSchedule\n        : localBackupSettings.schedule;\n    const parsedExpression = cronParser.parseExpression(expression).next();\n    if (!parsedExpression) return;\n\n    await browser.alarms.create('schedule-local-backup', {\n      when: parsedExpression.getTime(),\n    });\n  } catch (error) {\n    console.error(error);\n  }\n}\n\nclass BackgroundEventsListeners {\n  static onActionClicked() {\n    BackgroundUtils.openDashboard();\n  }\n\n  static onCommand(name) {\n    if (name === 'open-dashboard') {\n      BackgroundUtils.openDashboard();\n    } else if (name === 'element-picker') {\n      initElementSelector();\n    }\n  }\n\n  static onAlarms(event) {\n    if (event.name === 'schedule-local-backup') {\n      handleScheduleBackup();\n      return;\n    }\n\n    BackgroundWorkflowTriggers.scheduleWorkflow(event);\n  }\n\n  static onWebNavigationCompleted({ tabId, url, frameId }) {\n    if (frameId > 0) return;\n\n    BackgroundWorkflowTriggers.visitWebTriggers(tabId, url);\n  }\n\n  static onContextMenuClicked(event, tab) {\n    BackgroundWorkflowTriggers.contextMenu(event, tab);\n  }\n\n  static async onNotificationClicked(notificationId) {\n    if (notificationId.startsWith('logs')) {\n      const { 1: logId } = notificationId.split(':');\n\n      const [tab] = await browser.tabs.query({\n        url: browser.runtime.getURL('/newtab.html'),\n      });\n      if (!tab) await BackgroundUtils.openDashboard('');\n\n      await BackgroundUtils.sendMessageToDashboard('open-logs', { logId });\n    }\n  }\n\n  static onRuntimeStartup() {\n    browser.storage.local.remove('workflowStates');\n    (browser.action || browser.browserAction).setBadgeText({ text: '' });\n    BackgroundWorkflowTriggers.reRegisterTriggers(true);\n  }\n\n  static onHistoryStateUpdated({ frameId, url, tabId }) {\n    if (frameId !== 0) return;\n\n    BackgroundWorkflowTriggers.visitWebTriggers(tabId, url, true);\n  }\n\n  static async onRuntimeInstalled({ reason }) {\n    try {\n      if (reason === 'install') {\n        await browser.storage.local.set({\n          logs: [],\n          shortcuts: {},\n          workflows: [],\n          collections: [],\n          workflowState: {},\n          isFirstTime: true,\n          visitWebTriggers: [],\n        });\n        await browser.windows.create({\n          type: 'popup',\n          state: 'maximized',\n          url: browser.runtime.getURL('newtab.html#/welcome'),\n        });\n\n        return;\n      }\n\n      if (reason === 'update') {\n        await BackgroundWorkflowTriggers.reRegisterTriggers();\n      }\n    } catch (error) {\n      console.error(error);\n    }\n  }\n}\n\nexport default BackgroundEventsListeners;\n"
  },
  {
    "path": "src/background/BackgroundOffscreen.js",
    "content": "/* eslint-disable class-methods-use-this */\nimport { IS_FIREFOX } from '@/common/utils/constant';\nimport { sleep } from '@/utils/helper';\nimport { MessageListener } from '@/utils/message';\nimport Browser from 'webextension-polyfill';\n\nconst OFFSCREEN_URL = Browser.runtime.getURL('/offscreen.html');\n\nclass BackgroundOffscreen {\n  /** @type {BackgroundOffscreen} */\n  static #_instance;\n\n  /**\n   * OffscreenService singleton\n   * @returns {BackgroundOffscreen}\n   */\n  static get instance() {\n    if (!this.#_instance) {\n      this.#_instance = new BackgroundOffscreen();\n    }\n\n    return this.#_instance;\n  }\n\n  /** @type {MessageListener} */\n  #messageListener;\n\n  constructor() {\n    this.#messageListener = new MessageListener('offscreen');\n\n    this.on = this.#messageListener.on;\n  }\n\n  /**\n   *\n   * @returns {Promise<boolean>}\n   */\n  async #ensureDocument() {\n    if (IS_FIREFOX) return;\n\n    const isOpened = await this.isOpened();\n    if (isOpened) return;\n\n    await chrome.offscreen.createDocument({\n      url: OFFSCREEN_URL,\n      reasons: [\n        chrome.offscreen.Reason.BLOBS,\n        chrome.offscreen.Reason.CLIPBOARD,\n        chrome.offscreen.Reason.IFRAME_SCRIPTING,\n      ],\n      justification: 'For running the workflow',\n    });\n\n    await sleep(500);\n  }\n\n  /**\n   *\n   * @returns {Promise<boolean>}\n   */\n  async isOpened() {\n    if (IS_FIREFOX) return false;\n\n    const contexts = await chrome.runtime.getContexts({\n      documentUrls: [OFFSCREEN_URL],\n      contextTypes: ['OFFSCREEN_DOCUMENT'],\n    });\n\n    return Boolean(contexts.length);\n  }\n\n  /**\n   *\n   * @param {string} name\n   * @param {*} data\n   * @returns {Promise<*>}\n   */\n  async sendMessage(name, data) {\n    await this.#ensureDocument();\n\n    return this.#messageListener.sendMessage(name, data);\n  }\n}\n\nexport default BackgroundOffscreen;\n"
  },
  {
    "path": "src/background/BackgroundUtils.js",
    "content": "import browser from 'webextension-polyfill';\nimport { waitTabLoaded } from '@/workflowEngine/helper';\n\nclass BackgroundUtils {\n  static async openDashboard(url, updateTab = true) {\n    const tabUrl = browser.runtime.getURL(\n      `/newtab.html#${typeof url === 'string' ? url : ''}`\n    );\n\n    try {\n      const [tab] = await browser.tabs.query({\n        url: browser.runtime.getURL('/newtab.html'),\n      });\n\n      if (tab) {\n        const tabOptions = { active: true };\n        if (updateTab) tabOptions.url = tabUrl;\n\n        await browser.tabs.update(tab.id, tabOptions);\n\n        if (updateTab) {\n          await browser.windows.update(tab.windowId, {\n            focused: true,\n            state: 'maximized',\n          });\n        }\n      } else {\n        const curWin = await browser.windows.getCurrent();\n        const windowOptions = {\n          top: 0,\n          left: 0,\n          width: Math.min(curWin.width, 715),\n          height: Math.min(curWin.height, 715),\n          url: tabUrl,\n          type: 'popup',\n        };\n\n        if (updateTab) {\n          windowOptions.focused = true;\n        }\n\n        await browser.windows.create(windowOptions);\n      }\n    } catch (error) {\n      console.error(error);\n      throw error;\n    }\n  }\n\n  static async sendMessageToDashboard(type, data) {\n    const [tab] = await browser.tabs.query({\n      url: browser.runtime.getURL('/newtab.html'),\n    });\n\n    await waitTabLoaded({ tabId: tab.id });\n    const result = await browser.tabs.sendMessage(tab.id, { type, data });\n\n    return result;\n  }\n}\n\nexport default BackgroundUtils;\n"
  },
  {
    "path": "src/background/BackgroundWorkflowTriggers.js",
    "content": "import browser from 'webextension-polyfill';\nimport dayjs from 'dayjs';\nimport { findTriggerBlock, parseJSON } from '@/utils/helper';\nimport {\n  registerCronJob,\n  registerSpecificDay,\n  registerWorkflowTrigger,\n} from '@/utils/workflowTrigger';\nimport BackgroundWorkflowUtils from './BackgroundWorkflowUtils';\n\nclass BackgroundWorkflowTriggers {\n  static async visitWebTriggers(tabId, tabUrl, spa = false) {\n    const { visitWebTriggers } = await browser.storage.local.get(\n      'visitWebTriggers'\n    );\n    if (!visitWebTriggers || visitWebTriggers.length === 0) return;\n\n    const triggeredWorkflow = visitWebTriggers.find(\n      ({ url, isRegex, supportSPA }) => {\n        if (!url.trim() || (spa && !supportSPA)) return false;\n\n        return tabUrl.match(isRegex ? new RegExp(url, 'g') : url);\n      }\n    );\n\n    if (triggeredWorkflow) {\n      let workflowId = triggeredWorkflow.id;\n      if (triggeredWorkflow.id.startsWith('trigger')) {\n        const { 1: triggerWorkflowId } = triggeredWorkflow.id.split(':');\n        workflowId = triggerWorkflowId;\n      }\n\n      const workflowData = await BackgroundWorkflowUtils.getWorkflow(\n        workflowId\n      );\n      if (workflowData) {\n        BackgroundWorkflowUtils.instance.executeWorkflow(workflowData, {\n          tabId,\n        });\n      }\n    }\n  }\n\n  static async scheduleWorkflow({ name }) {\n    try {\n      let workflowId = name;\n      let triggerId = null;\n\n      if (name.startsWith('trigger')) {\n        const { 1: triggerWorkflowId, 2: triggerItemId } = name.split(':');\n        triggerId = triggerItemId;\n        workflowId = triggerWorkflowId;\n      }\n\n      const currentWorkflow = await BackgroundWorkflowUtils.getWorkflow(\n        workflowId\n      );\n      if (!currentWorkflow) return;\n\n      let data = currentWorkflow.trigger;\n      if (!data) {\n        const drawflow =\n          typeof currentWorkflow.drawflow === 'string'\n            ? parseJSON(currentWorkflow.drawflow, {})\n            : currentWorkflow.drawflow;\n        const { data: triggerBlockData } = findTriggerBlock(drawflow) || {};\n        data = triggerBlockData;\n      }\n\n      if (triggerId) {\n        data = data.triggers.find((trigger) => trigger.id === triggerId);\n        if (data) data = { ...data, ...data.data };\n      }\n\n      if (data && data.type === 'interval' && data.fixedDelay) {\n        const { workflowStates } = await browser.storage.local.get(\n          'workflowStates'\n        );\n        const workflowState = (workflowStates || []).find(\n          (item) => item.workflowId === workflowId\n        );\n\n        if (workflowState) {\n          let { workflowQueue } = await browser.storage.local.get(\n            'workflowQueue'\n          );\n          workflowQueue = workflowQueue || [];\n\n          if (!workflowQueue.includes(workflowId)) {\n            (workflowQueue = workflowQueue || []).push(workflowId);\n            await browser.storage.local.set({ workflowQueue });\n          }\n\n          return;\n        }\n      } else if (data && data.type === 'date') {\n        const [hour, minute, second] = data.time.split(':');\n        const date = dayjs(data.date)\n          .hour(hour)\n          .minute(minute)\n          .second(second || 0);\n\n        const isAfter = dayjs(Date.now() - 60 * 1000).isAfter(date);\n        if (isAfter) return;\n      }\n\n      BackgroundWorkflowUtils.instance.executeWorkflow(currentWorkflow);\n\n      if (!data) return;\n\n      if (['specific-day', 'cron-job'].includes(data.type)) {\n        if (data.type === 'specific-day') {\n          registerSpecificDay(name, data);\n        } else {\n          registerCronJob(name, data);\n        }\n      }\n    } catch (error) {\n      console.error(error);\n    }\n  }\n\n  static async contextMenu({ parentMenuItemId, menuItemId, frameId }, tab) {\n    try {\n      if (parentMenuItemId !== 'automaContextMenu') return;\n      const message = await browser.tabs.sendMessage(\n        tab.id,\n        {\n          type: 'context-element',\n        },\n        { frameId }\n      );\n\n      let workflowId = menuItemId;\n      if (menuItemId.startsWith('trigger')) {\n        const { 1: triggerWorkflowId } = menuItemId.split(':');\n        workflowId = triggerWorkflowId;\n      }\n\n      const workflowData = await BackgroundWorkflowUtils.getWorkflow(\n        workflowId\n      );\n      BackgroundWorkflowUtils.instance.executeWorkflow(workflowData, {\n        data: {\n          variables: message,\n        },\n      });\n    } catch (error) {\n      console.error(error);\n    }\n  }\n\n  static async reRegisterTriggers(isStartup = false) {\n    const { workflows, workflowHosts, teamWorkflows } =\n      await browser.storage.local.get([\n        'workflows',\n        'workflowHosts',\n        'teamWorkflows',\n      ]);\n    const convertToArr = (value) =>\n      Array.isArray(value) ? value : Object.values(value);\n\n    const workflowsArr = convertToArr(workflows);\n\n    if (workflowHosts) {\n      workflowsArr.push(...convertToArr(workflowHosts));\n    }\n    if (teamWorkflows) {\n      workflowsArr.push(\n        ...BackgroundWorkflowUtils.flattenTeamWorkflows(teamWorkflows)\n      );\n    }\n\n    for (const currWorkflow of workflowsArr) {\n      // eslint-disable-next-line no-continue\n      if (currWorkflow.isDisabled) continue;\n\n      let triggerBlock = currWorkflow.trigger;\n\n      if (!triggerBlock) {\n        const flow =\n          typeof currWorkflow.drawflow === 'string'\n            ? parseJSON(currWorkflow.drawflow, {})\n            : currWorkflow.drawflow;\n\n        triggerBlock = findTriggerBlock(flow)?.data;\n      }\n\n      if (triggerBlock) {\n        if (isStartup && triggerBlock.type === 'on-startup') {\n          BackgroundWorkflowUtils.instance.executeWorkflow(currWorkflow);\n        } else {\n          if (isStartup && triggerBlock.triggers) {\n            for (const trigger of triggerBlock.triggers) {\n              if (trigger.type === 'on-startup') {\n                await BackgroundWorkflowUtils.executeWorkflow(currWorkflow);\n              }\n            }\n          }\n\n          await registerWorkflowTrigger(currWorkflow.id, {\n            data: triggerBlock,\n          });\n        }\n      }\n    }\n  }\n}\n\nexport default BackgroundWorkflowTriggers;\n"
  },
  {
    "path": "src/background/BackgroundWorkflowUtils.js",
    "content": "import { IS_FIREFOX } from '@/common/utils/constant';\nimport browser from 'webextension-polyfill';\nimport BackgroundOffscreen from './BackgroundOffscreen';\n\nclass BackgroundWorkflowUtils {\n  /** @type {BackgroundWorkflowUtils} */\n  static #_instance;\n\n  /**\n   * BackgroundWorkflowUtils singleton\n   * @type {BackgroundWorkflowUtils}\n   */\n  static get instance() {\n    if (!this.#_instance) this.#_instance = new BackgroundWorkflowUtils();\n\n    return this.#_instance;\n  }\n\n  /** @type {import('@/workflowEngine/WorkflowManager').default} */\n  #workflowManager;\n\n  constructor() {\n    this.#workflowManager = null;\n  }\n\n  static flattenTeamWorkflows(workflows) {\n    return Object.values(Object.values(workflows || {})[0] || {});\n  }\n\n  static async getWorkflow(workflowId) {\n    if (!workflowId) return null;\n\n    if (workflowId.startsWith('team')) {\n      const { teamWorkflows } = await browser.storage.local.get(\n        'teamWorkflows'\n      );\n      if (!teamWorkflows) return null;\n\n      const workflows = this.flattenTeamWorkflows(teamWorkflows);\n\n      return workflows.find((item) => item.id === workflowId);\n    }\n\n    const { workflows, workflowHosts } = await browser.storage.local.get([\n      'workflows',\n      'workflowHosts',\n    ]);\n    let findWorkflow = Array.isArray(workflows)\n      ? workflows.find(({ id }) => id === workflowId)\n      : workflows[workflowId];\n\n    if (!findWorkflow) {\n      findWorkflow = Object.values(workflowHosts || {}).find(\n        ({ hostId }) => hostId === workflowId\n      );\n\n      if (findWorkflow) findWorkflow.id = findWorkflow.hostId;\n    }\n\n    return findWorkflow;\n  }\n\n  async #ensureWorkflowManager() {\n    if (!IS_FIREFOX) return;\n\n    this.#workflowManager = (\n      await import('@/workflowEngine/WorkflowManager')\n    ).default.instance;\n  }\n\n  /**\n   * Stop workflow execution\n   * @param {string} stateId\n   * @returns {Promise<void>}\n   */\n  async stopExecution(stateId) {\n    if (IS_FIREFOX) {\n      await this.#ensureWorkflowManager();\n      this.#workflowManager.stopExecution(stateId);\n      return;\n    }\n\n    await BackgroundOffscreen.instance.sendMessage('workflow:stop', stateId);\n  }\n\n  /**\n   * Resume workflow execution\n   * @param {string} stateId\n   * @param {object} nextBlock\n   * @returns {Promise<void>}\n   */\n  async resumeExecution(stateId, nextBlock) {\n    if (IS_FIREFOX) {\n      await this.#ensureWorkflowManager();\n      this.#workflowManager.resumeExecution(stateId, nextBlock);\n      return;\n    }\n\n    await BackgroundOffscreen.instance.sendMessage('workflow:resume', {\n      id: stateId,\n      nextBlock,\n    });\n  }\n\n  /**\n   * Update workflow execution state\n   * @param {string} stateId\n   * @param {object} data\n   * @returns {Promise<void>}\n   */\n  async updateExecutionState(stateId, data) {\n    if (IS_FIREFOX) {\n      await this.#ensureWorkflowManager();\n      this.#workflowManager.updateExecution(stateId, data);\n      return;\n    }\n\n    await BackgroundOffscreen.instance.sendMessage('workflow:update', {\n      data,\n      id: stateId,\n    });\n  }\n\n  async executeWorkflow(workflowData, options) {\n    if (workflowData.isDisabled) return;\n\n    if (IS_FIREFOX) {\n      await this.#ensureWorkflowManager();\n      this.#workflowManager.execute(workflowData, options);\n      return;\n    }\n\n    await BackgroundOffscreen.instance.sendMessage('workflow:execute', {\n      workflow: workflowData,\n      options,\n    });\n  }\n}\n\nexport default BackgroundWorkflowUtils;\n"
  },
  {
    "path": "src/background/index.js",
    "content": "import { IS_FIREFOX } from '@/common/utils/constant';\nimport BrowserAPIEventHandler from '@/service/browser-api/BrowserAPIEventHandler';\nimport BrowserAPIService from '@/service/browser-api/BrowserAPIService';\nimport { useUserStore } from '@/stores/user';\nimport {\n  executeCallbacksInData,\n  isCallbackBridge,\n} from '@/utils/callbackBridge';\nimport getFile, { readFileAsBase64 } from '@/utils/getFile';\nimport { sleep } from '@/utils/helper';\nimport { MessageListener } from '@/utils/message';\n\n// import { getDocumentCtx } from '@/content/handleSelector';\nimport { automaRefDataStr } from '@/workflowEngine/helper';\n\nimport automa from '@business';\nimport browser from 'webextension-polyfill';\nimport { registerWorkflowTrigger } from '../utils/workflowTrigger';\nimport BackgroundEventsListeners from './BackgroundEventsListeners';\nimport BackgroundOffscreen from './BackgroundOffscreen';\nimport BackgroundUtils from './BackgroundUtils';\nimport BackgroundWorkflowUtils from './BackgroundWorkflowUtils';\n\nBackgroundOffscreen.instance.sendMessage('halo');\n\nbrowser.alarms.onAlarm.addListener(BackgroundEventsListeners.onAlarms);\n\nbrowser.commands.onCommand.addListener(BackgroundEventsListeners.onCommand);\n\n(browser.action || browser.browserAction).onClicked.addListener(\n  BackgroundEventsListeners.onActionClicked\n);\n\nbrowser.runtime.onStartup.addListener(\n  BackgroundEventsListeners.onRuntimeStartup\n);\nbrowser.runtime.onInstalled.addListener(\n  BackgroundEventsListeners.onRuntimeInstalled\n);\n\nbrowser.webNavigation.onCompleted.addListener(\n  BackgroundEventsListeners.onWebNavigationCompleted\n);\nbrowser.webNavigation.onHistoryStateUpdated.addListener(\n  BackgroundEventsListeners.onHistoryStateUpdated\n);\n\nconst contextMenu = IS_FIREFOX ? browser.menus : browser.contextMenus;\nif (contextMenu && contextMenu.onClicked) {\n  contextMenu.onClicked.addListener(\n    BackgroundEventsListeners.onContextMenuClicked\n  );\n}\n\nif (browser.notifications && browser.notifications.onClicked) {\n  browser.notifications.onClicked.addListener(\n    BackgroundEventsListeners.onNotificationClicked\n  );\n}\n\nconst message = new MessageListener('background');\n\nmessage.on('browser-api', (payload) => {\n  return BrowserAPIService.runtimeMessageHandler.call(\n    BrowserAPIService,\n    payload\n  );\n});\nmessage.on(BrowserAPIEventHandler.RuntimeEvents.TOGGLE, (data) =>\n  BrowserAPIEventHandler.instance.onToggleBrowserEventListener(data)\n);\n\nmessage.on('fetch', async ({ type, resource }) => {\n  const response = await fetch(resource.url, resource);\n  if (!response.ok) throw new Error(response.statusText);\n\n  let result = null;\n\n  if (type === 'base64') {\n    const blob = await response.blob();\n    const base64 = await readFileAsBase64(blob);\n\n    result = base64;\n  } else {\n    result = await response[type]();\n  }\n\n  return result;\n});\nmessage.on('fetch:text', (url) => {\n  return fetch(url).then((response) => response.text());\n});\n\nmessage.on('open:dashboard', (url) => BackgroundUtils.openDashboard(url));\nmessage.on('set:active-tab', (tabId) => {\n  return browser.tabs.update(tabId, { active: true });\n});\n\nmessage.on('debugger:send-command', ({ tabId, method, params }) => {\n  return new Promise((resolve) => {\n    chrome.debugger.sendCommand({ tabId }, method, params, resolve);\n  });\n});\nmessage.on('debugger:type', ({ tabId, commands, delay }) => {\n  return new Promise((resolve) => {\n    let index = 0;\n    async function executeCommands() {\n      const command = commands[index];\n      if (!command) {\n        resolve();\n        return;\n      }\n\n      chrome.debugger.sendCommand(\n        { tabId },\n        'Input.dispatchKeyEvent',\n        command,\n        async () => {\n          if (delay > 0) await sleep(delay);\n\n          index += 1;\n          executeCommands();\n        }\n      );\n    }\n    executeCommands();\n  });\n});\n\nmessage.on('get:sender', (_, sender) => sender);\nmessage.on('get:file', (path) => getFile(path));\nmessage.on('get:tab-screenshot', (options, sender) =>\n  browser.tabs.captureVisibleTab(sender.tab.windowId, options)\n);\n\nmessage.on('dashboard:refresh-packages', async () => {\n  const tabs = await browser.tabs.query({\n    url: browser.runtime.getURL('/newtab.html'),\n  });\n\n  tabs.forEach((tab) => {\n    browser.tabs.sendMessage(tab.id, {\n      type: 'refresh-packages',\n    });\n  });\n});\n\nmessage.on('workflow:stop', (stateId) =>\n  BackgroundWorkflowUtils.instance.stopExecution(stateId)\n);\nmessage.on('workflow:execute', async (workflowData, sender) => {\n  if (workflowData.includeTabId) {\n    if (!workflowData.options) workflowData.options = {};\n    workflowData.options.tabId = sender.tab.id;\n  }\n\n  BackgroundWorkflowUtils.instance.executeWorkflow(\n    workflowData,\n    workflowData?.options || {}\n  );\n});\nmessage.on(\n  'workflow:added',\n  ({ workflowId, teamId, workflowData, source = 'community' }) => {\n    let path = `/workflows/${workflowId}`;\n\n    if (source === 'team') {\n      if (!teamId) return;\n      path = `/teams/${teamId}/workflows/${workflowId}`;\n    }\n\n    browser.tabs\n      .query({ url: browser.runtime.getURL('/newtab.html') })\n      .then((tabs) => {\n        if (tabs.length >= 1) {\n          const lastTab = tabs.at(-1);\n\n          tabs.forEach((tab) => {\n            browser.tabs.sendMessage(tab.id, {\n              data: { workflowId, teamId, source, workflowData },\n              type: 'workflow:added',\n            });\n          });\n\n          browser.tabs.update(lastTab.id, {\n            active: true,\n          });\n          browser.windows.update(lastTab.windowId, { focused: true });\n        } else {\n          BackgroundUtils.openDashboard(`${path}?permission=true`);\n        }\n      });\n  }\n);\nmessage.on('workflow:register', ({ triggerBlock, workflowId }) => {\n  registerWorkflowTrigger(workflowId, triggerBlock);\n});\nmessage.on('recording:stop', async () => {\n  try {\n    await BackgroundUtils.openDashboard('', false);\n    await BackgroundUtils.sendMessageToDashboard('recording:stop');\n  } catch (error) {\n    console.error(error);\n  }\n});\nmessage.on('workflow:resume', ({ id, nextBlock }) => {\n  if (!id) return;\n  BackgroundWorkflowUtils.instance.resumeExecution(id, nextBlock);\n});\nmessage.on('workflow:breakpoint', (id) => {\n  if (!id) return;\n  BackgroundWorkflowUtils.instance.updateExecutionState(id, {\n    status: 'breakpoint',\n  });\n});\n\nmessage.on('get:user-id', async () => {\n  const userStore = useUserStore();\n  return { userId: userStore.user?.id };\n});\n\nmessage.on(\n  'check-csp-and-inject',\n  async ({ target, debugMode, callback, options, injectOptions }) => {\n    try {\n      const [isBlockedByCSP] = await browser.scripting.executeScript({\n        target,\n        // eslint-disable-next-line object-shorthand\n        func: function () {\n          return new Promise((resolve) => {\n            const escapePolicy = (script) => {\n              if (window?.trustedTypes?.createPolicy) {\n                try {\n                  // 生成基于白名单的唯一策略名称，避免与现有策略冲突\n                  const baseNames = [\n                    'default',\n                    'dompurify',\n                    'jSecure',\n                    'forceInner',\n                  ];\n                  let escapeElPolicy = null;\n\n                  // 为每个基础名称尝试添加唯一后缀\n                  for (const baseName of baseNames) {\n                    try {\n                      const uniqueName = `${baseName}-automa-${Date.now().toString(\n                        36\n                      )}`;\n                      escapeElPolicy = window.trustedTypes.createPolicy(\n                        uniqueName,\n                        {\n                          createHTML: (to_escape) => to_escape,\n                          createScript: (to_escape) => to_escape,\n                        }\n                      );\n                      // 如果成功创建，跳出循环\n                      break;\n                    } catch (e) {\n                      // 该名称失败（可能不在白名单中），继续尝试下一个\n                    }\n                  }\n\n                  // 如果基于白名单的策略都失败，尝试纯 automa 策略名\n                  if (!escapeElPolicy) {\n                    try {\n                      const automaName = `automa-policy-${Date.now().toString(\n                        36\n                      )}`;\n                      escapeElPolicy = window.trustedTypes.createPolicy(\n                        automaName,\n                        {\n                          createHTML: (to_escape) => to_escape,\n                          createScript: (to_escape) => to_escape,\n                        }\n                      );\n                    } catch (e) {\n                      // 最后的尝试也失败了\n                    }\n                  }\n\n                  // 如果成功创建了策略，使用它\n                  if (escapeElPolicy) {\n                    return escapeElPolicy.createScript(script);\n                  }\n                  // 如果所有策略名称都失败，返回原始脚本\n                  return script;\n                } catch (e) {\n                  // 捕获任何其他错误并降级\n                  return script;\n                }\n              }\n              return script;\n            };\n\n            const eventListener = ({ srcElement }) => {\n              if (!srcElement || srcElement.id !== 'automa-csp') return;\n              srcElement.remove();\n              resolve(true);\n            };\n\n            document.addEventListener('securitypolicyviolation', eventListener);\n            const script = document.createElement('script');\n            script.id = 'automa-csp';\n            script.innerText = escapePolicy('console.log(\"...\")');\n\n            setTimeout(() => {\n              document.removeEventListener(\n                'securitypolicyviolation',\n                eventListener\n              );\n              resolve(false);\n            }, 500);\n\n            document.body.appendChild(script);\n          });\n        },\n        world: 'MAIN',\n        ...injectOptions,\n      });\n\n      // CSP blocked\n      if (isBlockedByCSP.result) {\n        await new Promise((resolve) => {\n          chrome.debugger.attach({ tabId: target.tabId }, '1.3', resolve);\n        });\n\n        // 首先执行回调函数以获取JS代码字符串\n        // 这里的关键是回调函数本身就是一个字符串，直接在debugger中执行\n        const callbackString =\n          typeof callback === 'function' ? callback.toString() : callback;\n\n        if (!callbackString) {\n          throw new Error('Callback is missing or invalid');\n        }\n\n        // 直接执行回调函数的字符串表示，不再通过额外的包装函数\n        const wrappedCallback = `\n          (function() {\n            try {\n              const fn = ${callbackString};\n              return fn();\n            } catch (err) {\n              console.error(\"Error in callback execution:\", err);\n              return JSON.stringify({ error: err.message });\n            }\n          })()\n        `;\n\n        // 执行回调函数以获取JavaScript代码\n        const jsCodeResult = await chrome.debugger.sendCommand(\n          { tabId: target.tabId },\n          'Runtime.evaluate',\n          {\n            expression: wrappedCallback,\n            userGesture: true,\n            returnByValue: true,\n            ...options,\n          }\n        );\n\n        if (!jsCodeResult || !jsCodeResult.result) {\n          console.error('无法获取JavaScript代码，结果为空');\n          throw new Error('Unable to get JavaScript code');\n        }\n\n        if (\n          jsCodeResult.result.subtype === 'error' ||\n          jsCodeResult.exceptionDetails\n        ) {\n          console.error(\n            '执行回调函数时出错:',\n            jsCodeResult.result.description || jsCodeResult.exceptionDetails\n          );\n          throw new Error(\n            jsCodeResult.result.description || 'Error executing callback'\n          );\n        }\n\n        // 确保我们获取到的是字符串类型的JavaScript代码\n        if (typeof jsCodeResult.result.value !== 'string') {\n          console.error('回调函数返回的不是JavaScript代码字符串');\n          throw new Error('Callback did not return JavaScript code string');\n        }\n\n        const jsCode = jsCodeResult.result.value;\n\n        // 执行生成的JavaScript代码\n        const execResult = await chrome.debugger.sendCommand(\n          { tabId: target.tabId },\n          'Runtime.evaluate',\n          {\n            expression: jsCode,\n            userGesture: true,\n            awaitPromise: true,\n            returnByValue: true,\n            ...options,\n          }\n        );\n\n        if (!debugMode) {\n          await chrome.debugger.detach({ tabId: target.tabId });\n        }\n\n        if (!execResult || !execResult.result) {\n          console.error('无法执行代码，结果为空');\n          throw new Error('Unable execute code');\n        }\n\n        if (\n          execResult.result.subtype === 'error' ||\n          execResult.exceptionDetails\n        ) {\n          console.error(\n            '执行JavaScript代码时出错:',\n            execResult.result.description || execResult.exceptionDetails\n          );\n          throw new Error(\n            execResult.result.description || 'Error executing JavaScript code'\n          );\n        }\n\n        return {\n          isBlocked: true,\n          value: execResult.result.value || null,\n        };\n      }\n\n      return { isBlocked: false };\n    } catch (error) {\n      console.error(error);\n      return { isBlocked: false, error: error.message };\n    }\n  }\n);\n\nconst getAutomaScript = ({ varName, refData, everyNewTab, isEval = false }) => {\n  let str = `\nconst ${varName} = ${JSON.stringify(refData)};\n${automaRefDataStr(varName)}\nfunction automaSetVariable(name, value) {\n  const variables = ${varName}.variables;\n  if (!variables) ${varName}.variables = {}\n\n  ${varName}.variables[name] = value;\n}\nfunction automaNextBlock(data, insert = true) {\n  if (${isEval}) {\n    Promise.resolve({\n      columns: {\n        data,\n        insert,\n      },\n      variables: ${varName}.variables,\n    });\n  } else{\n    document.body.dispatchEvent(new CustomEvent('__automa-next-block__', { detail: { data, insert, refData: ${varName} } }));\n  }\n}\nfunction automaResetTimeout() {\n  if (${isEval}) {\n    clearTimeout($automaTimeout);\n    $automaTimeout = setTimeout(() => {\n      resolve();\n    }, $automaTimeoutMs);\n  } else {\n    document.body.dispatchEvent(new CustomEvent('__automa-reset-timeout__'));\n  }\n}\n\nfunction automaFetchClient(id, { type, resource }) {\n  return new Promise((resolve, reject) => {\n    const validType = ['text', 'json', 'base64'];\n    if (!type || !validType.includes(type)) {\n      reject(new Error('The \"type\" must be \"text\" or \"json\"'));\n      return;\n    }\n\n    const eventName = \\`__automa-fetch-response-\\${id}__\\`;\n    const eventListener = ({ detail }) => {\n      if (detail.id !== id) return;\n\n      window.removeEventListener(eventName, eventListener);\n\n      if (detail.isError) {\n        reject(new Error(detail.result));\n      } else {\n        resolve(detail.result);\n      }\n    };\n\n    window.addEventListener(eventName, eventListener);\n    window.dispatchEvent(\n      new CustomEvent(\\`__automa-fetch__\\`, {\n        detail: {\n          id,\n          type,\n          resource,\n        },\n      })\n    );\n  });\n}\n\nfunction automaFetch(type, resource) {\n  return automaFetchClient('${varName}', { type, resource });\n}\n  `;\n\n  if (everyNewTab) str = automaRefDataStr(varName);\n\n  return str;\n};\n\nmessage.on(\n  'script:execute',\n  async ({ target, blockData, varName, preloadScripts }) => {\n    try {\n      const automaScript = getAutomaScript({\n        varName,\n        isEval: false,\n        refData: blockData.refData,\n        everyNewTab: blockData.data.everyNewTab,\n      });\n\n      const result = await browser.scripting.executeScript({\n        target,\n        func: ($blockData, $preloadScripts, $automaScript) => {\n          return new Promise((resolve, reject) => {\n            try {\n              const $documentCtx = document;\n\n              // fixme: 需要处理iframe的情况\n              // if ($blockData.frameSelector) {\n              //   const iframeCtx = getDocumentCtx($blockData.frameSelector);\n              //   if (!iframeCtx) {\n              //     reject(new Error('iframe-not-found'));\n              //     return;\n              //   }\n              //   $documentCtx = iframeCtx;\n              // }\n\n              const scriptAttr = `block--${$blockData.id}`;\n              const isScriptExists = $documentCtx.querySelector(\n                `.automa-custom-js[${scriptAttr}]`\n              );\n              if (isScriptExists) {\n                resolve('');\n                return;\n              }\n\n              const script = $documentCtx.createElement('script');\n              script.setAttribute(scriptAttr, '');\n              script.classList.add('automa-custom-js');\n              script.textContent = `\n                (() => {\n\n                  // Setup context\n                  ${$automaScript}\n\n                  // Execute user code\n                  try {\n                    ${$blockData.data.code}\n                    ${\n                      $blockData.data.everyNewTab ||\n                      $blockData.data.code.includes('automaNextBlock')\n                        ? ''\n                        : 'automaNextBlock()'\n                    }\n                  } catch (error) {\n                    console.error(error);\n                    ${\n                      $blockData.data.everyNewTab\n                        ? ''\n                        : 'automaNextBlock({ $error: true, message: error.message })'\n                    }\n                  }\n                })();\n              `;\n\n              const preloadScriptsEl = $preloadScripts.map((item) => {\n                const scriptEl = $documentCtx.createElement('script');\n                scriptEl.id = item.id;\n                scriptEl.textContent = item.script;\n                return {\n                  element: scriptEl,\n                  removeAfterExec: item.removeAfterExec,\n                };\n              });\n\n              if (!$blockData.data.everyNewTab) {\n                let timeout;\n                let onNextBlock;\n                let onResetTimeout;\n\n                const cleanUp = () => {\n                  script.remove();\n                  preloadScriptsEl.forEach((item) => {\n                    if (item.removeAfterExec) item.element.remove();\n                  });\n\n                  clearTimeout(timeout);\n\n                  $documentCtx.body.removeEventListener(\n                    '__automa-reset-timeout__',\n                    onResetTimeout\n                  );\n                  $documentCtx.body.removeEventListener(\n                    '__automa-next-block__',\n                    onNextBlock\n                  );\n                };\n\n                onNextBlock = ({ detail }) => {\n                  cleanUp();\n                  if (!detail) {\n                    resolve({ columns: {}, variables: {} });\n                    return;\n                  }\n\n                  const payload = {\n                    insert: detail.insert,\n                    data: detail.data?.$error\n                      ? detail.data\n                      : JSON.stringify(detail?.data ?? {}),\n                  };\n                  resolve({\n                    columns: payload,\n                    variables: detail.refData?.variables,\n                  });\n                };\n                onResetTimeout = () => {\n                  clearTimeout(timeout);\n                  timeout = setTimeout(cleanUp, $blockData.data.timeout);\n                };\n\n                $documentCtx.body.addEventListener(\n                  '__automa-next-block__',\n                  onNextBlock\n                );\n                $documentCtx.body.addEventListener(\n                  '__automa-reset-timeout__',\n                  onResetTimeout\n                );\n\n                timeout = setTimeout(cleanUp, $blockData.data.timeout);\n              } else {\n                resolve();\n              }\n\n              // Inject scripts in the correct order\n              preloadScriptsEl.forEach((item) => {\n                $documentCtx.head.appendChild(item.element);\n              });\n              $documentCtx.head.appendChild(script);\n            } catch (error) {\n              console.error('javascriptBlockUtil error', error);\n              reject(error);\n            }\n          });\n        },\n        world: 'MAIN',\n        args: [blockData, preloadScripts, automaScript],\n      });\n\n      return [{ result: result[0].result }];\n    } catch (err) {\n      return { result: null, msg: err.message, error: err };\n    }\n  }\n);\n\nmessage.on('script:execute-callback', async ({ target, callback }) => {\n  try {\n    // 首先尝试使用scripting API执行脚本\n    const result = await browser.scripting.executeScript({\n      target,\n      func: ($callbackFn) => {\n        try {\n          const script = document.createElement('script');\n          script.textContent = `\n          (() => {\n            ${$callbackFn}\n          })()\n          `;\n          document.body.appendChild(script);\n          return { success: true };\n        } catch (error) {\n          console.error('执行脚本时出错:', error);\n          return { success: false, error: error.message };\n        }\n      },\n      world: 'MAIN',\n      args: [callback],\n    });\n\n    // 检查执行结果\n    const executionResult = result[0]?.result;\n    if (executionResult && executionResult.success) {\n      return true;\n    }\n\n    // 如果常规方法失败，尝试使用debugger API\n    await new Promise((resolve) => {\n      chrome.debugger.attach({ tabId: target.tabId }, '1.3', resolve);\n    });\n\n    // 使用debugger API执行脚本\n    const execResult = await chrome.debugger.sendCommand(\n      { tabId: target.tabId },\n      'Runtime.evaluate',\n      {\n        expression: `(() => { ${callback} })()`,\n        userGesture: true,\n        awaitPromise: false,\n        returnByValue: true,\n      }\n    );\n\n    // 执行完成后分离debugger\n    await chrome.debugger.detach({ tabId: target.tabId });\n\n    if (!execResult || !execResult.result) {\n      console.error('使用debugger API执行脚本失败');\n      return false;\n    }\n\n    return true;\n  } catch (error) {\n    console.error('执行script:execute-callback时出错:', error);\n    return false;\n  }\n});\n\nconst DOWNLOADS_STORAGE_KEY = 'automa-rename-downloaded-files';\n\nconst getFileExtension = (str) => /(?:\\.([^.]+))?$/.exec(str)[1];\n\nconst downloadListeners = {\n  registered: false,\n  changedCallbacks: new Map(),\n  pendingRequests: [],\n  downloadDataCache: new Map(),\n  handledFilenameCallbacks: new Set(),\n  suggestCalled: new Set(),\n  downloadInfo: new Map(),\n};\n\n/**\n * @param {Object} item\n * @param {Function} suggest\n * @returns {boolean}\n */\nfunction determineFilenameListener(item, suggest) {\n  const downloadKey = `download-${item.id}`;\n\n  if (downloadListeners.suggestCalled.has(downloadKey)) {\n    return true;\n  }\n\n  downloadListeners.suggestCalled.add(downloadKey);\n\n  setTimeout(async () => {\n    try {\n      let suggestion = null;\n      if (downloadListeners.downloadDataCache.has(item.id)) {\n        suggestion = downloadListeners.downloadDataCache.get(item.id);\n      } else {\n        const result = await browser.storage.session.get(DOWNLOADS_STORAGE_KEY);\n        const filesData = result[DOWNLOADS_STORAGE_KEY] || {};\n\n        suggestion = filesData[item.id];\n      }\n\n      if (!suggestion) {\n        // we should not call suggest again, because Chrome expects us to handle it\n        return;\n      }\n\n      if (!suggestion.filename || suggestion.filename.trim() === '') {\n        return;\n      }\n\n      const hasFileExt = getFileExtension(suggestion.filename);\n\n      if (!hasFileExt) {\n        const fileExtension = getFileExtension(item.filename);\n        suggestion.filename += `.${fileExtension}`;\n      }\n\n      let conflictAction = 'uniquify';\n      const validActions = ['uniquify', 'overwrite', 'prompt'];\n\n      if (\n        suggestion.onConflict &&\n        validActions.includes(suggestion.onConflict)\n      ) {\n        conflictAction = suggestion.onConflict;\n      }\n\n      if (!suggestion.waitForDownload) {\n        downloadListeners.downloadDataCache.delete(item.id);\n\n        const result = await browser.storage.session.get(DOWNLOADS_STORAGE_KEY);\n        const filesData = result[DOWNLOADS_STORAGE_KEY] || {};\n        delete filesData[item.id];\n        await browser.storage.session.set({\n          [DOWNLOADS_STORAGE_KEY]: filesData,\n        });\n      }\n\n      downloadListeners.handledFilenameCallbacks.add(downloadKey);\n\n      try {\n        suggest({\n          filename: suggestion.filename,\n          conflictAction,\n        });\n      } catch (callbackError) {\n        console.error('❌ failed to call suggest callback:', callbackError);\n      }\n    } catch (error) {\n      console.error('❌ failed to handle download filename:', error);\n    }\n  }, 0);\n\n  // important: we use async processing, so we must return true\n  return true;\n}\n\nfunction handleDownloadChanged(downloadDelta) {\n  const { id, state, filename } = downloadDelta;\n\n  if (!id || !downloadListeners.changedCallbacks.has(id)) return;\n\n  if (!downloadListeners.downloadInfo.has(id)) {\n    downloadListeners.downloadInfo.set(id, {\n      downloadId: id,\n      state: null,\n      filename: null,\n    });\n  }\n\n  const downloadInfo = downloadListeners.downloadInfo.get(id);\n\n  if (state) {\n    downloadInfo.state = state.current;\n  }\n\n  if (filename) {\n    downloadInfo.filename = filename.current;\n  }\n\n  if (\n    downloadInfo.state &&\n    ['complete', 'interrupted'].includes(downloadInfo.state)\n  ) {\n    const callback = downloadListeners.changedCallbacks.get(id);\n\n    const completeInfo = {\n      ...downloadInfo,\n\n      filename:\n        downloadInfo.filename ||\n        (downloadListeners.downloadDataCache.has(id)\n          ? downloadListeners.downloadDataCache.get(id).filename\n          : null),\n    };\n\n    try {\n      callback(completeInfo);\n    } catch (callbackError) {\n      console.error(\n        '❌ failed to call download changed callback:',\n        callbackError\n      );\n    }\n\n    downloadListeners.changedCallbacks.delete(id);\n    downloadListeners.downloadDataCache.delete(id);\n    downloadListeners.downloadInfo.delete(id);\n\n    const downloadKey = `download-${id}`;\n    downloadListeners.handledFilenameCallbacks.delete(downloadKey);\n    downloadListeners.suggestCalled.delete(downloadKey);\n  }\n}\n\nasync function handleDownloadCreated(downloadItem) {\n  try {\n    let isHandled = false;\n    const pendingDownloads = downloadListeners.pendingRequests || [];\n\n    if (pendingDownloads.length > 0) {\n      const pendingRequest = pendingDownloads.shift();\n      const { downloadData, callback } = pendingRequest;\n\n      // save to memory cache immediately to avoid race condition\n      downloadListeners.downloadDataCache.set(downloadItem.id, downloadData);\n\n      // save to storage\n      const result = await browser.storage.session.get(DOWNLOADS_STORAGE_KEY);\n      const filesData = result[DOWNLOADS_STORAGE_KEY] || {};\n      filesData[downloadItem.id] = downloadData;\n      await browser.storage.session.set({ [DOWNLOADS_STORAGE_KEY]: filesData });\n\n      if (downloadData.waitForDownload && callback) {\n        downloadListeners.changedCallbacks.set(downloadItem.id, callback);\n      }\n\n      isHandled = true;\n    }\n\n    if (!isHandled) {\n      const result = await browser.storage.session.get(DOWNLOADS_STORAGE_KEY);\n      const filesData = result[DOWNLOADS_STORAGE_KEY] || {};\n\n      if (filesData[downloadItem.id]) {\n        downloadListeners.downloadDataCache.set(\n          downloadItem.id,\n          filesData[downloadItem.id]\n        );\n      }\n    }\n  } catch (error) {\n    console.error('❌ failed to handle download created:', error);\n  }\n}\n\nfunction cleanupDownloadListeners() {\n  const MAX_AGE = 60 * 60 * 1000; // 1 hour\n  const now = Date.now();\n\n  if (downloadListeners.handledFilenameCallbacksTimestamp) {\n    if (now - downloadListeners.handledFilenameCallbacksTimestamp > MAX_AGE) {\n      downloadListeners.handledFilenameCallbacks.clear();\n    }\n  }\n\n  downloadListeners.handledFilenameCallbacksTimestamp = now;\n}\n\nsetInterval(cleanupDownloadListeners, 60 * 60 * 1000);\n\nasync function registerBackgroundDownloadListeners() {\n  try {\n    if (browser.downloads.onCreated.hasListener(handleDownloadCreated)) {\n      browser.downloads.onCreated.removeListener(handleDownloadCreated);\n    }\n\n    if (\n      !IS_FIREFOX &&\n      browser.downloads.onDeterminingFilename &&\n      browser.downloads.onDeterminingFilename.hasListener(\n        determineFilenameListener\n      )\n    ) {\n      browser.downloads.onDeterminingFilename.removeListener(\n        determineFilenameListener\n      );\n    }\n\n    if (browser.downloads.onChanged.hasListener(handleDownloadChanged)) {\n      browser.downloads.onChanged.removeListener(handleDownloadChanged);\n    }\n\n    downloadListeners.handledFilenameCallbacks.clear();\n    downloadListeners.suggestCalled.clear();\n    downloadListeners.downloadInfo.clear();\n\n    if (downloadListeners.registered) {\n      downloadListeners.registered = false;\n    }\n  } catch (cleanupError) {\n    console.warn('⚠️ failed to cleanup existing listeners:', cleanupError);\n  }\n\n  if (downloadListeners.registered) return;\n\n  try {\n    // 确保有下载权限\n    const hasPermission = await browser.permissions.contains({\n      permissions: ['downloads'],\n    });\n    if (!hasPermission) {\n      console.error('❌ no download permission, cannot register listeners');\n      return;\n    }\n\n    browser.downloads.onCreated.addListener(handleDownloadCreated);\n\n    if (!IS_FIREFOX && browser.downloads.onDeterminingFilename) {\n      browser.downloads.onDeterminingFilename.addListener(\n        determineFilenameListener\n      );\n    }\n\n    browser.downloads.onChanged.addListener(handleDownloadChanged);\n\n    downloadListeners.registered = true;\n    downloadListeners.pendingRequests = [];\n\n    downloadListeners.handledFilenameCallbacksTimestamp = Date.now();\n  } catch (error) {\n    console.error('❌ failed to register download listeners:', error);\n  }\n}\n\nmessage.on('downloads:register-listeners', async () => {\n  await registerBackgroundDownloadListeners();\n  return true;\n});\n\nmessage.on('downloads:watch-created', async (data) => {\n  await registerBackgroundDownloadListeners();\n\n  // save pending download requests\n  downloadListeners.pendingRequests = downloadListeners.pendingRequests || [];\n\n  // Handle callback bridge or regular function\n  let safeCallback = null;\n  if (data.onComplete) {\n    if (isCallbackBridge(data.onComplete)) {\n      // Use callback bridge for cross-context communication\n      safeCallback = async (response) => {\n        try {\n          await executeCallbacksInData(data.onComplete, response);\n        } catch (callbackError) {\n          console.error(\n            '❌ failed to call download complete callback:',\n            callbackError\n          );\n        }\n      };\n    } else if (typeof data.onComplete === 'function') {\n      // Fallback for regular functions (should not happen in fixed version)\n      safeCallback = (response) => {\n        try {\n          data.onComplete(response);\n        } catch (callbackError) {\n          console.error(\n            '❌ failed to call download complete callback:',\n            callbackError\n          );\n        }\n      };\n    }\n  }\n\n  downloadListeners.pendingRequests.push({\n    downloadData: data.downloadData,\n    tabId: data.tabId,\n    callback: safeCallback,\n  });\n\n  return true;\n});\n\nmessage.on('downloads:watch-changed', async ({ downloadId, onComplete }) => {\n  await registerBackgroundDownloadListeners();\n\n  if (downloadId && onComplete) {\n    let safeCallback = null;\n\n    if (isCallbackBridge(onComplete)) {\n      // Use callback bridge for cross-context communication\n      safeCallback = async (response) => {\n        try {\n          await executeCallbacksInData(onComplete, response);\n        } catch (callbackError) {\n          console.error(\n            '❌ failed to call download changed callback:',\n            callbackError\n          );\n        }\n      };\n    } else if (typeof onComplete === 'function') {\n      // Fallback for regular functions (should not happen in fixed version)\n      safeCallback = (response) => {\n        try {\n          onComplete(response);\n        } catch (callbackError) {\n          console.error(\n            '❌ failed to call download changed callback:',\n            callbackError\n          );\n        }\n      };\n    }\n\n    if (safeCallback) {\n      downloadListeners.changedCallbacks.set(downloadId, safeCallback);\n    }\n  }\n\n  return true;\n});\n\nautoma('background', message);\n\nbrowser.runtime.onMessage.addListener(message.listener);\n"
  },
  {
    "path": "src/common/utils/constant.js",
    "content": "export const IS_FIREFOX = BROWSER_TYPE === 'firefox';\n"
  },
  {
    "path": "src/components/block/BlockBase.vue",
    "content": "<template>\n  <div\n    class=\"block-base relative w-48\"\n    :data-block-id=\"blockId\"\n    @dblclick.stop=\"$emit('edit')\"\n  >\n    <div\n      class=\"block-menu-container absolute top-0 hidden w-full\"\n      style=\"transform: translateY(-100%)\"\n    >\n      <div class=\"pointer-events-none\">\n        <p\n          title=\"Block id (click to copy)\"\n          class=\"block-menu pointer-events-auto text-overflow inline-block px-1 dark:text-gray-300\"\n          style=\"max-width: 96px; margin-bottom: 0\"\n          @click=\"insertToClipboard\"\n        >\n          {{ isCopied ? '✅ Copied' : blockId }}\n        </p>\n      </div>\n      <div class=\"block-menu inline-flex items-center dark:text-gray-300\">\n        <button\n          v-if=\"!blockData.details?.disableDelete\"\n          title=\"Delete block\"\n          @click.stop=\"$emit('delete')\"\n        >\n          <v-remixicon size=\"20\" name=\"riDeleteBin7Line\" />\n        </button>\n        <button\n          :title=\"$t('workflow.blocks.base.settings.title')\"\n          @click.stop=\"\n            $emit('settings', { details: blockData.details, data, blockId })\n          \"\n        >\n          <v-remixicon size=\"20\" name=\"riSettings3Line\" />\n        </button>\n        <button\n          v-if=\"!excludeGroupBlocks.includes(blockData.details?.id)\"\n          :title=\"$t('workflow.blocks.base.moveToGroup')\"\n          draggable=\"true\"\n          class=\"cursor-move\"\n          @dragstart=\"handleStartDrag\"\n          @mousedown.stop\n        >\n          <v-remixicon name=\"riDragDropLine\" size=\"20\" />\n        </button>\n        <button\n          v-if=\"blockData.details?.id !== 'trigger'\"\n          title=\"Enable/Disable block\"\n          @click.stop=\"$emit('update', { disableBlock: !data.disableBlock })\"\n        >\n          <v-remixicon\n            size=\"20\"\n            :name=\"data.disableBlock ? 'riToggleLine' : 'riToggleFill'\"\n          />\n        </button>\n        <button title=\"Run workflow from here\" @click.stop=\"runWorkflow\">\n          <v-remixicon size=\"20\" name=\"riPlayLine\" />\n        </button>\n        <button\n          v-if=\"!blockData.details?.disableEdit\"\n          title=\"Edit block\"\n          @click=\"$emit('edit')\"\n        >\n          <v-remixicon size=\"20\" name=\"riPencilLine\" />\n        </button>\n        <slot name=\"action\" />\n      </div>\n    </div>\n    <slot name=\"prepend\" />\n    <ui-card :class=\"contentClass\" class=\"block-base__content relative z-10\">\n      <v-remixicon\n        v-if=\"workflow?.data?.value.testingMode\"\n        :class=\"{ 'text-red-500 dark:text-red-400': data.$breakpoint }\"\n        class=\"absolute left-0 top-0\"\n        name=\"riRecordCircleFill\"\n        title=\"Set as breakpoint\"\n        size=\"20\"\n        @click=\"$emit('update', { $breakpoint: !data.$breakpoint })\"\n      />\n      <slot></slot>\n    </ui-card>\n    <slot name=\"append\" />\n  </div>\n</template>\n<script setup>\nimport { excludeGroupBlocks } from '@/utils/shared';\nimport { inject, ref } from 'vue';\n\nconst props = defineProps({\n  contentClass: {\n    type: String,\n    default: '',\n  },\n  blockData: {\n    type: Object,\n    default: () => ({}),\n  },\n  data: {\n    type: Object,\n    default: () => ({}),\n  },\n  blockId: {\n    type: String,\n    default: '',\n  },\n});\ndefineEmits(['delete', 'edit', 'update', 'settings']);\n\nconst isCopied = ref(false);\nconst workflow = inject('workflow', null);\nconst workflowUtils = inject('workflow-utils', null);\n\nfunction insertToClipboard() {\n  navigator.clipboard.writeText(props.blockId);\n\n  isCopied.value = true;\n  setTimeout(() => {\n    isCopied.value = false;\n  }, 1000);\n}\nfunction handleStartDrag(event) {\n  const payload = {\n    data: props.data,\n    fromBlockBasic: true,\n    blockId: props.blockId,\n    id: props.blockData.details.id,\n  };\n\n  event.dataTransfer.setData('block', JSON.stringify(payload));\n}\nfunction runWorkflow() {\n  if (!workflowUtils) return;\n\n  workflowUtils.executeFromBlock(props.blockId);\n}\n</script>\n<style>\n.block-menu {\n  @apply mb-1 bg-box-transparent-2 rounded-md;\n  button {\n    padding-left: 6px;\n    padding-right: 6px;\n    @apply focus:ring-0 py-1 hover:text-primary;\n  }\n}\n</style>\n"
  },
  {
    "path": "src/components/block/BlockBasic.vue",
    "content": "<template>\n  <block-base\n    :id=\"componentId\"\n    :data=\"data\"\n    :block-id=\"id\"\n    :block-data=\"block\"\n    :data-position=\"JSON.stringify(position)\"\n    class=\"block-basic group\"\n    @edit=\"$emit('edit')\"\n    @delete=\"$emit('delete', id)\"\n    @update=\"$emit('update', $event)\"\n    @settings=\"$emit('settings', $event)\"\n  >\n    <Handle\n      v-if=\"label !== 'trigger'\"\n      :id=\"`${id}-input-1`\"\n      type=\"target\"\n      :position=\"Position.Left\"\n    />\n    <div class=\"flex items-center\">\n      <span\n        :class=\"data.disableBlock ? 'bg-box-transparent' : block.category.color\"\n        class=\"mr-2 inline-block rounded-lg p-2 dark:text-black\"\n      >\n        <svg\n          v-if=\"block.details.name === 'AI Workflow'\"\n          width=\"31.2\"\n          height=\"31.2\"\n          viewBox=\"0 0 14 12\"\n          fill=\"none\"\n          xmlns=\"http://www.w3.org/2000/svg\"\n        >\n          <path\n            d=\"M5.22626 4.28601V8.14343H1.36884V4.28601H5.22626Z\"\n            stroke=\"black\"\n          />\n          <path\n            d=\"M12.6076 0.50061V3.64319H9.46503V0.50061H12.6076Z\"\n            stroke=\"black\"\n          />\n          <path\n            d=\"M12.6309 8.35657V11.4991H9.48834V8.35657H12.6309Z\"\n            stroke=\"black\"\n          />\n          <path d=\"M5.66516 6.37384H7.27247V2.13159H9.45839\" stroke=\"black\" />\n          <path d=\"M5.15082 6.43445H7.26688V9.9986H9.91184\" stroke=\"black\" />\n        </svg>\n\n        <v-remixicon\n          v-else\n          :path=\"getIconPath(block.details.icon)\"\n          :name=\"block.details.icon || 'riGlobalLine'\"\n        />\n      </span>\n      <div class=\"flex-1 overflow-hidden\">\n        <span\n          v-if=\"blockErrors\"\n          v-tooltip=\"{\n            allowHTML: true,\n            content: blockErrors,\n          }\"\n          class=\"absolute top-2 right-2 text-red-500 dark:text-red-400\"\n        >\n          <v-remixicon name=\"riAlertLine\" size=\"20\" />\n        </span>\n        <p\n          v-if=\"block.details.id\"\n          class=\"text-overflow whitespace-nowrap font-semibold leading-tight\"\n        >\n          {{ getBlockName() }}\n        </p>\n        <p\n          :class=\"{ 'mb-1': data.description && data.loopId }\"\n          class=\"text-overflow leading-tight text-gray-600 dark:text-gray-200\"\n        >\n          {{ data.description }}\n        </p>\n        <span\n          v-if=\"showTextToCopy\"\n          :title=\"showTextToCopy.name + ' (click to copy)'\"\n          class=\"bg-box-transparent text-overflow absolute bottom-0 right-0 rounded-sm rounded-br-lg py-px px-1 text-xs text-gray-600 dark:text-gray-200\"\n          style=\"max-width: 40%; cursor: pointer\"\n          @click.stop=\"insertToClipboard(showTextToCopy.value)\"\n        >\n          {{ state.isCopied ? '✅ Copied' : showTextToCopy.value }}\n        </span>\n      </div>\n    </div>\n    <slot :block=\"block\"></slot>\n    <div\n      v-if=\"data.onError?.enable && data.onError?.toDo === 'fallback'\"\n      class=\"fallback flex items-center justify-end\"\n    >\n      <v-remixicon\n        v-if=\"block\"\n        :title=\"t('workflow.blocks.base.onError.fallbackTitle')\"\n        name=\"riInformationLine\"\n        size=\"18\"\n      />\n      <span class=\"ml-1\">\n        {{ t('common.fallback') }}\n      </span>\n    </div>\n    <Handle :id=\"`${id}-output-1`\" type=\"source\" :position=\"Position.Right\" />\n    <Handle\n      v-if=\"data.onError?.enable && data.onError?.toDo === 'fallback'\"\n      :id=\"`${id}-output-fallback`\"\n      type=\"source\"\n      :position=\"Position.Right\"\n      style=\"top: auto; bottom: 10px\"\n    />\n  </block-base>\n</template>\n<script setup>\nimport { useBlockValidation } from '@/composable/blockValidation';\nimport { useComponentId } from '@/composable/componentId';\nimport { useEditorBlock } from '@/composable/editorBlock';\nimport { Handle, Position } from '@vue-flow/core';\nimport { computed, shallowReactive } from 'vue';\nimport { useI18n } from 'vue-i18n';\nimport BlockBase from './BlockBase.vue';\n\nconst props = defineProps({\n  id: {\n    type: String,\n    default: '',\n  },\n  label: {\n    type: String,\n    default: '',\n  },\n  data: {\n    type: Object,\n    default: () => ({}),\n  },\n  position: {\n    type: Object,\n    default: () => ({}),\n  },\n  events: {\n    type: Object,\n    default: () => ({}),\n  },\n  dimensions: {\n    type: Object,\n    default: () => ({}),\n  },\n});\ndefineEmits(['delete', 'edit', 'update', 'settings']);\n\nconst loopBlocks = ['loop-data', 'loop-elements'];\n\nconst { t, te } = useI18n();\nconst block = useEditorBlock(props.label);\nconst componentId = useComponentId('block-base');\nconst { errors: blockErrors } = useBlockValidation(\n  props.label,\n  () => props.data\n);\n\nconst state = shallowReactive({\n  isCopied: false,\n});\n\nconst showTextToCopy = computed(() => {\n  if (loopBlocks.includes(block.details.id) && props.data.loopId) {\n    return {\n      name: 'Loop id',\n      value: props.data.loopId,\n    };\n  }\n\n  if (block.details.id === 'google-sheets' && props.data.refKey) {\n    return {\n      name: 'Reference key',\n      value: props.data.refKey,\n    };\n  }\n\n  return null;\n});\n\nfunction insertToClipboard(text) {\n  navigator.clipboard.writeText(text);\n\n  state.isCopied = true;\n  setTimeout(() => {\n    state.isCopied = false;\n  }, 1000);\n}\nfunction getBlockName() {\n  const key = `workflow.blocks.${block.details.id}.name`;\n\n  return te(key) ? t(key) : block.details.name;\n}\nfunction getIconPath(path) {\n  if (path && path.startsWith('path')) {\n    const { 1: iconPath } = path.split(':');\n    return iconPath;\n  }\n\n  return '';\n}\n</script>\n"
  },
  {
    "path": "src/components/block/BlockBasicWithFallback.vue",
    "content": "<template>\n  <block-base\n    :id=\"componentId\"\n    :data=\"data\"\n    :block-id=\"id\"\n    :block-data=\"block\"\n    class=\"block-basic group\"\n    @edit=\"$emit('edit')\"\n    @delete=\"$emit('delete', id)\"\n    @update=\"$emit('update', $event)\"\n    @settings=\"$emit('settings', $event)\"\n  >\n    <Handle :id=\"`${id}-input-1`\" type=\"target\" :position=\"Position.Left\" />\n    <div class=\"flex items-center\">\n      <span\n        :class=\"data.disableBlock ? 'bg-box-transparent' : block.category.color\"\n        class=\"mr-2 inline-block rounded-lg p-2 dark:text-black\"\n      >\n        <v-remixicon :name=\"block.details.icon || 'riGlobalLine'\" />\n      </span>\n      <div class=\"flex-1 overflow-hidden\">\n        <p\n          v-if=\"block.details.id\"\n          class=\"text-overflow whitespace-nowrap font-semibold leading-tight\"\n        >\n          {{ t(`workflow.blocks.${block.details.id}.name`) }}\n        </p>\n        <p class=\"text-overflow leading-tight text-gray-600 dark:text-gray-200\">\n          {{ data.description }}\n        </p>\n      </div>\n    </div>\n    <span\n      v-if=\"blockErrors\"\n      v-tooltip=\"{\n        allowHTML: true,\n        content: blockErrors,\n      }\"\n      class=\"absolute top-2 right-2 text-red-500 dark:text-red-400\"\n    >\n      <v-remixicon name=\"riAlertLine\" size=\"20\" />\n    </span>\n    <slot :block=\"block\"></slot>\n    <div class=\"fallback flex items-center justify-end\">\n      <v-remixicon\n        v-if=\"block\"\n        :title=\"t('workflow.blocks.base.onError.fallbackTitle')\"\n        name=\"riInformationLine\"\n        size=\"18\"\n      />\n      <span class=\"ml-1\">\n        {{ t('common.fallback') }}\n      </span>\n    </div>\n    <Handle :id=\"`${id}-output-1`\" type=\"source\" :position=\"Position.Right\" />\n    <Handle\n      :id=\"`${id}-output-fallback`\"\n      type=\"source\"\n      :position=\"Position.Right\"\n      style=\"top: auto; bottom: 10px\"\n    />\n  </block-base>\n</template>\n<script setup>\nimport { Handle, Position } from '@vue-flow/core';\nimport { useI18n } from 'vue-i18n';\nimport { useBlockValidation } from '@/composable/blockValidation';\nimport { useEditorBlock } from '@/composable/editorBlock';\nimport { useComponentId } from '@/composable/componentId';\nimport BlockBase from './BlockBase.vue';\n\nconst props = defineProps({\n  id: {\n    type: String,\n    default: '',\n  },\n  label: {\n    type: String,\n    default: '',\n  },\n  data: {\n    type: Object,\n    default: () => ({}),\n  },\n});\ndefineEmits(['delete', 'edit', 'update', 'settings']);\n\nconst { t } = useI18n();\nconst block = useEditorBlock(props.label);\nconst componentId = useComponentId('block-base');\nconst { errors: blockErrors } = useBlockValidation(\n  props.label,\n  () => props.data\n);\n</script>\n"
  },
  {
    "path": "src/components/block/BlockConditions.vue",
    "content": "<template>\n  <block-base\n    :id=\"componentId\"\n    :data=\"data\"\n    :block-id=\"id\"\n    :block-data=\"block\"\n    class=\"w-64\"\n    @edit=\"$emit('edit')\"\n    @delete=\"$emit('delete', id)\"\n    @update=\"$emit('update', $event)\"\n    @settings=\"$emit('settings', $event)\"\n  >\n    <Handle :id=\"`${id}-input-1`\" type=\"target\" :position=\"Position.Left\" />\n    <div class=\"flex items-center\">\n      <div\n        :class=\"data.disableBlock ? 'bg-box-transparent' : block.category.color\"\n        class=\"mr-4 inline-block rounded-lg p-2 text-sm dark:text-black\"\n      >\n        <v-remixicon name=\"riAB\" size=\"20\" class=\"mr-1 inline-block\" />\n        <span>{{ t('workflow.blocks.conditions.name') }}</span>\n      </div>\n    </div>\n    <p\n      v-show=\"data.description\"\n      class=\"text-overflow mt-2 leading-tight text-gray-600 dark:text-gray-200\"\n    >\n      {{ data.description }}\n    </p>\n    <ul\n      v-if=\"data.conditions && data.conditions.length !== 0\"\n      class=\"mt-4 space-y-2\"\n    >\n      <li\n        v-for=\"item in data.conditions\"\n        :key=\"item.id\"\n        class=\"bg-box-transparent relative flex w-full flex-1 items-center rounded-lg p-2\"\n        @dblclick.stop=\"$emit('edit', { editCondition: item.id })\"\n      >\n        <p\n          v-if=\"item.name\"\n          class=\"text-overflow w-full text-right\"\n          :title=\"item.name\"\n        >\n          {{ item.name }}\n        </p>\n        <template v-else>\n          <p class=\"text-overflow w-5/12 text-right\">\n            {{ item.compareValue || '_____' }}\n          </p>\n          <p class=\"mx-1 w-2/12 text-center font-mono\">\n            {{ item.type }}\n          </p>\n          <p class=\"text-overflow w-5/12\">\n            {{ item.value || '_____' }}\n          </p>\n        </template>\n        <Handle\n          :id=\"`${id}-output-${item.id}`\"\n          :position=\"Position.Right\"\n          style=\"margin-right: -33px\"\n          type=\"source\"\n        />\n      </li>\n      <p\n        v-if=\"data.conditions && data.conditions.length !== 0\"\n        class=\"text-right text-gray-600 dark:text-gray-200\"\n      >\n        <span title=\"Fallback\"> &#9432; </span>\n        Fallback\n      </p>\n    </ul>\n    <Handle\n      v-if=\"data.conditions.length > 0\"\n      :id=\"`${id}-output-fallback`\"\n      :position=\"Position.Right\"\n      type=\"source\"\n      style=\"top: auto; bottom: 10px\"\n    />\n  </block-base>\n</template>\n<script setup>\nimport { useI18n } from 'vue-i18n';\nimport { Handle, Position } from '@vue-flow/core';\nimport { useComponentId } from '@/composable/componentId';\nimport { useEditorBlock } from '@/composable/editorBlock';\nimport BlockBase from './BlockBase.vue';\n\nconst props = defineProps({\n  id: {\n    type: String,\n    default: '',\n  },\n  label: {\n    type: String,\n    default: '',\n  },\n  data: {\n    type: Object,\n    default: () => ({}),\n  },\n});\ndefineEmits(['delete', 'settings', 'edit', 'update']);\n\nconst { t } = useI18n();\nconst componentId = useComponentId('block-conditions');\nconst block = useEditorBlock(props.label);\n</script>\n<style>\n.condition-handle {\n  position: relative !important;\n  top: 82px !important;\n  margin-bottom: 32px !important;\n}\n</style>\n"
  },
  {
    "path": "src/components/block/BlockDelay.vue",
    "content": "<template>\n  <block-base\n    :id=\"componentId\"\n    :data=\"data\"\n    :block-id=\"id\"\n    :block-data=\"block\"\n    class=\"w-48\"\n    @delete=\"$emit('delete', id)\"\n    @update=\"$emit('update', $event)\"\n    @settings=\"$emit('settings', $event)\"\n  >\n    <Handle :id=\"`${id}-input-1`\" type=\"target\" :position=\"Position.Left\" />\n    <div class=\"mb-2 flex items-center\">\n      <div\n        :class=\"data.disableBlock ? 'bg-box-transparent' : block.category.color\"\n        class=\"mr-4 inline-block rounded-lg p-2 text-sm dark:text-black\"\n      >\n        <v-remixicon name=\"riTimerLine\" size=\"20\" class=\"mr-1 inline-block\" />\n        <span>{{ t('workflow.blocks.delay.name') }}</span>\n      </div>\n      <div class=\"grow\"></div>\n      <v-remixicon\n        name=\"riDeleteBin7Line\"\n        class=\"cursor-pointer\"\n        @click.stop=\"$emit('delete', id)\"\n      />\n    </div>\n    <input\n      :value=\"data.time\"\n      min=\"0\"\n      :title=\"t('workflow.blocks.delay.input.title')\"\n      :placeholder=\"t('workflow.blocks.delay.input.placeholder')\"\n      class=\"bg-input w-full rounded-lg px-4 py-2\"\n      type=\"text\"\n      required\n      @keydown.stop\n      @input=\"$emit('update', { time: $event.target.value })\"\n    />\n    <div\n      v-if=\"block.details.id !== 'trigger'\"\n      :title=\"t('workflow.blocks.base.moveToGroup')\"\n      draggable=\"true\"\n      class=\"move-to-group invisible absolute -top-2 -right-2 z-50 rounded-md bg-white p-1 shadow-md dark:bg-gray-700\"\n      @dragstart=\"handleStartDrag\"\n      @mousedown.stop\n    >\n      <v-remixicon name=\"riDragDropLine\" size=\"20\" />\n    </div>\n    <Handle :id=\"`${id}-output-1`\" type=\"source\" :position=\"Position.Right\" />\n  </block-base>\n</template>\n<script setup>\nimport { useI18n } from 'vue-i18n';\nimport { Handle, Position } from '@vue-flow/core';\nimport { useComponentId } from '@/composable/componentId';\nimport { useEditorBlock } from '@/composable/editorBlock';\nimport BlockBase from './BlockBase.vue';\n\nconst props = defineProps({\n  id: {\n    type: String,\n    default: '',\n  },\n  label: {\n    type: String,\n    default: '',\n  },\n  data: {\n    type: Object,\n    default: () => ({}),\n  },\n});\ndefineEmits(['update', 'delete', 'settings']);\n\nconst { t } = useI18n();\nconst block = useEditorBlock(props.label);\nconst componentId = useComponentId('block-delay');\n\nfunction handleStartDrag(event) {\n  const payload = {\n    id: props.label,\n    data: props.data,\n    blockId: props.id,\n    fromBlockBasic: true,\n  };\n\n  event.dataTransfer.setData('block', JSON.stringify(payload));\n}\n</script>\n"
  },
  {
    "path": "src/components/block/BlockElementExists.vue",
    "content": "<template>\n  <block-base\n    :id=\"componentId\"\n    :data=\"data\"\n    :block-id=\"id\"\n    :block-data=\"block\"\n    style=\"width: 195px\"\n    @edit=\"$emit('edit')\"\n    @delete=\"$emit('delete', id)\"\n    @update=\"$emit('update', $event)\"\n    @settings=\"$emit('settings', $event)\"\n  >\n    <Handle :id=\"`${id}-input-1`\" type=\"target\" :position=\"Position.Left\" />\n    <div\n      :class=\"data.disableBlock ? 'bg-box-transparent' : block.category.color\"\n      class=\"mb-2 inline-block rounded-lg p-2 text-sm dark:text-black\"\n    >\n      <v-remixicon name=\"riFocus3Line\" size=\"20\" class=\"mr-1 inline-block\" />\n      <span>{{ t('workflow.blocks.element-exists.name') }}</span>\n    </div>\n    <p\n      :title=\"t('workflow.blocks.element-exists.selector')\"\n      :class=\"{ 'font-mono': !data.description }\"\n      class=\"text-overflow bg-box-transparent mb-2 rounded-lg p-2 text-right text-sm\"\n      style=\"max-width: 200px\"\n    >\n      {{\n        data.description ||\n        data.selector ||\n        t('workflow.blocks.element-exists.selector')\n      }}\n    </p>\n    <p class=\"text-right text-gray-600 dark:text-gray-200\">\n      <span :title=\"t('workflow.blocks.element-exists.fallbackTitle')\">\n        &#9432;\n      </span>\n      {{ t('common.fallback') }}\n    </p>\n    <Handle :id=\"`${id}-output-1`\" type=\"source\" :position=\"Position.Right\" />\n    <Handle\n      :id=\"`${id}-output-2`\"\n      type=\"source\"\n      :position=\"Position.Right\"\n      style=\"top: auto; bottom: 12px\"\n    />\n  </block-base>\n</template>\n<script setup>\nimport { useI18n } from 'vue-i18n';\nimport { Handle, Position } from '@vue-flow/core';\nimport { useComponentId } from '@/composable/componentId';\nimport { useEditorBlock } from '@/composable/editorBlock';\nimport BlockBase from './BlockBase.vue';\n\nconst props = defineProps({\n  id: {\n    type: String,\n    default: '',\n  },\n  label: {\n    type: String,\n    default: '',\n  },\n  data: {\n    type: Object,\n    default: () => ({}),\n  },\n});\ndefineEmits(['delete', 'edit', 'update', 'settings']);\n\nconst { t } = useI18n();\nconst block = useEditorBlock(props.label);\nconst componentId = useComponentId('block-delay');\n</script>\n<style>\n.drawflow .element-exists .outputs {\n  top: 70px !important;\n  transform: none !important;\n}\n.drawflow .element-exists .output {\n  margin-bottom: 22px;\n}\n</style>\n"
  },
  {
    "path": "src/components/block/BlockGroup.vue",
    "content": "<template>\n  <block-base\n    :id=\"componentId\"\n    :data=\"data\"\n    :block-id=\"id\"\n    :block-data=\"block\"\n    class=\"w-64\"\n    content-class=\"!p-0\"\n    @edit=\"$emit('edit')\"\n    @delete=\"$emit('delete', id)\"\n    @update=\"$emit('update', $event)\"\n    @settings=\"$emit('settings', $event)\"\n  >\n    <Handle :id=\"`${id}-input-1`\" type=\"target\" :position=\"Position.Left\" />\n    <div class=\"p-4\">\n      <div class=\"mb-2 flex items-center\">\n        <div\n          :class=\"\n            data.disableBlock ? 'bg-box-transparent' : block.category.color\n          \"\n          class=\"mr-4 inline-flex items-center rounded-lg p-2 text-sm dark:text-black\"\n        >\n          <v-remixicon\n            :name=\"block.details.icon || 'riFolderZipLine'\"\n            size=\"20\"\n            class=\"mr-2 inline-block\"\n          />\n          <span>{{ t('workflow.blocks.blocks-group.name') }}</span>\n        </div>\n      </div>\n      <input\n        :value=\"data.name\"\n        :placeholder=\"t('workflow.blocks.blocks-group.groupName')\"\n        type=\"text\"\n        class=\"w-full bg-transparent focus:ring-0\"\n        @keydown.stop\n        @input=\"$emit('update', { name: $event.target.value })\"\n      />\n    </div>\n    <draggable\n      :model-value=\"blocks\"\n      item-key=\"itemId\"\n      class=\"nowheel scroll max-h-60 space-y-1 overflow-auto px-4 pb-4 text-sm\"\n      @mousedown.stop\n      @dragover.prevent\n      @drop=\"handleDrop\"\n      @update:modelValue=\"$emit('update', { blocks: $event })\"\n    >\n      <template #item=\"{ element, index }\">\n        <div\n          class=\"bg-input group flex items-center space-x-2 rounded-lg p-2\"\n          style=\"cursor: grab\"\n          :data-block-id=\"element.id\"\n          @dragstart=\"onDragStart(element, $event)\"\n          @dragend=\"onDragEnd(element.itemId)\"\n        >\n          <v-remixicon\n            :name=\"tasks[element.id].icon\"\n            size=\"20\"\n            class=\"shrink-0\"\n          />\n          <div class=\"flex-1 overflow-hidden leading-tight\">\n            <p class=\"text-overflow\">\n              {{\n                getTranslation(\n                  `workflow.blocks.${element.id}.name`,\n                  tasks[element.id].name\n                )\n              }}\n            </p>\n            <p\n              :title=\"element.data.description\"\n              class=\"text-overflow text-gray-600 dark:text-gray-200\"\n            >\n              {{ element.data.description }}\n            </p>\n          </div>\n          <div class=\"invisible group-hover:visible\">\n            <v-remixicon\n              v-if=\"workflow?.data?.value.testingMode\"\n              :class=\"{\n                'text-red-500 dark:text-red-400': element.data.$breakpoint,\n              }\"\n              title=\"Set as breakpoint\"\n              name=\"riRecordCircleLine\"\n              size=\"18\"\n              class=\"mr-2 inline-block cursor-pointer\"\n              @click=\"toggleBreakpoint(element, index)\"\n            />\n            <v-remixicon\n              name=\"riPencilLine\"\n              size=\"18\"\n              class=\"mr-2 inline-block cursor-pointer\"\n              @click=\"editBlock(element)\"\n            />\n            <v-remixicon\n              name=\"riSettings3Line\"\n              size=\"18\"\n              class=\"mr-2 inline-block cursor-pointer\"\n              @click=\"editItemSettings(element)\"\n            />\n            <v-remixicon\n              name=\"riDeleteBin7Line\"\n              size=\"18\"\n              class=\"inline-block cursor-pointer\"\n              @click=\"deleteItem(index, element.itemId)\"\n            />\n          </div>\n        </div>\n      </template>\n      <template #footer>\n        <div\n          class=\"rounded-lg border border-dashed p-2 text-center text-gray-600 dark:text-gray-200\"\n        >\n          {{ t('workflow.blocks.blocks-group.dropText') }}\n        </div>\n      </template>\n    </draggable>\n    <Handle :id=\"`${id}-output-1`\" type=\"source\" :position=\"Position.Right\" />\n  </block-base>\n</template>\n<script setup>\nimport { inject, computed, shallowReactive } from 'vue';\nimport { useI18n } from 'vue-i18n';\nimport { nanoid } from 'nanoid';\nimport { useToast } from 'vue-toastification';\nimport { Handle, Position } from '@vue-flow/core';\nimport draggable from 'vuedraggable';\nimport { tasks, excludeGroupBlocks } from '@/utils/shared';\nimport { useComponentId } from '@/composable/componentId';\nimport { useEditorBlock } from '@/composable/editorBlock';\nimport BlockBase from './BlockBase.vue';\n\nconst props = defineProps({\n  id: {\n    type: String,\n    default: '',\n  },\n  label: {\n    type: String,\n    default: '',\n  },\n  data: {\n    type: Object,\n    default: () => ({}),\n  },\n  editor: {\n    type: Object,\n    default: () => ({}),\n  },\n  events: {\n    type: Object,\n    default: () => ({}),\n  },\n});\nconst emit = defineEmits(['update', 'delete', 'edit', 'settings']);\n\nconst { t, te } = useI18n();\nconst toast = useToast();\nconst componentId = useComponentId('blocks-group');\nconst block = useEditorBlock(props.label);\n\nconst workflow = inject('workflow', {});\n\nconst blocks = computed(() =>\n  Array.isArray(props.data.blocks)\n    ? props.data.blocks\n    : Object.values(props.data.blocks)\n);\n\nfunction editItemSettings(element) {\n  emit('settings', {\n    blockId: props.id,\n    data: element.data,\n    itemId: element.itemId,\n    details: { id: element.id },\n  });\n}\nfunction onDragStart(item, event) {\n  event.dataTransfer.setData(\n    'block',\n    JSON.stringify({ ...tasks[item.id], ...item, fromGroup: true })\n  );\n}\nfunction onDragEnd(itemId) {\n  setTimeout(() => {\n    const blockEl = document.querySelector(`[group-item-id=\"${itemId}\"]`);\n\n    if (blockEl) {\n      const blockIndex = blocks.value.findIndex(\n        (item) => item.itemId === itemId\n      );\n\n      if (blockIndex !== -1) {\n        const copyBlocks = [...props.data.blocks];\n        copyBlocks.splice(blockIndex, 1);\n        emit('update', { blocks: copyBlocks });\n      }\n    }\n  }, 200);\n}\nfunction editBlock(payload) {\n  emit('edit', payload);\n}\nfunction deleteItem(index, itemId) {\n  const copyBlocks = [...props.data.blocks];\n\n  if (workflow.editState.blockData.itemId === itemId) {\n    workflow.editState.editing = false;\n    workflow.editState.blockData = false;\n  }\n\n  copyBlocks.splice(index, 1);\n  emit('update', { blocks: copyBlocks });\n}\nfunction getTranslation(key, defText = '') {\n  return te(key) ? t(key) : defText;\n}\nfunction handleDrop(event) {\n  event.preventDefault();\n  event.stopPropagation();\n\n  const droppedBlock = JSON.parse(event.dataTransfer.getData('block') || null);\n  if (!droppedBlock || droppedBlock.fromGroup) return;\n\n  const { id, data, blockId } = droppedBlock;\n\n  if (excludeGroupBlocks.includes(id)) {\n    toast.error(\n      t('workflow.blocks.blocks-group.cantAdd', {\n        blockName: t(`workflow.blocks.${id}.name`),\n      })\n    );\n\n    return;\n  }\n\n  if (blockId) {\n    emit('delete', blockId);\n  }\n\n  const copyBlocks = [\n    ...props.data.blocks,\n    shallowReactive({ id, data, itemId: nanoid(5) }),\n  ];\n  emit('update', { blocks: copyBlocks });\n}\nfunction toggleBreakpoint(item, index) {\n  const copyBlocks = [...props.data.blocks];\n  copyBlocks[index].data = {\n    ...copyBlocks[index].data,\n    $breakpoint: !item.data.$breakpoint,\n  };\n\n  emit('update', { blocks: copyBlocks });\n}\n</script>\n"
  },
  {
    "path": "src/components/block/BlockGroup2.vue",
    "content": "<template>\n  <div\n    :style=\"{\n      width: `${data.width || 400}px`,\n      height: `${data.height || 300}px`,\n    }\"\n    class=\"group-block-2 group relative rounded-lg border-2\"\n    style=\"\n      min-width: 400px;\n      min-height: 300px;\n      border-color: #2563eb;\n      background-color: rgb(37, 99, 235, 0.3);\n    \"\n  >\n    <div class=\"flex items-center p-4\">\n      <input\n        :value=\"data.name\"\n        placeholder=\"name\"\n        type=\"text\"\n        class=\"rounded-lg bg-white px-4 py-2\"\n        @input=\"emit('update', { name: $event.target.value })\"\n      />\n      <div class=\"flex-1\" />\n      <v-remixicon\n        name=\"riDeleteBin7Line\"\n        class=\"cursor-pointer\"\n        @click=\"$emit('delete', id)\"\n      />\n    </div>\n    <span\n      ref=\"dragHandle\"\n      style=\"cursor: nw-resize\"\n      class=\"drag-handle invisible absolute bottom-0 right-0 h-4 w-4 bg-accent group-hover:visible\"\n    />\n  </div>\n</template>\n<script setup>\nimport { ref, onMounted, onBeforeUnmount } from 'vue';\n\ndefineProps({\n  id: {\n    type: String,\n    default: '',\n  },\n  label: {\n    type: String,\n    default: '',\n  },\n  data: {\n    type: Object,\n    default: () => ({}),\n  },\n  position: {\n    type: Object,\n    default: () => ({}),\n  },\n  events: {\n    type: Object,\n    default: () => ({}),\n  },\n  dimensions: {\n    type: Object,\n    default: () => ({}),\n  },\n});\nconst emit = defineEmits(['delete', 'edit', 'update']);\n\nlet parent = null;\nconst initialRect = {\n  x: 0,\n  y: 0,\n  width: 0,\n  height: 0,\n};\n\nconst dragHandle = ref(null);\n\nfunction onMousemove(event) {\n  event.preventDefault();\n  event.stopPropagation();\n\n  const width = initialRect.width + event.clientX - initialRect.x;\n  const height = initialRect.height + event.clientY - initialRect.y;\n\n  parent.style.width = `${width}px`;\n  parent.style.height = `${height}px`;\n\n  emit('update', { height, width });\n}\n\nfunction onMouseup() {\n  document.documentElement.removeEventListener('mouseup', onMouseup);\n  document.documentElement.removeEventListener('mousemove', onMousemove);\n}\nfunction initDragging(event) {\n  event.preventDefault();\n  event.stopPropagation();\n\n  const { height, width } = getComputedStyle(parent);\n\n  initialRect.x = event.clientX;\n  initialRect.y = event.clientY;\n  initialRect.width = parseInt(width, 10);\n  initialRect.height = parseInt(height, 10);\n\n  document.documentElement.addEventListener('mouseup', onMouseup);\n  document.documentElement.addEventListener('mousemove', onMousemove);\n}\n\nonMounted(() => {\n  parent = dragHandle.value.closest('.group-block-2');\n  dragHandle.value.addEventListener('mousedown', initDragging);\n});\nonBeforeUnmount(() => {\n  dragHandle.value.removeEventListener('mousedown', initDragging);\n});\n</script>\n"
  },
  {
    "path": "src/components/block/BlockLoopBreakpoint.vue",
    "content": "<template>\n  <block-base\n    :id=\"componentId\"\n    :data=\"data\"\n    :block-id=\"id\"\n    :block-data=\"block\"\n    class=\"w-48\"\n    @delete=\"$emit('delete', id)\"\n    @update=\"$emit('update', $event)\"\n    @settings=\"$emit('settings', $event)\"\n  >\n    <Handle :id=\"`${id}-input-1`\" type=\"target\" :position=\"Position.Left\" />\n    <div class=\"mb-2 flex items-center\">\n      <div\n        :class=\"data.disableBlock ? 'bg-box-transparent' : block.category.color\"\n        class=\"text-overflow mr-4 inline-block rounded-lg p-2 text-sm dark:text-black\"\n      >\n        <v-remixicon name=\"riStopLine\" size=\"20\" class=\"mr-1 inline-block\" />\n        <span>{{ t('workflow.blocks.loop-breakpoint.name') }}</span>\n      </div>\n      <div class=\"grow\"></div>\n      <v-remixicon\n        name=\"riDeleteBin7Line\"\n        class=\"cursor-pointer\"\n        @click.stop=\"$emit('delete', id)\"\n      />\n    </div>\n    <input\n      :value=\"data.loopId\"\n      class=\"bg-input w-full rounded-lg px-4 py-2\"\n      placeholder=\"Loop ID\"\n      type=\"text\"\n      required\n      @keydown.stop\n      @input=\"handleInput\"\n    />\n    <ui-checkbox\n      :model-value=\"data.clearLoop\"\n      class=\"mt-2\"\n      @change=\"$emit('update', { clearLoop: $event })\"\n    >\n      Stop loop\n    </ui-checkbox>\n    <Handle :id=\"`${id}-output-1`\" type=\"source\" :position=\"Position.Right\" />\n  </block-base>\n</template>\n<script setup>\nimport { useI18n } from 'vue-i18n';\nimport { Handle, Position } from '@vue-flow/core';\nimport { useComponentId } from '@/composable/componentId';\nimport { useEditorBlock } from '@/composable/editorBlock';\nimport BlockBase from './BlockBase.vue';\n\nconst props = defineProps({\n  id: {\n    type: String,\n    default: '',\n  },\n  label: {\n    type: String,\n    default: '',\n  },\n  data: {\n    type: Object,\n    default: () => ({}),\n  },\n});\nconst emit = defineEmits(['delete', 'update', 'settings']);\n\nconst { t } = useI18n();\nconst block = useEditorBlock(props.label);\nconst componentId = useComponentId('block-delay');\n\nfunction handleInput({ target }) {\n  const loopId = target.value.replace(/\\s/g, '');\n  emit('update', { loopId });\n}\n</script>\n"
  },
  {
    "path": "src/components/block/BlockNote.vue",
    "content": "<template>\n  <div\n    :class=\"[data.color || 'white', colors[data.color || 'white']]\"\n    class=\"block-note rounded-lg p-4\"\n    style=\"min-width: 192px\"\n  >\n    <div class=\"flex items-center border-b pb-2\">\n      <v-remixicon name=\"riFileEditLine\" size=\"20\" />\n      <p class=\"mx-2 flex-1 font-semibold\">Note</p>\n      <ui-popover class=\"note-color\">\n        <template #trigger>\n          <v-remixicon\n            name=\"riSettings3Line\"\n            size=\"20\"\n            class=\"cursor-pointer\"\n          />\n        </template>\n        <p class=\"mb-1 ml-1 text-sm text-gray-600 dark:text-gray-200\">Colors</p>\n        <div class=\"flex items-center space-x-2\">\n          <span\n            v-for=\"(color, colorId) in colors\"\n            :key=\"colorId\"\n            :class=\"color\"\n            style=\"border-width: 3px\"\n            class=\"inline-block h-8 w-8 cursor-pointer rounded-full\"\n            @click=\"updateData({ color: colorId })\"\n          />\n        </div>\n        <ui-select\n          :model-value=\"data.fontSize\"\n          label=\"Font size\"\n          class=\"mt-2 w-full\"\n          @change=\"updateData({ fontSize: $event })\"\n        >\n          <option\n            v-for=\"(size, fontId) in fontSize\"\n            :key=\"fontId\"\n            :value=\"fontId\"\n          >\n            {{ size.name }}\n          </option>\n        </ui-select>\n      </ui-popover>\n      <hr class=\"mx-2 h-7 border-r\" />\n      <v-remixicon\n        name=\"riDeleteBin7Line\"\n        size=\"20\"\n        class=\"cursor-pointer\"\n        @click=\"$emit('delete', id)\"\n      />\n    </div>\n    <textarea\n      :value=\"data.note\"\n      :style=\"initialSize\"\n      :class=\"[fontSize[data.fontSize || 'regular'].class]\"\n      placeholder=\"Write a note here...\"\n      cols=\"30\"\n      rows=\"7\"\n      style=\"resize: both; min-width: 280px; min-height: 168px\"\n      class=\"mt-2 bg-transparent focus:ring-0\"\n      @keydown.stop\n      @input=\"updateData({ note: $event.target.value })\"\n      @mousedown.stop\n      @mouseup=\"onMouseup\"\n    />\n  </div>\n</template>\n<script setup>\nimport { debounce } from '@/utils/helper';\n\nconst props = defineProps({\n  id: {\n    type: String,\n    default: '',\n  },\n  label: {\n    type: String,\n    default: '',\n  },\n  data: {\n    type: Object,\n    default: () => ({}),\n  },\n});\nconst emit = defineEmits(['update', 'delete']);\n\nconst initialSize = {\n  width: `${props.data.width}px`,\n  height: `${props.data.height}px`,\n};\n\nconst colors = {\n  white: 'bg-white dark:bg-gray-800',\n  red: 'bg-red-200 dark:bg-red-300',\n  indigo: 'bg-indigo-200 dark:bg-indigo-300',\n  green: 'bg-green-200 dark:bg-green-300',\n  amber: 'bg-amber-200 dark:bg-amber-300',\n  sky: 'bg-sky-200 dark:bg-sky-300',\n};\nconst fontSize = {\n  regular: {\n    name: 'Regular',\n    class: 'text-base',\n  },\n  medium: {\n    name: 'Medium',\n    class: 'text-xl',\n  },\n  large: {\n    name: 'Large',\n    class: 'text-2xl',\n  },\n  'extra-large': {\n    name: 'Extra Large',\n    class: 'text-3xl',\n  },\n};\n\nconst updateData = debounce((data) => {\n  emit('update', data);\n}, 250);\n\nfunction onMouseup({ target }) {\n  let { height, width } = target.style;\n  width = parseInt(width, 10);\n  height = parseInt(height, 10);\n\n  if (width === props.data.width && height === props.data.height) return;\n\n  updateData({ height, width });\n}\n</script>\n<style>\n.note-color .ui-popover__trigger {\n  @apply flex items-center;\n}\n.block-note * {\n  border-color: rgb(0 0 0 / 12%);\n}\n.dark .block-note {\n  &:not(.white) {\n    @apply text-gray-900;\n  }\n  &.white * {\n    border-color: rgb(255 255 255 / 12%);\n  }\n  * {\n    border-color: rgb(0 0 0 / 12%);\n  }\n}\n</style>\n"
  },
  {
    "path": "src/components/block/BlockPackage.vue",
    "content": "<template>\n  <block-base\n    :id=\"componentId\"\n    :data=\"data\"\n    :block-id=\"id\"\n    :block-data=\"block\"\n    class=\"block-package w-64\"\n    @delete=\"$emit('delete', id)\"\n    @update=\"$emit('update', $event)\"\n    @settings=\"$emit('settings', $event)\"\n  >\n    <div class=\"flex items-center\">\n      <img\n        v-if=\"data.icon.startsWith('http')\"\n        :src=\"data.icon\"\n        width=\"36\"\n        height=\"36\"\n        class=\"mr-2 rounded-lg\"\n      />\n      <div\n        :class=\"data.disableBlock ? 'bg-box-transparent' : block.category.color\"\n        class=\"mr-4 inline-block overflow-hidden rounded-lg p-2 text-sm dark:text-black\"\n      >\n        <v-remixicon\n          v-if=\"!data.icon.startsWith('http')\"\n          :name=\"data.icon\"\n          size=\"20\"\n          class=\"mr-1 inline-block\"\n        />\n        <span class=\"text-overflow\">{{ data.name || 'Unnamed package' }}</span>\n      </div>\n      <div class=\"grow\" />\n      <v-remixicon\n        v-if=\"state.isInstalled\"\n        title=\"Update package\"\n        name=\"riRefreshLine\"\n        class=\"cursor-pointer\"\n        @click=\"updatePackage\"\n      />\n      <v-remixicon\n        v-else\n        title=\"Install package\"\n        name=\"riDownloadLine\"\n        class=\"cursor-pointer\"\n        @click=\"installPackage\"\n      />\n    </div>\n    <div class=\"mt-4 grid grid-cols-2 gap-x-2\">\n      <ul class=\"pkg-handle-container\">\n        <li\n          v-for=\"input in data.inputs\"\n          :key=\"input.id\"\n          :title=\"input.name\"\n          class=\"target relative\"\n        >\n          <Handle\n            :id=\"`${id}-input-${input.id}`\"\n            type=\"target\"\n            :position=\"Position.Left\"\n          />\n          <p class=\"text-overflow\">{{ input.name }}</p>\n        </li>\n      </ul>\n      <ul class=\"pkg-handle-container\">\n        <li\n          v-for=\"output in data.outputs\"\n          :key=\"output.id\"\n          :title=\"output.name\"\n          class=\"source relative\"\n        >\n          <Handle\n            :id=\"`${id}-output-${output.id}`\"\n            type=\"source\"\n            :position=\"Position.Right\"\n          />\n          <p class=\"text-overflow\">{{ output.name }}</p>\n        </li>\n      </ul>\n    </div>\n    <div\n      v-if=\"data.author\"\n      class=\"mt-1 flex items-center text-sm text-gray-600 dark:text-gray-200\"\n    >\n      <p>By {{ data.author }}</p>\n      <a\n        :href=\"`https://extension.automa.site/packages/${data.id}`\"\n        target=\"_blank\"\n        title=\"Open package page\"\n        class=\"ml-2\"\n      >\n        <v-remixicon size=\"18\" name=\"riExternalLinkLine\" />\n      </a>\n    </div>\n  </block-base>\n</template>\n<script setup>\nimport { useComponentId } from '@/composable/componentId';\nimport { useEditorBlock } from '@/composable/editorBlock';\nimport { usePackageStore } from '@/stores/package';\nimport { Handle, Position } from '@vue-flow/core';\nimport cloneDeep from 'lodash.clonedeep';\nimport { onMounted, shallowReactive } from 'vue';\nimport BlockBase from './BlockBase.vue';\n\nconst props = defineProps({\n  id: {\n    type: String,\n    default: '',\n  },\n  label: {\n    type: String,\n    default: '',\n  },\n  data: {\n    type: Object,\n    default: () => ({}),\n  },\n  editor: {\n    type: Object,\n    default: null,\n  },\n});\nconst emit = defineEmits(['update', 'delete', 'settings']);\n\nconst packageStore = usePackageStore();\nconst block = useEditorBlock(props.label);\nconst componentId = useComponentId('block-package');\n\nconst state = shallowReactive({\n  isInstalled: false,\n});\n\nfunction installPackage() {\n  packageStore\n    .insert({ ...props.data, isExternal: Boolean(props.data.author) }, false)\n    .then(() => {\n      state.isInstalled = true;\n    });\n}\nfunction removeConnections(type, old, newEdges) {\n  const removedEdges = [];\n  old.forEach((edge) => {\n    const isNotDeleted = newEdges.find((item) => item.id === edge.id);\n    if (isNotDeleted) return;\n\n    const handleType = type.slice(0, -1);\n\n    removedEdges.push(`${props.id}-${handleType}-${edge.id}`);\n  });\n\n  const edgesToRemove = props.editor.getEdges.value.filter(\n    ({ sourceHandle, targetHandle }) => {\n      if (type === 'outputs') {\n        return removedEdges.includes(sourceHandle);\n      }\n\n      return removedEdges.includes(targetHandle);\n    }\n  );\n\n  props.editor.removeEdges(edgesToRemove);\n}\nfunction updatePackage() {\n  const pkg = packageStore.getById(props.data.id);\n  if (!pkg) return;\n\n  const currentInputs = [...props.data.inputs];\n  const currentOutputs = [...props.data.outputs];\n\n  removeConnections('inputs', currentInputs, pkg.inputs);\n  removeConnections('outputs', currentOutputs, pkg.outputs);\n\n  emit('update', cloneDeep(pkg));\n}\n\nonMounted(() => {\n  state.isInstalled = packageStore.getById(props.data.id);\n});\n</script>\n<style>\n.pkg-handle-container li {\n  @apply h-8 flex items-center text-sm;\n\n  &.target .vue-flow__handle {\n    margin-left: -33px;\n  }\n  &.source {\n    @apply justify-end;\n    .vue-flow__handle {\n      margin-right: -33px;\n    }\n  }\n}\n</style>\n"
  },
  {
    "path": "src/components/block/BlockRepeatTask.vue",
    "content": "<template>\n  <block-base\n    :id=\"componentId\"\n    :data=\"data\"\n    :block-id=\"id\"\n    :block-data=\"block\"\n    class=\"repeat-task w-64\"\n    @delete=\"$emit('delete', id)\"\n    @update=\"$emit('update', $event)\"\n    @settings=\"$emit('settings', $event)\"\n  >\n    <Handle :id=\"`${id}-input-1`\" type=\"target\" :position=\"Position.Left\" />\n    <div class=\"mb-2 flex items-center\">\n      <div\n        :class=\"data.disableBlock ? 'bg-box-transparent' : block.category.color\"\n        class=\"mr-4 inline-block rounded-lg p-2 text-sm dark:text-black\"\n      >\n        <v-remixicon name=\"riRepeat2Line\" size=\"20\" class=\"mr-1 inline-block\" />\n        <span>{{ t('workflow.blocks.repeat-task.name') }}</span>\n      </div>\n    </div>\n    <div class=\"bg-input relative flex items-center rounded-lg\">\n      <input\n        :value=\"data.repeatFor\"\n        placeholder=\"0\"\n        class=\"bg-transparent py-2 px-4 focus:ring-0\"\n        style=\"padding-right: 57px; width: 95%\"\n        @keydown.stop\n        @input=\"handleInput\"\n      />\n      <span class=\"absolute right-4 text-gray-600 dark:text-gray-200\">\n        {{ t('workflow.blocks.repeat-task.times') }}\n      </span>\n    </div>\n    <p class=\"text-right text-gray-600 dark:text-gray-200\">\n      {{ t('workflow.blocks.repeat-task.repeatFrom') }}\n    </p>\n    <Handle :id=\"`${id}-output-1`\" type=\"source\" :position=\"Position.Right\" />\n    <Handle\n      :id=\"`${id}-output-2`\"\n      type=\"source\"\n      :position=\"Position.Right\"\n      style=\"top: auto; bottom: 12px\"\n    />\n  </block-base>\n</template>\n<script setup>\nimport { useI18n } from 'vue-i18n';\nimport { Handle, Position } from '@vue-flow/core';\nimport { useComponentId } from '@/composable/componentId';\nimport { useEditorBlock } from '@/composable/editorBlock';\nimport BlockBase from './BlockBase.vue';\n\nconst { t } = useI18n();\nconst props = defineProps({\n  id: {\n    type: String,\n    default: '',\n  },\n  label: {\n    type: String,\n    default: '',\n  },\n  data: {\n    type: Object,\n    default: () => ({}),\n  },\n});\nconst emit = defineEmits(['delete', 'update', 'settings']);\n\nconst block = useEditorBlock(props.label);\nconst componentId = useComponentId('block-delay');\n\nfunction handleInput({ target }) {\n  emit('update', { repeatFor: target.value });\n}\n</script>\n<style>\n.drawflow .repeat-task .outputs {\n  top: 74px !important;\n  transform: none !important;\n}\n.drawflow .repeat-task .output {\n  margin-bottom: 22px;\n}\n</style>\n"
  },
  {
    "path": "src/components/content/selector/SelectorBlocks.vue",
    "content": "<template>\n  <div class=\"events mt-4\">\n    <div class=\"flex items-center\">\n      <ui-select\n        v-model=\"state.selectedBlock\"\n        class=\"mr-4 flex-1 p-0.5\"\n        placeholder=\"Select block\"\n        @change=\"onSelectChanged\"\n      >\n        <option v-for=\"(block, id) in blocks\" :key=\"id\" :value=\"id\">\n          {{ block.name }}\n        </option>\n      </ui-select>\n      <ui-button\n        :disabled=\"!state.selectedBlock\"\n        variant=\"accent\"\n        @click=\"executeBlock\"\n      >\n        Execute\n      </ui-button>\n    </div>\n    <component\n      :is=\"blocks[state.selectedBlock].component\"\n      v-if=\"state.selectedBlock && blocks[state.selectedBlock].component\"\n      :data=\"state.params\"\n      :hide-base=\"true\"\n      @update:data=\"updateParams\"\n    />\n    <pre\n      v-if=\"state.blockResult\"\n      class=\"mt-2 h-full overflow-auto rounded-lg bg-accent p-2 text-sm text-gray-100\"\n      >{{ state.blockResult }}</pre\n    >\n  </div>\n</template>\n<script setup>\nimport { shallowReactive } from 'vue';\nimport { tasks } from '@/utils/shared';\nimport EditForms from '@/components/newtab/workflow/edit/EditForms.vue';\nimport EditTriggerEvent from '@/components/newtab/workflow/edit/EditTriggerEvent.vue';\nimport EditScrollElement from '@/components/newtab/workflow/edit/EditScrollElement.vue';\nimport handleForms from '@/content/blocksHandler/handlerForms';\nimport handleGetText from '@/content/blocksHandler/handlerGetText';\nimport handleEventClick from '@/content/blocksHandler/handlerEventClick';\nimport handelTriggerEvent from '@/content/blocksHandler/handlerTriggerEvent';\nimport handleElementScroll from '@/content/blocksHandler/handlerElementScroll';\n\nconst props = defineProps({\n  selector: {\n    type: String,\n    default: '',\n  },\n  elements: {\n    type: Array,\n    default: () => [],\n  },\n});\nconst emit = defineEmits(['update', 'execute']);\n\nconst blocks = {\n  forms: {\n    ...tasks.forms,\n    component: EditForms,\n    handler: handleForms,\n  },\n  'get-text': {\n    ...tasks['get-text'],\n    component: '',\n    handler: handleGetText,\n  },\n  'event-click': {\n    ...tasks['event-click'],\n    component: '',\n    handler: handleEventClick,\n  },\n  'trigger-event': {\n    ...tasks['trigger-event'],\n    component: EditTriggerEvent,\n    handler: handelTriggerEvent,\n  },\n  'element-scroll': {\n    ...tasks['element-scroll'],\n    component: EditScrollElement,\n    handler: handleElementScroll,\n  },\n};\n\nconst state = shallowReactive({\n  params: {},\n  blockResult: '',\n  selectedBlock: '',\n});\n\nfunction updateParams(data = {}) {\n  state.params = data;\n  emit('update');\n}\nfunction onSelectChanged(value) {\n  state.params = tasks[value].data;\n  state.blockResult = '';\n  emit('update');\n}\nfunction executeBlock() {\n  const params = {\n    ...state.params,\n    selector: props.selector,\n    multiple: props.elements.length > 1,\n  };\n\n  emit('execute', true);\n\n  blocks[state.selectedBlock].handler({ data: params }).then((result) => {\n    state.blockResult = JSON.stringify(result, null, 2).trim();\n    emit('update');\n    emit('execute', false);\n  });\n}\n</script>\n"
  },
  {
    "path": "src/components/content/selector/SelectorElementList.vue",
    "content": "<template>\n  <ul class=\"mt-2 space-y-4\">\n    <li\n      v-for=\"(element, index) in elements\"\n      :key=\"index\"\n      @mouseenter=\"$emit('highlight', { highlight: true, index, element })\"\n      @mouseleave=\"$emit('highlight', { highlight: false, index, element })\"\n    >\n      <p class=\"mb-1\">#{{ index + 1 }} {{ elementName }}</p>\n      <slot name=\"item\" v-bind=\"{ element }\" />\n    </li>\n  </ul>\n</template>\n<script setup>\ndefineProps({\n  elements: {\n    type: Array,\n    default: () => [],\n  },\n  elementName: {\n    type: String,\n    default: 'Element',\n  },\n});\ndefineEmits(['highlight']);\n</script>\n"
  },
  {
    "path": "src/components/content/selector/SelectorElementsDetail.vue",
    "content": "<template>\n  <ui-tabs\n    v-if=\"!hideBlocks || selectElements.length > 0\"\n    :model-value=\"activeTab\"\n    class=\"mt-2\"\n    fill\n    @change=\"$emit('update:activeTab', $event)\"\n  >\n    <ui-tab value=\"attributes\"> Attributes </ui-tab>\n    <ui-tab v-if=\"selectElements.length > 0\" value=\"options\"> Options </ui-tab>\n    <ui-tab v-if=\"!hideBlocks\" value=\"blocks\"> Blocks </ui-tab>\n  </ui-tabs>\n  <ui-tab-panels\n    :model-value=\"activeTab\"\n    class=\"scroll overflow-y-auto\"\n    style=\"max-height: calc(100vh - 17rem)\"\n  >\n    <ui-tab-panel value=\"attributes\">\n      <selector-element-list\n        :elements=\"selectedElements\"\n        @highlight=\"$emit('highlight', $event)\"\n      >\n        <template #item=\"{ element }\">\n          <div\n            v-for=\"(value, name) in element.attributes\"\n            :key=\"name\"\n            class=\"bg-box-transparent mb-1 rounded-lg py-2 px-3\"\n          >\n            <p\n              class=\"text-overflow text-sm leading-tight text-gray-600\"\n              title=\"Attribute name\"\n            >\n              {{ name }}\n            </p>\n            <ui-input\n              :model-value=\"value\"\n              :placeholder=\"!value ? 'EMPTY' : null\"\n              :data-testid=\"name\"\n              :title=\"name\"\n              readonly\n              class=\"w-full\"\n            >\n              <template #prepend>\n                <button\n                  class=\"absolute ml-2\"\n                  @click=\"copySelector(name, value)\"\n                >\n                  <v-remixicon name=\"riFileCopyLine\" />\n                </button>\n              </template>\n            </ui-input>\n          </div>\n        </template>\n      </selector-element-list>\n    </ui-tab-panel>\n    <ui-tab-panel value=\"options\">\n      <selector-element-list\n        :elements=\"selectElements\"\n        element-name=\"Select element options\"\n        @highlight=\"\n          $emit('highlight', {\n            index: $event.element.elIndex,\n            highlight: $event.highlight,\n          })\n        \"\n      >\n        <template #item=\"{ element }\">\n          <div\n            v-for=\"option in element.options\"\n            :key=\"option.name\"\n            class=\"bg-box-transparent mb-1 rounded-lg py-2 px-3\"\n          >\n            <p\n              class=\"text-overflow text-sm leading-tight text-gray-600\"\n              title=\"Option name\"\n            >\n              {{ option.name }}\n            </p>\n            <input\n              :value=\"option.value\"\n              title=\"Option value\"\n              class=\"text-overflow w-full bg-transparent focus:ring-0\"\n              readonly\n              @click=\"$event.target.select()\"\n            />\n          </div>\n        </template>\n      </selector-element-list>\n    </ui-tab-panel>\n    <ui-tab-panel value=\"blocks\">\n      <selector-blocks\n        :elements=\"selectedElements\"\n        :selector=\"elSelector\"\n        @execute=\"$emit('execute', $event)\"\n        @update=\"$emit('update')\"\n      />\n    </ui-tab-panel>\n  </ui-tab-panels>\n</template>\n<script setup>\nimport { inject } from 'vue';\nimport SelectorBlocks from './SelectorBlocks.vue';\nimport SelectorElementList from './SelectorElementList.vue';\n\nconst props = defineProps({\n  activeTab: {\n    type: String,\n    default: '',\n  },\n  selectElements: {\n    type: Array,\n    default: () => [],\n  },\n  selectedElements: {\n    type: Array,\n    default: () => [],\n  },\n  elSelector: {\n    type: String,\n    default: '',\n  },\n  hideBlocks: Boolean,\n});\ndefineEmits(['update:activeTab', 'execute', 'highlight', 'update']);\n\nconst rootElement = inject('rootElement');\n\nfunction copySelector(name, value) {\n  rootElement.shadowRoot\n    .querySelector(`[data-testid=\"${name}\"] input`)\n    ?.select();\n  const type = rootElement.shadowRoot.querySelector(`select#select--1`)?.value;\n  navigator.clipboard\n    .writeText(\n      type === 'css'\n        ? `${props.selectedElements[0].tagName.toLowerCase()}[${name}=\"${value}\"]`\n        : `//${props.selectedElements[0].tagName.toLowerCase()}[@${name}='${value}']`\n    )\n    .catch((error) => {\n      document.execCommand('copy');\n      console.error(error);\n    });\n}\n</script>\n"
  },
  {
    "path": "src/components/content/selector/SelectorQuery.vue",
    "content": "<template>\n  <div>\n    <div class=\"flex items-center\">\n      <ui-select\n        :model-value=\"selectorType\"\n        :disabled=\"selectList\"\n        class=\"w-full\"\n        @change=\"$emit('update:selectorType', $event)\"\n      >\n        <option value=\"css\">CSS Selector</option>\n        <option value=\"xpath\">XPath</option>\n      </ui-select>\n      <template v-if=\"selectorType === 'css'\">\n        <ui-button\n          :class=\"{ 'text-primary': selectList }\"\n          icon\n          class=\"ml-2\"\n          title=\"Select a list of elements\"\n          @click.stop.prevent=\"$emit('update:selectList', !selectList)\"\n        >\n          <v-remixicon name=\"riListUnordered\" />\n        </ui-button>\n        <ui-button\n          icon\n          class=\"ml-2\"\n          title=\"Selector settings\"\n          @click=\"$emit('settings', !settingsActive)\"\n        >\n          <v-remixicon\n            :name=\"settingsActive ? 'riCloseLine' : 'riSettings3Line'\"\n          />\n        </ui-button>\n      </template>\n    </div>\n    <div class=\"mt-2 flex items-center\">\n      <ui-input\n        :model-value=\"selector\"\n        placeholder=\"Element selector\"\n        class=\"element-selector h-full flex-1 leading-normal\"\n        @change=\"$emit('selector', $event)\"\n      >\n        <template #prepend>\n          <button\n            class=\"absolute left-0 ml-2\"\n            @click.stop.prevent=\"copySelector\"\n          >\n            <v-remixicon name=\"riFileCopyLine\" />\n          </button>\n        </template>\n      </ui-input>\n      <template v-if=\"selectedCount === 1 && !selector.includes('|>')\">\n        <button\n          class=\"mr-1 ml-2\"\n          title=\"Parent element\"\n          @click.stop.prevent=\"$emit('parent')\"\n        >\n          <v-remixicon rotate=\"90\" name=\"riArrowLeftLine\" />\n        </button>\n        <button title=\"Child element\" @click.stop.prevent=\"$emit('child')\">\n          <v-remixicon rotate=\"-90\" name=\"riArrowLeftLine\" />\n        </button>\n      </template>\n    </div>\n  </div>\n</template>\n<script setup>\nimport { inject } from 'vue';\nimport UiInput from '@/components/ui/UiInput.vue';\n\nconst props = defineProps({\n  selector: {\n    type: String,\n    default: '',\n  },\n  selectedCount: {\n    type: Number,\n    default: 0,\n  },\n  selectorType: {\n    type: String,\n    default: '',\n  },\n  selectList: {\n    type: Boolean,\n    default: false,\n  },\n  settingsActive: Boolean,\n});\ndefineEmits([\n  'change',\n  'list',\n  'parent',\n  'child',\n  'selector',\n  'settings',\n  'update:selectorType',\n  'update:selectList',\n]);\n\nconst rootElement = inject('rootElement');\n\nfunction copySelector() {\n  rootElement.shadowRoot.querySelector('input')?.select();\n\n  navigator.clipboard.writeText(props.selector).catch((error) => {\n    document.execCommand('copy');\n    console.error(error);\n  });\n}\n</script>\n"
  },
  {
    "path": "src/components/content/shared/SharedElementHighlighter.vue",
    "content": "<template>\n  <rect\n    v-for=\"(item, index) in items\"\n    v-bind=\"{\n      x: getNumber(item?.x),\n      y: getNumber(item?.y),\n      fill: getFillColor(item),\n      stroke: getStrokeColor(item),\n      width: getNumber(item?.width),\n      height: getNumber(item?.height),\n      'stroke-dasharray': item?.outline ? '5,5' : null,\n    }\"\n    :key=\"index\"\n    stroke-width=\"2\"\n  ></rect>\n</template>\n<script setup>\nconst props = defineProps({\n  items: {\n    type: Object,\n    default: () => ({}),\n  },\n  stroke: {\n    type: String,\n    default: null,\n  },\n  activeStroke: {\n    type: String,\n    default: null,\n  },\n  fill: {\n    type: String,\n    default: null,\n  },\n  activeFill: {\n    type: String,\n    default: null,\n  },\n});\n\nfunction getNumber(num) {\n  if (Number.isNaN(num) || !num) return 0;\n\n  return num;\n}\nfunction getFillColor(item) {\n  if (!item) return null;\n  if (item.outline) return null;\n\n  return item.highlight ? props.fill : props.activeFill || props.fill;\n}\nfunction getStrokeColor(item) {\n  if (!item) return null;\n\n  return item.highlight ? props.stroke : props.activeStroke || props.stroke;\n}\n</script>\n"
  },
  {
    "path": "src/components/content/shared/SharedElementSelector.vue",
    "content": "<template>\n  <svg\n    v-if=\"!disabled\"\n    class=\"automa-element-highlighter\"\n    style=\"\n      height: 100%;\n      width: 100%;\n      top: 0;\n      left: 0;\n      pointer-events: none;\n      position: fixed;\n      z-index: 999999;\n    \"\n  >\n    <shared-element-highlighter\n      :items=\"elementsState.hovered\"\n      stroke=\"#fbbf24\"\n      fill=\"rgba(251, 191, 36, 0.1)\"\n    />\n    <shared-element-highlighter\n      :items=\"elementsState.selected\"\n      stroke=\"#2563EB\"\n      active-stroke=\"#f87171\"\n      fill=\"rgba(37, 99, 235, 0.1)\"\n      active-fill=\"rgba(248, 113, 113, 0.1)\"\n    />\n  </svg>\n  <teleport to=\"html\">\n    <div\n      v-if=\"!disabled\"\n      id=\"automa-selector-overlay\"\n      style=\"\n        z-index: 9999999;\n        position: fixed;\n        left: 0;\n        top: 0;\n        width: 100%;\n        height: 100%;\n      \"\n    ></div>\n  </teleport>\n</template>\n<script setup>\nimport { reactive, watch, onBeforeUnmount, toRaw } from 'vue';\nimport { finder } from '@medv/finder';\nimport { debounce } from '@/utils/helper';\nimport getSelectorOptions from '@/content/elementSelector/getSelectorOptions';\nimport { generateXPath, getElementPath, getElementRect } from '@/content/utils';\nimport findElementList from '@/content/elementSelector/listSelector';\nimport generateElementsSelector from '@/content/elementSelector/generateElementsSelector';\nimport SharedElementHighlighter from './SharedElementHighlighter.vue';\n\nconst props = defineProps({\n  selectorType: {\n    type: String,\n    default: 'css',\n  },\n  selectedEls: {\n    type: Array,\n    default: () => [],\n  },\n  selectorSettings: {\n    type: Object,\n    default: () => ({}),\n  },\n  list: Boolean,\n  hide: Boolean,\n  pause: Boolean,\n  disabled: Boolean,\n  onlyInList: Boolean,\n  withAttributes: Boolean,\n});\nconst emit = defineEmits(['selected']);\n\nlet frameElement = null;\nlet frameElementRect = null;\nlet lastScrollPosY = window.scrollY;\nlet lastScrollPosX = window.scrollX;\nconst mousePosition = {\n  x: 0,\n  y: 0,\n};\n\nlet hoveredElements = [];\nconst elementsState = reactive({\n  hovered: [],\n  selected: [],\n});\n\nconst onScroll = debounce(() => {\n  if (props.disabled) return;\n\n  hoveredElements = [];\n  elementsState.hovered = [];\n\n  const yPos = window.scrollY - lastScrollPosY;\n  const xPos = window.scrollX - lastScrollPosX;\n\n  elementsState.selected.forEach((_, index) => {\n    elementsState.selected[index].x -= xPos;\n    elementsState.selected[index].y -= yPos;\n  });\n\n  lastScrollPosX = window.scrollX;\n  lastScrollPosY = window.scrollY;\n}, 100);\n\nfunction getElementRectWithOffset(\n  element,\n  { withAttribute, withElOptions } = {}\n) {\n  const rect = getElementRect(element, withAttribute);\n\n  if (frameElementRect) {\n    rect.y += frameElementRect.top;\n    rect.x += frameElementRect.left;\n  }\n  if (withElOptions && element.tagName === 'SELECT') {\n    rect.options = Array.from(element.querySelectorAll('option')).map((el) => ({\n      value: el.value,\n      name: el.innerText,\n    }));\n  }\n\n  return rect;\n}\nfunction removeElementsList() {\n  const prevSelectedList = document.querySelectorAll('[automa-el-list]');\n  prevSelectedList.forEach((el) => {\n    el.removeAttribute('automa-el-list');\n  });\n}\nfunction resetFramesElements(options = {}) {\n  const elements = document.querySelectorAll('iframe, frame');\n\n  elements.forEach((element) => {\n    element.contentWindow.postMessage(\n      {\n        ...options,\n        type: 'automa:reset-element-selector',\n      },\n      '*'\n    );\n  });\n}\nfunction retrieveElementsRect({ clientX, clientY, target: eventTarget }, type) {\n  const isAutomaContainer = eventTarget.classList.contains(\n    'automa-element-selector'\n  );\n  if (props.disabled || isAutomaContainer) return;\n\n  const isSelectList = props.list && props.selectorType === 'css';\n\n  let { 1: target } = document.elementsFromPoint(clientX, clientY);\n  if (!target) return;\n\n  const onlyInList = props.onlyInList && elementsState.selected.length > 0;\n  const framesEl = ['IFRAME', 'FRAME'];\n\n  if (framesEl.includes(target.tagName)) {\n    if (type === 'selected') removeElementsList();\n\n    if (target.contentDocument) {\n      frameElement = target;\n      frameElementRect = target.getBoundingClientRect();\n\n      const yPos = clientY - frameElementRect.top;\n      const xPos = clientX - frameElementRect.left;\n\n      target = target.contentDocument.elementFromPoint(xPos, yPos);\n    } else {\n      const { top, left } = target.getBoundingClientRect();\n      const payload = {\n        top,\n        left,\n        clientX,\n        clientY,\n        onlyInList,\n        list: isSelectList,\n        type: 'automa:get-element-rect',\n        withAttributes: props.withAttributes,\n      };\n\n      if (type === 'selected') {\n        Object.assign(payload, {\n          click: true,\n          selectorType: props.selectorType,\n          selectorSettings: toRaw(props.selectorSettings),\n        });\n      }\n\n      target.contentWindow.postMessage(payload, '*');\n      frameElement = target;\n      frameElementRect = target.getBoundingClientRect();\n      return;\n    }\n  } else {\n    frameElement = null;\n    frameElementRect = null;\n  }\n\n  let elementsRect = [];\n  const withElOptions = type === 'selected';\n  const withAttribute = props.withAttributes && type === 'selected';\n\n  if (isSelectList) {\n    const elements =\n      findElementList(target, {\n        onlyInList,\n        frameElement,\n      }) || [];\n\n    if (type === 'hovered') hoveredElements = elements;\n\n    elementsRect = elements.map((el) =>\n      getElementRectWithOffset(el, { withAttribute, withElOptions })\n    );\n  } else {\n    if (type === 'hovered') hoveredElements = [target];\n\n    elementsRect = [\n      getElementRectWithOffset(target, { withAttribute, withElOptions }),\n    ];\n  }\n\n  elementsState[type] = elementsRect;\n\n  if (type === 'selected') {\n    if (!frameElement) resetFramesElements();\n\n    const selectorOptions = getSelectorOptions(props.selectorSettings);\n    let selector = generateElementsSelector({\n      target,\n      frameElement,\n      hoveredElements,\n      list: isSelectList,\n      selectorType: props.selectorType,\n      selectorSettings: selectorOptions,\n    });\n\n    if (frameElement) {\n      const frameSelector = finder(frameElement, selectorOptions);\n      selector = `${frameSelector} |> ${selector}`;\n    }\n\n    const selectElements = elementsRect.reduce((acc, rect, index) => {\n      if (rect.tagName !== 'SELECT') return acc;\n\n      acc.push({ ...rect, elIndex: index });\n\n      return acc;\n    }, []);\n\n    emit('selected', {\n      selector,\n      selectElements,\n      elements: elementsRect,\n      path: getElementPath(target),\n    });\n  }\n}\nfunction onMousemove(event) {\n  if (props.pause) return;\n\n  mousePosition.x = event.clientX;\n  mousePosition.y = event.clientY;\n  retrieveElementsRect(event, 'hovered');\n}\nfunction onKeydown(event) {\n  if (props.pause || event.repeat || event.code !== 'Space') return;\n\n  const { 1: selectedElement } = document.elementsFromPoint(\n    mousePosition.x,\n    mousePosition.y\n  );\n  if (selectedElement.id === 'automa-selector-overlay') return;\n\n  event.preventDefault();\n  event.stopPropagation();\n\n  retrieveElementsRect(\n    {\n      target: selectedElement,\n      clientX: mousePosition.x,\n      clientY: mousePosition.y,\n    },\n    'selected'\n  );\n}\nfunction onMousedown(event) {\n  if (event.target.id === 'automa-selector-overlay') {\n    event.preventDefault();\n    event.stopPropagation();\n  }\n  retrieveElementsRect(event, 'selected');\n}\nfunction onMessage({ data }) {\n  if (data.type !== 'automa:iframe-element-rect') return;\n  if (data.click) {\n    const frameSelector =\n      props.selectorType === 'css'\n        ? finder(frameElement, { tagName: () => true })\n        : generateXPath(frameElement);\n\n    emit('selected', {\n      elements: data.elements,\n      selector: `${frameSelector} |> ${data.selector}`,\n    });\n  }\n\n  const key = data.click ? 'selected' : 'hovered';\n  elementsState[key] = data.elements;\n}\nfunction attachListeners() {\n  window.addEventListener('scroll', onScroll);\n  window.addEventListener('message', onMessage);\n  document.addEventListener('keydown', onKeydown);\n  window.addEventListener('mousemove', onMousemove);\n  document.addEventListener('mousedown', onMousedown);\n}\nfunction detachListeners() {\n  window.removeEventListener('scroll', onScroll);\n  window.removeEventListener('message', onMessage);\n  document.removeEventListener('keydown', onKeydown);\n  window.removeEventListener('mousemove', onMousemove);\n  document.removeEventListener('mousedown', onMousedown);\n}\n\nwatch(\n  () => [props.list, props.disabled],\n  () => {\n    removeElementsList();\n    resetFramesElements({ clearCache: true });\n  }\n);\nwatch(\n  () => props.selectedEls,\n  () => {\n    elementsState.selected = props.selectedEls;\n  }\n);\nwatch(\n  () => props.hide,\n  () => {\n    if (!props.hide) attachListeners();\n    else detachListeners();\n  },\n  { immediate: true }\n);\n\nonBeforeUnmount(detachListeners);\n</script>\n"
  },
  {
    "path": "src/components/newtab/app/AppLogs.vue",
    "content": "<template>\n  <ui-modal\n    v-model=\"state.show\"\n    custom-content\n    content-position=\"start\"\n    @close=\"clearState\"\n  >\n    <ui-card class=\"mt-8 w-full\" style=\"max-width: 1400px; min-height: 600px\">\n      <app-logs-items\n        v-if=\"!state.logId\"\n        :workflow-id=\"state.workflowId\"\n        @select=\"onSelectLog\"\n        @close=\"clearState\"\n      />\n      <app-logs-item-running\n        v-else-if=\"state.runningWorkflow\"\n        :log-id=\"state.logId\"\n        @close=\"closeItemPage\"\n      />\n      <app-logs-item v-else :log-id=\"state.logId\" @close=\"closeItemPage\" />\n    </ui-card>\n  </ui-modal>\n</template>\n<script setup>\nimport { reactive } from 'vue';\nimport emitter from '@/lib/mitt';\nimport AppLogsItem from './AppLogsItem.vue';\nimport AppLogsItems from './AppLogsItems.vue';\nimport AppLogsItemRunning from './AppLogsItemRunning.vue';\n\nconst state = reactive({\n  logId: '',\n  source: '',\n  show: false,\n  workflowId: '',\n  runningWorkflow: false,\n});\n\nemitter.on('ui:logs', (event = {}) => {\n  Object.assign(state, event);\n});\n\nfunction clearState() {\n  state.show = false;\n  state.logId = '';\n  state.source = '';\n  state.runningWorkflow = false;\n}\nfunction closeItemPage(closeModal = false) {\n  state.logId = '';\n\n  if (closeModal) clearState();\n}\nfunction onSelectLog({ id, type }) {\n  state.runningWorkflow = type === 'running';\n  state.logId = id;\n}\n</script>\n"
  },
  {
    "path": "src/components/newtab/app/AppLogsItem.vue",
    "content": "<template>\n  <div v-if=\"currentLog.id\">\n    <div class=\"flex items-center\">\n      <button\n        v-tooltip:bottom=\"t('workflow.blocks.go-back.name')\"\n        role=\"button\"\n        class=\"bg-input mr-2 h-12 rounded-lg px-1 text-gray-600 transition dark:text-gray-300\"\n        @click=\"$emit('close')\"\n      >\n        <v-remixicon name=\"riArrowLeftSLine\" />\n      </button>\n      <div>\n        <h1 class=\"text-overflow max-w-md text-2xl font-semibold\">\n          {{ currentLog.name }}\n        </h1>\n        <p class=\"text-gray-600 dark:text-gray-200\">\n          {{\n            t(`log.description.text`, {\n              status: t(\n                `log.description.status.${currentLog.status || 'success'}`\n              ),\n              date: dayjs(currentLog.startedAt).format('DD MMM'),\n              duration: countDuration(currentLog.startedAt, currentLog.endedAt),\n            })\n          }}\n        </p>\n      </div>\n      <div class=\"grow\"></div>\n      <ui-button\n        v-if=\"state.workflowExists\"\n        v-tooltip=\"t('log.goWorkflow')\"\n        icon\n        class=\"mr-4\"\n        @click=\"goToWorkflow\"\n      >\n        <v-remixicon name=\"riExternalLinkLine\" />\n      </ui-button>\n      <ui-button class=\"text-red-500 dark:text-red-400\" @click=\"deleteLog\">\n        {{ t('common.delete') }}\n      </ui-button>\n    </div>\n    <ui-tabs v-model=\"state.activeTab\" class=\"mt-4\" @change=\"onTabChange\">\n      <ui-tab v-for=\"tab in tabs\" :key=\"tab.id\" class=\"mr-4\" :value=\"tab.id\">\n        {{ tab.name }}\n      </ui-tab>\n    </ui-tabs>\n    <ui-tab-panels\n      :model-value=\"state.activeTab\"\n      class=\"scroll mt-4 overflow-auto px-2 pb-4\"\n      style=\"min-height: 500px; max-height: calc(100vh - 15rem)\"\n    >\n      <ui-tab-panel value=\"logs\">\n        <logs-history\n          :current-log=\"currentLog\"\n          :ctx-data=\"ctxData\"\n          :parent-log=\"parentLog\"\n        />\n      </ui-tab-panel>\n      <ui-tab-panel value=\"table\">\n        <logs-table :current-log=\"currentLog\" :table-data=\"tableData\" />\n      </ui-tab-panel>\n      <ui-tab-panel value=\"variables\">\n        <logs-variables :current-log=\"currentLog\" />\n      </ui-tab-panel>\n    </ui-tab-panels>\n  </div>\n</template>\n<script setup>\nimport { shallowReactive, shallowRef, watch } from 'vue';\nimport { useRouter } from 'vue-router';\nimport { useI18n } from 'vue-i18n';\nimport dbLogs from '@/db/logs';\nimport dayjs from '@/lib/dayjs';\nimport { useWorkflowStore } from '@/stores/workflow';\nimport { countDuration, convertArrObjTo2DArr } from '@/utils/helper';\nimport LogsTable from '@/components/newtab/logs/LogsTable.vue';\nimport LogsHistory from '@/components/newtab/logs/LogsHistory.vue';\nimport LogsVariables from '@/components/newtab/logs/LogsVariables.vue';\n\nconst props = defineProps({\n  logId: {\n    type: String,\n    default: '',\n  },\n});\nconst emit = defineEmits(['close']);\n\nconst { t } = useI18n();\nconst router = useRouter();\nconst workflowStore = useWorkflowStore();\n\nconst ctxData = shallowRef({});\nconst parentLog = shallowRef(null);\n\nconst tabs = [\n  { id: 'logs', name: t('common.log', 2) },\n  { id: 'table', name: t('workflow.table.title') },\n  { id: 'variables', name: t('workflow.variables.title', 2) },\n];\n\nconst state = shallowReactive({\n  activeTab: 'logs',\n  workflowExists: false,\n});\nconst tableData = shallowReactive({\n  converted: false,\n  body: [],\n  header: [],\n});\nconst currentLog = shallowRef({\n  history: [],\n  data: {\n    table: [],\n    variables: {},\n  },\n});\n\nfunction deleteLog() {\n  dbLogs.items\n    .where('id')\n    .equals(props.logId)\n    .delete()\n    .then(() => {\n      emit('close');\n    });\n}\nfunction goToWorkflow() {\n  const path = `/workflows/${currentLog.value.workflowId}`;\n\n  router.push(path);\n  emit('close', true);\n}\nfunction convertToTableData() {\n  const data = currentLog.value.data?.table;\n  if (!data) return;\n\n  const [header] = convertArrObjTo2DArr(data);\n\n  tableData.converted = true;\n  tableData.body = data.map((item, index) => ({ ...item, id: index + 1 }));\n  tableData.header = header.map((name) => ({\n    text: name,\n    value: name,\n    filterable: true,\n  }));\n  tableData.header.unshift({ value: 'id', text: '', sortable: false });\n}\nfunction onTabChange(value) {\n  if (value === 'table' && !tableData.converted) {\n    convertToTableData();\n  }\n}\nasync function fetchLog() {\n  if (!props.logId) return;\n\n  const logDetail = await dbLogs.items.where('id').equals(props.logId).last();\n  if (!logDetail) return;\n\n  tableData.body = [];\n  tableData.header = [];\n  parentLog.value = null;\n  tableData.converted = false;\n\n  const [logCtxData, logHistory, logsData] = await Promise.all(\n    ['ctxData', 'histories', 'logsData'].map((key) =>\n      dbLogs[key].where('logId').equals(props.logId).last()\n    )\n  );\n\n  ctxData.value = logCtxData?.data || {};\n  currentLog.value = {\n    history: logHistory?.data || [],\n    data: logsData?.data || {},\n    ...logDetail,\n  };\n\n  state.workflowExists = Boolean(workflowStore.getById(logDetail.workflowId));\n\n  const parentLogId = logDetail.collectionLogId || logDetail.parentLog?.id;\n  if (parentLogId) {\n    parentLog.value =\n      (await dbLogs.items.where('id').equals(parentLogId).last()) || null;\n  }\n}\n\nwatch(() => props.logId, fetchLog, { immediate: true });\n</script>\n<style>\n.logs-details .cm-editor {\n  max-height: calc(100vh - 15rem);\n}\n</style>\n"
  },
  {
    "path": "src/components/newtab/app/AppLogsItemRunning.vue",
    "content": "<template>\n  <div v-if=\"running\">\n    <div class=\"flex items-center\">\n      <button\n        v-tooltip:bottom=\"t('workflow.blocks.go-back.name')\"\n        role=\"button\"\n        class=\"bg-input mr-2 h-12 rounded-lg px-1 text-gray-600 transition dark:text-gray-300\"\n        @click=\"$emit('close')\"\n      >\n        <v-remixicon name=\"riArrowLeftSLine\" />\n      </button>\n      <div class=\"grow overflow-hidden\">\n        <h1 class=\"text-overflow max-w-md text-2xl font-semibold\">\n          {{ running.state.name }}\n        </h1>\n        <p>\n          {{\n            t('running.start', {\n              date: dayjs(running.state.startedTimestamp).format(\n                'DD MMM, hh:mm A'\n              ),\n            })\n          }}\n        </p>\n      </div>\n      <ui-button @click=\"stopWorkflow\">\n        {{ t('common.stop') }}\n      </ui-button>\n    </div>\n    <div class=\"mt-8\">\n      <logs-history\n        :is-running=\"true\"\n        :current-log=\"{\n          history: running.state.logs,\n          workflowId: running.workflowId,\n        }\"\n      >\n        <template #header-prepend>\n          <div>\n            <h3 class=\"leading-tight\">\n              {{ t('common.log', 2) }}\n            </h3>\n            <p class=\"leading-tight text-gray-600 dark:text-gray-300\">\n              {{ t('running.message') }}\n            </p>\n          </div>\n        </template>\n        <template #append-items>\n          <div\n            v-for=\"block in running.state.currentBlock\"\n            :key=\"block.id\"\n            class=\"hoverable group flex w-full items-center rounded-md px-2 py-1\"\n          >\n            <span\n              :key=\"key\"\n              :title=\"`Duration: ${Math.round(\n                (Date.now() - block.startedAt) / 1000\n              )}s`\"\n              class=\"text-overflow ml-6 w-14 shrink-0 text-gray-400\"\n            >\n              {{ countDuration(block.startedAt, Date.now()) }}\n            </span>\n            <ui-spinner size=\"16\" class=\"mr-2\" color=\"text-accent\" />\n            <p class=\"flex-1\">\n              {{ t(`workflow.blocks.${block.name}.name`) }}\n            </p>\n          </div>\n        </template>\n      </logs-history>\n    </div>\n  </div>\n</template>\n<script setup>\nimport { computed, watch, shallowRef, onBeforeUnmount } from 'vue';\nimport { useRouter } from 'vue-router';\nimport { useI18n } from 'vue-i18n';\nimport { countDuration } from '@/utils/helper';\nimport { useWorkflowStore } from '@/stores/workflow';\nimport dbLogs from '@/db/logs';\nimport dayjs from '@/lib/dayjs';\nimport LogsHistory from '@/components/newtab/logs/LogsHistory.vue';\nimport RendererWorkflowService from '@/service/renderer/RendererWorkflowService';\n\nconst props = defineProps({\n  logId: {\n    type: String,\n    default: '',\n  },\n});\nconst emit = defineEmits(['close']);\n\nconst { t } = useI18n();\nconst router = useRouter();\nconst workflowStore = useWorkflowStore();\n\nconst key = shallowRef(0);\nconst interval = setInterval(() => {\n  key.value += 1;\n}, 1000);\n\nconst running = computed(() =>\n  workflowStore.getAllStates.find(({ id }) => id === props.logId)\n);\n\nfunction stopWorkflow() {\n  RendererWorkflowService.stopWorkflowExecution(running.value.id);\n  emit('close');\n}\n\nwatch(\n  running,\n  async () => {\n    if (!running.value && props.logId) {\n      const log = await dbLogs.items.where('id').equals(props.logId).first();\n      let path = '/logs';\n\n      if (log) {\n        path = `/logs/${props.logId}`;\n      }\n\n      router.replace(path);\n    }\n  },\n  { immediate: true }\n);\nonBeforeUnmount(() => {\n  clearInterval(interval);\n});\n</script>\n"
  },
  {
    "path": "src/components/newtab/app/AppLogsItems.vue",
    "content": "<template>\n  <div class=\"logs-list overflow-auto pb-4 pt-1\">\n    <div class=\"mb-8 flex items-center\">\n      <h1 class=\"flex-1 text-2xl font-semibold\">\n        {{ $t('common.log', 2) }}\n      </h1>\n      <v-remixicon\n        name=\"riCloseLine\"\n        class=\"cursor-pointer text-gray-600 dark:text-gray-300\"\n        @click=\"$emit('close')\"\n      />\n    </div>\n    <logs-filters\n      :sorts=\"sortsBuilder\"\n      :filters=\"filtersBuilder\"\n      @clear=\"clearLogs\"\n      @updateSorts=\"sortsBuilder[$event.key] = $event.value\"\n      @updateFilters=\"filtersBuilder[$event.key] = $event.value\"\n    >\n      <ui-popover padding=\"\" @click=\"filtersBuilder.workflowQuery = ''\">\n        <template #trigger>\n          <ui-button>\n            <span class=\"text-overflow text-left\" style=\"max-width: 160px\">\n              {{ activeWorkflowName }}\n            </span>\n            <v-remixicon name=\"riArrowDropDownLine\" class=\"-mr-1 ml-2\" />\n          </ui-button>\n        </template>\n        <div class=\"w-64\">\n          <div class=\"p-4\">\n            <ui-input\n              v-model=\"filtersBuilder.workflowQuery\"\n              autofocus\n              placeholder=\"Search...\"\n              class=\"w-full\"\n              prepend-icon=\"riSearch2Line\"\n            />\n            <div class=\"text-right\">\n              <span\n                class=\"cursor-pointer text-sm text-gray-600 underline dark:text-gray-300\"\n                @click=\"filtersBuilder.workflowId = ''\"\n              >\n                Clear\n              </span>\n            </div>\n          </div>\n          <ui-list class=\"scroll mb-4 max-h-96 space-y-1 overflow-auto px-4\">\n            <ui-list-item\n              v-for=\"workflow in workflows\"\n              :key=\"workflow.id\"\n              :active=\"filtersBuilder.workflowId === workflow.id\"\n              class=\"cursor-pointer\"\n              @click=\"filtersBuilder.workflowId = workflow.id\"\n            >\n              <p class=\"text-overflow\">{{ workflow.name }}</p>\n            </ui-list-item>\n          </ui-list>\n        </div>\n      </ui-popover>\n    </logs-filters>\n    <div v-if=\"logs\" style=\"min-height: 320px\">\n      <shared-logs-table\n        :logs=\"logs\"\n        :modal=\"true\"\n        :running=\"workflowStates\"\n        class=\"w-full\"\n        style=\"max-height: calc(100vh - 18rem)\"\n        @select=\"$emit('select', $event)\"\n      >\n        <template #item-prepend=\"{ log }\">\n          <td class=\"w-8\">\n            <ui-checkbox\n              :model-value=\"selectedLogs.includes(log.id)\"\n              class=\"align-text-bottom\"\n              @change=\"toggleSelectedLog($event, log.id)\"\n            />\n          </td>\n        </template>\n        <template #item-append=\"{ log }\">\n          <td class=\"ml-4 text-right\">\n            <v-remixicon\n              name=\"riDeleteBin7Line\"\n              class=\"inline-block cursor-pointer text-red-500 dark:text-red-400\"\n              @click=\"deleteLog(log.id)\"\n            />\n          </td>\n        </template>\n      </shared-logs-table>\n    </div>\n    <div class=\"mt-4 md:flex md:items-center md:justify-between\">\n      <div>\n        {{ t('components.pagination.text1') }}\n        <select v-model=\"pagination.perPage\" class=\"bg-input rounded-md p-1\">\n          <option v-for=\"num in [10, 15, 25, 50, 100]\" :key=\"num\" :value=\"num\">\n            {{ num }}\n          </option>\n        </select>\n        {{ t('components.pagination.text2', { count: filteredLogs.length }) }}\n      </div>\n      <ui-pagination\n        v-model=\"pagination.currentPage\"\n        :per-page=\"pagination.perPage\"\n        :records=\"filteredLogs.length\"\n        class=\"mt-4 md:mt-0\"\n      />\n    </div>\n    <ui-card\n      v-if=\"selectedLogs.length !== 0\"\n      class=\"fixed right-0 bottom-0 m-5 space-x-2 shadow-xl\"\n    >\n      <ui-button @click=\"selectAllLogs\">\n        {{\n          t(\n            `log.${\n              selectedLogs.length >= logs?.length ? 'deselectAll' : 'selectAll'\n            }`\n          )\n        }}\n      </ui-button>\n      <ui-button variant=\"danger\" @click=\"deleteSelectedLogs\">\n        {{ t('log.deleteSelected') }} ({{ selectedLogs.length }})\n      </ui-button>\n    </ui-card>\n    <ui-modal v-model=\"exportDataModal.show\" content-class=\"max-w-2xl\">\n      <template #header>\n        <span class=\"capitalize\">{{ t('common.data') }}</span>\n      </template>\n      <logs-data-viewer\n        :log=\"exportDataModal.log\"\n        editor-class=\"logs-list-data\"\n      />\n    </ui-modal>\n  </div>\n</template>\n<script setup>\nimport { shallowReactive, ref, computed, watch } from 'vue';\nimport { useI18n } from 'vue-i18n';\nimport { useDialog } from '@/composable/dialog';\nimport dbLogs from '@/db/logs';\nimport { useWorkflowStore } from '@/stores/workflow';\nimport { useHostedWorkflowStore } from '@/stores/hostedWorkflow';\nimport { useLiveQuery } from '@/composable/liveQuery';\nimport LogsFilters from '@/components/newtab/logs/LogsFilters.vue';\nimport LogsDataViewer from '@/components/newtab/logs/LogsDataViewer.vue';\nimport SharedLogsTable from '@/components/newtab/shared/SharedLogsTable.vue';\n\nconst props = defineProps({\n  workflowId: {\n    type: String,\n    default: '',\n  },\n});\ndefineEmits(['select', 'close']);\n\nconst { t } = useI18n();\nconst dialog = useDialog();\nconst workflowStore = useWorkflowStore();\nconst hostedWorkflows = useHostedWorkflowStore();\nconst storedlogs = useLiveQuery(() => dbLogs.items.toArray());\n\nconst savedSorts = JSON.parse(localStorage.getItem('logs-sorts') || '{}');\n\nconst selectedLogs = ref([]);\nconst pagination = shallowReactive({\n  perPage: 10,\n  currentPage: 1,\n});\nconst filtersBuilder = shallowReactive({\n  query: '',\n  byDate: 0,\n  byStatus: 'all',\n  workflowQuery: '',\n  workflowId: props.workflowId,\n});\nconst sortsBuilder = shallowReactive({\n  order: savedSorts.order || 'desc',\n  by: savedSorts.by || 'endedAt',\n});\nconst exportDataModal = shallowReactive({\n  show: false,\n  log: {},\n});\n\nconst allWorkflows = computed(() =>\n  [...hostedWorkflows.toArray, ...workflowStore.getWorkflows].sort((a, b) =>\n    a.createdAt > b.createdAt ? -1 : 1\n  )\n);\nconst workflows = computed(() =>\n  allWorkflows.value.filter((workflow) =>\n    workflow.name\n      .toLocaleLowerCase()\n      .includes(filtersBuilder.workflowQuery.toLocaleLowerCase())\n  )\n);\nconst activeWorkflowName = computed(() => {\n  if (!filtersBuilder.workflowId) return 'All workflows';\n\n  const workflow = allWorkflows.value.find(\n    (item) => item.id === filtersBuilder.workflowId\n  );\n\n  return workflow?.name ?? 'All workflows';\n});\n\nconst workflowStates = computed(() => {\n  const states = workflowStore.getAllStates;\n  if (!filtersBuilder.workflowId) return states;\n\n  return states.filter(\n    (state) => state.workflowId === filtersBuilder.workflowId\n  );\n});\n\nconst filteredLogs = computed(() => {\n  if (!storedlogs.value) return [];\n\n  return storedlogs.value\n    .filter(({ name, status, endedAt, workflowId }) => {\n      let dateFilter = true;\n      let statusFilter = true;\n      const workflowIdFilter = filtersBuilder.workflowId\n        ? filtersBuilder.workflowId === workflowId\n        : true;\n      const searchFilter = name\n        .toLocaleLowerCase()\n        .includes(filtersBuilder.query.toLocaleLowerCase());\n\n      if (filtersBuilder.byStatus !== 'all') {\n        statusFilter = status === filtersBuilder.byStatus;\n      }\n\n      if (filtersBuilder.byDate > 0) {\n        const date = Date.now() - filtersBuilder.byDate * 24 * 60 * 60 * 1000;\n\n        dateFilter = date <= endedAt;\n      }\n\n      return searchFilter && workflowIdFilter && statusFilter && dateFilter;\n    })\n    .slice()\n    .sort((a, b) => {\n      const valueA = a[sortsBuilder.by];\n      const valueB = b[sortsBuilder.by];\n\n      if (sortsBuilder.order === 'asc') return valueA > valueB ? 1 : -1;\n\n      return valueB > valueA ? 1 : -1;\n    });\n});\nconst logs = computed(() =>\n  filteredLogs.value.slice(\n    (pagination.currentPage - 1) * pagination.perPage,\n    pagination.currentPage * pagination.perPage\n  )\n);\n\nfunction deleteLog(id) {\n  dbLogs.items.delete(id).then(() => {\n    dbLogs.ctxData.where('logId').equals(id).delete();\n    dbLogs.histories.where('logId').equals(id).delete();\n    dbLogs.logsData.where('logId').equals(id).delete();\n  });\n}\nfunction toggleSelectedLog(selected, logId) {\n  if (selected) {\n    selectedLogs.value.push(logId);\n    return;\n  }\n\n  const index = selectedLogs.value.indexOf(logId);\n\n  if (index !== -1) selectedLogs.value.splice(index, 1);\n}\nfunction deleteSelectedLogs() {\n  dialog.confirm({\n    title: t('log.delete.title'),\n    okVariant: 'danger',\n    body: t('log.delete.description'),\n    onConfirm: () => {\n      dbLogs.items.bulkDelete(selectedLogs.value).then(() => {\n        selectedLogs.value = [];\n      });\n    },\n  });\n}\nfunction clearLogs() {\n  dialog.confirm({\n    title: t('log.clearLogs.title'),\n    okVariant: 'danger',\n    body: t('log.clearLogs.description'),\n    onConfirm: () => {\n      dbLogs.items.clear();\n      dbLogs.ctxData.clear();\n      dbLogs.logsData.clear();\n      dbLogs.histories.clear();\n    },\n  });\n}\nfunction selectAllLogs() {\n  if (selectedLogs.value.length >= logs.value?.length) {\n    selectedLogs.value = [];\n    return;\n  }\n\n  const logIds = logs?.value.map(({ id }) => id);\n\n  selectedLogs.value = logIds;\n}\n\nwatch(\n  () => sortsBuilder,\n  (value) => {\n    localStorage.setItem('logs-sorts', JSON.stringify(value));\n  },\n  { deep: true }\n);\n</script>\n<style>\n.logs-list-data {\n  max-height: calc(100vh - 12rem);\n}\n</style>\n"
  },
  {
    "path": "src/components/newtab/app/AppSidebar.vue",
    "content": "<template>\n  <aside\n    class=\"fixed left-0 top-0 z-50 flex h-screen w-16 flex-col items-center bg-white py-6 dark:bg-gray-800\"\n  >\n    <img\n      :title=\"`v${extensionVersion}`\"\n      src=\"@/assets/svg/logo.svg\"\n      class=\"mx-auto mb-4 w-10\"\n    />\n    <div\n      class=\"relative w-full space-y-2 text-center\"\n      @mouseleave=\"showHoverIndicator = false\"\n    >\n      <div\n        v-show=\"showHoverIndicator\"\n        ref=\"hoverIndicator\"\n        class=\"bg-box-transparent absolute left-1/2 h-10 w-10 rounded-lg transition-transform duration-200\"\n        style=\"transform: translate(-50%, 0)\"\n      ></div>\n      <router-link\n        v-for=\"tab in tabs\"\n        v-slot=\"{ href, navigate, isActive }\"\n        :key=\"tab.id\"\n        :to=\"tab.path\"\n        custom\n      >\n        <a\n          v-tooltip:right.group=\"\n            `${t(`common.${tab.id}`, 2)} ${\n              tab.shortcut && `(${tab.shortcut.readable})`\n            }`\n          \"\n          :class=\"{ 'is-active': isActive }\"\n          :href=\"tab.id === 'log' ? '#' : href\"\n          class=\"tab relative z-10 flex w-full items-center justify-center\"\n          @click=\"navigateLink($event, navigate, tab)\"\n          @mouseenter=\"hoverHandler\"\n        >\n          <div class=\"inline-block rounded-lg p-2 transition-colors\">\n            <v-remixicon :name=\"tab.icon\" />\n          </div>\n          <span\n            v-if=\"tab.id === 'log' && runningWorkflowsLen > 0\"\n            class=\"absolute -top-1 right-2 h-4 w-4 rounded-full bg-accent text-xs text-white dark:text-black\"\n          >\n            {{ runningWorkflowsLen }}\n          </span>\n        </a>\n      </router-link>\n    </div>\n    <hr class=\"my-4 w-8/12\" />\n    <button\n      v-tooltip:right.group=\"$t('home.elementSelector.name')\"\n      class=\"focus:ring-0\"\n      @click=\"injectElementSelector\"\n    >\n      <v-remixicon name=\"riFocus3Line\" />\n    </button>\n    <div class=\"grow\"></div>\n    <router-link\n      v-if=\"userStore.user\"\n      v-tooltip:right.group=\"t('settings.menu.profile')\"\n      to=\"/profile\"\n      class=\"bg-box-transparent inline-block rounded-full p-1 transition-transform hover:scale-110\"\n    >\n      <img\n        :src=\"userStore.user.avatar_url\"\n        height=\"32\"\n        width=\"32\"\n        class=\"rounded-full\"\n        alt=\"User avatar\"\n      />\n    </router-link>\n    <ui-popover trigger=\"mouseenter\" placement=\"right\" class=\"my-4\">\n      <template #trigger>\n        <v-remixicon name=\"riGroupLine\" />\n      </template>\n      <p class=\"mb-2\">{{ t('home.communities') }}</p>\n      <ui-list class=\"w-40\">\n        <ui-list-item\n          v-for=\"item in communities\"\n          :key=\"item.name\"\n          :href=\"item.url\"\n          small\n          tag=\"a\"\n          target=\"_blank\"\n          rel=\"noopener\"\n        >\n          <v-remixicon :name=\"item.icon\" class=\"mr-2\" />\n          {{ item.name }}\n        </ui-list-item>\n      </ui-list>\n    </ui-popover>\n    <router-link v-tooltip:right.group=\"t('settings.menu.about')\" to=\"/about\">\n      <v-remixicon class=\"cursor-pointer\" name=\"riInformationLine\" />\n    </router-link>\n  </aside>\n</template>\n<script setup>\nimport { ref, computed } from 'vue';\nimport { useI18n } from 'vue-i18n';\nimport { useRouter } from 'vue-router';\nimport { useToast } from 'vue-toastification';\nimport browser from 'webextension-polyfill';\nimport { useUserStore } from '@/stores/user';\nimport { useWorkflowStore } from '@/stores/workflow';\nimport { useShortcut, getShortcut } from '@/composable/shortcut';\nimport { useGroupTooltip } from '@/composable/groupTooltip';\nimport { communities } from '@/utils/shared';\nimport { initElementSelector } from '@/newtab/utils/elementSelector';\nimport emitter from '@/lib/mitt';\n\nuseGroupTooltip();\n\nconst { t } = useI18n();\nconst toast = useToast();\nconst router = useRouter();\nconst userStore = useUserStore();\nconst workflowStore = useWorkflowStore();\n\nconst extensionVersion = browser.runtime.getManifest().version;\nconst tabs = [\n  {\n    id: 'workflow',\n    icon: 'riFlowChart',\n    path: '/workflows',\n    shortcut: getShortcut('page:workflows', '/workflows'),\n  },\n  {\n    id: 'packages',\n    icon: 'mdiPackageVariantClosed',\n    path: '/packages',\n    shortcut: '',\n  },\n  {\n    id: 'schedule',\n    icon: 'riTimeLine',\n    path: '/schedule',\n    shortcut: getShortcut('page:schedule', '/triggers'),\n  },\n  {\n    id: 'storage',\n    icon: 'riHardDrive2Line',\n    path: '/storage',\n    shortcut: getShortcut('page:storage', '/storage'),\n  },\n  {\n    id: 'log',\n    icon: 'riHistoryLine',\n    path: '/logs',\n    shortcut: getShortcut('page:logs', '/logs'),\n  },\n  {\n    id: 'settings',\n    icon: 'riSettings3Line',\n    path: '/settings',\n    shortcut: getShortcut('page:settings', '/settings'),\n  },\n];\nconst hoverIndicator = ref(null);\nconst showHoverIndicator = ref(false);\nconst runningWorkflowsLen = computed(() => workflowStore.getAllStates.length);\n\nuseShortcut(\n  tabs.reduce((acc, { shortcut }) => {\n    if (shortcut) {\n      acc.push(shortcut);\n    }\n\n    return acc;\n  }, []),\n  ({ data }) => {\n    if (!data) return;\n\n    if (data.includes('/logs')) {\n      emitter.emit('ui:logs', { show: true });\n      return;\n    }\n\n    router.push(data);\n  }\n);\n\nfunction navigateLink(event, navigateFn, tab) {\n  event.preventDefault();\n\n  if (tab.id === 'log') {\n    emitter.emit('ui:logs', { show: true });\n  } else {\n    navigateFn();\n  }\n}\nfunction hoverHandler({ target }) {\n  showHoverIndicator.value = true;\n  hoverIndicator.value.style.transform = `translate(-50%, ${target.offsetTop}px)`;\n}\nasync function injectElementSelector() {\n  try {\n    const [tab] = await browser.tabs.query({ active: true, url: '*://*/*' });\n    if (!tab) {\n      toast.error(t('home.elementSelector.noAccess'));\n      return;\n    }\n\n    await initElementSelector();\n  } catch (error) {\n    console.error(error);\n  }\n}\n</script>\n<style scoped>\n.tab.is-active:after {\n  content: '';\n  position: absolute;\n  right: 0;\n  top: 0;\n  height: 100%;\n  width: 4px;\n  @apply bg-accent dark:bg-gray-100;\n}\n</style>\n"
  },
  {
    "path": "src/components/newtab/app/AppSurvey.vue",
    "content": "<template>\n  <ui-card\n    v-if=\"modalState.show\"\n    class=\"group fixed bottom-8 right-8 w-72 border-2 shadow-2xl\"\n  >\n    <button\n      class=\"absolute -right-2 -top-2 scale-0 rounded-full bg-white shadow-md transition group-hover:scale-100\"\n      @click=\"closeModal\"\n    >\n      <v-remixicon class=\"text-gray-600\" name=\"riCloseLine\" />\n    </button>\n    <h2 class=\"text-lg font-semibold\">\n      {{ activeModal.title }}\n    </h2>\n    <p class=\"mt-1 text-gray-700 dark:text-gray-100\">\n      {{ activeModal.body }}\n    </p>\n    <div class=\"mt-4 space-y-2\">\n      <ui-button\n        :href=\"activeModal.url\"\n        tag=\"a\"\n        target=\"_blank\"\n        rel=\"noopener\"\n        class=\"block w-full\"\n        variant=\"accent\"\n      >\n        {{ activeModal.button }}\n      </ui-button>\n    </div>\n  </ui-card>\n</template>\n<script setup>\nimport dayjs from '@/lib/dayjs';\nimport { computed, onMounted, shallowReactive } from 'vue';\nimport browser from 'webextension-polyfill';\n\nconst modalTypes = {\n  testimonial: {\n    title: 'Hi There 👋',\n    body: 'Thank you for using Automa, and if you have a great experience. Would you like to give us a testimonial?',\n    button: 'Give Testimonial',\n    url: 'https://testimonial.to/automa',\n  },\n  survey: {\n    title: \"How do you think we're doing?\",\n    body: 'To help us make Automa as best it can be, we need a few minutes of your time to get your feedback.',\n    button: 'Take Survey',\n    url: 'https://extension.automa.site/survey',\n  },\n};\n\nconst modalState = shallowReactive({\n  show: true,\n  type: 'survey',\n});\n\nfunction closeModal() {\n  let value = true;\n\n  if (modalState.type === 'survey') {\n    value = new Date().toString();\n  }\n\n  modalState.show = false;\n  localStorage.setItem(`has-${modalState.type}`, value);\n}\nasync function checkModal() {\n  try {\n    const { isFirstTime } = await browser.storage.local.get('isFirstTime');\n\n    if (isFirstTime) {\n      modalState.show = false;\n      localStorage.setItem('has-testimonial', true);\n      localStorage.setItem('has-survey', Date.now());\n      return;\n    }\n\n    const survey = localStorage.getItem('has-survey');\n\n    if (!survey) return;\n\n    const daysDiff = dayjs().diff(survey, 'day');\n    const showTestimonial =\n      daysDiff >= 2 && !localStorage.getItem('has-testimonial');\n\n    if (showTestimonial) {\n      modalState.show = true;\n      modalState.type = 'testimonial';\n    } else {\n      modalState.show = false;\n    }\n  } catch (error) {\n    console.error(error);\n  }\n}\n\nconst activeModal = computed(() => modalTypes[modalState.type]);\n\nonMounted(checkModal);\n</script>\n"
  },
  {
    "path": "src/components/newtab/logs/LogsDataViewer.vue",
    "content": "<template>\n  <div v-if=\"state.status === 'loading'\" class=\"py-8 text-center\">\n    <ui-spinner color=\"text-primary\" />\n  </div>\n  <template v-else-if=\"state.status === 'idle'\">\n    <div class=\"mb-2 flex items-center\">\n      <ui-input\n        v-model=\"state.fileName\"\n        :placeholder=\"t('common.fileName')\"\n        :title=\"t('common.fileName')\"\n      />\n      <div class=\"grow\"></div>\n      <ui-popover trigger-width>\n        <template #trigger>\n          <ui-button variant=\"accent\">\n            <span>{{ t('log.exportData.title') }}</span>\n            <v-remixicon name=\"riArrowDropDownLine\" class=\"ml-2 -mr-1\" />\n          </ui-button>\n        </template>\n        <ui-list class=\"space-y-1\">\n          <ui-list-item\n            v-for=\"type in dataExportTypes\"\n            :key=\"type.id\"\n            v-close-popover\n            class=\"cursor-pointer\"\n            @click=\"exportData(type.id)\"\n          >\n            {{ t(`log.exportData.types.${type.id}`) }}\n          </ui-list-item>\n        </ui-list>\n      </ui-popover>\n    </div>\n    <ui-tabs v-if=\"objectHasKey(logsData, 'table')\" v-model=\"state.activeTab\">\n      <ui-tab value=\"table\">\n        {{ t('workflow.table.title') }}\n      </ui-tab>\n      <ui-tab value=\"variables\">\n        {{ t('workflow.variables.title', 2) }}\n      </ui-tab>\n    </ui-tabs>\n    <shared-codemirror\n      :model-value=\"dataStr\"\n      :class=\"editorClass\"\n      class=\"rounded-t-none\"\n      lang=\"json\"\n      readonly\n    />\n  </template>\n</template>\n<script setup>\nimport {\n  shallowReactive,\n  computed,\n  defineAsyncComponent,\n  onMounted,\n} from 'vue';\nimport { useI18n } from 'vue-i18n';\nimport dbLogs from '@/db/logs';\nimport { dataExportTypes } from '@/utils/shared';\nimport { objectHasKey } from '@/utils/helper';\nimport dataExporter from '@/utils/dataExporter';\n\nconst SharedCodemirror = defineAsyncComponent(() =>\n  import('@/components/newtab/shared/SharedCodemirror.vue')\n);\n\nconst props = defineProps({\n  log: {\n    type: Object,\n    default: () => ({}),\n  },\n  editorClass: {\n    type: String,\n    default: '',\n  },\n});\n\nconst { t } = useI18n();\n\nconst state = shallowReactive({\n  status: 'loading',\n  activeTab: 'table',\n  fileName: props.log.name,\n});\nconst logsData = {\n  table: '',\n  variables: '',\n};\n\nconst dataStr = computed(() => {\n  if (state.status !== 'idle') return '';\n\n  return logsData[state.activeTab] ? logsData[state.activeTab] : '';\n});\n\nfunction exportData(type) {\n  dataExporter(\n    logsData?.table || logsData,\n    { name: state.fileName, type },\n    true\n  );\n}\n\nonMounted(async () => {\n  const data = await dbLogs.logsData.where('logId').equals(props.log.id).last();\n\n  if (!data) {\n    state.status = 'error';\n    return;\n  }\n\n  Object.keys(data.data).forEach((key) => {\n    logsData[key] = JSON.stringify(data.data[key], null, 2);\n  });\n  state.status = 'idle';\n});\n</script>\n"
  },
  {
    "path": "src/components/newtab/logs/LogsFilters.vue",
    "content": "<template>\n  <div class=\"mb-6 flex flex-wrap items-center md:space-x-4\">\n    <ui-input\n      id=\"search-input\"\n      :model-value=\"filters.query\"\n      :placeholder=\"`${t('common.search')}...`\"\n      prepend-icon=\"riSearch2Line\"\n      class=\"w-6/12 md:w-auto md:flex-1\"\n      @change=\"updateFilters('query', $event)\"\n    />\n    <slot />\n    <div class=\"workflow-sort ml-4 flex w-5/12 items-center md:ml-0 md:w-auto\">\n      <ui-button\n        icon\n        class=\"rounded-r-none border-r border-gray-300\"\n        @click=\"updateSorts('order', sorts.order === 'asc' ? 'desc' : 'asc')\"\n      >\n        <v-remixicon\n          :name=\"sorts.order === 'asc' ? 'riSortAsc' : 'riSortDesc'\"\n        />\n      </ui-button>\n      <ui-select\n        :model-value=\"sorts.by\"\n        :placeholder=\"t('sort.sortBy')\"\n        @change=\"updateSorts('by', $event)\"\n      >\n        <option v-for=\"sort in sortsList\" :key=\"sort.id\" :value=\"sort.id\">\n          {{ sort.name }}\n        </option>\n      </ui-select>\n    </div>\n    <ui-popover class=\"mt-4 md:mt-0\">\n      <template #trigger>\n        <ui-button>\n          <v-remixicon name=\"riFilter2Line\" class=\"mr-2 -ml-1\" />\n          <span>{{ t('log.filter.title') }}</span>\n        </ui-button>\n      </template>\n      <div class=\"w-48\">\n        <p class=\"mb-2 flex-1 font-semibold\">{{ t('log.filter.title') }}</p>\n        <p class=\"mb-2 text-sm text-gray-600 dark:text-gray-200\">\n          {{ t('log.filter.byStatus') }}\n        </p>\n        <div class=\"grid grid-cols-2 gap-2\">\n          <ui-radio\n            v-for=\"status in filterByStatus\"\n            :key=\"status.id\"\n            :model-value=\"filters.byStatus\"\n            :value=\"status.id\"\n            class=\"text-sm capitalize\"\n            @change=\"updateFilters('byStatus', $event)\"\n          >\n            {{ status.name }}\n          </ui-radio>\n        </div>\n        <p class=\"mb-1 mt-3 text-sm text-gray-600 dark:text-gray-200\">\n          {{ t('log.filter.byDate.title') }}\n        </p>\n        <ui-select\n          :model-value=\"filters.byDate\"\n          class=\"w-full\"\n          @change=\"updateFilters('byDate', $event)\"\n        >\n          <option v-for=\"date in filterByDate\" :key=\"date.id\" :value=\"date.id\">\n            {{ date.name }}\n          </option>\n        </ui-select>\n      </div>\n    </ui-popover>\n    <ui-button class=\"ml-4 mt-4 md:ml-0 md:mt-0\" @click=\"$emit('clear')\">\n      <v-remixicon name=\"riDeleteBin7Line\" class=\"mr-2 -ml-1\" />\n      <span>\n        {{ t('log.clearLogs.title') }}\n      </span>\n    </ui-button>\n  </div>\n</template>\n<script setup>\nimport { useI18n } from 'vue-i18n';\n\ndefineProps({\n  filters: {\n    type: Object,\n    default: () => ({}),\n  },\n  sorts: {\n    type: Object,\n    default: () => ({}),\n  },\n  workflows: {\n    type: Array,\n    default: () => [],\n  },\n});\nconst emit = defineEmits(['updateSorts', 'updateFilters', 'clear']);\n\nconst { t } = useI18n();\n\nconst filterByStatus = [\n  { id: 'all', name: t('common.all') },\n  { id: 'success', name: t('logStatus.success') },\n  { id: 'stopped', name: t('logStatus.stopped') },\n  { id: 'error', name: t('logStatus.error') },\n];\nconst filterByDate = [\n  { id: 0, name: t('common.all') },\n  { id: 1, name: t('log.filter.byDate.items.lastDay') },\n  { id: 7, name: t('log.filter.byDate.items.last7Days') },\n  { id: 30, name: t('log.filter.byDate.items.last30Days') },\n];\nconst sortsList = [\n  { id: 'name', name: t('sort.name') },\n  { id: 'startedAt', name: t('sort.createdAt') },\n];\n\nfunction updateFilters(key, value) {\n  emit('updateFilters', { key, value });\n}\nfunction updateSorts(key, value) {\n  emit('updateSorts', { key, value });\n}\n</script>\n"
  },
  {
    "path": "src/components/newtab/logs/LogsHistory.vue",
    "content": "<template>\n  <router-link\n    v-if=\"parentLog\"\n    replace\n    :to=\"'/logs/' + currentLog.parentLog?.id || currentLog.collectionLogId\"\n    class=\"mb-4 flex\"\n  >\n    <v-remixicon name=\"riArrowLeftLine\" class=\"mr-2\" />\n    {{ t('log.goBack', { name: parentLog.name }) }}\n  </router-link>\n  <div class=\"flex flex-col-reverse items-start lg:flex-row\">\n    <div class=\"w-full lg:w-auto lg:flex-1\">\n      <div class=\"dark rounded-lg bg-gray-900 text-gray-100\">\n        <div class=\"mb-4 flex items-center border-b p-4 text-gray-200\">\n          <div v-if=\"currentLog.status === 'error' && errorBlock\">\n            <p class=\"line-clamp leading-tight\">\n              {{ errorBlock.message }}\n              <a\n                v-if=\"errorBlock.messageId\"\n                :href=\"`https://docs.extension.automa.site/reference/workflow-common-errors.html#${errorBlock.messageId}`\"\n                target=\"_blank\"\n                title=\"About the error\"\n                @click.stop\n              >\n                <v-remixicon\n                  name=\"riArrowLeftLine\"\n                  size=\"20\"\n                  class=\"inline-block text-gray-300\"\n                  rotate=\"135\"\n                />\n              </a>\n            </p>\n            <p class=\"cursor-pointer\" title=\"Jump to item\" @click=\"jumpToError\">\n              On the {{ errorBlock.name }} block\n              <v-remixicon\n                name=\"riArrowLeftLine\"\n                class=\"-ml-1 inline-block\"\n                size=\"18\"\n                rotate=\"135\"\n              />\n            </p>\n          </div>\n          <slot name=\"header-prepend\" />\n          <div class=\"grow\" />\n          <ui-popover v-if=\"!isRunning\" trigger-width class=\"mr-4\">\n            <template #trigger>\n              <ui-button>\n                <span>\n                  Export <span class=\"hidden lg:inline-block\">logs</span>\n                </span>\n                <v-remixicon name=\"riArrowDropDownLine\" class=\"ml-2 -mr-1\" />\n              </ui-button>\n            </template>\n            <ui-list class=\"space-y-1\">\n              <ui-list-item\n                v-for=\"type in dataExportTypes\"\n                :key=\"type.id\"\n                v-close-popover\n                class=\"cursor-pointer\"\n                @click=\"exportLogs(type.id)\"\n              >\n                {{ t(`log.exportData.types.${type.id}`) }}\n              </ui-list-item>\n            </ui-list>\n          </ui-popover>\n          <ui-input\n            v-if=\"!isRunning\"\n            v-model=\"state.search\"\n            :placeholder=\"t('common.search')\"\n            prepend-icon=\"riSearch2Line\"\n          />\n        </div>\n        <div\n          id=\"log-history\"\n          style=\"max-height: 500px\"\n          class=\"scroll overflow-auto p-4\"\n        >\n          <slot name=\"prepend\" />\n          <p\n            v-if=\"currentLog.history.length === 0\"\n            class=\"text-center text-gray-300\"\n          >\n            The workflow log is not saved\n          </p>\n          <div class=\"w-full space-y-1 overflow-auto font-mono text-sm\">\n            <div\n              v-for=\"(item, index) in history\"\n              :key=\"item.id || index\"\n              :disabled=\"!ctxData[item.id]\"\n              :class=\"{ 'bg-box-transparent': item.id === state.itemId }\"\n              hide-header-icon\n              class=\"hoverable group flex w-full cursor-default items-start rounded-md px-2 py-1 text-left focus:ring-0\"\n              @click=\"setActiveLog(item)\"\n            >\n              <div\n                style=\"min-width: 54px\"\n                class=\"text-overflow mr-4 shrink-0 text-gray-400\"\n              >\n                <span\n                  v-if=\"item.timestamp\"\n                  :title=\"\n                    dayjs(item.timestamp).format('YYYY-MM-DDTHH:mm:ss.SSS')\n                  \"\n                >\n                  {{ dayjs(item.timestamp).format('HH:mm:ss') }}\n                  {{ `(${countDuration(0, item.duration || 0).trim()})` }}\n                </span>\n                <span v-else :title=\"`${Math.round(item.duration / 1000)}s`\">\n                  {{ countDuration(0, item.duration || 0) }}\n                </span>\n              </div>\n              <span\n                :class=\"logsType[item.type]?.color\"\n                :title=\"item.type\"\n                class=\"text-overflow w-2/12 shrink-0\"\n              >\n                <v-remixicon\n                  :name=\"logsType[item.type]?.icon\"\n                  size=\"18\"\n                  class=\"-mr-1 inline-block align-text-top\"\n                />\n                {{ item.name }}\n              </span>\n              <span\n                :title=\"`${t('common.description')} (${item.description})`\"\n                class=\"text-overflow ml-2 w-2/12 shrink-0\"\n              >\n                {{ item.description }}\n              </span>\n              <p\n                :title=\"item.message\"\n                class=\"line-clamp ml-2 flex-1 text-sm leading-tight text-gray-600 dark:text-gray-200\"\n              >\n                {{ item.message }}\n                <a\n                  v-if=\"item.messageId\"\n                  :href=\"`https://docs.extension.automa.site/reference/workflow-common-errors.html#${item.messageId}`\"\n                  target=\"_blank\"\n                  title=\"About the error\"\n                  @click.stop\n                >\n                  <v-remixicon\n                    name=\"riArrowLeftLine\"\n                    size=\"20\"\n                    class=\"inline-block text-gray-300\"\n                    rotate=\"135\"\n                  />\n                </a>\n              </p>\n              <router-link\n                v-if=\"item.logId\"\n                v-slot=\"{ navigate }\"\n                :to=\"{ name: 'logs-details', params: { id: item.logId } }\"\n                custom\n              >\n                <v-remixicon\n                  title=\"Open log detail\"\n                  class=\"ml-2 cursor-pointer text-gray-300\"\n                  size=\"20\"\n                  name=\"riFileTextLine\"\n                  @click.stop=\"navigate\"\n                />\n              </router-link>\n              <router-link\n                v-if=\"!isRunning && getBlockPath(item.blockId)\"\n                v-show=\"currentLog.workflowId && item.blockId\"\n                :to=\"getBlockPath(item.blockId)\"\n              >\n                <v-remixicon\n                  name=\"riExternalLinkLine\"\n                  size=\"20\"\n                  title=\"Go to block\"\n                  class=\"invisible ml-2 cursor-pointer text-gray-300 group-hover:visible\"\n                />\n              </router-link>\n            </div>\n            <slot name=\"append-items\" />\n          </div>\n        </div>\n      </div>\n      <div\n        v-if=\"currentLog.history.length >= 25\"\n        class=\"mt-4 lg:flex lg:items-center lg:justify-between\"\n      >\n        <div class=\"mb-4 lg:mb-0\">\n          {{ t('components.pagination.text1') }}\n          <select v-model=\"pagination.perPage\" class=\"bg-input rounded-md p-1\">\n            <option\n              v-for=\"num in [25, 50, 75, 100, 150, 200]\"\n              :key=\"num\"\n              :value=\"num\"\n            >\n              {{ num }}\n            </option>\n          </select>\n          {{\n            t('components.pagination.text2', {\n              count: filteredLog.length,\n            })\n          }}\n        </div>\n        <ui-pagination\n          v-model=\"pagination.currentPage\"\n          :per-page=\"pagination.perPage\"\n          :records=\"filteredLog.length\"\n        />\n      </div>\n    </div>\n    <div\n      v-if=\"state.itemId && activeLog\"\n      class=\"dark mb-4 w-full rounded-lg bg-gray-900 text-gray-100 lg:ml-8 lg:mb-0 lg:w-4/12\"\n    >\n      <div class=\"relative p-4\">\n        <v-remixicon\n          name=\"riCloseLine\"\n          class=\"absolute top-2 right-2 cursor-pointer text-gray-500\"\n          @click=\"clearActiveItem\"\n        />\n        <table class=\"ctx-data-table w-full\">\n          <thead>\n            <tr>\n              <td class=\"w-5/12\"></td>\n              <td></td>\n            </tr>\n          </thead>\n          <tbody>\n            <tr>\n              <td class=\"text-gray-300\">Name</td>\n              <td>{{ activeLog.name }}</td>\n            </tr>\n            <tr>\n              <td class=\"text-gray-300\">Description</td>\n              <td>\n                <p class=\"line-clamp leading-tight\">\n                  {{ activeLog.description }}\n                </p>\n              </td>\n            </tr>\n            <tr>\n              <td class=\"text-gray-300\">Status</td>\n              <td class=\"capitalize\">{{ activeLog.type }}</td>\n            </tr>\n            <tr>\n              <td class=\"text-gray-300\">Timestamp/Duration</td>\n              <td>\n                <span v-if=\"activeLog.timestamp\">\n                  {{ dayjs(activeLog.timestamp).format('DD MMM, HH:mm:ss') }}\n                  /\n                </span>\n                {{ countDuration(0, activeLog.duration || 0).trim() }}\n              </td>\n            </tr>\n            <tr v-if=\"activeLog.message\">\n              <td class=\"text-gray-300\">Message</td>\n              <td>\n                <p class=\"line-clamp leading-tight\">\n                  {{ activeLog.message }}\n                </p>\n              </td>\n            </tr>\n          </tbody>\n        </table>\n      </div>\n      <div class=\"flex items-center px-4 pb-4\">\n        <p>Log data</p>\n        <div class=\"grow\" />\n        <ui-select v-model=\"state.activeTab\">\n          <option v-for=\"option in tabs\" :key=\"option.id\" :value=\"option.id\">\n            {{ option.name }}\n          </option>\n        </ui-select>\n      </div>\n      <div class=\"log-data-prev px-2 pb-4\">\n        <shared-codemirror\n          :model-value=\"logCtxData\"\n          readonly\n          hide-lang\n          lang=\"json\"\n          style=\"max-height: 460px\"\n          class=\"scroll\"\n        />\n      </div>\n    </div>\n  </div>\n</template>\n<script setup>\n/* eslint-disable no-use-before-define */\nimport dayjs from '@/lib/dayjs';\nimport { getBlocks } from '@/utils/getSharedData';\nimport { countDuration, fileSaver } from '@/utils/helper';\nimport { dataExportTypes, messageHasReferences } from '@/utils/shared';\nimport objectPath from 'object-path';\nimport Papa from 'papaparse';\nimport {\n  computed,\n  defineAsyncComponent,\n  shallowReactive,\n  shallowRef,\n} from 'vue';\nimport { useI18n } from 'vue-i18n';\n\nconst SharedCodemirror = defineAsyncComponent(() =>\n  import('@/components/newtab/shared/SharedCodemirror.vue')\n);\nconst blocks = getBlocks();\n\nconst props = defineProps({\n  currentLog: {\n    type: Object,\n    default: () => ({}),\n  },\n  ctxData: {\n    type: Object,\n    default: () => ({}),\n  },\n  parentLog: {\n    type: Object,\n    default: null,\n  },\n  isRunning: Boolean,\n});\n\nconst files = {\n  'plain-text': {\n    mime: 'text/plain',\n    ext: '.txt',\n  },\n  json: {\n    mime: 'application/json',\n    ext: '.json',\n  },\n  csv: {\n    mime: 'text/csv',\n    ext: '.csv',\n  },\n};\nconst logsType = {\n  success: {\n    color: 'text-green-400',\n    icon: 'riCheckLine',\n  },\n  stop: {\n    color: 'text-yellow-400',\n    icon: 'riStopLine',\n  },\n  stopped: {\n    color: 'text-yellow-400',\n    icon: 'riStopLine',\n  },\n  error: {\n    color: 'text-red-400',\n    icon: 'riErrorWarningLine',\n  },\n  finish: {\n    color: 'text-blue-300',\n    icon: 'riFlagLine',\n  },\n};\nconst tabs = [\n  { id: 'all', name: 'All' },\n  { id: 'referenceData.loopData', name: 'Loop data' },\n  { id: 'referenceData.variables', name: 'Variables' },\n  { id: 'referenceData.prevBlockData', name: 'Previous block data' },\n  { id: 'replacedValue', name: 'Replaced value' },\n];\n\nconst { t, te } = useI18n();\n\nconst state = shallowReactive({\n  itemId: '',\n  search: '',\n  activeTab: 'all',\n});\nconst pagination = shallowReactive({\n  perPage: 25,\n  currentPage: 1,\n});\nconst activeLog = shallowRef(null);\n\nconst translatedLog = computed(() =>\n  props.currentLog.history.map(translateLog)\n);\nconst filteredLog = computed(() => {\n  const query = state.search.toLocaleLowerCase();\n\n  return translatedLog.value.filter(\n    (log) =>\n      log.name.toLocaleLowerCase().includes(query) ||\n      log.description?.toLocaleLowerCase().includes(query)\n  );\n});\nconst history = computed(() =>\n  filteredLog.value.slice(\n    (pagination.currentPage - 1) * pagination.perPage,\n    pagination.currentPage * pagination.perPage\n  )\n);\nconst errorBlock = computed(() => {\n  if (props.currentLog.status !== 'error') return null;\n\n  let block = props.currentLog.history.at(-1);\n  if (!block || block.type !== 'error' || block.id < 25) return null;\n\n  block = translateLog(block);\n\n  let { name } = block;\n  if (block.description) name += ` (${block.description})`;\n\n  return {\n    name,\n    id: block.id,\n    message: block.message,\n    messageId: block.messageId,\n  };\n});\nconst logCtxData = computed(() => {\n  let logData = props.ctxData;\n  if (logData.ctxData) logData = logData.ctxData;\n\n  if (!state.itemId || !logData[state.itemId]) return '';\n\n  const data = logData[state.itemId];\n  /* eslint-disable-next-line */\n  if (data?.referenceData) getDataSnapshot(data.referenceData);\n  const itemLogData =\n    state.activeTab === 'all' ? data : objectPath.get(data, state.activeTab);\n\n  return JSON.stringify(itemLogData, null, 2);\n});\n\nfunction getDataSnapshot(refData) {\n  if (!props.ctxData?.dataSnapshot) return;\n\n  const data = props.ctxData.dataSnapshot;\n  const getData = (key) => {\n    const currentData = refData[key];\n    if (typeof currentData !== 'string') return currentData;\n\n    return data[currentData] ?? {};\n  };\n\n  refData.loopData = getData('loopData');\n  refData.variables = getData('variables');\n}\nfunction exportLogs(type) {\n  let data = type === 'plain-text' ? '' : [];\n  const getItemData = {\n    'plain-text': ([\n      timestamp,\n      status,\n      name,\n      description,\n      message,\n      ctxData,\n    ]) => {\n      data += `${timestamp} - ${status} - ${name} - ${description} - ${message} - ${JSON.stringify(\n        ctxData\n      )} \\n`;\n    },\n    json: ([timestamp, status, name, description, message, ctxData]) => {\n      data.push({\n        timestamp,\n        status,\n        name,\n        description,\n        message,\n        data: ctxData,\n      });\n    },\n    csv: (item, index) => {\n      if (index === 0) {\n        data.unshift([\n          'timestamp',\n          'status',\n          'name',\n          'description',\n          'message',\n          'data',\n        ]);\n      }\n\n      item[item.length - 1] = JSON.stringify(item[item.length - 1]);\n\n      data.push(item);\n    },\n  };\n  translatedLog.value.forEach((item, index) => {\n    let logData = props.ctxData;\n    if (logData.ctxData) logData = logData.ctxData;\n\n    const itemData = logData[item.id] || null;\n    if (itemData) getDataSnapshot(itemData.referenceData);\n\n    getItemData[type](\n      [\n        dayjs(item.timestamp || Date.now()).format('DD-MM-YYYY, hh:mm:ss'),\n        item.type.toUpperCase(),\n        item.name,\n        item.description || 'NULL',\n        item.message || 'NULL',\n        itemData,\n      ],\n      index\n    );\n  });\n\n  switch (type) {\n    case 'plain-text':\n      data = [data];\n      break;\n    case 'csv':\n      data = [Papa.unparse(data)];\n      data.unshift(new Uint8Array([0xef, 0xbb, 0xbf]));\n      break;\n    case 'json':\n      data = [JSON.stringify(data, null, 2)];\n      break;\n    default:\n  }\n\n  const { mime, ext } = files[type];\n  const blobUrl = URL.createObjectURL(new Blob(data, { type: mime }));\n  const filename = `[${dayjs().format('DD-MM-YYYY, HH:mm:ss')}] ${\n    props.currentLog.name\n  } - logs`;\n\n  fileSaver(`${filename}${ext}`, blobUrl);\n\n  URL.revokeObjectURL(blobUrl);\n}\nfunction clearActiveItem() {\n  state.itemId = '';\n  activeLog.value = null;\n}\nfunction translateLog(log) {\n  const copyLog = { ...log };\n  const getTranslatation = (path, def) => {\n    const params = typeof path === 'string' ? { path } : path;\n\n    return te(params.path) ? t(params.path, params.params) : def;\n  };\n\n  if (['finish', 'stop'].includes(log.type)) {\n    copyLog.name = t(`log.types.${log.type}`);\n  } else {\n    copyLog.name = getTranslatation(\n      `workflow.blocks.${log.name}.name`,\n      blocks[log.name].name\n    );\n  }\n\n  if (copyLog.message && messageHasReferences.includes(copyLog.message)) {\n    copyLog.messageId = `${copyLog.message}`;\n  }\n\n  copyLog.message = getTranslatation(\n    { path: `log.messages.${log.message}`, params: log },\n    log.message\n  );\n\n  return copyLog;\n}\nfunction setActiveLog(item) {\n  state.itemId = item.id;\n  activeLog.value = item;\n}\nfunction getBlockPath(blockId) {\n  const { workflowId, teamId } = props.currentLog;\n  let path = `/workflows/${workflowId}`;\n\n  if (workflowId.startsWith('team') && teamId) {\n    path = `/teams/${teamId}/workflows/${workflowId}`;\n  }\n\n  return `${path}?blockId=${blockId}`;\n}\nfunction jumpToError() {\n  pagination.currentPage = Math.ceil(errorBlock.value.id / pagination.perPage);\n\n  const element = document.querySelector('#log-history');\n  if (!element) return;\n\n  element.scrollTo(0, element.scrollHeight);\n  document.documentElement.scrollTo(0, document.documentElement.scrollHeight);\n}\n</script>\n<style>\n.ctx-data-table {\n  thead td {\n    padding: 0;\n  }\n  td {\n    @apply p-1;\n  }\n  tr {\n    vertical-align: baseline;\n  }\n}\n.log-data-prev .cm-editor {\n  background-color: transparent;\n  .cm-activeLine.cm-line {\n    background-color: rgb(255 255 255 / 0.05) !important;\n  }\n  .cm-gutters,\n  .cm-activeLineGutter,\n  .cm-gutterElement {\n    background-color: transparent !important;\n  }\n}\n</style>\n"
  },
  {
    "path": "src/components/newtab/logs/LogsTable.vue",
    "content": "<template>\n  <div v-if=\"tableData.body.length === 0\" class=\"text-center\">\n    <img src=\"@/assets/svg/files-and-folder.svg\" class=\"mx-auto max-w-sm\" />\n    <p class=\"text-xl font-semibold\">{{ t('message.noData') }}</p>\n  </div>\n  <template v-else>\n    <div class=\"flex items-center\">\n      <ui-tabs\n        v-model=\"state.activeTab\"\n        type=\"fill\"\n        class=\"mb-4\"\n        color=\"\"\n        style=\"padding: 0\"\n      >\n        <ui-tab value=\"table\"> Table </ui-tab>\n        <ui-tab value=\"raw\"> Raw </ui-tab>\n      </ui-tabs>\n      <div class=\"grow\"></div>\n      <ui-input\n        v-if=\"state.activeTab === 'table'\"\n        v-model=\"state.query\"\n        :placeholder=\"t('common.search')\"\n        class=\"mr-4\"\n        prepend-icon=\"riSearch2Line\"\n        type=\"search\"\n      />\n      <ui-popover trigger-width>\n        <template #trigger>\n          <ui-button variant=\"accent\">\n            <span>{{ t('log.exportData.title') }}</span>\n            <v-remixicon name=\"riArrowDropDownLine\" class=\"ml-2 -mr-1\" />\n          </ui-button>\n        </template>\n        <ui-list class=\"space-y-1\">\n          <ui-list-item\n            v-for=\"type in dataExportTypes\"\n            :key=\"type.id\"\n            v-close-popover\n            class=\"cursor-pointer\"\n            @click=\"exportData(type.id)\"\n          >\n            {{ t(`log.exportData.types.${type.id}`) }}\n          </ui-list-item>\n        </ui-list>\n      </ui-popover>\n    </div>\n    <shared-codemirror\n      v-show=\"state.activeTab === 'raw'\"\n      :model-value=\"JSON.stringify(currentLog.data.table, null, 2)\"\n      readonly\n      lang=\"json\"\n      style=\"max-height: 600px\"\n    />\n    <ui-table\n      v-show=\"state.activeTab === 'table'\"\n      with-pagination\n      :headers=\"tableData.header\"\n      :items=\"tableData.body\"\n      :search=\"state.query\"\n      item-key=\"id\"\n      class=\"w-full\"\n    />\n  </template>\n</template>\n<script setup>\nimport { shallowReactive, defineAsyncComponent } from 'vue';\nimport { useI18n } from 'vue-i18n';\nimport { dataExportTypes } from '@/utils/shared';\nimport dataExporter from '@/utils/dataExporter';\n\nconst SharedCodemirror = defineAsyncComponent(() =>\n  import('@/components/newtab/shared/SharedCodemirror.vue')\n);\n\nconst props = defineProps({\n  tableData: {\n    type: Object,\n    default: () => ({}),\n  },\n  currentLog: {\n    type: Object,\n    default: () => ({}),\n  },\n});\n\nconst { t } = useI18n();\n\nconst state = shallowReactive({\n  query: '',\n  activeTab: 'table',\n});\n\nfunction exportData(type) {\n  dataExporter(\n    props.currentLog.data.table,\n    { name: props.currentLog.name, type },\n    true\n  );\n}\n</script>\n"
  },
  {
    "path": "src/components/newtab/logs/LogsVariables.vue",
    "content": "<template>\n  <div v-if=\"Object.keys(variables).length === 0\" class=\"text-center\">\n    <img src=\"@/assets/svg/files-and-folder.svg\" class=\"mx-auto max-w-sm\" />\n    <p class=\"text-xl font-semibold\">{{ t('message.noData') }}</p>\n  </div>\n  <template v-else>\n    <ui-tabs\n      v-model=\"state.activeTab\"\n      type=\"fill\"\n      class=\"mb-4\"\n      color=\"\"\n      style=\"padding: 0\"\n    >\n      <ui-tab value=\"gui\"> GUI </ui-tab>\n      <ui-tab value=\"raw\"> Raw </ui-tab>\n    </ui-tabs>\n    <div v-if=\"state.activeTab === 'gui'\" class=\"mt-4\">\n      <ul class=\"grid grid-cols-1 gap-4 md:grid-cols-2\">\n        <li\n          v-for=\"(varValue, varName) in variables\"\n          :key=\"varName\"\n          class=\"flex items-center space-x-2 rounded-lg border-2 px-2 pb-2 pt-1\"\n        >\n          <ui-input\n            :model-value=\"varName\"\n            :label=\"t('common.name')\"\n            class=\"w-full\"\n            placeholder=\"EMPTY\"\n            readonly\n          />\n          <ui-input\n            :model-value=\"\n              typeof varValue === 'string' ? varValue : JSON.stringify(varValue)\n            \"\n            label=\"Value\"\n            class=\"w-full\"\n            placeholder=\"EMPTY\"\n            readonly\n          />\n        </li>\n      </ul>\n    </div>\n    <shared-codemirror\n      v-else\n      :model-value=\"JSON.stringify(variables, null, 2)\"\n      class=\"mt-4\"\n      lang=\"json\"\n      readonly\n    />\n  </template>\n</template>\n<script setup>\nimport { computed, defineAsyncComponent, shallowReactive } from 'vue';\nimport { useI18n } from 'vue-i18n';\n\nconst SharedCodemirror = defineAsyncComponent(() =>\n  import('@/components/newtab/shared/SharedCodemirror.vue')\n);\n\nconst props = defineProps({\n  currentLog: {\n    type: Object,\n    default: () => ({}),\n  },\n});\n\nconst { t } = useI18n();\nconst state = shallowReactive({\n  activeTab: 'gui',\n});\n\nconst variables = computed(() => props.currentLog.data?.variables || {});\n</script>\n"
  },
  {
    "path": "src/components/newtab/package/PackageDetails.vue",
    "content": "<template>\n  <div class=\"w-full max-w-2xl pb-8\">\n    <ui-input\n      :model-value=\"data.name\"\n      label=\"Package name\"\n      class=\"w-full\"\n      placeholder=\"My package\"\n      @change=\"updatePackage({ name: $event })\"\n    />\n    <label class=\"mt-4 block w-full\">\n      <span class=\"ml-1 text-sm text-gray-600 dark:text-gray-200\">\n        Short description\n      </span>\n      <ui-textarea\n        :model-value=\"data.description\"\n        placeholder=\"Short description\"\n        @change=\"updatePackage({ description: $event })\"\n      />\n    </label>\n    <shared-wysiwyg\n      :model-value=\"data.content\"\n      :placeholder=\"$t('common.description')\"\n      :limit=\"5000\"\n      class=\"content-editor bg-box-transparent prose prose-zinc relative mt-4 max-w-none rounded-lg p-4 dark:prose-invert\"\n      @change=\"updatePackage({ content: $event })\"\n      @count=\"state.contentLength = $event\"\n    >\n      <template #append>\n        <p\n          class=\"absolute bottom-2 right-2 text-sm text-gray-600 dark:text-gray-200\"\n        >\n          {{ state.contentLength }}/5000\n        </p>\n      </template>\n    </shared-wysiwyg>\n  </div>\n</template>\n<script setup>\nimport { reactive } from 'vue';\nimport { debounce } from '@/utils/helper';\nimport SharedWysiwyg from '@/components/newtab/shared/SharedWysiwyg.vue';\n\ndefineProps({\n  data: {\n    type: Object,\n    default: () => ({}),\n  },\n});\nconst emit = defineEmits(['update']);\n\nconst state = reactive({\n  contentLength: 0,\n});\n\nconst updatePackage = debounce((data) => {\n  emit('update', data);\n}, 400);\n</script>\n"
  },
  {
    "path": "src/components/newtab/package/PackageSettingIOSelect.vue",
    "content": "<template>\n  <ui-popover>\n    <template #trigger>\n      <ui-button class=\"w-full\">\n        Select block {{ props.data.blockId }}\n      </ui-button>\n    </template>\n    <div class=\"w-64\">\n      <ui-input\n        v-if=\"state.selectType === 'nodes'\"\n        v-model=\"state.query\"\n        placeholder=\"Search...\"\n        class=\"mb-4 w-full\"\n      />\n      <template v-else>\n        <div\n          class=\"flex cursor-pointer items-center\"\n          @click=\"state.selectType = 'nodes'\"\n        >\n          <v-remixicon\n            name=\"riArrowLeftSLine\"\n            title=\"Go back\"\n            class=\"mr-1 -ml-1\"\n          />\n          <span class=\"text-overflow flex-1\">\n            {{ getBlockName(selectedNode) }}\n          </span>\n        </div>\n        <p class=\"mt-2 mb-4\">Select {{ type }}</p>\n      </template>\n      <ui-list class=\"space-y-1\">\n        <ui-list-item\n          v-for=\"(item, index) in items\"\n          :key=\"item.id\"\n          :active=\"isActive(item)\"\n          class=\"cursor-pointer\"\n          @click=\"selectItem(item)\"\n        >\n          <p class=\"text-overflow\">\n            {{\n              state.selectType === 'nodes'\n                ? getBlockName(item, state.selectType)\n                : `${type} ${index + 1}`\n            }}\n          </p>\n        </ui-list-item>\n      </ui-list>\n      {{ data }}\n    </div>\n  </ui-popover>\n</template>\n<script setup>\n/* eslint-disable */\nimport { reactive, computed, onMounted } from 'vue';\nimport { getBlocks } from '@/utils/getSharedData';\n\nconst props = defineProps({\n  nodes: {\n    type: Array,\n    default: () => [],\n  },\n  data: {\n    type: Object,\n    default: () => ({}),\n  },\n  type: {\n    type: String,\n    default: 'inputs',\n  },\n});\nconst emit = defineEmits(['update']);\n\nconst blocks = getBlocks();\nconst handleType = props.type === 'inputs' ? 'target' : 'source';\n\nconst state = reactive({\n  query: '',\n  selectType: 'nodes',\n});\n\nconst includeQuery = (str) =>\n  str.toLocaleLowerCase().includes(state.query.toLocaleLowerCase());\n\nconst selectedNode = computed(() =>\n  props.nodes.find((node) => node.id === props.data.blockId)\n);\nconst items = computed(() => {\n  const query = state.query.toLocaleLowerCase();\n\n  if (state.selectType === 'nodes') {\n    return props.nodes.filter(({ data, label }) => {\n      let additionalKey = false;\n\n      if (data.name) additionalKey = includeQuery(data.name);\n      else if (data.description) additionalKey = includeQuery(data.description);\n\n      return includeQuery(blocks[label]?.name || '') || additionalKey;\n    });\n  }\n\n  return selectedNode.value.handleBounds[handleType];\n});\n\nfunction updateData(data) {\n  emit('update', data);\n}\nfunction selectItem(item) {\n  if (state.selectType === 'nodes') {\n    const payload = { blockId: item.id };\n\n    if (props.data.blockId && props.data.blockId !== item.id) {\n      payload.handleId = '';\n    }\n\n    updateData(payload);\n    state.selectType = 'handle';\n  } else {\n    updateData({ handleId: item.id });\n  }\n}\nfunction getBlockName(item, type) {\n  const { label, data } = item;\n  let name = blocks[label]?.name || '';\n\n  if (data.name) name += ` (${data.name})`;\n  else if (data.description) name += ` (${data.description})`;\n\n  return name;\n}\nfunction isActive(item) {\n  if (state.selectType === 'nodes') {\n    return item.id === props.data.blockId;\n  }\n\n  return item.id === props.data.handleId;\n}\n\nonMounted(() => {\n  if (props.data.blockId) {\n    const blockExists = props.nodes.some(\n      (node) => node.id === props.data.blockId\n    );\n    if (blockExists) {\n      state.selectType = 'handle';\n    } else {\n      emit('update', { blockId: '', handleId: '' });\n    }\n  }\n});\n</script>\n"
  },
  {
    "path": "src/components/newtab/package/PackageSettings.vue",
    "content": "<template>\n  <label class=\"inline-flex items-center\">\n    <ui-switch v-model=\"packageState.settings.asBlock\" />\n    <span class=\"ml-4\">\n      {{ $t('packages.settings.asBlock') }}\n    </span>\n  </label>\n  <div v-if=\"packageState.settings.asBlock\" class=\"mt-6 flex space-x-6 pb-8\">\n    <div class=\"flex-1\">\n      <p class=\"font-semibold\">Block inputs</p>\n      <div class=\"mt-4\">\n        <div\n          v-if=\"packageState.inputs.length > 0\"\n          class=\"grid grid-cols-12 gap-x-4\"\n        >\n          <div class=\"col-span-5 pl-1 text-sm\">Input name</div>\n          <div class=\"col-span-6 pl-1 text-sm\">Block</div>\n        </div>\n        <draggable\n          v-model=\"packageState.inputs\"\n          group=\"inputs\"\n          handle=\".handle\"\n          item-key=\"id\"\n        >\n          <template #item=\"{ element, index }\">\n            <div\n              class=\"group relative mb-2 grid grid-cols-12 items-center gap-x-4\"\n            >\n              <span\n                class=\"handle invisible absolute left-0 -ml-6 cursor-move group-hover:visible\"\n              >\n                <v-remixicon name=\"mdiDrag\" />\n              </span>\n              <ui-input\n                v-model=\"element.name\"\n                class=\"col-span-5\"\n                :placeholder=\"`Input ${index + 1}`\"\n              />\n              <div class=\"col-span-6 flex items-center\">\n                <ui-button\n                  v-tooltip=\"'Go to block'\"\n                  class=\"mr-2\"\n                  icon\n                  @click=\"$emit('goBlock', element.blockId)\"\n                >\n                  <v-remixicon name=\"riFocus3Line\" />\n                </ui-button>\n                <p\n                  :title=\"getBlockIOName('inputs', element)\"\n                  class=\"text-overflow flex-1\"\n                >\n                  {{ getBlockIOName('inputs', element) }}\n                </p>\n              </div>\n              <div class=\"col-span-1 text-right\">\n                <v-remixicon\n                  name=\"riDeleteBin7Line\"\n                  class=\"inline-block cursor-pointer text-gray-600 dark:text-gray-200\"\n                  @click=\"deleteBlockIo('inputs', index)\"\n                />\n              </div>\n            </div>\n          </template>\n        </draggable>\n      </div>\n    </div>\n    <hr class=\"border-r\" />\n    <div class=\"flex-1\">\n      <p class=\"font-semibold\">Block outputs</p>\n      <div class=\"mt-4\">\n        <div\n          v-if=\"packageState.outputs.length > 0\"\n          class=\"grid grid-cols-12 gap-x-4\"\n        >\n          <div class=\"col-span-5 pl-1 text-sm\">Output name</div>\n          <div class=\"col-span-6 pl-1 text-sm\">Block</div>\n        </div>\n        <draggable\n          v-model=\"packageState.outputs\"\n          group=\"outputs\"\n          handle=\".handle\"\n          item-key=\"id\"\n        >\n          <template #item=\"{ element, index }\">\n            <div\n              class=\"group relative mb-2 grid grid-cols-12 items-center gap-x-4\"\n            >\n              <span\n                class=\"handle invisible absolute left-0 -ml-6 cursor-move group-hover:visible\"\n              >\n                <v-remixicon name=\"mdiDrag\" />\n              </span>\n              <ui-input\n                v-model=\"element.name\"\n                class=\"col-span-5\"\n                :placeholder=\"`Output ${index + 1}`\"\n              />\n              <div class=\"col-span-6 flex items-center\">\n                <ui-button\n                  v-tooltip=\"'Go to block'\"\n                  class=\"mr-2\"\n                  icon\n                  @click=\"$emit('goBlock', element.blockId)\"\n                >\n                  <v-remixicon name=\"riFocus3Line\" />\n                </ui-button>\n                <p\n                  :title=\"getBlockIOName('outputs', element)\"\n                  class=\"text-overflow flex-1\"\n                >\n                  {{ getBlockIOName('outputs', element) }}\n                </p>\n              </div>\n              <div class=\"col-span-1 text-right\">\n                <v-remixicon\n                  name=\"riDeleteBin7Line\"\n                  class=\"inline-block cursor-pointer text-gray-600 dark:text-gray-200\"\n                  @click=\"deleteBlockIo('outputs', index)\"\n                />\n              </div>\n            </div>\n          </template>\n        </draggable>\n      </div>\n    </div>\n  </div>\n</template>\n<script setup>\nimport { reactive, watch, onMounted } from 'vue';\nimport cloneDeep from 'lodash.clonedeep';\nimport Draggable from 'vuedraggable';\nimport { getBlocks } from '@/utils/getSharedData';\nimport { debounce } from '@/utils/helper';\n\nconst props = defineProps({\n  data: {\n    type: Object,\n    default: () => ({}),\n  },\n  editor: {\n    type: Object,\n    default: () => ({}),\n  },\n});\nconst emit = defineEmits(['update', 'goBlock']);\n\nconst blocks = getBlocks();\n\nconst state = reactive({\n  retrieved: false,\n});\nconst packageState = reactive({\n  inputs: [],\n  outputs: [],\n  settings: { asBlock: false },\n});\n\nfunction deleteBlockIo(type, index) {\n  packageState[type].splice(index, 1);\n}\n\nconst cacheIOName = new Map();\n\nfunction getNodeName({ label, data }) {\n  let name = blocks[label]?.name || '';\n\n  if (data.name) name += ` (${data.name})`;\n  else if (data.description) name += ` (${data.description})`;\n\n  return name;\n}\nfunction getBlockIOName(type, data) {\n  if (!props.editor) return '';\n\n  const cacheId = `${data.blockId}-${data.handleId}`;\n  if (cacheIOName.has(cacheId)) return cacheIOName.get(cacheId);\n\n  let name = '';\n\n  const node = props.editor.getNode.value(data.blockId);\n  if (!node) {\n    name = 'Block not found';\n  } else {\n    const nodeName = getNodeName(node);\n    const handleType = type === 'outputs' ? 'source' : 'target';\n    const index = node.handleBounds[handleType].findIndex(\n      (item) => item.id === data.handleId\n    );\n    const handleName =\n      index === -1 ? 'Not found' : `${type.slice(0, -1)} ${index + 1}`;\n\n    name = `${nodeName} > ${handleName}`;\n  }\n\n  cacheIOName.set(cacheId, name);\n\n  return name;\n}\n\nwatch(\n  packageState,\n  debounce(() => {\n    if (state.retrieved) {\n      emit('update', packageState);\n    }\n  }, 500),\n  { deep: true }\n);\n\nonMounted(() => {\n  Object.assign(\n    packageState,\n    cloneDeep({\n      inputs: props.data.inputs,\n      outputs: props.data.outputs,\n      settings: props.data.settings || {},\n    })\n  );\n\n  setTimeout(() => {\n    state.retrieved = true;\n  }, 1000);\n});\n</script>\n"
  },
  {
    "path": "src/components/newtab/settings/SettingsBackupItems.vue",
    "content": "<template>\n  <div class=\"scroll content w-full overflow-auto\">\n    <div v-if=\"!query && workflows.length === 0\" class=\"text-center\">\n      <img src=\"@/assets/svg/files-and-folder.svg\" class=\"mx-auto max-w-sm\" />\n      <p class=\"text-xl font-semibold\">{{ t('message.noData') }}</p>\n    </div>\n    <ui-list class=\"space-y-1\">\n      <ui-list-item\n        v-for=\"workflow in workflows\"\n        :key=\"workflow.id\"\n        :class=\"{ 'bg-box-transparent': isActive(workflow.id) }\"\n        class=\"group overflow-hidden\"\n      >\n        <ui-checkbox\n          v-if=\"!isLocal || !workflow.isInCloud\"\n          :disabled=\"exceedLimit && !isActive(workflow.id)\"\n          :model-value=\"isActive(workflow.id)\"\n          class=\"mr-4\"\n          @change=\"toggleDeleteWorkflow($event, workflow.id)\"\n        />\n        <div v-else class=\"mr-4 h-5 w-5\" />\n        <ui-img\n          v-if=\"workflow.icon?.startsWith('http')\"\n          :src=\"workflow.icon\"\n          style=\"height: 24px; width: 24px\"\n          alt=\"Can not display\"\n        />\n        <v-remixicon v-else :name=\"workflow.icon\" />\n        <div class=\"ml-2 flex-1 overflow-hidden\">\n          <p class=\"text-overflow flex-1\">{{ workflow.name }}</p>\n          <p\n            class=\"text-overflow text-sm leading-tight text-gray-600 dark:text-gray-200\"\n          >\n            {{ workflow.description }}\n          </p>\n        </div>\n        <slot :workflow=\"workflow\" />\n      </ui-list-item>\n    </ui-list>\n  </div>\n  <div class=\"flex items-center\">\n    <ui-checkbox\n      :model-value=\"exceedLimit\"\n      :indeterminate=\"modelValue.length > 0 && modelValue.length < limit\"\n      class=\"mt-2 ml-4\"\n      @change=\"$emit('select', $event)\"\n    >\n      {{\n        t(\n          `settings.backupWorkflows.cloud.${\n            modelValue.length > 0 && modelValue.length >= limit\n              ? 'deselectAll'\n              : 'selectAll'\n          }`\n        )\n      }}\n    </ui-checkbox>\n    <div class=\"grow\"></div>\n    <span> {{ modelValue.length }}/{{ limit }} </span>\n  </div>\n</template>\n<script setup>\nimport { computed } from 'vue';\nimport { useI18n } from 'vue-i18n';\n\nconst props = defineProps({\n  workflows: {\n    type: Array,\n    default: () => [],\n  },\n  modelValue: {\n    type: Array,\n    default: () => [],\n  },\n  limit: {\n    type: Number,\n    default: Infinity,\n  },\n  query: {\n    type: String,\n    default: '',\n  },\n  isLocal: Boolean,\n});\nconst emit = defineEmits(['update:modelValue', 'select']);\n\nconst { t } = useI18n();\n\nconst exceedLimit = computed(() => props.modelValue.length >= props.limit);\n\nfunction toggleDeleteWorkflow(selected, workflowId) {\n  const workflows = [...props.modelValue];\n\n  if (selected) {\n    workflows.push(workflowId);\n  } else {\n    const index = workflows.indexOf(workflowId);\n\n    if (index !== -1) workflows.splice(index, 1);\n  }\n\n  emit('update:modelValue', workflows);\n}\nfunction isActive(workflowId) {\n  return props.modelValue.includes(workflowId);\n}\n</script>\n"
  },
  {
    "path": "src/components/newtab/settings/SettingsCloudBackup.vue",
    "content": "<template>\n  <div class=\"cloud-backup mt-4 flex items-start\">\n    <div class=\"w-56\">\n      <ui-input\n        v-model=\"state.query\"\n        :placeholder=\"t('common.search')\"\n        autocomplete=\"off\"\n        prepend-icon=\"riSearch2Line\"\n      />\n      <ui-list class=\"mt-4\">\n        <p class=\"mb-1 text-sm text-gray-600 dark:text-gray-200\">\n          {{ t('settings.backupWorkflows.cloud.location') }}\n        </p>\n        <ui-list-item\n          v-for=\"location in ['local', 'cloud']\"\n          :key=\"location\"\n          :active=\"location === state.activeTab\"\n          :disabled=\"backupState.uploading || backupState.deleting\"\n          color=\"bg-box-transparent\"\n          class=\"mb-1 cursor-pointer\"\n          @click=\"state.activeTab = location\"\n        >\n          {{ t(`settings.backupWorkflows.cloud.buttons.${location}`) }}\n          <span\n            v-if=\"location === 'cloud'\"\n            class=\"ml-2 rounded-full bg-accent text-center text-sm text-gray-100 dark:text-black\"\n            style=\"height: 29px; width: 29px; line-height: 29px\"\n          >\n            {{ state.cloudWorkflows.length }}\n          </span>\n        </ui-list-item>\n      </ui-list>\n      <ui-button\n        v-if=\"state.selectedWorkflows.length > 0 && state.activeTab === 'local'\"\n        :loading=\"backupState.uploading\"\n        variant=\"accent\"\n        class=\"mt-4 w-8/12\"\n        @click=\"backupWorkflowsToCloud()\"\n      >\n        {{ t('settings.backupWorkflows.backup.button') }}\n        ({{ state.selectedWorkflows.length }})\n      </ui-button>\n      <ui-button\n        v-if=\"state.deleteIds.length > 0 && state.activeTab === 'cloud'\"\n        :loading=\"backupState.deleting\"\n        variant=\"danger\"\n        class=\"mt-4\"\n        @click=\"deleteBackup()\"\n      >\n        {{ t('settings.backupWorkflows.cloud.delete') }}\n        ({{ state.deleteIds.length }})\n      </ui-button>\n    </div>\n    <div v-if=\"!state.backupRetrieved\" class=\"content block flex-1 text-center\">\n      <ui-spinner color=\"text-accent\" />\n    </div>\n    <div v-else class=\"ml-4 flex-1 overflow-hidden\">\n      <template v-if=\"state.activeTab === 'cloud'\">\n        <settings-backup-items\n          v-slot=\"{ workflow }\"\n          v-model=\"state.deleteIds\"\n          :workflows=\"backupWorkflows\"\n          :limit=\"state.cloudWorkflows.length\"\n          :query=\"state.query\"\n          @select=\"selectAllCloud\"\n        >\n          <p\n            :title=\"`Last updated: ${formatDate(\n              workflow,\n              'DD MMMM YYYY, hh:mm A'\n            )}`\"\n            class=\"ml-4 mr-8 w-3/12\"\n          >\n            {{ formatDate(workflow, 'DD MMM YYYY') }}\n          </p>\n          <ui-spinner\n            v-if=\"backupState.workflowId === workflow.id\"\n            color=\"text-accent\"\n            class=\"ml-4\"\n          />\n          <div v-else class=\"invisible ml-4 group-hover:visible\">\n            <button\n              v-if=\"workflow.hasLocalCopy\"\n              title=\"Sync cloud backup to local\"\n              @click=\"syncCloudToLocal(workflow)\"\n            >\n              <v-remixicon name=\"riRefreshLine\" />\n            </button>\n            <button\n              v-else\n              title=\"Add to local\"\n              @click=\"syncCloudToLocal(workflow)\"\n            >\n              <v-remixicon name=\"riDownloadCloud2Line\" />\n            </button>\n            <button\n              v-if=\"!backupState.deleting\"\n              :aria-label=\"t('settings.backupWorkflows.cloud.delete')\"\n              class=\"ml-4\"\n              title=\"Delete backup\"\n              @click=\"deleteBackup(workflow.id)\"\n            >\n              <v-remixicon name=\"riDeleteBin7Line\" />\n            </button>\n          </div>\n        </settings-backup-items>\n      </template>\n      <template v-else>\n        <p class=\"mb-2\">\n          {{ t('settings.backupWorkflows.cloud.selectText') }}\n        </p>\n        <settings-backup-items\n          v-slot=\"{ workflow }\"\n          v-model=\"state.selectedWorkflows\"\n          :workflows=\"workflows\"\n          :limit=\"workflowLimit\"\n          :query=\"state.query\"\n          :is-local=\"true\"\n          @select=\"selectAllLocal\"\n        >\n          <ui-spinner\n            v-if=\"backupState.workflowId === workflow.id\"\n            color=\"text-accent\"\n            class=\"ml-4\"\n          />\n          <template v-else>\n            <button\n              v-if=\"workflow.isInCloud\"\n              @click=\"updateCloudBackup(workflow)\"\n            >\n              <v-remixicon\n                name=\"riRefreshLine\"\n                title=\"Sync local workflow to cloud backup\"\n              />\n            </button>\n            <button\n              v-else-if=\"\n                !backupState.uploading &&\n                state.selectedWorkflows.length <= workflowLimit\n              \"\n              class=\"invisible ml-4 group-hover:visible\"\n              title=\"Backup workflow\"\n              @click=\"backupWorkflowsToCloud(workflow.id)\"\n            >\n              <v-remixicon name=\"riUploadCloud2Line\" />\n            </button>\n          </template>\n        </settings-backup-items>\n      </template>\n    </div>\n  </div>\n</template>\n<script setup>\nimport { computed, reactive, onMounted } from 'vue';\nimport { useI18n } from 'vue-i18n';\nimport { useToast } from 'vue-toastification';\nimport browser from 'webextension-polyfill';\nimport { fetchApi, cacheApi } from '@/utils/api';\nimport { convertWorkflow } from '@/utils/workflowData';\nimport { parseJSON } from '@/utils/helper';\nimport { useUserStore } from '@/stores/user';\nimport { useWorkflowStore } from '@/stores/workflow';\nimport dayjs from '@/lib/dayjs';\nimport SettingsBackupItems from './SettingsBackupItems.vue';\n\ndefineEmits(['close']);\n\nconst { t } = useI18n();\nconst toast = useToast();\nconst userStore = useUserStore();\nconst workflowStore = useWorkflowStore();\n\nconst state = reactive({\n  query: '',\n  deleteIds: [],\n  activeTab: 'local',\n  cloudWorkflows: [],\n  selectedWorkflows: [],\n  backupRetrieved: false,\n});\nconst backupState = reactive({\n  workflowId: '',\n  deleting: false,\n  uploading: false,\n});\n\nconst localWorkflows = computed(() =>\n  workflowStore.getWorkflows.map((workflow) => {\n    const isInCloud = state.cloudWorkflows.some(\n      (item) => item.id === workflow.id\n    );\n    workflow.isInCloud = isInCloud;\n\n    return workflow;\n  })\n);\nconst workflows = computed(() =>\n  localWorkflows.value\n    .filter(({ name }) => {\n      return name.toLocaleLowerCase().includes(state.query.toLowerCase());\n    })\n    .sort((a, b) => a.createdAt - b.createdAt)\n);\nconst backupWorkflows = computed(() =>\n  state.cloudWorkflows.filter(({ name }) =>\n    name.toLocaleLowerCase().includes(state.query.toLowerCase())\n  )\n);\nconst workflowLimit = computed(() => {\n  const maxWorkflow = userStore.user?.limit?.backupWorkflow ?? 15;\n\n  return maxWorkflow - state.cloudWorkflows.length;\n});\n\nfunction formatDate(workflow, format) {\n  return dayjs(workflow.updatedAt || Date.now()).format(format);\n}\nasync function syncCloudToLocal(workflow) {\n  try {\n    backupState.uploading = true;\n    backupState.workflowId = workflow.id;\n\n    const response = await fetchApi(`/me/workflows/${workflow.id}`, {\n      auth: true,\n    });\n    const data = await response.json();\n    if (!response.ok) throw new Error(data.message);\n\n    await workflowStore.insertOrUpdate([data]);\n\n    const index = state.cloudWorkflows.findIndex(\n      (item) => item.id === workflow.id\n    );\n    if (index !== -1) {\n      state.cloudWorkflows[index].hasLocalCopy = true;\n    }\n  } catch (error) {\n    console.error(error);\n    toast.error('Something went wrong');\n  } finally {\n    backupState.workflowId = '';\n    backupState.loading = false;\n  }\n}\nfunction selectAllCloud(value) {\n  if (value) {\n    state.deleteIds = state.cloudWorkflows.map(({ id }) => id);\n  } else {\n    state.deleteIds = [];\n  }\n}\nfunction selectAllLocal() {\n  let limit = state.selectedWorkflows.length;\n\n  if (limit >= workflowLimit.value) {\n    state.selectedWorkflows = [];\n    return;\n  }\n\n  workflows.value.forEach(({ id, isInCloud }) => {\n    if (\n      limit >= workflowLimit.value ||\n      isInCloud ||\n      state.selectedWorkflows.includes(id)\n    )\n      return;\n\n    state.selectedWorkflows.push(id);\n\n    limit += 1;\n  });\n}\nasync function deleteBackup(workflowId) {\n  try {\n    backupState.deleting = true;\n\n    if (workflowId) backupState.workflowId = workflowId;\n\n    const ids = workflowId ? [workflowId] : state.deleteIds;\n    const response = await fetchApi(\n      `/me/workflows?id=${ids.join(',')}&type=backup`,\n      {\n        auth: true,\n        method: 'DELETE',\n      }\n    );\n\n    if (!response.ok) throw new Error(response.statusText);\n\n    ids.forEach((id) => {\n      const index = state.cloudWorkflows.findIndex((item) => item.id === id);\n\n      if (index !== -1) state.cloudWorkflows.splice(index, 1);\n    });\n\n    await browser.storage.local.set({\n      backupIds: state.cloudWorkflows.map(({ id }) => id),\n    });\n\n    state.deleteIds = [];\n    backupState.workflowId = '';\n    backupState.deleting = false;\n    sessionStorage.removeItem('backup-workflows');\n  } catch (error) {\n    console.error(error);\n    backupState.workflowId = '';\n    backupState.deleting = false;\n    toast.error(t('message.somethingWrong'));\n    backupState.uploading = false;\n  }\n}\nasync function fetchCloudWorkflows() {\n  if (state.backupRetrieved) return;\n\n  state.deleteIds = [];\n\n  try {\n    const data = await cacheApi('backup-workflows', async () => {\n      const response = await fetchApi('/me/workflows?type=backup', {\n        auth: true,\n      });\n\n      if (!response.ok) throw new Error(response.statusText);\n\n      const result = await response.json();\n\n      return result;\n    });\n\n    state.cloudWorkflows = data.map((item) => {\n      const hasLocalCopy = Boolean(workflowStore.workflows[item.id]);\n      item.hasLocalCopy = hasLocalCopy;\n\n      return item;\n    });\n    state.backupRetrieved = true;\n  } catch (error) {\n    console.error(error);\n    state.loadingBackup = false;\n  }\n}\nasync function updateCloudBackup(workflow) {\n  try {\n    backupState.loading = true;\n    backupState.workflowId = workflow.id;\n\n    const keys = [\n      'description',\n      'drawflow',\n      'globalData',\n      'icon',\n      'name',\n      'settings',\n      'table',\n    ];\n    const payload = {};\n\n    keys.forEach((key) => {\n      payload[key] = workflow[key];\n    });\n\n    const response = await fetchApi(`/me/workflows/${workflow.id}`, {\n      auth: true,\n      method: 'PUT',\n      body: JSON.stringify({ workflow: payload }),\n    });\n    const data = await response.json();\n    if (!response.ok) throw new Error(data.message);\n\n    const index = state.cloudWorkflows.findIndex(\n      (item) => item.id === workflow.id\n    );\n    if (index !== -1) {\n      state.cloudWorkflows[index].updatedAt = Date.now();\n    }\n  } catch (error) {\n    console.error(error);\n    toast.error('Something went wrong!');\n  } finally {\n    backupState.workflowId = '';\n    backupState.loading = false;\n  }\n}\nasync function backupWorkflowsToCloud(workflowId) {\n  if (backupState.uploading) return;\n\n  try {\n    backupState.uploading = true;\n\n    if (workflowId) backupState.workflowId = workflowId;\n\n    const workflowIds = workflowId ? [workflowId] : state.selectedWorkflows;\n    const workflowsPayload = workflowIds.reduce((acc, id) => {\n      const findWorkflow = workflowStore.getById(id);\n\n      if (!findWorkflow) return acc;\n\n      const workflow = convertWorkflow(findWorkflow, ['dataColumns', 'id']);\n\n      delete workflow.extVersion;\n\n      workflow.drawflow =\n        typeof workflow.drawflow === 'string'\n          ? parseJSON(workflow.drawflow, { drawflow: { nodes: [], edges: [] } })\n          : workflow.drawflow;\n\n      acc.push(workflow);\n\n      return acc;\n    }, []);\n\n    const response = await fetchApi('/me/workflows/backup', {\n      auth: true,\n      method: 'POST',\n      body: JSON.stringify({ workflows: workflowsPayload }),\n    });\n    const result = await response.json();\n\n    if (!response.ok) {\n      throw new Error(result.message);\n    }\n\n    const { lastBackup, data, ids } = result;\n\n    backupState.uploading = false;\n    backupState.workflowId = '';\n\n    ids.forEach((id) => {\n      const isExists = state.cloudWorkflows.some(\n        (workflow) => workflow.id === id\n      );\n      if (isExists) return;\n\n      state.cloudWorkflows.push(workflowStore.getById(id));\n    });\n\n    state.lastSync = lastBackup;\n    state.selectedWorkflows = [];\n    state.lastBackup = lastBackup;\n\n    const userWorkflows = parseJSON('user-workflows', {\n      backup: [],\n      hosted: {},\n    });\n    userWorkflows.backup = state.cloudWorkflows;\n    sessionStorage.setItem('user-workflows', JSON.stringify(userWorkflows));\n\n    await Promise.allSettled(\n      data.map(async () => {\n        workflowStore.update({\n          data,\n          id: data.id,\n        });\n      })\n    );\n    await browser.storage.local.set({\n      lastBackup,\n      backupIds: ids,\n      lastSync: lastBackup,\n    });\n\n    sessionStorage.removeItem('backup-workflows');\n    sessionStorage.removeItem('user-workflows');\n    sessionStorage.removeItem('cache-time:backup-workflows');\n  } catch (error) {\n    console.error(error);\n    toast.error(error.message);\n    backupState.workflowId = '';\n    backupState.uploading = false;\n  }\n}\n\nonMounted(async () => {\n  await fetchCloudWorkflows();\n});\n</script>\n<style>\n.cloud-backup .content {\n  height: calc(100vh - 10rem);\n  max-height: 800px;\n}\n</style>\n"
  },
  {
    "path": "src/components/newtab/settings/jsBlockWrap.js",
    "content": "import { reactive } from 'vue';\n\nexport const store = reactive({\n  whiteSpace: 'pre',\n  statePrettier: Math.random(),\n});\n"
  },
  {
    "path": "src/components/newtab/shared/SharedCard.vue",
    "content": "<template>\n  <ui-card\n    :data-workflow-id=\"data.hostId\"\n    class=\"group flex flex-col hover:ring-2 hover:ring-accent dark:hover:ring-gray-200\"\n  >\n    <slot name=\"header\">\n      <div class=\"mb-4 flex items-center\">\n        <ui-img\n          v-if=\"data.icon?.startsWith('http')\"\n          :src=\"data.icon\"\n          class=\"overflow-hidden rounded-lg\"\n          style=\"height: 40px; width: 40px\"\n          alt=\"Can not display\"\n        />\n        <span v-else class=\"bg-box-transparent rounded-lg p-2\">\n          <v-remixicon :name=\"data.icon || icon\" />\n        </span>\n        <div class=\"grow\"></div>\n        <span\n          v-if=\"data.isDisabled\"\n          class=\"text-sm text-gray-600 dark:text-gray-200\"\n        >\n          Disabled\n        </span>\n        <button\n          v-else-if=\"!disabled\"\n          class=\"invisible group-hover:visible\"\n          @click=\"$emit('execute', data)\"\n        >\n          <v-remixicon name=\"riPlayLine\" />\n        </button>\n        <ui-popover v-if=\"showDetails\" class=\"ml-2 h-6\">\n          <template #trigger>\n            <button>\n              <v-remixicon name=\"riMoreLine\" />\n            </button>\n          </template>\n          <ui-list class=\"space-y-1\" style=\"min-width: 150px\">\n            <ui-list-item\n              v-for=\"item in menu\"\n              :key=\"item.id\"\n              v-close-popover\n              v-bind=\"item.attrs || {}\"\n              class=\"cursor-pointer\"\n              @click=\"$emit('menuSelected', { id: item.id, data })\"\n            >\n              <v-remixicon :name=\"item.icon\" class=\"mr-2 -ml-1\" />\n              <span class=\"capitalize\">{{ item.name }}</span>\n            </ui-list-item>\n          </ui-list>\n        </ui-popover>\n      </div>\n    </slot>\n    <div class=\"flex-1 cursor-pointer\" @click=\"$emit('click', data)\">\n      <p class=\"line-clamp font-semibold leading-tight\">\n        {{ data.name }}\n      </p>\n      <p\n        v-show=\"data.description\"\n        class=\"line-clamp mb-1 leading-tight text-gray-600 dark:text-gray-200\"\n      >\n        {{ data.description }}\n      </p>\n    </div>\n    <div class=\"flex items-center text-gray-600 dark:text-gray-200\">\n      <p class=\"flex-1\">{{ state.date }}</p>\n      <slot name=\"footer-content\" />\n    </div>\n  </ui-card>\n</template>\n<script setup>\nimport dayjs from '@/lib/dayjs';\nimport { shallowReactive } from 'vue';\n\nconst props = defineProps({\n  disabled: Boolean,\n  data: {\n    type: Object,\n    default: () => ({}),\n  },\n  icon: {\n    type: String,\n    default: 'riGlobalLine',\n  },\n  showDetails: {\n    type: Boolean,\n    default: true,\n  },\n  menu: {\n    type: Array,\n    default: () => [],\n  },\n});\n\ndefineEmits(['execute', 'click', 'menuSelected']);\n\nconst state = shallowReactive({\n  triggerText: null,\n  date: dayjs(props.data.createdAt).fromNow(),\n});\n</script>\n"
  },
  {
    "path": "src/components/newtab/shared/SharedCodemirror.vue",
    "content": "<template>\n  <div\n    ref=\"containerEl\"\n    :class=\"{ 'hide-gutters': !lineNumbers }\"\n    class=\"codemirror relative rounded-lg\"\n  >\n    <div\n      v-if=\"!hideLang\"\n      class=\"absolute bottom-0 left-0 z-10 flex h-6 w-full items-center px-2 text-xs text-gray-300\"\n    >\n      <div class=\"grow\" />\n      <span>\n        {{ lang }}\n      </span>\n    </div>\n  </div>\n</template>\n<script setup>\nimport { indentWithTab } from '@codemirror/commands';\nimport { css } from '@codemirror/lang-css';\nimport { html } from '@codemirror/lang-html';\nimport { javascript } from '@codemirror/lang-javascript';\nimport { json } from '@codemirror/lang-json';\nimport { EditorState } from '@codemirror/state';\nimport { oneDark } from '@codemirror/theme-one-dark';\nimport { keymap } from '@codemirror/view';\nimport { EditorView, basicSetup } from 'codemirror';\nimport { onBeforeUnmount, onMounted, ref, watch } from 'vue';\n// don't remove this unused import, the css is used in dynamic style\nimport { store } from '../settings/jsBlockWrap';\n\nconst props = defineProps({\n  lang: {\n    type: String,\n    default: 'javascript',\n  },\n  modelValue: {\n    type: String,\n    default: '',\n  },\n  readonly: {\n    type: Boolean,\n    default: false,\n  },\n  lineNumbers: {\n    type: Boolean,\n    default: true,\n  },\n  extensions: {\n    type: [Object, Array],\n    default: () => [],\n  },\n  hideLang: Boolean,\n});\nconst emit = defineEmits(['change', 'update:modelValue']);\n\nlet view = null;\nconst langs = { json, javascript, html, css };\n\nconst containerEl = ref(null);\n\nconst updateListener = EditorView.updateListener.of((event) => {\n  if (event.docChanged) {\n    event.state.sliceDoc(0, 20);\n\n    const newValue = event.state.doc.toString();\n\n    emit('change', newValue);\n    emit('update:modelValue', newValue);\n  }\n});\n\nconst customExtension = Array.isArray(props.extensions)\n  ? props.extensions\n  : [props.extensions];\n\nconst state = EditorState.create({\n  doc: props.modelValue,\n  extensions: [\n    oneDark,\n    basicSetup,\n    updateListener,\n    langs[props.lang]?.(),\n    EditorState.tabSize.of(2),\n    keymap.of([indentWithTab]),\n    EditorState.readOnly.of(props.readonly),\n    ...customExtension,\n  ],\n});\n\nwatch(\n  () => props.modelValue,\n  (value) => {\n    if (value === view.state.doc.toString()) return;\n\n    view.dispatch({\n      changes: { from: 0, to: view.state.doc.length, insert: value },\n    });\n  }\n);\n\nonMounted(() => {\n  view = new EditorView({\n    state,\n    parent: containerEl.value,\n  });\n});\nonBeforeUnmount(() => {\n  view?.destroy();\n});\n</script>\n<style>\n.cm-content {\n  flex-basis: fit-content;\n}\n.cm-line {\n  white-space: v-bind(store.whiteSpace);\n}\n.codemirror.hide-gutters .cm-gutters {\n  display: none !important;\n}\n\n.cm-editor {\n  height: 100%;\n  font-size: 15px;\n  @apply pb-6;\n}\n\n.cm-editor .cm-gutters,\n.cm-editor .cm-content,\n.cm-tooltip.cm-tooltip-autocomplete > ul {\n  font-family: 'Source Code Pro', Fira code, Fira Mono, Consolas, Menlo, Courier,\n    monospace !important;\n}\n\n.cm-tooltip-autocomplete {\n  margin-left: 0px;\n  margin-top: 16px;\n}\n\n.cm-tooltip-autocomplete li[aria-selected] {\n  background-color: #095fff !important;\n  color: #ffffff !important;\n}\n</style>\n"
  },
  {
    "path": "src/components/newtab/shared/SharedConditionBuilder/ConditionBuilderInputs.vue",
    "content": "<template>\n  <div\n    v-for=\"(item, index) in inputsData\"\n    :key=\"item.id\"\n    class=\"condition-input scroll\"\n  >\n    <div\n      v-if=\"item.category === 'value'\"\n      class=\"flex flex-wrap items-end space-x-2\"\n    >\n      <ui-select\n        :model-value=\"item.type\"\n        class=\"shrink-0\"\n        @change=\"updateValueType($event, index)\"\n      >\n        <optgroup\n          v-for=\"(types, label) in filterValueTypes(index)\"\n          :key=\"label\"\n          :label=\"label\"\n        >\n          <option v-for=\"type in types\" :key=\"type.id\" :value=\"type.id\">\n            {{ type.name }}\n          </option>\n        </optgroup>\n      </ui-select>\n      <template\n        v-for=\"name in getConditionDataList(item)\"\n        :key=\"item.id + name\"\n      >\n        <template v-if=\"name === 'code'\">\n          <ui-select\n            v-model=\"inputsData[index].data.context\"\n            :placeholder=\"t('workflow.blocks.javascript-code.context.name')\"\n            class=\"mr-2\"\n          >\n            <option\n              :disabled=\"\n                isFirefox ||\n                (workflow?.data?.value.settings?.execContext || 'popup') !==\n                  'popup'\n              \"\n              value=\"background\"\n            >\n              {{\n                t(`workflow.blocks.javascript-code.context.items.background`)\n              }}\n            </option>\n            <option value=\"website\">\n              {{ t(`workflow.blocks.javascript-code.context.items.website`) }}\n            </option>\n          </ui-select>\n          <v-remixicon\n            :title=\"t('workflow.conditionBuilder.topAwait')\"\n            name=\"riInformationLine\"\n          />\n        </template>\n        <edit-autocomplete\n          :disabled=\"name === 'code'\"\n          :class=\"[name === 'code' ? 'w-full' : 'flex-1']\"\n          :style=\"{ marginLeft: name === 'code' ? 0 : null }\"\n        >\n          <shared-codemirror\n            v-if=\"name === 'code'\"\n            v-model=\"inputsData[index].data[name]\"\n            :extensions=\"codemirrorExts\"\n            class=\"code-condition mt-2\"\n            style=\"margin-left: 0\"\n          />\n          <ui-input\n            v-else\n            v-model=\"inputsData[index].data[name]\"\n            :title=\"conditionBuilder.inputTypes[name].label\"\n            :placeholder=\"conditionBuilder.inputTypes[name].label\"\n            autocomplete=\"off\"\n            class=\"w-full\"\n          />\n        </edit-autocomplete>\n        <SharedElSelectorActions\n          v-if=\"name === 'selector'\"\n          v-model:selector=\"inputsData[index].data[name]\"\n        />\n      </template>\n    </div>\n    <ui-select\n      v-else-if=\"item.category === 'compare'\"\n      :model-value=\"inputsData[index].type\"\n      @change=\"updateCompareType($event, index)\"\n    >\n      <optgroup\n        v-for=\"(types, category) in conditionOperators\"\n        :key=\"category\"\n        :label=\"category\"\n      >\n        <option v-for=\"type in types\" :key=\"type.id\" :value=\"type.id\">\n          {{ type.name }}\n        </option>\n      </optgroup>\n    </ui-select>\n  </div>\n</template>\n<script setup>\nimport { ref, watch, defineAsyncComponent, inject } from 'vue';\nimport { nanoid } from 'nanoid';\nimport { useI18n } from 'vue-i18n';\nimport { autocompletion } from '@codemirror/autocomplete';\nimport cloneDeep from 'lodash.clonedeep';\nimport {\n  automaFuncsSnippets,\n  automaFuncsCompletion,\n  completeFromGlobalScope,\n} from '@/utils/codeEditorAutocomplete';\nimport { conditionBuilder } from '@/utils/shared';\nimport SharedElSelectorActions from '@/components/newtab/shared/SharedElSelectorActions.vue';\nimport EditAutocomplete from '../../workflow/edit/EditAutocomplete.vue';\n\nconst SharedCodemirror = defineAsyncComponent(() =>\n  import('../SharedCodemirror.vue')\n);\n\nconst props = defineProps({\n  data: {\n    type: Array,\n    default: () => [],\n  },\n  autocomplete: {\n    type: Array,\n    default: () => [],\n  },\n});\nconst emit = defineEmits(['update']);\n\nconst isFirefox = BROWSER_TYPE === 'firefox';\nconst autocompleteList = [automaFuncsSnippets.automaRefData];\nconst codemirrorExts = [\n  autocompletion({\n    override: [\n      automaFuncsCompletion(autocompleteList),\n      completeFromGlobalScope,\n    ],\n  }),\n];\nconst conditionOperators = conditionBuilder.compareTypes.reduce((acc, type) => {\n  if (!acc[type.category]) acc[type.category] = [];\n\n  acc[type.category].push(type);\n\n  return acc;\n}, {});\n\nconst excludeData = ['context'];\nconst workflow = inject('workflow');\n\nconst { t } = useI18n();\nconst inputsData = ref(cloneDeep(props.data));\n\nfunction getConditionDataList(inputData) {\n  const keys = Object.keys(inputData.data);\n  const filteredKeys = keys.filter((item) => !excludeData.includes(item));\n\n  return filteredKeys;\n}\nfunction getDefaultValues(items) {\n  const defaultValues = {\n    value: {\n      id: nanoid(),\n      type: 'value',\n      category: 'value',\n      data: { value: '' },\n    },\n    compare: { id: nanoid(), category: 'compare', type: 'eq' },\n  };\n\n  if (typeof items === 'string') return defaultValues[items];\n\n  return items.map((item) => defaultValues[item]);\n}\nfunction filterValueTypes(index) {\n  return conditionBuilder.valueTypes.reduce((acc, item) => {\n    if (index < 1 || item.compareable) {\n      (acc[item.category] = acc[item.category] || []).push(item);\n    }\n\n    return acc;\n  }, {});\n}\nfunction updateValueType(newType, index) {\n  const type = conditionBuilder.valueTypes.find(({ id }) => id === newType);\n\n  if (index === 0 && !type.compareable) {\n    inputsData.value.splice(index + 1);\n  } else if (inputsData.value.length === 1) {\n    inputsData.value.push(...getDefaultValues(['compare', 'value']));\n  }\n\n  inputsData.value[index].type = newType;\n  inputsData.value[index].data = { ...type.data };\n}\nfunction updateCompareType(newType, index) {\n  const { needValue } = conditionBuilder.compareTypes.find(\n    ({ id }) => id === newType\n  );\n\n  if (!needValue) {\n    inputsData.value.splice(index + 1);\n  } else if (inputsData.value.length === 2) {\n    inputsData.value.push(getDefaultValues('value'));\n  }\n\n  inputsData.value[index].type = newType;\n}\n\nwatch(\n  inputsData,\n  (value) => {\n    emit('update', value);\n  },\n  { deep: true }\n);\n</script>\n<style>\n.code-condition .cm-content {\n  white-space: pre-wrap;\n}\n</style>\n"
  },
  {
    "path": "src/components/newtab/shared/SharedConditionBuilder/index.vue",
    "content": "<template>\n  <div class=\"space-y-4\">\n    <ui-button v-if=\"conditions.length === 0\" @click=\"addOrCondition\">\n      {{ t('workflow.conditionBuilder.add') }}\n    </ui-button>\n    <div v-for=\"(item, index) in conditions\" :key=\"item.id\">\n      <div class=\"condition-group relative flex\">\n        <div\n          v-show=\"item.conditions.length > 1\"\n          class=\"and-text relative mr-4 mb-12 flex items-center\"\n          :class=\"{ 'add-line': item.conditions.length > 1 }\"\n        >\n          <span\n            class=\"relative z-10 inline-block w-14 rounded-md bg-blue-500 py-1 text-center text-white dark:bg-blue-300 dark:text-black\"\n          >\n            {{ t('workflow.conditionBuilder.and') }}\n          </span>\n        </div>\n        <div class=\"flex-1 space-y-2\">\n          <draggable\n            v-model=\"conditions[index].conditions\"\n            item-key=\"id\"\n            handle=\".handle\"\n            group=\"conditions\"\n            class=\"space-y-2\"\n            @end=\"onDragEnd\"\n          >\n            <template #item=\"{ element: inputs, index: inputsIndex }\">\n              <div class=\"condition-item\">\n                <ui-expand\n                  class=\"w-full rounded-lg border\"\n                  header-class=\"px-4 py-2 w-full flex items-center h-full rounded-lg overflow-hidden group focus:ring-0\"\n                >\n                  <template #header>\n                    <p class=\"text-overflow w-64 flex-1 space-x-2 text-left\">\n                      <span\n                        v-for=\"input in inputs.items\"\n                        :key=\"`text-${input.id}`\"\n                        :class=\"[\n                          input.category === 'compare'\n                            ? 'font-semibold'\n                            : 'text-gray-600 dark:text-gray-200',\n                        ]\"\n                      >\n                        {{ getConditionText(input) }}\n                      </span>\n                    </p>\n                    <v-remixicon\n                      name=\"riDeleteBin7Line\"\n                      class=\"invisible ml-4 group-hover:visible\"\n                      @click.stop=\"deleteCondition(index, inputsIndex)\"\n                    />\n                    <v-remixicon\n                      name=\"mdiDrag\"\n                      class=\"handle ml-2 cursor-move\"\n                    />\n                  </template>\n                  <div class=\"space-y-2 px-4 py-2\">\n                    <condition-builder-inputs\n                      :autocomplete=\"autocomplete\"\n                      :data=\"inputs.items\"\n                      @update=\"\n                        conditions[index].conditions[inputsIndex].items = $event\n                      \"\n                    />\n                  </div>\n                </ui-expand>\n              </div>\n            </template>\n          </draggable>\n          <div class=\"condition-action mt-2 space-x-2 text-sm\">\n            <ui-button @click=\"addAndCondition(index)\">\n              <v-remixicon name=\"riAddLine\" class=\"-ml-2 mr-1\" size=\"20\" />\n              {{ t('workflow.conditionBuilder.and') }}\n            </ui-button>\n            <ui-button\n              v-if=\"index === conditions.length - 1\"\n              @click=\"addOrCondition\"\n            >\n              <v-remixicon name=\"riAddLine\" class=\"-ml-2 mr-1\" size=\"20\" />\n              {{ t('workflow.conditionBuilder.or') }}\n            </ui-button>\n          </div>\n        </div>\n      </div>\n      <div\n        v-show=\"index !== conditions.length - 1\"\n        class=\"or-text relative mt-4 text-left\"\n      >\n        <span\n          class=\"line absolute top-1/2 left-0 w-full -translate-y-1/2 bg-indigo-500 dark:bg-indigo-400\"\n          style=\"height: 2px\"\n        ></span>\n        <span\n          class=\"relative z-10 inline-block w-14 rounded-md bg-indigo-500 py-1 text-center text-white dark:bg-indigo-300 dark:text-black\"\n        >\n          {{ t('workflow.conditionBuilder.or') }}\n        </span>\n      </div>\n    </div>\n  </div>\n</template>\n<script setup>\nimport { ref, watch } from 'vue';\nimport { useI18n } from 'vue-i18n';\nimport { nanoid } from 'nanoid';\nimport Draggable from 'vuedraggable';\nimport cloneDeep from 'lodash.clonedeep';\nimport { conditionBuilder } from '@/utils/shared';\nimport ConditionBuilderInputs from './ConditionBuilderInputs.vue';\n\nconst props = defineProps({\n  modelValue: {\n    type: Array,\n    default: () => [],\n  },\n  autocomplete: {\n    type: Array,\n    default: () => [],\n  },\n});\nconst emit = defineEmits(['update:modelValue', 'change']);\n\nconst { t } = useI18n();\n\nconst conditions = ref(cloneDeep(props.modelValue));\n\nfunction getDefaultValues(items = ['value', 'compare', 'value']) {\n  const defaultValues = {\n    value: {\n      type: 'value',\n      category: 'value',\n      data: { value: '' },\n    },\n    compare: { category: 'compare', type: 'eq' },\n  };\n  const selectValue = (type) =>\n    cloneDeep({\n      ...defaultValues[type],\n      id: nanoid(),\n    });\n\n  if (typeof items === 'string') {\n    return selectValue(items);\n  }\n\n  return items.map((item) => selectValue(item));\n}\nfunction getConditionText({ category, type, data }) {\n  if (category === 'compare') {\n    return conditionBuilder.compareTypes.find(({ id }) => id === type).name;\n  }\n\n  let text = '';\n\n  if (type === 'value') {\n    text = data.value || 'Empty';\n  } else if (type.startsWith('code')) {\n    text = 'JS Code';\n  } else if (type.startsWith('element')) {\n    text = type;\n\n    const textDetail = data.attrName || data.selector;\n\n    if (textDetail) text += `(${textDetail})`;\n  } else if (type.startsWith('data')) {\n    text = `Data exists (${data.dataPath})`;\n  }\n\n  return text;\n}\nfunction addOrCondition() {\n  const newOrCondition = getDefaultValues();\n\n  conditions.value.push({\n    id: nanoid(),\n    conditions: [{ id: nanoid(), items: newOrCondition }],\n  });\n}\nfunction addAndCondition(index) {\n  const newAndCondition = getDefaultValues();\n\n  conditions.value[index].conditions.push({\n    id: nanoid(),\n    items: newAndCondition,\n  });\n}\nfunction deleteCondition(index, itemIndex) {\n  const condition = conditions.value[index].conditions;\n\n  condition.splice(itemIndex, 1);\n\n  if (condition.length === 0) conditions.value.splice(index, 1);\n}\nfunction onDragEnd() {\n  conditions.value.forEach((item, index) => {\n    if (item.conditions.length > 0) return;\n\n    conditions.value.splice(index, 1);\n  });\n}\n\nwatch(\n  conditions,\n  (value) => {\n    emit('change', value);\n    emit('update:modelValue', value);\n  },\n  { deep: true }\n);\n</script>\n<style scoped>\n.and-text.add-line:before {\n  content: '';\n  position: absolute;\n  top: 0;\n  width: 30px;\n  height: 100%;\n  left: 50%;\n  @apply dark:border-blue-400 border-blue-500 border-2 border-r-0 rounded-bl-lg rounded-tl-lg;\n}\n.ghost-condition .condition-action {\n  display: none;\n}\n</style>\n"
  },
  {
    "path": "src/components/newtab/shared/SharedElSelectorActions.vue",
    "content": "<template>\n  <div class=\"inline-flex items-center\">\n    <ui-button\n      v-tooltip.group=\"$t('workflow.blocks.base.element.select')\"\n      icon\n      class=\"mr-2\"\n      @click=\"selectElement\"\n    >\n      <v-remixicon name=\"riFocus3Line\" />\n    </ui-button>\n    <ui-button\n      v-tooltip.group=\"$t('workflow.blocks.base.element.verify')\"\n      :disabled=\"!selector\"\n      icon\n      @click=\"verifySelector\"\n    >\n      <v-remixicon name=\"riCheckDoubleLine\" />\n    </ui-button>\n  </div>\n</template>\n<script setup>\nimport { useToast } from 'vue-toastification';\nimport { useGroupTooltip } from '@/composable/groupTooltip';\nimport elementSelector from '@/newtab/utils/elementSelector';\n\nconst props = defineProps({\n  findBy: {\n    type: String,\n    default: null,\n  },\n  multiple: {\n    type: Boolean,\n    default: false,\n  },\n  selector: {\n    type: String,\n    default: '',\n  },\n});\nconst emit = defineEmits(['update:selector']);\n\nuseGroupTooltip();\nconst toast = useToast();\n\nfunction selectElement() {\n  elementSelector.selectElement().then((selector) => {\n    emit('update:selector', selector);\n  });\n}\nfunction verifySelector() {\n  elementSelector\n    .verifySelector({\n      selector: props.selector,\n      multiple: props.multiple,\n      findBy: props.findBy,\n    })\n    .then((result) => {\n      if (!result.notFound) return;\n\n      toast.error('Element not found');\n    });\n}\n</script>\n"
  },
  {
    "path": "src/components/newtab/shared/SharedLogsTable.vue",
    "content": "<template>\n  <div class=\"logs-table scroll overflow-x-auto\">\n    <transition-expand>\n      <div v-if=\"state.selected.length > 0\" class=\"border-x border-t px-4 py-2\">\n        <ui-button @click=\"stopSelectedWorkflow\"> Stop selected </ui-button>\n      </div>\n    </transition-expand>\n    <table class=\"w-full\">\n      <tbody class=\"divide-y dark:divide-gray-800\">\n        <template v-if=\"running && running[0]?.state\">\n          <tr v-for=\"item in running\" :key=\"item.id\" class=\"border p-2\">\n            <td v-if=\"!hideSelect\" class=\"w-8\">\n              <ui-checkbox\n                :model-value=\"state.selected.includes(item.id)\"\n                class=\"align-text-bottom\"\n                @change=\"toggleSelectedLog($event, item.id)\"\n              />\n            </td>\n            <td class=\"w-4/12\">\n              <p\n                v-if=\"modal\"\n                class=\"log-link text-overflow\"\n                @click=\"$emit('select', { type: 'running', id: item.id })\"\n              >\n                {{ item.state.name }}\n              </p>\n              <router-link\n                v-else\n                :to=\"`/logs/${item.id}/running`\"\n                class=\"log-link text-overflow\"\n              >\n                {{ item.state.name }}\n              </router-link>\n            </td>\n            <td\n              :title=\"t('log.duration')\"\n              class=\"log-time w-2/12 dark:text-gray-200\"\n            >\n              <v-remixicon name=\"riTimerLine\"></v-remixicon>\n              <span>{{\n                countDuration(item.state?.startedTimestamp, Date.now())\n              }}</span>\n            </td>\n            <td title=\"Executing block\" class=\"text-overflow\">\n              <ui-spinner color=\"text-accent\" size=\"20\" />\n              <span class=\"text-overflow ml-3 inline-block align-middle\">\n                {{\n                  getTranslation(\n                    `workflow.blocks.${item.state.currentBlock[0].name}.name`,\n                    item.state.currentBlock[0].name\n                  )\n                }}\n              </span>\n            </td>\n            <td class=\"text-right\">\n              <span\n                class=\"inline-block w-16 rounded-md bg-blue-300 py-1 text-center text-sm dark:text-black\"\n              >\n                {{ t('common.running') }}\n              </span>\n            </td>\n            <td class=\"text-right\">\n              <ui-button small class=\"text-sm\" @click=\"stopWorkflow(item.id)\">\n                {{ t('common.stop') }}\n              </ui-button>\n            </td>\n          </tr>\n        </template>\n        <tr v-for=\"log in logs\" :key=\"log.id\" class=\"hoverable\">\n          <slot name=\"item-prepend\" :log=\"log\" />\n          <td\n            class=\"text-overflow w-4/12\"\n            style=\"min-width: 140px; max-width: 330px\"\n          >\n            <p\n              v-if=\"modal\"\n              class=\"log-link text-overflow\"\n              @click=\"$emit('select', { type: 'log', id: log.id })\"\n            >\n              {{ log.name }}\n            </p>\n            <router-link\n              v-else\n              :to=\"`/logs/${log.id}`\"\n              class=\"log-link text-overflow\"\n            >\n              {{ log.name }}\n            </router-link>\n          </td>\n          <td\n            class=\"log-time w-3/12 dark:text-gray-200\"\n            style=\"min-width: 200px\"\n          >\n            <v-remixicon\n              :title=\"t('log.startedDate')\"\n              name=\"riCalendarLine\"\n              class=\"mr-2 inline-block align-middle\"\n            />\n            <span :title=\"formatDate(log.startedAt, 'DD MMM YYYY, hh:mm A')\">\n              {{ formatDate(log.startedAt, 'relative') }}\n            </span>\n          </td>\n          <td\n            :title=\"t('log.duration')\"\n            class=\"log-time w-2/12 dark:text-gray-200\"\n            style=\"min-width: 85px\"\n          >\n            <v-remixicon name=\"riTimerLine\"></v-remixicon>\n            <span>{{ countDuration(log.startedAt, log.endedAt) }}</span>\n          </td>\n          <td class=\"text-right\">\n            <span\n              :class=\"statusColors[log.status]\"\n              :title=\"log.status === 'error' ? getErrorMessage(log) : null\"\n              class=\"inline-block w-24 rounded-md py-1 text-center text-sm dark:text-black\"\n            >\n              {{ t(`logStatus.${log.status}`) }}\n            </span>\n          </td>\n          <slot name=\"item-append\" :log=\"log\" />\n        </tr>\n        <slot name=\"table:append\" />\n      </tbody>\n    </table>\n  </div>\n</template>\n<script setup>\nimport { reactive } from 'vue';\nimport { useI18n } from 'vue-i18n';\nimport { countDuration } from '@/utils/helper';\nimport dayjs from '@/lib/dayjs';\nimport RendererWorkflowService from '@/service/renderer/RendererWorkflowService';\n\ndefineProps({\n  logs: {\n    type: Array,\n    default: () => [],\n  },\n  running: {\n    type: Array,\n    default: () => [],\n  },\n  modal: Boolean,\n  hideSelect: Boolean,\n});\ndefineEmits(['select']);\n\nconst { t, te } = useI18n();\n\nconst statusColors = {\n  error: 'bg-red-200 dark:bg-red-300',\n  success: 'bg-green-200 dark:bg-green-300',\n  stopped: 'bg-yellow-200 dark:bg-yellow-300',\n};\nconst state = reactive({\n  selected: [],\n});\n\nfunction getTranslation(key, defText = '') {\n  return te(key) ? t(key) : defText;\n}\nfunction stopWorkflow(stateId) {\n  RendererWorkflowService.stopWorkflowExecution(stateId);\n}\nfunction toggleSelectedLog(selected, id) {\n  if (selected) {\n    state.selected.push(id);\n    return;\n  }\n\n  const index = state.selected.indexOf(id);\n\n  if (index !== -1) state.selected.splice(index, 1);\n}\nfunction formatDate(date, format) {\n  if (format === 'relative') return dayjs(date).fromNow();\n\n  return dayjs(date).format(format);\n}\nfunction getErrorMessage({ message }) {\n  const messagePath = `log.messages.${message}`;\n\n  if (message && te(messagePath)) {\n    return t(messagePath);\n  }\n\n  return '';\n}\nfunction stopSelectedWorkflow() {\n  state.selected.forEach((id) => {\n    stopWorkflow(id);\n  });\n  state.selected = [];\n}\n</script>\n<style scoped>\n.log-time svg {\n  @apply mr-2;\n}\n.log-time svg,\n.log-time span {\n  display: inline-block;\n  vertical-align: middle;\n}\n\n.log-link {\n  @apply inline-block w-full align-middle;\n  cursor: pointer;\n  min-height: 28px;\n}\n</style>\n"
  },
  {
    "path": "src/components/newtab/shared/SharedPermissionsModal.vue",
    "content": "<template>\n  <ui-modal :title=\"t('workflowPermissions.title')\" persist>\n    <p class=\"font-semibold\">\n      {{ t('workflowPermissions.description') }}\n    </p>\n    <ui-list class=\"mt-2 space-y-1\">\n      <ui-list-item\n        v-for=\"permission in permissions\"\n        :key=\"permission\"\n        small\n        style=\"align-items: flex-start\"\n      >\n        <v-remixicon :name=\"icons[permission]\" class=\"mt-1\" />\n        <div class=\"ml-4 flex-1 overflow-hidden\">\n          <p class=\"leading-tight\">\n            {{ t(`workflowPermissions.${permission}.title`) }}\n          </p>\n          <p class=\"leading-tight text-gray-600 dark:text-gray-200\">\n            {{ t(`workflowPermissions.${permission}.description`) }}\n          </p>\n        </div>\n      </ui-list-item>\n    </ui-list>\n    <div class=\"mt-8 text-right\">\n      <ui-button class=\"mr-2\" @click=\"emit('update:modelValue', false)\">\n        {{ t('common.cancel') }}\n      </ui-button>\n      <ui-button variant=\"accent\" @click=\"requestPermission\">\n        {{ t('workflow.blocks.clipboard.grantPermission') }}\n      </ui-button>\n    </div>\n  </ui-modal>\n</template>\n<script setup>\nimport { toRaw } from 'vue';\nimport { useI18n } from 'vue-i18n';\nimport browser from 'webextension-polyfill';\n\nconst props = defineProps({\n  permissions: {\n    type: Array,\n    default: () => [],\n  },\n});\nconst emit = defineEmits(['update:modelValue', 'granted']);\n\nconst { t } = useI18n();\n\nconst icons = {\n  cookies: 'mdiCookieOutline',\n  downloads: 'riDownloadLine',\n  clipboardRead: 'riClipboardLine',\n  contextMenus: 'riFileListLine',\n  notifications: 'riNotification3Line',\n};\n\nfunction requestPermission() {\n  browser.permissions\n    .request({ permissions: toRaw(props.permissions) })\n    .then(() => {\n      emit('update:modelValue', false);\n      emit('granted', true);\n    });\n}\n</script>\n"
  },
  {
    "path": "src/components/newtab/shared/SharedWorkflowState.vue",
    "content": "<template>\n  <ui-card>\n    <div class=\"mb-4 flex items-center\">\n      <div class=\"text-overflow mr-4 flex-1\">\n        <p class=\"text-overflow mr-2 w-full\">{{ data.state.name }}</p>\n        <p\n          class=\"text-overflow mr-2 w-full leading-tight text-gray-600 dark:text-gray-200\"\n          :title=\"`Started at: ${formatDate(\n            data.state.startedTimestamp,\n            'DD MMM, hh:mm A'\n          )}`\"\n        >\n          {{ formatDate(data.state.startedTimestamp, 'relative') }}\n        </p>\n      </div>\n      <ui-button\n        v-if=\"data.state.tabId\"\n        icon\n        class=\"mr-2\"\n        title=\"Open tab\"\n        @click=\"openTab\"\n      >\n        <v-remixicon name=\"riExternalLinkLine\" />\n      </ui-button>\n      <ui-button variant=\"accent\" @click=\"stopWorkflow\">\n        <v-remixicon name=\"riStopLine\" class=\"mr-2 -ml-1\" />\n        <span>{{ t('common.stop') }}</span>\n      </ui-button>\n    </div>\n    <div class=\"bg-box-transparent divide-y rounded-lg px-4\">\n      <div\n        v-for=\"block in data.state.currentBlock\"\n        :key=\"block.id || block.name\"\n        class=\"flex items-center py-2\"\n      >\n        <v-remixicon :name=\"blocks[block.name].icon\" />\n        <p class=\"text-overflow ml-2 mr-4 flex-1\">\n          {{ blocks[block.name].name }}\n        </p>\n        <ui-spinner color=\"text-accent\" size=\"20\" />\n      </div>\n    </div>\n    <div\n      v-if=\"data.parentState\"\n      class=\"mt-2 rounded-lg bg-yellow-200 py-2 px-4 text-sm\"\n    >\n      {{ t('workflow.state.executeBy', { name: data.parentState.name }) }}\n      <span class=\"lowercase\">\n        {{\n          data.parentState.isCollection\n            ? t('common.collection')\n            : t('common.workflow')\n        }}\n      </span>\n    </div>\n  </ui-card>\n</template>\n<script setup>\nimport browser from 'webextension-polyfill';\nimport { useI18n } from 'vue-i18n';\nimport { getBlocks } from '@/utils/getSharedData';\nimport RendererWorkflowService from '@/service/renderer/RendererWorkflowService';\nimport dayjs from '@/lib/dayjs';\n\nconst props = defineProps({\n  data: {\n    type: Object,\n    default: () => ({}),\n  },\n});\n\nconst blocks = getBlocks();\nconst { t } = useI18n();\n\nfunction formatDate(date, format) {\n  if (format === 'relative') return dayjs(date).fromNow();\n\n  return dayjs(date).format(format);\n}\nfunction openTab() {\n  browser.tabs.update(props.data.state.tabId, { active: true });\n}\nfunction stopWorkflow() {\n  RendererWorkflowService.stopWorkflowExecution(props.data.id);\n}\n</script>\n"
  },
  {
    "path": "src/components/newtab/shared/SharedWorkflowTriggers.vue",
    "content": "<template>\n  <div\n    class=\"scroll overflow-auto\"\n    style=\"min-height: 350px; max-height: calc(100vh - 14rem)\"\n  >\n    <ui-expand\n      v-for=\"(trigger, index) in triggersList\"\n      :key=\"index\"\n      class=\"trigger-item mb-2 rounded-lg border\"\n    >\n      <template #header>\n        <p class=\"flex-1\">\n          {{ t(`workflow.blocks.trigger.items.${trigger.type}`) }}\n        </p>\n        <v-remixicon\n          name=\"riDeleteBin7Line\"\n          size=\"20\"\n          class=\"delete-btn cursor-pointer\"\n          @click.stop=\"triggersList.splice(index, 1)\"\n        />\n      </template>\n      <div class=\"px-4 py-2\">\n        <component\n          :is=\"triggersData[trigger.type]?.component\"\n          :data=\"trigger.data\"\n          @update=\"updateTriggerData(index, $event)\"\n        />\n      </div>\n    </ui-expand>\n    <ui-popover class=\"mt-4\">\n      <template #trigger>\n        <ui-button>\n          Add trigger\n          <hr class=\"h-4 border-r\" />\n          <v-remixicon\n            name=\"riArrowLeftSLine\"\n            class=\"ml-2 -mr-1\"\n            rotate=\"-90\"\n          />\n        </ui-button>\n      </template>\n      <ui-list class=\"space-y-1\">\n        <ui-list-item\n          v-for=\"triggerType in triggersTypes\"\n          :key=\"triggerType\"\n          v-close-popover\n          class=\"cursor-pointer\"\n          small\n          @click=\"addTrigger(triggerType)\"\n        >\n          {{ t(`workflow.blocks.trigger.items.${triggerType}`) }}\n        </ui-list-item>\n      </ui-list>\n    </ui-popover>\n  </div>\n</template>\n<script setup>\nimport { ref, watch } from 'vue';\nimport { useI18n } from 'vue-i18n';\nimport { nanoid } from 'nanoid/non-secure';\nimport cloneDeep from 'lodash.clonedeep';\nimport TriggerDate from '../workflow/edit/Trigger/TriggerDate.vue';\nimport TriggerCronJob from '../workflow/edit/Trigger/TriggerCronJob.vue';\nimport TriggerInterval from '../workflow/edit/Trigger/TriggerInterval.vue';\nimport TriggerVisitWeb from '../workflow/edit/Trigger/TriggerVisitWeb.vue';\nimport TriggerContextMenu from '../workflow/edit/Trigger/TriggerContextMenu.vue';\nimport TriggerSpecificDay from '../workflow/edit/Trigger/TriggerSpecificDay.vue';\n// import TriggerElementChange from '../workflow/edit/Trigger/TriggerElementChange.vue';\nimport TriggerKeyboardShortcut from '../workflow/edit/Trigger/TriggerKeyboardShortcut.vue';\n\nconst props = defineProps({\n  triggers: {\n    type: Array,\n    default: () => [],\n  },\n  exclude: {\n    type: Array,\n    default: null,\n  },\n});\nconst emit = defineEmits(['update:triggers', 'update']);\n\nconst triggersData = {\n  // 'element-change': TriggerElementChange,\n  interval: {\n    component: TriggerInterval,\n    data: {\n      interval: 60,\n      delay: 5,\n      fixedDelay: false,\n    },\n  },\n  'cron-job': {\n    component: TriggerCronJob,\n    data: {\n      expression: '',\n    },\n  },\n  'context-menu': {\n    onlyOne: true,\n    component: TriggerContextMenu,\n    data: {\n      contextMenuName: '',\n      contextTypes: [],\n    },\n  },\n  date: {\n    component: TriggerDate,\n    data: {\n      date: '',\n    },\n  },\n  'specific-day': {\n    component: TriggerSpecificDay,\n    data: {\n      days: [],\n      time: '00:00',\n    },\n  },\n  'on-startup': {\n    onlyOne: true,\n    component: null,\n    data: null,\n  },\n  'visit-web': {\n    component: TriggerVisitWeb,\n    data: {\n      url: '',\n      isUrlRegex: false,\n      supportSPA: false,\n    },\n  },\n  'keyboard-shortcut': {\n    component: TriggerKeyboardShortcut,\n    data: {\n      shortcut: '',\n    },\n  },\n};\n\nconst triggersTypes = props.exclude\n  ? Object.keys(triggersData).filter((type) => !props.exclude.includes(type))\n  : Object.keys(triggersData);\n\nconst { t } = useI18n();\nconst triggersList = ref([...(props.triggers || [])]);\n\nfunction addTrigger(type) {\n  if (triggersData[type].onlyOne) {\n    const trigerExists = triggersList.value.some(\n      (trigger) => trigger.type === type\n    );\n    if (trigerExists) return;\n  }\n\n  triggersList.value.push({\n    id: nanoid(5),\n    type,\n    data: cloneDeep(triggersData[type].data),\n  });\n}\nfunction updateTriggerData(index, data) {\n  Object.assign(triggersList.value[index].data, data);\n}\n\nwatch(\n  triggersList,\n  (newData) => {\n    emit('update', newData);\n    emit('update:triggers', newData);\n  },\n  { deep: true }\n);\n</script>\n"
  },
  {
    "path": "src/components/newtab/shared/SharedWysiwyg.vue",
    "content": "<template>\n  <div class=\"wysiwyg-editor\">\n    <slot v-if=\"editor\" name=\"prepend\" :editor=\"editor\" />\n    <div\n      v-if=\"editor && toolbar && !readonly\"\n      class=\"bg-box-transparent sticky top-0 z-50 mb-2 flex items-center space-x-1 rounded-lg p-2 backdrop-blur\"\n    >\n      <button\n        :class=\"{\n          'bg-box-transparent text-primary': editor.isActive('heading', {\n            level: 1,\n          }),\n        }\"\n        title=\"Heading 1\"\n        class=\"editor-menu-btn hoverable\"\n        @click=\"editor.commands.toggleHeading({ level: 1 })\"\n      >\n        <v-remixicon name=\"riH1\" />\n      </button>\n      <button\n        :class=\"{\n          'bg-box-transparent text-primary': editor.isActive('heading', {\n            level: 2,\n          }),\n        }\"\n        title=\"Heading 2\"\n        class=\"editor-menu-btn hoverable\"\n        @click=\"editor.commands.toggleHeading({ level: 2 })\"\n      >\n        <v-remixicon name=\"riH2\" />\n      </button>\n      <span\n        class=\"h-5 w-px bg-gray-300 dark:bg-gray-600\"\n        style=\"margin: 0 12px\"\n      ></span>\n      <button\n        v-for=\"item in menuItems\"\n        :key=\"item.id\"\n        :title=\"item.name\"\n        :class=\"{\n          'bg-box-transparent text-primary': editor.isActive(item.id),\n        }\"\n        class=\"editor-menu-btn hoverable\"\n        @click=\"editor.chain().focus()[item.action]().run()\"\n      >\n        <v-remixicon :name=\"item.icon\" />\n      </button>\n      <span\n        class=\"h-5 w-px bg-gray-300 dark:bg-gray-600\"\n        style=\"margin: 0 12px\"\n      ></span>\n      <button\n        :class=\"{\n          'bg-box-transparent text-primary': editor.isActive('blockquote'),\n        }\"\n        title=\"Blockquote\"\n        class=\"editor-menu-btn hoverable\"\n        @click=\"editor.commands.toggleBlockquote()\"\n      >\n        <v-remixicon name=\"riDoubleQuotesL\" />\n      </button>\n      <button\n        title=\"Insert image\"\n        class=\"editor-menu-btn hoverable\"\n        @click=\"insertImage(editor)\"\n      >\n        <v-remixicon name=\"riImageLine\" />\n      </button>\n      <button\n        :class=\"{\n          'bg-box-transparent text-primary': editor.isActive('link'),\n        }\"\n        title=\"Link\"\n        class=\"editor-menu-btn hoverable\"\n        @click=\"setLink(editor)\"\n      >\n        <v-remixicon name=\"riLinkM\" />\n      </button>\n      <button\n        v-show=\"editor.isActive('link')\"\n        title=\"Remove link\"\n        class=\"editor-menu-btn hoverable\"\n        @click=\"editor.commands.unsetLink()\"\n      >\n        <v-remixicon name=\"riLinkUnlinkM\" />\n      </button>\n    </div>\n    <editor-content :editor=\"editor\" />\n    <slot name=\"append\" />\n  </div>\n</template>\n<script setup>\nimport { shallowRef, onMounted, onBeforeUnmount, watch } from 'vue';\nimport { Editor, EditorContent } from '@tiptap/vue-3';\nimport StarterKit from '@tiptap/starter-kit';\nimport Link from '@tiptap/extension-link';\nimport Image from '@tiptap/extension-image';\nimport Placeholder from '@tiptap/extension-placeholder';\nimport CharacterCount from '@tiptap/extension-character-count';\n\nconst props = defineProps({\n  modelValue: {\n    type: [String, Object],\n    default: null,\n  },\n  placeholder: {\n    type: String,\n    default: '',\n  },\n  limit: {\n    type: Number,\n    default: Infinity,\n  },\n  options: {\n    type: Object,\n    default: () => ({}),\n  },\n  toolbar: {\n    type: Boolean,\n    default: true,\n  },\n  readonly: Boolean,\n});\nconst emit = defineEmits(['update:modelValue', 'count', 'change']);\n\nconst editor = shallowRef(null);\nconst menuItems = [\n  { id: 'bold', name: 'Bold', icon: 'riBold', action: 'toggleBold' },\n  { id: 'italic', name: 'Italic', icon: 'riItalic', action: 'toggleItalic' },\n  {\n    id: 'strike',\n    name: 'Strikethrough',\n    icon: 'riStrikethrough2',\n    action: 'toggleStrike',\n  },\n];\n\nfunction setLink() {\n  const previousUrl = editor.value.getAttributes('link').href;\n  const url = window.prompt('URL', previousUrl);\n\n  if (url === null) return;\n\n  if (url === '') {\n    editor.value.chain().focus().extendMarkRange('link').unsetLink().run();\n\n    return;\n  }\n\n  editor.value\n    .chain()\n    .focus()\n    .extendMarkRange('link')\n    .setLink({ href: url, target: '_blank' })\n    .run();\n}\nfunction insertImage() {\n  const url = window.prompt('URL');\n\n  if (url) {\n    editor.value.chain().focus().setImage({ src: url }).run();\n  }\n}\n\nwatch(\n  () => props.modelValue,\n  (value) => {\n    const isSame =\n      JSON.stringify(editor.value.getJSON()) === JSON.stringify(value);\n\n    if (isSame) return;\n\n    editor.value.commands.setContent(value, false);\n  }\n);\n\nonMounted(() => {\n  editor.value = new Editor({\n    content: props.modelValue,\n    editable: !props.readonly,\n    onUpdate: () => {\n      const editorValue = editor.value.getJSON();\n\n      emit('count', editor.value.storage.characterCount.characters());\n      emit('change', editorValue);\n      emit('update:modelValue', editorValue);\n    },\n    extensions: [\n      Link,\n      Image,\n      StarterKit,\n      Placeholder.configure({\n        placeholder: props.placeholder,\n      }),\n      CharacterCount.configure({\n        limit: props.limit,\n      }),\n    ],\n    ...props.options,\n  });\n\n  emit('count', editor.value.storage.characterCount.characters());\n});\nonBeforeUnmount(() => {\n  editor.value?.destroy();\n});\n</script>\n<style>\n.ProseMirror pre,\n.ProseMirror code {\n  font-family: 'Source Code Pro', monospace;\n}\n.ProseMirror:focus {\n  outline: none;\n}\n.ProseMirror p.is-editor-empty:first-child::before {\n  @apply text-gray-400;\n  content: attr(data-placeholder);\n  float: left;\n  pointer-events: none;\n  height: 0;\n}\n</style>\n"
  },
  {
    "path": "src/components/newtab/storage/StorageCredentials.vue",
    "content": "<template>\n  <div class=\"mt-6 flex\">\n    <ui-input\n      v-model=\"state.query\"\n      :placeholder=\"t('common.search')\"\n      prepend-icon=\"riSearch2Line\"\n    />\n    <div class=\"grow\"></div>\n    <ui-button\n      variant=\"accent\"\n      style=\"min-width: 120px\"\n      class=\"ml-4\"\n      @click=\"addState.show = true\"\n    >\n      {{ t('credential.add') }}\n    </ui-button>\n  </div>\n  <ui-table\n    item-key=\"id\"\n    :headers=\"tableHeaders\"\n    :items=\"credentials\"\n    :search=\"state.query\"\n    class=\"mt-4 w-full\"\n  >\n    <template #item-value> ************ </template>\n    <template #item-createdAt=\"{ item }\">\n      {{ dayjs(item.createdAt).format('DD MMMM YYYY, hh:mm A') }}\n    </template>\n    <template #item-actions=\"{ item }\">\n      <v-remixicon\n        name=\"riDeleteBin7Line\"\n        class=\"inline-block cursor-pointer\"\n        @click=\"deleteCredential(item)\"\n      />\n    </template>\n  </ui-table>\n  <ui-modal v-model=\"addState.show\" :title=\"t('credential.add')\">\n    <ui-input v-model=\"addState.name\" placeholder=\"Name\" class=\"w-full\" />\n    <ui-textarea\n      v-model=\"addState.value\"\n      placeholder=\"value\"\n      class=\"mt-4 w-full\"\n    />\n    <div class=\"mt-8 text-right\">\n      <ui-button class=\"mr-4\" @click=\"addState.show = false\">\n        {{ t('common.cancel') }}\n      </ui-button>\n      <ui-button\n        :disabled=\"!addState.name\"\n        variant=\"accent\"\n        @click=\"saveCredential\"\n      >\n        {{ t('common.save') }}\n      </ui-button>\n    </div>\n  </ui-modal>\n</template>\n<script setup>\nimport { shallowReactive, watch } from 'vue';\nimport { useI18n } from 'vue-i18n';\nimport { useToast } from 'vue-toastification';\nimport dayjs from 'dayjs';\nimport credentialUtil from '@/utils/credentialUtil';\nimport { useLiveQuery } from '@/composable/liveQuery';\nimport dbStorage from '@/db/storage';\n\nconst { t } = useI18n();\nconst toast = useToast();\nconst credentials = useLiveQuery(() => dbStorage.credentials.toArray());\n\nconst tableHeaders = [\n  {\n    value: 'name',\n    filterable: true,\n    text: t('common.name'),\n    attrs: {\n      class: 'w-3/12 text-overflow',\n    },\n  },\n  {\n    value: 'value',\n    sortable: false,\n    filterable: false,\n    text: 'Value',\n  },\n  {\n    value: 'createdAt',\n    filterable: false,\n    text: 'Created date',\n  },\n  {\n    value: 'actions',\n    filterable: false,\n    sortable: false,\n    text: '',\n    attrs: {\n      class: 'w-24',\n    },\n  },\n];\n\nconst state = shallowReactive({\n  id: '',\n  query: '',\n});\nconst addState = shallowReactive({\n  type: '',\n  name: '',\n  value: '',\n  show: false,\n});\n\nfunction deleteCredential({ id }) {\n  dbStorage.credentials.delete(id);\n}\nfunction saveCredential() {\n  if (!addState.name) return;\n\n  const trimmedName = addState.name.trim();\n  const duplicateName = credentials.value.some(\n    ({ name, id }) => name.trim() === trimmedName && id !== state.id\n  );\n\n  if (duplicateName) {\n    toast.error(`You alread add \"${trimmedName}\" credential`);\n    return;\n  }\n\n  const encryptedValue = credentialUtil.encrypt(addState.value);\n\n  dbStorage.credentials\n    .add({\n      name: trimmedName,\n      createdAt: Date.now(),\n      value: encryptedValue,\n    })\n    .then(() => {\n      addState.show = false;\n    });\n}\n\nwatch(\n  () => addState.show,\n  (value) => {\n    if (value) return;\n\n    state.id = '';\n    Object.assign(addState, {\n      name: '',\n      type: '',\n      value: '',\n      show: false,\n    });\n  }\n);\n</script>\n"
  },
  {
    "path": "src/components/newtab/storage/StorageEditTable.vue",
    "content": "<template>\n  <ui-modal :model-value=\"modelValue\" persist custom-content>\n    <ui-card\n      padding=\"p-0\"\n      class=\"flex w-full max-w-xl flex-col\"\n      style=\"height: 600px\"\n    >\n      <p class=\"p-4 font-semibold\">\n        {{ title || t('storage.table.add') }}\n      </p>\n      <div class=\"scroll flex-1 overflow-auto px-4 pb-4\">\n        <ui-input\n          v-model=\"state.name\"\n          class=\"-mt-1 w-full\"\n          label=\"Table name\"\n          placeholder=\"My table\"\n        />\n        <div class=\"mt-4 flex items-center\">\n          <p class=\"flex-1\">Columns</p>\n          <ui-button icon :title=\"t('common.add')\" @click=\"addColumn\">\n            <v-remixicon name=\"riAddLine\" />\n          </ui-button>\n        </div>\n        <p\n          v-if=\"state.columns && state.columns.length === 0\"\n          class=\"my-4 text-center text-gray-600 dark:text-gray-300\"\n        >\n          {{ t('message.noData') }}\n        </p>\n        <draggable\n          v-model=\"state.columns\"\n          tag=\"ul\"\n          handle=\".handle\"\n          item-key=\"id\"\n          class=\"mt-4 space-y-2\"\n        >\n          <template #item=\"{ element: column, index }\">\n            <li class=\"flex items-center space-x-2\">\n              <span class=\"handle cursor-move\">\n                <v-remixicon name=\"mdiDrag\" />\n              </span>\n              <ui-input\n                :model-value=\"column.name\"\n                :placeholder=\"t('workflow.table.column.name')\"\n                class=\"flex-1\"\n                @blur=\"updateColumnName(index, $event.target)\"\n              />\n              <ui-select\n                v-model=\"column.type\"\n                class=\"flex-1\"\n                :placeholder=\"t('workflow.table.column.type')\"\n              >\n                <option\n                  v-for=\"type in dataTypes\"\n                  :key=\"type.id\"\n                  :value=\"type.id\"\n                >\n                  {{ type.name }}\n                </option>\n              </ui-select>\n              <button @click=\"deleteColumn(index)\">\n                <v-remixicon name=\"riDeleteBin7Line\" />\n              </button>\n            </li>\n          </template>\n        </draggable>\n      </div>\n      <div class=\"p-4 text-right\">\n        <ui-button class=\"mr-4\" @click=\"clearTempTables(true)\">\n          {{ t('common.cancel') }}\n        </ui-button>\n        <ui-button\n          :disabled=\"!state.name || state.columns.length === 0\"\n          variant=\"accent\"\n          @click=\"saveTable\"\n        >\n          {{ t('common.save') }}\n        </ui-button>\n      </div>\n    </ui-card>\n  </ui-modal>\n</template>\n<script setup>\nimport { reactive, toRaw, watch } from 'vue';\nimport { useI18n } from 'vue-i18n';\nimport { nanoid } from 'nanoid';\nimport draggable from 'vuedraggable';\nimport cloneDeep from 'lodash.clonedeep';\nimport { dataTypes } from '@/utils/constants/table';\n\nconst props = defineProps({\n  modelValue: {\n    type: Boolean,\n    default: false,\n  },\n  name: {\n    type: String,\n    default: '',\n  },\n  title: {\n    type: String,\n    default: '',\n  },\n  columns: {\n    type: Array,\n    default: () => [],\n  },\n});\nconst emit = defineEmits(['update:modelValue', 'save']);\n\nconst { t } = useI18n();\n\nlet changes = {};\nconst state = reactive({\n  name: '',\n  columns: [],\n});\n\nfunction getColumnName(name) {\n  const columnName = name.replace(/[\\s@[\\]]/g, '');\n  const isColumnExists = state.columns.some(\n    (column) => column.name === columnName\n  );\n\n  if (isColumnExists || columnName.trim() === '') return '';\n\n  return columnName;\n}\nfunction updateColumnName(index, target) {\n  const columnName = getColumnName(target.value);\n  const { id, name } = state.columns[index];\n  if (!columnName) {\n    target.value = name;\n    return;\n  }\n\n  changes[id] = { type: 'rename', id, oldValue: name, newValue: columnName };\n  state.columns[index].name = columnName;\n}\nfunction saveTable() {\n  const rawState = {\n    ...toRaw(state),\n    columns: state.columns.map(toRaw),\n  };\n\n  emit('save', { ...rawState, changes });\n}\nfunction addColumn() {\n  const columnId = nanoid(5);\n  const columnName = `column_${columnId}`;\n\n  changes[columnId] = {\n    type: 'add',\n    id: columnId,\n    name: columnName,\n  };\n\n  state.columns.push({\n    id: columnId,\n    type: 'string',\n    name: columnName,\n  });\n}\nfunction clearTempTables(close = false) {\n  state.name = '';\n  state.columns = [];\n  changes = {};\n\n  if (close) {\n    emit('update:modelValue', false);\n  }\n}\nfunction deleteColumn(index) {\n  const column = state.columns[index];\n  changes[column.id] = { type: 'delete', id: column.id, name: column.name };\n\n  state.columns.splice(index, 1);\n}\n\nwatch(\n  () => props.modelValue,\n  () => {\n    if (props.modelValue) {\n      Object.assign(state, {\n        name: `${props.name}`,\n        columns: cloneDeep(props.columns),\n      });\n    } else {\n      clearTempTables();\n    }\n  }\n);\n</script>\n"
  },
  {
    "path": "src/components/newtab/storage/StorageTables.vue",
    "content": "<template>\n  <div class=\"mt-6 flex\">\n    <ui-input\n      v-model=\"state.query\"\n      :placeholder=\"t('common.search')\"\n      prepend-icon=\"riSearch2Line\"\n    />\n    <div class=\"grow\"></div>\n    <ui-button\n      variant=\"accent\"\n      class=\"ml-4\"\n      style=\"min-width: 120px\"\n      @click=\"state.showAddTable = true\"\n    >\n      {{ t('storage.table.add') }}\n    </ui-button>\n  </div>\n  <div class=\"scroll w-full overflow-x-auto\">\n    <ui-table\n      item-key=\"id\"\n      :headers=\"tableHeaders\"\n      :items=\"items\"\n      :search=\"state.query\"\n      class=\"mt-4 w-full\"\n    >\n      <template #item-name=\"{ item }\">\n        <router-link\n          :to=\"`/storage/tables/${item.id}`\"\n          class=\"block w-full\"\n          style=\"min-height: 29px\"\n        >\n          {{ item.name }}\n        </router-link>\n      </template>\n      <template #item-createdAt=\"{ item }\">\n        {{ formatDate(item.createdAt) }}\n      </template>\n      <template #item-modifiedAt=\"{ item }\">\n        {{ formatDate(item.modifiedAt) }}\n      </template>\n      <template #item-actions=\"{ item }\">\n        <v-remixicon\n          name=\"riDeleteBin7Line\"\n          class=\"cursor-pointer\"\n          @click=\"deleteTable(item)\"\n        />\n      </template>\n    </ui-table>\n  </div>\n  <storage-edit-table v-model=\"state.showAddTable\" @save=\"saveTable\" />\n</template>\n<script setup>\nimport { reactive } from 'vue';\nimport { useI18n } from 'vue-i18n';\nimport dayjs from 'dayjs';\nimport { useDialog } from '@/composable/dialog';\nimport { useWorkflowStore } from '@/stores/workflow';\nimport { useLiveQuery } from '@/composable/liveQuery';\nimport dbStorage from '@/db/storage';\nimport StorageEditTable from './StorageEditTable.vue';\n\nconst { t } = useI18n();\nconst dialog = useDialog();\nconst workflowStore = useWorkflowStore();\n\nconst state = reactive({\n  query: '',\n  showAddTable: false,\n});\n\nconst tableHeaders = [\n  {\n    value: 'name',\n    filterable: true,\n    text: t('common.name'),\n    attrs: {\n      class: 'w-4/12',\n      style: 'min-width: 120px',\n    },\n  },\n  {\n    align: 'center',\n    value: 'createdAt',\n    text: t('storage.table.createdAt'),\n    attrs: {\n      style: 'min-width: 200px',\n    },\n  },\n  {\n    align: 'center',\n    value: 'modifiedAt',\n    text: t('storage.table.modifiedAt'),\n    attrs: {\n      style: 'min-width: 200px',\n    },\n  },\n  {\n    value: 'rowsCount',\n    align: 'center',\n    text: t('storage.table.rowsCount'),\n  },\n  {\n    value: 'actions',\n    align: 'right',\n    text: '',\n    sortable: false,\n  },\n];\nconst items = useLiveQuery(() => dbStorage.tablesItems.reverse().toArray());\n\nfunction formatDate(date) {\n  return dayjs(date).format('DD MMM YYYY, hh:mm:ss A');\n}\nasync function saveTable({ columns, name }) {\n  try {\n    const columnsIndex = columns.reduce(\n      (acc, column) => {\n        acc[column.id] = {\n          index: 0,\n          type: column.type,\n          name: column.name,\n        };\n\n        return acc;\n      },\n      { column: { index: 0, type: 'any', name: 'column' } }\n    );\n\n    const tableId = await dbStorage.tablesItems.add({\n      rowsCount: 0,\n      name,\n      createdAt: Date.now(),\n      modifiedAt: Date.now(),\n      columns,\n    });\n    await dbStorage.tablesData.add({\n      tableId,\n      items: [],\n      columnsIndex,\n    });\n\n    state.showAddTable = false;\n  } catch (error) {\n    console.error(error);\n  }\n}\nfunction deleteTable(table) {\n  dialog.confirm({\n    title: t('storage.table.delete'),\n    okVariant: 'danger',\n    body: t('message.delete', { name: table.name }),\n    onConfirm: async () => {\n      try {\n        await dbStorage.tablesItems.where('id').equals(table.id).delete();\n        await dbStorage.tablesData.where('tableId').equals(table.id).delete();\n\n        await workflowStore.update({\n          id: (workflow) => workflow.connectedTable === table.id,\n          data: { connectedTable: null },\n        });\n      } catch (error) {\n        console.error(error);\n      }\n    },\n  });\n}\n</script>\n"
  },
  {
    "path": "src/components/newtab/storage/StorageVariables.vue",
    "content": "<template>\n  <div class=\"mt-6 flex\">\n    <ui-input\n      v-model=\"state.query\"\n      :placeholder=\"t('common.search')\"\n      prepend-icon=\"riSearch2Line\"\n    />\n    <div class=\"grow\"></div>\n    <ui-button\n      variant=\"accent\"\n      style=\"min-width: 125px\"\n      class=\"ml-4\"\n      @click=\"editState.show = true\"\n    >\n      Add variable\n    </ui-button>\n  </div>\n  <ui-table\n    item-key=\"id\"\n    :headers=\"tableHeaders\"\n    :items=\"variables\"\n    :search=\"state.query\"\n    class=\"mt-4 w-full\"\n  >\n    <template #item-actions=\"{ item }\">\n      <v-remixicon\n        name=\"riPencilLine\"\n        class=\"mr-4 inline-block cursor-pointer\"\n        @click=\"editVariable(item)\"\n      />\n      <v-remixicon\n        name=\"riDeleteBin7Line\"\n        class=\"inline-block cursor-pointer\"\n        @click=\"deleteVariable(item)\"\n      />\n    </template>\n  </ui-table>\n  <ui-modal\n    v-model=\"editState.show\"\n    :title=\"`${editState.type === 'edit' ? 'Edit' : 'Add'} variable`\"\n  >\n    <ui-input v-model=\"editState.name\" placeholder=\"Name\" class=\"w-full\" />\n    <ui-textarea\n      v-model=\"editState.value\"\n      placeholder=\"value\"\n      class=\"mt-4 w-full\"\n    />\n    <div class=\"mt-8 text-right\">\n      <ui-button class=\"mr-4\" @click=\"editState.show = false\">\n        {{ t('common.cancel') }}\n      </ui-button>\n      <ui-button\n        :disabled=\"!editState.name || editState.disabled\"\n        variant=\"accent\"\n        @click=\"saveVariable\"\n      >\n        {{ t('common.save') }}\n      </ui-button>\n    </div>\n  </ui-modal>\n</template>\n<script setup>\nimport { shallowReactive, watch } from 'vue';\nimport { useI18n } from 'vue-i18n';\nimport { useToast } from 'vue-toastification';\nimport { parseJSON } from '@/utils/helper';\nimport { useLiveQuery } from '@/composable/liveQuery';\nimport dbStorage from '@/db/storage';\n\nconst { t } = useI18n();\nconst toast = useToast();\nconst variables = useLiveQuery(() => dbStorage.variables.toArray());\n\nconst tableHeaders = [\n  {\n    value: 'name',\n    filterable: true,\n    text: t('common.name'),\n    attrs: {\n      class: 'w-3/12 text-overflow',\n    },\n  },\n  {\n    value: 'value',\n    filterable: false,\n    text: 'Value',\n    attrs: {\n      class: 'flex-1 line-clamp',\n    },\n  },\n  {\n    value: 'actions',\n    filterable: false,\n    sortable: false,\n    text: '',\n    attrs: {\n      class: 'w-24',\n    },\n  },\n];\n\nconst state = shallowReactive({\n  id: '',\n  query: '',\n});\nconst editState = shallowReactive({\n  type: '',\n  name: '',\n  value: '',\n  show: false,\n});\n\nfunction deleteVariable({ id }) {\n  dbStorage.variables.delete(id);\n}\nfunction editVariable({ id, name, value }) {\n  state.id = id;\n  editState.name = name;\n  editState.value =\n    typeof value !== 'string' ? JSON.stringify(value, null, 2) : value;\n  editState.type = 'edit';\n  editState.show = true;\n}\nfunction saveVariable() {\n  if (!editState.name) return;\n\n  const trimmedName = editState.name.trim();\n  const duplicateName = variables.value.some(\n    ({ name, id }) => name.trim() === trimmedName && id !== state.id\n  );\n\n  if (duplicateName) {\n    toast.error(`You alread add \"${trimmedName}\" variable`);\n    return;\n  }\n\n  const varValue = parseJSON(editState.value, editState.value);\n\n  if (editState.type === 'edit') {\n    dbStorage.variables\n      .update(state.id, {\n        value: varValue,\n        name: trimmedName,\n      })\n      .then(() => {\n        editState.show = false;\n      });\n  } else {\n    dbStorage.variables\n      .add({\n        value: varValue,\n        name: trimmedName,\n      })\n      .then(() => {\n        editState.show = false;\n      });\n  }\n}\n\nwatch(\n  () => editState.show,\n  (value) => {\n    if (value) return;\n\n    state.id = '';\n    Object.assign(editState, {\n      name: '',\n      type: '',\n      value: '',\n      show: false,\n    });\n  }\n);\n</script>\n"
  },
  {
    "path": "src/components/newtab/workflow/WorkflowBlockList.vue",
    "content": "<template>\n  <ui-expand\n    hide-header-icon\n    header-class=\"flex items-center py-2 focus:ring-0 w-full text-left text-gray-600 dark:text-gray-200\"\n  >\n    <template #header=\"{ show }\">\n      <span :class=\"category.color\" class=\"h-3 w-3 rounded-full\"></span>\n      <p class=\"ml-2 flex-1 capitalize\">\n        {{ category.name }}\n      </p>\n      <v-remixicon :name=\"show ? 'riSubtractLine' : 'riAddLine'\" size=\"20\" />\n    </template>\n    <div class=\"mb-4 grid grid-cols-2 gap-2\">\n      <div\n        v-for=\"block in blocks\"\n        :key=\"block.id\"\n        :title=\"getBlockTitle(block)\"\n        draggable=\"true\"\n        class=\"bg-input group relative cursor-move select-none rounded-lg p-4 transition\"\n        @dragstart=\"$event.dataTransfer.setData('block', JSON.stringify(block))\"\n      >\n        <div\n          class=\"invisible absolute right-2 top-2 flex items-center text-gray-600 group-hover:visible dark:text-gray-300\"\n        >\n          <a\n            :href=\"`https://docs.extension.automa.site/blocks/${block.id}.html`\"\n            :title=\"t('common.docs')\"\n            target=\"_blank\"\n            rel=\"noopener\"\n          >\n            <v-remixicon name=\"riInformationLine\" size=\"18\" />\n          </a>\n          <span\n            :title=\"`${pinned.includes(block.id) ? 'Unpin' : 'Pin'} block`\"\n            class=\"ml-1 cursor-pointer\"\n            @click=\"$emit('pin', block)\"\n          >\n            <v-remixicon\n              size=\"18\"\n              :name=\"\n                pinned.includes(block.id) ? 'riPushpin2Fill' : 'riPushpin2Line'\n              \"\n            />\n          </span>\n        </div>\n        <img\n          v-if=\"block.icon.startsWith('http')\"\n          :src=\"block.icon\"\n          alt=\"\"\n          width=\"24\"\n          class=\"mb-2 dark:invert\"\n        />\n        <v-remixicon\n          v-else\n          :path=\"getIconPath(block.icon)\"\n          :name=\"block.icon\"\n          size=\"24\"\n          class=\"mb-2\"\n        />\n        <p class=\"text-overflow capitalize leading-tight\">\n          {{ block.name }}\n        </p>\n        <div\n          v-if=\"block.tag\"\n          class=\"flex items-center justify-center absolute top-0 right-0 min-w-[52px] h-[22px] group-hover:invisible rounded-tr-lg rounded-bl-[22px] rounded-tl-0 rounded-br-0 bg-[#79FFEB] dark:bg-[#2DD4BF] text-sm font-semibold dark:text-gray-900\"\n        >\n          {{ block.tag }}\n        </div>\n      </div>\n    </div>\n  </ui-expand>\n</template>\n<script setup>\nimport { getBlocks } from '@/utils/getSharedData';\nimport { useI18n } from 'vue-i18n';\n\ndefineProps({\n  category: {\n    type: Object,\n    default: () => ({}),\n  },\n  blocks: {\n    type: Array,\n    default: () => [],\n  },\n  pinned: {\n    type: Array,\n    default: () => [],\n  },\n});\ndefineEmits(['pin']);\n\nconst { t, te } = useI18n();\nconst blocksDetail = getBlocks();\n\nfunction getBlockTitle({ description, id, name }) {\n  const blockPath = `workflow.blocks.${id}`;\n  if (!te(blockPath)) return blocksDetail[id].name;\n\n  const descPath = `${blockPath}.${description ? 'description' : 'name'}`;\n  let blockDescription = te(descPath) ? t(descPath) : name;\n\n  if (description) {\n    blockDescription = `[${t(`${blockPath}.name`)}]\\n${blockDescription}`;\n  }\n\n  return blockDescription;\n}\nfunction getIconPath(path) {\n  if (path && path.startsWith('path')) {\n    const { 1: iconPath } = path.split(':');\n    return iconPath;\n  }\n\n  return '';\n}\n</script>\n"
  },
  {
    "path": "src/components/newtab/workflow/WorkflowDataTable.vue",
    "content": "<template>\n  <template v-if=\"!workflow.connectedTable\">\n    <ui-popover class=\"mb-4\">\n      <template #trigger>\n        <ui-button> Connect to a storage table </ui-button>\n      </template>\n      <p>Select a table</p>\n      <ui-list class=\"mt-2 max-h-80 w-64 space-y-1 overflow-auto\">\n        <p v-if=\"state.tableList.length === 0\">\n          {{ t('message.noData') }}\n        </p>\n        <ui-list-item\n          v-for=\"item in state.tableList\"\n          :key=\"item.id\"\n          class=\"text-overflow cursor-pointer\"\n          @click=\"connectTable(item)\"\n        >\n          {{ item.name }}\n        </ui-list-item>\n      </ui-list>\n    </ui-popover>\n    <div class=\"mb-4 flex\">\n      <ui-input\n        v-model=\"state.query\"\n        autofocus\n        autocomplete=\"off\"\n        :placeholder=\"t('workflow.table.column.name')\"\n        class=\"mr-2 flex-1\"\n        @keyup.enter=\"addColumn\"\n        @keyup.esc=\"$emit('close')\"\n      />\n      <ui-button variant=\"accent\" @click=\"addColumn\">\n        {{ t('common.add') }}\n      </ui-button>\n    </div>\n  </template>\n  <div\n    v-else-if=\"state.connectedTable\"\n    class=\"mb-4 flex items-center rounded-md bg-green-200 py-2 px-4 text-black dark:bg-green-300\"\n  >\n    <p class=\"mr-1\">\n      This workflow is connected to the\n      <router-link\n        :to=\"`/storage/tables/${state.connectedTable.id}`\"\n        class=\"underline\"\n      >\n        {{ state.connectedTable.name }}\n      </router-link>\n      table\n    </p>\n    <v-remixicon\n      name=\"riLinkUnlinkM\"\n      title=\"Disconnect table\"\n      class=\"cursor-pointer\"\n      @click=\"disconnectTable\"\n    />\n  </div>\n  <div\n    class=\"scroll overflow-y-auto px-1\"\n    style=\"max-height: calc(100vh - 16rem); min-height: 300px\"\n  >\n    <p v-if=\"columns.length === 0\" class=\"mt-4 text-center\">\n      {{ t('message.noData') }}\n    </p>\n    <ul v-else class=\"space-y-2 py-1\">\n      <li\n        v-for=\"(column, index) in columns\"\n        :key=\"column.id\"\n        class=\"flex items-center space-x-2\"\n      >\n        <ui-input\n          :disabled=\"Boolean(workflow.connectedTable)\"\n          :model-value=\"columns[index].name\"\n          :placeholder=\"t('workflow.table.column.name')\"\n          class=\"flex-1\"\n          @blur=\"updateColumnName(index, $event.target)\"\n        />\n        <ui-select\n          v-model=\"columns[index].type\"\n          :disabled=\"Boolean(workflow.connectedTable)\"\n          :placeholder=\"t('workflow.table.column.type')\"\n          class=\"flex-1\"\n        >\n          <option v-for=\"type in dataTypes\" :key=\"type.id\" :value=\"type.id\">\n            {{ type.name }}\n          </option>\n        </ui-select>\n        <button\n          v-if=\"!Boolean(workflow.connectedTable)\"\n          @click=\"state.columns.splice(index, 1)\"\n        >\n          <v-remixicon name=\"riDeleteBin7Line\" />\n        </button>\n      </li>\n    </ul>\n  </div>\n</template>\n<script setup>\nimport { computed, onMounted, watch, reactive } from 'vue';\nimport { nanoid } from 'nanoid';\nimport { useI18n } from 'vue-i18n';\nimport dbStorage from '@/db/storage';\nimport { debounce } from '@/utils/helper';\nimport { dataTypes } from '@/utils/constants/table';\nimport { useWorkflowStore } from '@/stores/workflow';\n\nconst props = defineProps({\n  workflow: {\n    type: Object,\n    default: () => ({}),\n  },\n});\nconst emit = defineEmits([\n  'update',\n  'close',\n  'change',\n  'connect',\n  'disconnect',\n]);\n\nconst { t } = useI18n();\nconst workflowStore = useWorkflowStore();\n\nconst state = reactive({\n  query: '',\n  columns: [],\n  tableList: [],\n  connectedTable: null,\n});\nconst columns = computed({\n  get() {\n    if (state.connectedTable) return state.connectedTable.columns;\n\n    return state.columns;\n  },\n  set(value) {\n    state.columns = value;\n  },\n});\n\nfunction getColumnName(name) {\n  const columnName = name.replace(/[\\s@[\\]]/g, '');\n  const isColumnExists = state.columns.some(\n    (column) => column.name === columnName\n  );\n\n  if (isColumnExists || columnName.trim() === '') return '';\n\n  return columnName;\n}\nfunction updateColumnName(index, target) {\n  const columnName = getColumnName(target.value);\n\n  if (!columnName) {\n    target.value = state.columns[index].name;\n    return;\n  }\n\n  state.columns[index].name = columnName;\n}\nfunction addColumn() {\n  const columnName = getColumnName(state.query);\n\n  if (!columnName) return;\n\n  state.columns.push({ id: nanoid(5), name: columnName, type: 'string' });\n  state.query = '';\n}\nfunction connectTable(table) {\n  workflowStore\n    .update({\n      id: props.workflow.id,\n      data: { connectedTable: table.id },\n    })\n    .then(() => {\n      emit('connect');\n      state.query = '';\n      state.connectedTable = table;\n    });\n}\nfunction disconnectTable() {\n  workflowStore\n    .update({\n      id: props.workflow.id,\n      data: { connectedTable: null },\n    })\n    .then(() => {\n      state.columns = props.workflow.table;\n      state.connectedTable = null;\n      emit('disconnect');\n    });\n}\n\nwatch(\n  () => state.columns,\n  debounce((newValue) => {\n    if (props.workflow.connectedTable) return;\n\n    const data = { table: newValue };\n\n    emit('update', data);\n    emit('change', data);\n  }, 250),\n  { deep: true }\n);\n\nonMounted(async () => {\n  state.tableList = await dbStorage.tablesItems.toArray();\n  if (props.workflow.connectedTable) {\n    const findTable = state.tableList.find(\n      (table) => table.id === props.workflow.connectedTable\n    );\n\n    if (findTable) {\n      state.connectedTable = findTable;\n      return;\n    }\n    emit('change', { connectedTable: null });\n    emit('update', { connectedTable: null });\n  }\n\n  let isChanged = false;\n  state.columns =\n    props.workflow.table?.map((column) => {\n      if (!column.id) {\n        isChanged = true;\n        column.id = column.name;\n      }\n\n      return column;\n    }) || [];\n\n  if (isChanged) {\n    const data = { table: state.columns };\n\n    emit('change', data);\n    emit('update', data);\n  }\n});\n</script>\n"
  },
  {
    "path": "src/components/newtab/workflow/WorkflowDetailsCard.vue",
    "content": "<template>\n  <div class=\"mb-2 mt-1 flex items-start px-4\">\n    <ui-popover class=\"mr-2 h-8\">\n      <template #trigger>\n        <span\n          :title=\"t('workflow.sidebar.workflowIcon')\"\n          class=\"inline-block h-full cursor-pointer\"\n        >\n          <ui-img\n            v-if=\"workflow.icon.startsWith('http')\"\n            :src=\"workflow.icon\"\n            class=\"h-8 w-8\"\n          />\n          <v-remixicon v-else :name=\"workflow.icon\" size=\"26\" class=\"mt-1\" />\n        </span>\n      </template>\n      <div class=\"w-56\">\n        <p class=\"mb-2\">{{ t('workflow.sidebar.workflowIcon') }}</p>\n        <div class=\"mb-2 grid grid-cols-5 gap-1\">\n          <span\n            v-for=\"icon in icons\"\n            :key=\"icon\"\n            class=\"hoverable inline-block cursor-pointer rounded-lg p-2 text-center\"\n            @click=\"$emit('update', { icon })\"\n          >\n            <v-remixicon :name=\"icon\" />\n          </span>\n        </div>\n        <ui-input\n          :model-value=\"workflow.icon.startsWith('http') ? workflow.icon : ''\"\n          type=\"url\"\n          placeholder=\"http://example.com/img.png\"\n          label=\"Icon URL\"\n          @change=\"updateWorkflowIcon\"\n        />\n      </div>\n    </ui-popover>\n    <div class=\"flex-1 overflow-hidden\">\n      <p class=\"text-overflow mt-1 text-lg font-semibold leading-tight\">\n        {{ workflow.name }}\n      </p>\n      <p\n        class=\"cursor-pointer leading-tight\"\n        :class=\"descriptionCollapsed ? 'line-clamp' : 'whitespace-pre-wrap'\"\n        @click=\"descriptionCollapsed = !descriptionCollapsed\"\n      >\n        {{ workflow.description }}\n      </p>\n    </div>\n  </div>\n  <ui-input\n    id=\"search-input\"\n    v-model=\"query\"\n    :placeholder=\"`${t('common.search')}... (${\n      shortcut['action:search'].readable\n    })`\"\n    prepend-icon=\"riSearch2Line\"\n    class=\"mt-4 mb-2 w-full px-4\"\n  />\n  <div class=\"scroll relative flex-1 overflow-auto bg-scroll px-4\">\n    <workflow-block-list\n      v-if=\"pinnedBlocksList.length > 0\"\n      :model-value=\"true\"\n      :blocks=\"pinnedBlocksList\"\n      :category=\"pinnedCategory\"\n      :pinned=\"pinnedBlocks\"\n      @pin=\"pinBlock\"\n    />\n    <workflow-block-list\n      v-for=\"(items, catId) in blocks\"\n      :key=\"catId\"\n      :model-value=\"true\"\n      :blocks=\"items\"\n      :category=\"categories[catId]\"\n      :pinned=\"pinnedBlocks\"\n      @pin=\"pinBlock\"\n    />\n  </div>\n</template>\n<script setup>\nimport { computed, ref, onMounted, watch, toRaw } from 'vue';\nimport { useI18n } from 'vue-i18n';\nimport browser from 'webextension-polyfill';\nimport { useShortcut } from '@/composable/shortcut';\nimport { categories } from '@/utils/shared';\nimport { getBlocks } from '@/utils/getSharedData';\nimport WorkflowBlockList from './WorkflowBlockList.vue';\n\ndefineProps({\n  workflow: {\n    type: Object,\n    default: () => ({}),\n  },\n  dataChanged: {\n    type: Boolean,\n    default: false,\n  },\n});\nconst emit = defineEmits(['update']);\n\nconst { t, te } = useI18n();\nconst shortcut = useShortcut('action:search', () => {\n  const searchInput = document.querySelector('#search-input input');\n\n  searchInput?.focus();\n});\n\nconst pinnedCategory = {\n  name: 'Pinned blocks',\n  color: 'bg-accent',\n};\nconst icons = [\n  'mdiPackageVariantClosed',\n  'riGlobalLine',\n  'riFileTextLine',\n  'riEqualizerLine',\n  'riTimerLine',\n  'riCalendarLine',\n  'riFlashlightLine',\n  'riLightbulbFlashLine',\n  'riDatabase2Line',\n  'riWindowLine',\n  'riCursorLine',\n  'riDownloadLine',\n  'riCommandLine',\n];\n\nconst copyBlocks = getBlocks();\ndelete copyBlocks['block-package'];\n\nconst blocksArr = Object.entries(copyBlocks).map(([key, block]) => {\n  const localeKey = `workflow.blocks.${key}.name`;\n\n  return {\n    ...block,\n    id: key,\n    name: te(localeKey) ? t(localeKey) : block.name,\n  };\n});\n\nconst descriptionCollapsed = ref(true);\n\nconst query = ref('');\nconst pinnedBlocks = ref([]);\n\nconst blocks = computed(() =>\n  blocksArr.reduce((arr, block) => {\n    if (\n      block.name.toLocaleLowerCase().includes(query.value.toLocaleLowerCase())\n    ) {\n      (arr[block.category] = arr[block.category] || []).push(block);\n    }\n\n    return arr;\n  }, {})\n);\nconst pinnedBlocksList = computed(() =>\n  pinnedBlocks.value\n    .map((id) => {\n      const namePath = `workflow.blocks.${id}.name`;\n\n      return {\n        ...copyBlocks[id],\n        id,\n        name: te(namePath) ? t(namePath) : copyBlocks[id].name,\n      };\n    })\n    .filter(({ name }) =>\n      name.toLocaleLowerCase().includes(query.value.toLocaleLowerCase())\n    )\n);\n\nfunction updateWorkflowIcon(value) {\n  if (!value.startsWith('http')) return;\n\n  const iconUrl = value.slice(0, 1024);\n\n  emit('update', { icon: iconUrl });\n}\nfunction pinBlock({ id }) {\n  const index = pinnedBlocks.value.indexOf(id);\n\n  if (index !== -1) pinnedBlocks.value.splice(index, 1);\n  else pinnedBlocks.value.push(id);\n}\n\nwatch(\n  pinnedBlocks,\n  () => {\n    browser.storage.local.set({\n      pinnedBlocks: toRaw(pinnedBlocks.value),\n    });\n  },\n  { deep: true }\n);\n\nonMounted(() => {\n  browser.storage.local.get('pinnedBlocks').then((item) => {\n    pinnedBlocks.value = item.pinnedBlocks || [];\n  });\n});\n</script>\n"
  },
  {
    "path": "src/components/newtab/workflow/WorkflowEditBlock.vue",
    "content": "<template>\n  <div id=\"workflow-edit-block\" class=\"scroll h-full overflow-auto px-4 py-1\">\n    <div\n      class=\"sticky top-0 z-20 mb-2 flex items-center space-x-2 bg-white pb-4 dark:bg-gray-800\"\n    >\n      <button @click=\"handleClose\">\n        <v-remixicon name=\"riArrowLeftLine\" />\n      </button>\n      <p class=\"inline-block font-semibold capitalize\">\n        {{ getBlockName() }}\n      </p>\n      <div class=\"grow\"></div>\n      <a\n        :title=\"t('common.docs')\"\n        :href=\"`https://docs.extension.automa.site/blocks/${data.id}.html`\"\n        rel=\"noopener\"\n        target=\"_blank\"\n        class=\"text-gray-600 dark:text-gray-200\"\n      >\n        <v-remixicon name=\"riInformationLine\" />\n      </a>\n    </div>\n    <component\n      :is=\"getEditComponent()\"\n      v-if=\"blockData\"\n      :key=\"data.itemId || data.blockId\"\n      v-model:data=\"blockData\"\n      :block-id=\"data.blockId\"\n      v-bind=\"{\n        fullData: data.id === 'conditions' ? data : null,\n        editor: data.id === 'conditions' ? editor : null,\n        connections: data.id === 'wait-connections' ? data.connections : null,\n      }\"\n    />\n  </div>\n</template>\n<script setup>\nimport customEditComponents from '@business/blocks/editComponents';\nimport { computed } from 'vue';\nimport { useI18n } from 'vue-i18n';\nimport { useToast } from 'vue-toastification';\n\nconst editComponents = require.context(\n  './edit',\n  false,\n  /^(?:.*\\/)?Edit[^/]*\\.vue$/\n);\n/* eslint-disable-next-line */\nconst components = editComponents.keys().reduce((acc, key) => {\n  const name = key.replace(/(.\\/)|\\.vue$/g, '');\n  const componentObj = editComponents(key)?.default ?? {};\n\n  acc[name] = componentObj;\n\n  return acc;\n}, {});\n\nObject.assign(components, customEditComponents());\n\nconst props = defineProps({\n  data: {\n    type: Object,\n    default: () => ({}),\n  },\n  editor: {\n    type: Object,\n    default: () => ({}),\n  },\n  workflow: {\n    type: Object,\n    default: () => ({}),\n  },\n  autocomplete: {\n    type: Object,\n    default: () => ({}),\n  },\n  dataChanged: Boolean,\n});\nconst emit = defineEmits(['close', 'update', 'update:autocomplete']);\n\nconst { t, te } = useI18n();\nconst toast = useToast();\n\nconst blockData = computed({\n  get() {\n    return props.data.data;\n  },\n  set(data) {\n    emit('update', data);\n  },\n});\n\nfunction isGoogleSheetsBlock() {\n  return ['google-sheets'].includes(props.data.id);\n}\n\nfunction validateBeforeClose() {\n  // 检查是否为Google Sheets相关区块\n  if (isGoogleSheetsBlock()) {\n    // 检查spreadsheetId是否为空，且不是create或add-sheet操作\n    const { spreadsheetId, type, range } = blockData.value;\n    const isNotCreateAction = !['create', 'add-sheet'].includes(type);\n\n    if (isNotCreateAction) {\n      const errors = [];\n\n      if (!spreadsheetId) {\n        errors.push(\n          t(\n            'workflow.blocks.google-sheets.spreadsheetId.required',\n            'Spreadsheet ID is required'\n          )\n        );\n      }\n\n      if (!range) {\n        errors.push(\n          t('workflow.blocks.google-sheets.range.required', 'Range is required')\n        );\n      }\n\n      if (errors.length > 0) {\n        errors.forEach((error) => toast.error(error));\n        return false;\n      }\n    }\n  }\n  return true;\n}\n\nfunction handleClose() {\n  if (validateBeforeClose()) {\n    emit('close');\n  }\n}\n\nfunction getEditComponent() {\n  const editComp = props.data.editComponent;\n  if (typeof editComp === 'object') return editComp;\n\n  return components[editComp];\n}\nfunction getBlockName() {\n  const key = `workflow.blocks.${props.data.id}.name`;\n\n  return te(key) ? t(key) : props.data.name;\n}\n</script>\n<style>\n#workflow-edit-block hr {\n  @apply dark:border-gray-700 dark:border-opacity-40 my-4;\n}\n</style>\n"
  },
  {
    "path": "src/components/newtab/workflow/WorkflowEditor.vue",
    "content": "<template>\n  <vue-flow\n    :id=\"props.id\"\n    :class=\"{ disabled: isDisabled }\"\n    :default-edge-options=\"{\n      type: 'custom',\n      updatable: true,\n      selectable: true,\n      markerEnd: settings.arrow ? MarkerType.ArrowClosed : '',\n    }\"\n  >\n    <Background />\n    <MiniMap\n      v-if=\"minimap\"\n      :node-class-name=\"minimapNodeClassName\"\n      class=\"hidden md:block\"\n    />\n    <div\n      v-if=\"editorControls\"\n      class=\"absolute left-0 bottom-0 z-10 flex w-full items-center p-4 md:pr-60\"\n    >\n      <slot name=\"controls-prepend\" />\n      <editor-search-blocks :editor=\"editor\" />\n      <div class=\"pointer-events-none grow\" />\n      <slot name=\"controls-append\" />\n      <button\n        v-tooltip.group=\"t('workflow.editor.resetZoom')\"\n        class=\"control-button mr-2\"\n        @click=\"editor.fitView()\"\n      >\n        <v-remixicon name=\"riFullscreenLine\" />\n      </button>\n      <div class=\"inline-block rounded-lg bg-white dark:bg-gray-800\">\n        <button\n          v-tooltip.group=\"t('workflow.editor.zoomOut')\"\n          class=\"relative z-10 rounded-lg p-2\"\n          @click=\"editor.zoomOut()\"\n        >\n          <v-remixicon name=\"riSubtractLine\" />\n        </button>\n        <hr class=\"inline-block h-6 border-r\" />\n        <button\n          v-tooltip.group=\"t('workflow.editor.zoomIn')\"\n          class=\"rounded-lg p-2\"\n          @click=\"editor.zoomIn()\"\n        >\n          <v-remixicon name=\"riAddLine\" />\n        </button>\n      </div>\n    </div>\n    <template v-for=\"(node, name) in nodeTypes\" :key=\"name\" #[name]=\"nodeProps\">\n      <component\n        :is=\"node\"\n        v-bind=\"{\n          ...nodeProps,\n          editor: name === 'node-BlockPackage' ? editor : null,\n        }\"\n        @delete=\"deleteBlock\"\n        @settings=\"initEditBlockSettings\"\n        @edit=\"editBlock(nodeProps, $event)\"\n        @update=\"updateBlockData(nodeProps.id, $event)\"\n      />\n    </template>\n    <template #edge-custom=\"edgeProps\">\n      <editor-custom-edge v-bind=\"edgeProps\" />\n    </template>\n    <ui-modal\n      v-model=\"blockSettingsState.show\"\n      :title=\"t('workflow.blocks.base.settings.title')\"\n      content-class=\"max-w-xl modal-block-settings\"\n      @close=\"clearBlockSettings\"\n    >\n      <edit-block-settings\n        :data=\"blockSettingsState.data\"\n        @change=\"updateBlockSettingsData\"\n      />\n    </ui-modal>\n  </vue-flow>\n</template>\n<script setup>\nimport { onMounted, onBeforeUnmount, watch, computed, reactive } from 'vue';\nimport { useI18n } from 'vue-i18n';\nimport {\n  VueFlow,\n  useVueFlow,\n  MarkerType,\n  getConnectedEdges,\n} from '@vue-flow/core';\nimport { Background } from '@vue-flow/background';\nimport { MiniMap } from '@vue-flow/minimap';\nimport cloneDeep from 'lodash.clonedeep';\nimport { useStore } from '@/stores/main';\nimport { getBlocks } from '@/utils/getSharedData';\nimport { categories } from '@/utils/shared';\nimport EditBlockSettings from './edit/EditBlockSettings.vue';\nimport EditorCustomEdge from './editor/EditorCustomEdge.vue';\nimport EditorSearchBlocks from './editor/EditorSearchBlocks.vue';\nimport '@vue-flow/minimap/dist/style.css';\n\nconst props = defineProps({\n  id: {\n    type: String,\n    default: 'editor',\n  },\n  data: {\n    type: Object,\n    default: () => ({\n      x: 0,\n      y: 0,\n      zoom: 0,\n      nodes: [],\n      edges: [],\n    }),\n  },\n  options: {\n    type: Object,\n    default: () => ({}),\n  },\n  editorControls: {\n    type: Boolean,\n    default: true,\n  },\n  minimap: {\n    type: Boolean,\n    default: true,\n  },\n  disabled: Boolean,\n});\nconst emit = defineEmits([\n  'edit',\n  'init',\n  'update:node',\n  'delete:node',\n  'update:settings',\n]);\n\nconst fallbackBlocks = {\n  BlockBasic: ['BlockExportData'],\n  BlockBasicWithFallback: ['BlockWebhook'],\n};\n\nconst isMac = navigator.appVersion.indexOf('Mac') !== -1;\nconst blockComponents = require.context('@/components/block', false, /\\.vue$/);\nconst nodeTypes = blockComponents.keys().reduce((acc, key) => {\n  const name = key.replace(/(.\\/)|\\.vue$/g, '');\n  const component = blockComponents(key).default;\n\n  if (fallbackBlocks[name]) {\n    fallbackBlocks[name].forEach((fallbackBlock) => {\n      acc[`node-${fallbackBlock}`] = component;\n    });\n  }\n\n  acc[`node-${name}`] = component;\n\n  return acc;\n}, {});\nconst getPosition = (position) => (Array.isArray(position) ? position : [0, 0]);\nconst setMinValue = (num, min) => (num < min ? min : num);\n\nconst { t } = useI18n();\nconst store = useStore();\nconst editor = useVueFlow({\n  id: props.id,\n  edgeUpdaterRadius: 20,\n  deleteKeyCode: 'Delete',\n  elevateEdgesOnSelect: true,\n  defaultZoom: props.data?.zoom ?? 1,\n  minZoom: setMinValue(+store.settings.editor.minZoom || 0.5, 0.1),\n  maxZoom: setMinValue(\n    +store.settings.editor.maxZoom || 1.2,\n    +store.settings.editor.minZoom + 0.1\n  ),\n  multiSelectionKeyCode: isMac ? 'Meta' : 'Control',\n  defaultPosition: getPosition(props.data?.position),\n  ...props.options,\n});\neditor.onConnect((params) => {\n  params.class = `source-${params.sourceHandle} target-${params.targetHandle}`;\n  params.updatable = true;\n  editor.addEdges([params]);\n});\neditor.onEdgeUpdate(({ edge, connection }) => {\n  const isBothOutput =\n    connection.sourceHandle.includes('output') &&\n    connection.targetHandle.includes('output');\n  if (isBothOutput) return;\n\n  Object.assign(edge, connection);\n});\n\nconst blocks = getBlocks();\nconst settings = store.settings.editor;\nconst isDisabled = computed(() => props.options.disabled ?? props.disabled);\n\nconst blockSettingsState = reactive({\n  show: false,\n  data: {},\n});\n\nfunction initEditBlockSettings({ blockId, details, data, itemId }) {\n  blockSettingsState.data = {\n    itemId,\n    blockId,\n    id: details.id,\n    data: cloneDeep(data),\n  };\n  blockSettingsState.show = true;\n}\nfunction clearBlockSettings() {\n  Object.assign(blockSettingsState, {\n    data: null,\n    show: false,\n  });\n}\nfunction minimapNodeClassName({ label }) {\n  const { category } = blocks[label];\n  const { color } = categories[category];\n\n  return color;\n}\nfunction updateBlockData(nodeId, data = {}) {\n  if (isDisabled.value) return;\n\n  const node = editor.findNode(nodeId);\n  node.data = { ...node.data, ...data };\n\n  emit('update:node', node);\n}\nfunction updateBlockSettingsData(newSettings) {\n  if (isDisabled.value) return;\n\n  const nodeId = blockSettingsState.data.blockId;\n  const node = editor.findNode(nodeId);\n\n  if (blockSettingsState.data.itemId) {\n    const index = node.data.blocks.findIndex(\n      (item) => item.itemId === blockSettingsState.data.itemId\n    );\n    if (index === -1) return;\n\n    node.data.blocks[index].data = {\n      ...node.data.blocks[index].data,\n      ...newSettings,\n    };\n  } else {\n    node.data = { ...node.data, ...newSettings };\n  }\n\n  emit('update:settings', {\n    settings: newSettings,\n    itemId: blockSettingsState.data.itemId,\n    blockId: blockSettingsState.data.blockId,\n  });\n}\nfunction editBlock({ id, label, data }, additionalData = {}) {\n  if (isDisabled.value) return;\n\n  emit('edit', {\n    id: label,\n    blockId: id,\n    data: cloneDeep(data),\n    ...additionalData,\n  });\n}\nfunction deleteBlock(nodeId) {\n  if (isDisabled.value) return;\n\n  editor.removeNodes([nodeId]);\n  emit('delete:node', nodeId);\n}\nfunction onMousedown(event) {\n  if (isDisabled.value && event.shiftKey) {\n    event.stopPropagation();\n    event.preventDefault();\n  }\n}\nfunction applyFlowData() {\n  if (settings.snapToGrid) {\n    editor.snapToGrid.value = true;\n    editor.snapGrid.value = Object.values(settings.snapGrid);\n  }\n\n  editor.setNodes(\n    props.data?.nodes?.map((node) => ({ ...node, events: {} })) || []\n  );\n  editor.setEdges(props.data?.edges || []);\n  editor.setViewport({\n    x: props.data?.x || 0,\n    y: props.data?.y || 0,\n    zoom: props.data?.zoom || 1,\n  });\n}\n\nwatch(\n  () => props.disabled,\n  (value) => {\n    const keys = [\n      'nodesDraggable',\n      'edgesUpdatable',\n      'nodesConnectable',\n      'elementsSelectable',\n    ];\n\n    keys.forEach((key) => {\n      editor[key].value = !value;\n    });\n  },\n  { immediate: true }\n);\nwatch(editor.getSelectedNodes, (nodes, _, cleanup) => {\n  const connectedEdges = getConnectedEdges(nodes, editor.getEdges.value);\n\n  connectedEdges.forEach((edge) => {\n    edge.class = 'connected-edges';\n  });\n\n  cleanup(() => {\n    connectedEdges.forEach((edge) => {\n      edge.class = undefined;\n    });\n  });\n});\n\nonMounted(() => {\n  applyFlowData();\n  window.addEventListener('mousedown', onMousedown, true);\n  emit('init', editor);\n});\nonBeforeUnmount(() => {\n  window.removeEventListener('mousedown', onMousedown, true);\n});\n</script>\n<style>\n@import '@vue-flow/core/dist/style.css';\n@import '@vue-flow/core/dist/theme-default.css';\n\n.control-button {\n  @apply p-2 rounded-lg bg-white dark:bg-gray-800 transition-colors;\n}\n</style>\n"
  },
  {
    "path": "src/components/newtab/workflow/WorkflowGlobalData.vue",
    "content": "<template>\n  <div class=\"global-data\">\n    <p class=\"text-right\" title=\"Characters limit\">\n      {{ globalData.length }}/{{ maxLength.toLocaleString() }}\n    </p>\n    <shared-codemirror\n      v-model=\"globalData\"\n      style=\"height: calc(100vh - 10rem)\"\n      lang=\"json\"\n    />\n  </div>\n</template>\n<script setup>\nimport { ref, watch, defineAsyncComponent } from 'vue';\nimport { debounce } from '@/utils/helper';\n\nconst SharedCodemirror = defineAsyncComponent(() =>\n  import('@/components/newtab/shared/SharedCodemirror.vue')\n);\n\nconst props = defineProps({\n  workflow: {\n    type: Object,\n    default: () => ({}),\n  },\n});\nconst emit = defineEmits(['update']);\n\nconst maxLength = 1e4;\nconst globalData = ref(`${props.workflow.globalData}`);\n\nwatch(\n  globalData,\n  debounce((value) => {\n    let newValue = value;\n\n    if (value.length > maxLength) {\n      newValue = value.slice(0, maxLength);\n      globalData.value = newValue;\n    }\n\n    emit('update', { globalData: newValue });\n  }, 250)\n);\n</script>\n"
  },
  {
    "path": "src/components/newtab/workflow/WorkflowProtect.vue",
    "content": "<template>\n  <div>\n    <form\n      class=\"mb-4 flex w-full items-center\"\n      @submit.prevent=\"protectWorkflow\"\n    >\n      <ui-input\n        v-model=\"state.password\"\n        :placeholder=\"t('common.password')\"\n        :type=\"state.showPassword ? 'text' : 'password'\"\n        input-class=\"pr-10\"\n        autofocus\n        class=\"mr-6 flex-1\"\n      >\n        <template #append>\n          <v-remixicon\n            :name=\"state.showPassword ? 'riEyeOffLine' : 'riEyeLine'\"\n            class=\"absolute right-2\"\n            @click=\"state.showPassword = !state.showPassword\"\n          />\n        </template>\n      </ui-input>\n      <ui-button variant=\"accent\">\n        {{ t('workflow.protect.button') }}\n      </ui-button>\n    </form>\n    <p>\n      {{ t('workflow.protect.note') }}\n    </p>\n  </div>\n</template>\n<script setup>\nimport { shallowReactive } from 'vue';\nimport { useI18n } from 'vue-i18n';\nimport { nanoid } from 'nanoid';\nimport AES from 'crypto-js/aes';\nimport hmacSHA256 from 'crypto-js/hmac-sha256';\nimport getPassKey from '@/utils/getPassKey';\n\nconst props = defineProps({\n  workflow: {\n    type: Object,\n    default: () => ({}),\n  },\n});\nconst emit = defineEmits(['update', 'close']);\n\nconst { t } = useI18n();\n\nconst state = shallowReactive({\n  password: '',\n  showPassword: false,\n});\n\nasync function protectWorkflow() {\n  const key = getPassKey(nanoid());\n  const encryptedPass = AES.encrypt(state.password, key).toString();\n  const hmac = hmacSHA256(encryptedPass, state.password).toString();\n\n  const { drawflow } = props.workflow;\n  const flow =\n    typeof drawflow === 'string' ? drawflow : JSON.stringify(drawflow);\n\n  emit('update', {\n    isProtected: true,\n    pass: hmac + encryptedPass,\n    drawflow: AES.encrypt(flow, state.password).toString(),\n  });\n  emit('close');\n}\n</script>\n"
  },
  {
    "path": "src/components/newtab/workflow/WorkflowRunning.vue",
    "content": "<template>\n  <div class=\"grid grid-cols-2 gap-4\">\n    <ui-card v-for=\"item in data\" :key=\"item\">\n      <div class=\"mb-4 flex items-center\">\n        <div class=\"text-overflow mr-4 flex-1\">\n          <p class=\"text-overflow mr-2 w-full\">{{ item.state.name }}</p>\n          <p\n            class=\"text-overflow mr-2 w-full leading-tight text-gray-600 dark:text-gray-200\"\n            :title=\"`Started at: ${formatDate(\n              item.state.startedTimestamp,\n              'DD MMM, hh:mm A'\n            )}`\"\n          >\n            {{ formatDate(item.state.startedTimestamp, 'relative') }}\n          </p>\n        </div>\n        <ui-button\n          v-if=\"item.state.tabId\"\n          icon\n          class=\"mr-2\"\n          title=\"Open tab\"\n          @click=\"openTab(item.state.tabId)\"\n        >\n          <v-remixicon name=\"riExternalLinkLine\" />\n        </ui-button>\n        <ui-button variant=\"accent\" @click=\"stopWorkflow(item)\">\n          <v-remixicon name=\"riStopLine\" class=\"mr-2 -ml-1\" />\n          <span>{{ t('common.stop') }}</span>\n        </ui-button>\n      </div>\n      <div class=\"bg-box-transparent flex items-center rounded-lg px-4 py-2\">\n        <template v-if=\"item.state.currentBlock\">\n          <v-remixicon :name=\"getBlock(item).icon\" />\n          <p class=\"ml-2 mr-4 flex-1\">{{ getBlock(item).name }}</p>\n          <ui-spinner color=\"text-accent\" size=\"20\" />\n        </template>\n        <p v-else>{{ t('message.noBlock') }}</p>\n      </div>\n    </ui-card>\n  </div>\n</template>\n<script setup>\nimport browser from 'webextension-polyfill';\nimport { useI18n } from 'vue-i18n';\nimport { getBlocks } from '@/utils/getSharedData';\nimport dayjs from '@/lib/dayjs';\nimport RendererWorkflowService from '@/service/renderer/RendererWorkflowService';\n\ndefineProps({\n  data: {\n    type: Array,\n    default: () => [],\n  },\n});\n\nconst { t } = useI18n();\nconst blocks = getBlocks();\n\nfunction getBlock(item) {\n  if (!item.state.currentBlock) return {};\n\n  return blocks[item.state.currentBlock.name];\n}\nfunction formatDate(date, format) {\n  if (format === 'relative') return dayjs(date).fromNow();\n\n  return dayjs(date).format(format);\n}\nfunction openTab(tabId) {\n  browser.tabs.update(tabId, { active: true });\n}\nfunction stopWorkflow(item) {\n  RendererWorkflowService.stopWorkflowExecution(item);\n}\n</script>\n"
  },
  {
    "path": "src/components/newtab/workflow/WorkflowSettings.vue",
    "content": "<template>\n  <ui-card padding=\"p-0\" class=\"workflow-settings w-full max-w-2xl\">\n    <div class=\"flex items-center px-4 pt-4\">\n      <p class=\"flex-1\">\n        {{ t('common.settings') }}\n      </p>\n      <v-remixicon\n        name=\"riCloseLine\"\n        class=\"cursor-pointer\"\n        @click=\"$emit('close')\"\n      />\n    </div>\n    <div class=\"space-x-2 px-4 pt-2\">\n      <ui-tabs v-model=\"activeTab\" class=\"space-x-2\">\n        <ui-tab v-for=\"tab in tabs\" :key=\"tab.value\" :value=\"tab.value\">\n          {{ tab.name }}\n        </ui-tab>\n      </ui-tabs>\n    </div>\n    <ui-tab-panels\n      v-model=\"activeTab\"\n      class=\"scroll settings-content overflow-auto p-4\"\n      style=\"height: calc(100vh - 10rem); max-height: 600px\"\n    >\n      <ui-tab-panel v-for=\"tab in tabs\" :key=\"tab.value\" :value=\"tab.value\">\n        <component\n          :is=\"tab.component\"\n          :settings=\"settings\"\n          @update=\"settings[$event.key] = $event.value\"\n        />\n      </ui-tab-panel>\n    </ui-tab-panels>\n  </ui-card>\n</template>\n<script setup>\nimport { onMounted, ref, reactive, watch } from 'vue';\nimport { useI18n } from 'vue-i18n';\nimport cloneDeep from 'lodash.clonedeep';\nimport { debounce } from '@/utils/helper';\nimport SettingsTable from './settings/SettingsTable.vue';\nimport SettingsBlocks from './settings/SettingsBlocks.vue';\nimport SettingsEvents from './settings/SettingsEvents.vue';\nimport SettingsGeneral from './settings/SettingsGeneral.vue';\n\nconst props = defineProps({\n  workflow: {\n    type: Object,\n    default: () => ({}),\n  },\n});\nconst emit = defineEmits(['update', 'close']);\n\nconst { t } = useI18n();\n\nconst tabs = [\n  {\n    value: 'general',\n    component: SettingsGeneral,\n    name: t('settings.menu.general'),\n  },\n  {\n    value: 'table',\n    component: SettingsTable,\n    name: t('workflow.table.title'),\n  },\n  {\n    value: 'blocks',\n    component: SettingsBlocks,\n    name: t('workflow.blocks.base.title'),\n  },\n  {\n    value: 'events',\n    component: SettingsEvents,\n    name: t('workflow.events.title'),\n  },\n];\n\nconst activeTab = ref('general');\nconst settings = reactive({\n  publicId: '',\n  restartTimes: 3,\n  notification: true,\n  tabLoadTimeout: 30000,\n  inputAutocomplete: true,\n  insertDefaultColumn: true,\n  defaultColumnName: 'column',\n});\n\nwatch(\n  settings,\n  debounce(() => {\n    emit('update', { settings });\n  }, 500),\n  { deep: true }\n);\n\nonMounted(() => {\n  const copySettings = cloneDeep(props.workflow.settings);\n  Object.assign(settings, copySettings);\n});\n</script>\n<style>\n.settings-content .ui-tab-panel {\n  @apply space-y-4 space-y-4 divide-y dark:divide-gray-700 divide-gray-100;\n}\n</style>\n"
  },
  {
    "path": "src/components/newtab/workflow/WorkflowShare.vue",
    "content": "<template>\n  <ui-card class=\"share-workflow scroll w-full max-w-2xl overflow-auto\">\n    <template v-if=\"!userStore.user?.username\">\n      <div class=\"mb-12 flex items-center\">\n        <p>{{ t('workflow.share.title') }}</p>\n        <div class=\"grow\"></div>\n        <button @click=\"$emit('close')\">\n          <v-remixicon name=\"riCloseLine\" />\n        </button>\n      </div>\n      <p class=\"text-center\">\n        {{ t('auth.username') }}.\n        <a\n          class=\"underline\"\n          href=\"https://extension.automa.site/profile?username=true\"\n          target=\"_blank\"\n        >\n          {{ t('auth.clickHere') }}\n        </a>\n      </p>\n    </template>\n    <template v-else>\n      <div v-if=\"!isUpdate\" class=\"mb-4 flex items-center\">\n        <p>{{ t('workflow.share.title') }}</p>\n        <div class=\"grow\"></div>\n        <ui-button class=\"mr-2\" @click=\"$emit('close')\">\n          {{ t('common.cancel') }}\n        </ui-button>\n        <ui-button class=\"mr-2\" @click=\"saveDraft\"> Save draft </ui-button>\n        <ui-button\n          :loading=\"state.isPublishing\"\n          variant=\"accent\"\n          @click=\"publishWorkflow\"\n        >\n          {{ t('workflow.share.publish') }}\n        </ui-button>\n      </div>\n      <slot name=\"prepend\"></slot>\n      <div class=\"mb-4 flex\">\n        <input\n          v-model=\"state.workflow.name\"\n          :placeholder=\"t('workflow.name')\"\n          type=\"text\"\n          name=\"workflow name\"\n          class=\"mr-4 block w-full flex-1 bg-transparent text-2xl font-semibold leading-none focus:ring-0\"\n        />\n        <ui-select v-model=\"state.workflow.category\">\n          <option value=\"\">{{ t('common.category') }} (none)</option>\n          <option\n            v-for=\"(category, id) in workflowCategories\"\n            :key=\"id\"\n            :value=\"id\"\n          >\n            {{ category }}\n          </option>\n        </ui-select>\n      </div>\n      <div class=\"relative mb-2\">\n        <ui-textarea\n          v-model=\"state.workflow.description\"\n          :max=\"300\"\n          placeholder=\"Short description\"\n          class=\"scroll h-32 w-full resize-none\"\n        />\n        <p\n          class=\"absolute bottom-2 right-2 text-sm text-gray-600 dark:text-gray-200\"\n        >\n          {{ state.workflow.description.length }}/300\n        </p>\n      </div>\n      <shared-wysiwyg\n        v-model=\"state.workflow.content\"\n        :placeholder=\"t('common.description')\"\n        :limit=\"5000\"\n        class=\"content-editor bg-box-transparent prose prose-zinc relative max-w-none rounded-lg p-4 dark:prose-invert\"\n        @count=\"state.contentLength = $event\"\n      >\n        <template #append>\n          <p\n            class=\"absolute bottom-2 right-2 text-sm text-gray-600 dark:text-gray-200\"\n          >\n            {{ state.contentLength }}/5000\n          </p>\n        </template>\n      </shared-wysiwyg>\n    </template>\n  </ui-card>\n</template>\n<script setup>\nimport SharedWysiwyg from '@/components/newtab/shared/SharedWysiwyg.vue';\nimport { useSharedWorkflowStore } from '@/stores/sharedWorkflow';\nimport { useUserStore } from '@/stores/user';\nimport { fetchApi } from '@/utils/api';\nimport { debounce, parseJSON } from '@/utils/helper';\nimport { workflowCategories } from '@/utils/shared';\nimport { convertWorkflow } from '@/utils/workflowData';\nimport { onMounted, reactive, watch } from 'vue';\nimport { useI18n } from 'vue-i18n';\nimport { useToast } from 'vue-toastification';\nimport browser from 'webextension-polyfill';\n\nconst props = defineProps({\n  workflow: {\n    type: Object,\n    default: () => ({}),\n  },\n  isUpdate: Boolean,\n});\nconst emit = defineEmits(['close', 'publish', 'change']);\n\nconst { t } = useI18n();\nconst toast = useToast();\nconst userStore = useUserStore();\nconst sharedWorkflowStore = useSharedWorkflowStore();\n\nconst state = reactive({\n  contentLength: 0,\n  isPublishing: false,\n  workflow: JSON.parse(JSON.stringify(props.workflow)),\n});\n\nasync function publishWorkflow() {\n  try {\n    state.isPublishing = true;\n\n    const workflow = convertWorkflow(state.workflow, ['id', 'category']);\n    workflow.name = workflow.name || 'unnamed';\n    workflow.content = state.workflow.content || null;\n    workflow.drawflow = parseJSON(workflow.drawflow, workflow.drawflow);\n    workflow.description = state.workflow.description.slice(0, 300);\n\n    delete workflow.extVersion;\n\n    const response = await fetchApi('/me/workflows/shared', {\n      auth: true,\n      method: 'POST',\n      body: JSON.stringify({ workflow }),\n    });\n    const result = await response.json();\n\n    if (!response.ok) {\n      const error = new Error(response.statusText);\n      error.data = result.data;\n\n      throw error;\n    }\n\n    workflow.drawflow = props.workflow.drawflow;\n\n    sharedWorkflowStore.insert(workflow);\n    sessionStorage.setItem(\n      'shared-workflows',\n      JSON.stringify(sharedWorkflowStore.shared)\n    );\n\n    state.isPublishing = false;\n\n    emit('publish');\n  } catch (error) {\n    let errorMessage = t('message.somethingWrong');\n\n    if (error.data && error.data.show) {\n      errorMessage = error.message;\n    }\n\n    toast.error(errorMessage);\n    console.error(error);\n\n    state.isPublishing = false;\n  }\n}\nfunction saveDraft() {\n  const key = `draft:${props.workflow.id}`;\n  browser.storage.local.set({\n    [key]: {\n      content: state.workflow.content,\n      category: state.workflow.category,\n      description: state.workflow.description,\n    },\n  });\n}\n\nwatch(\n  () => state.workflow,\n  debounce(() => {\n    emit('change', state.workflow);\n  }, 200),\n  { deep: true }\n);\n\nonMounted(() => {\n  const key = `draft:${props.workflow.id}`;\n  browser.storage.local.get(key).then((data) => {\n    Object.assign(state.workflow, data[key]);\n  });\n});\n</script>\n<style scoped>\n.share-workflow {\n  min-height: 500px;\n  max-height: calc(100vh - 4rem);\n}\n.editor-menu-btn {\n  @apply p-1 rounded-md transition;\n}\n</style>\n<style>\n.content-editor .ProseMirror {\n  min-height: 200px;\n}\n.content-editor .ProseMirror :first-child {\n  margin-top: 0 !important;\n}\n</style>\n"
  },
  {
    "path": "src/components/newtab/workflow/WorkflowShareTeam.vue",
    "content": "<template>\n  <ui-card class=\"share-workflow scroll w-full max-w-4xl overflow-auto\">\n    <template v-if=\"!isUpdate\">\n      <h1 class=\"text-xl font-semibold\">Share workflow with team</h1>\n      <p class=\"text-gray-600 dark:text-gray-200\">\n        This workflow will be shared with your team\n      </p>\n    </template>\n    <p v-else class=\"font-semibold\">Update workflow</p>\n    <div class=\"mt-4 flex items-start\">\n      <div class=\"mr-8 flex-1\">\n        <div class=\"flex items-center\">\n          <ui-input\n            v-model=\"state.workflow.name\"\n            :label=\"t('workflow.name')\"\n            type=\"text\"\n            name=\"workflow name\"\n            class=\"flex-1\"\n          />\n          <ui-select\n            v-if=\"!isUpdate\"\n            v-model=\"state.activeTeam\"\n            label=\"Select team\"\n            class=\"ml-4\"\n            style=\"max-width: 220px\"\n          >\n            <option\n              v-for=\"team in state.userTeams\"\n              :key=\"team.id\"\n              :value=\"team.id\"\n            >\n              {{ team.name }}\n            </option>\n          </ui-select>\n        </div>\n        <div class=\"relative my-2\">\n          <label\n            for=\"short-description\"\n            class=\"ml-2 text-sm text-gray-600 dark:text-gray-200\"\n          >\n            Short description\n          </label>\n          <ui-textarea\n            id=\"short-description\"\n            v-model=\"state.workflow.description\"\n            :max=\"300\"\n            label=\"Short description\"\n            placeholder=\"Write here...\"\n            class=\"scroll h-28 w-full resize-none\"\n          />\n          <p\n            class=\"absolute bottom-2 right-2 text-sm text-gray-600 dark:text-gray-200\"\n          >\n            {{ state.workflow.description.length }}/300\n          </p>\n        </div>\n        <shared-wysiwyg\n          v-model=\"state.workflow.content\"\n          :placeholder=\"t('common.description')\"\n          :limit=\"5000\"\n          class=\"content-editor bg-box-transparent prose prose-zinc relative max-w-none rounded-lg p-4 dark:prose-invert\"\n          @count=\"state.contentLength = $event\"\n        >\n          <template #append>\n            <p\n              class=\"absolute bottom-2 right-2 text-sm text-gray-600 dark:text-gray-200\"\n            >\n              {{ state.contentLength }}/5000\n            </p>\n          </template>\n        </shared-wysiwyg>\n      </div>\n      <div class=\"sticky top-4 w-64 pb-4\">\n        <template v-if=\"isUpdate\">\n          <ui-button\n            variant=\"accent\"\n            class=\"w-full\"\n            @click=\"$emit('update', state.workflow)\"\n          >\n            Save\n          </ui-button>\n          <ui-button class=\"mt-2 w-full\" @click=\"$emit('close')\">\n            {{ t('common.cancel') }}\n          </ui-button>\n        </template>\n        <template v-else>\n          <div class=\"flex\">\n            <ui-button\n              v-tooltip=\"'Save as draft'\"\n              :disabled=\"state.isPublishing\"\n              icon\n              @click=\"saveDraft\"\n            >\n              <v-remixicon name=\"riSaveLine\" />\n            </ui-button>\n            <ui-button\n              :loading=\"state.isPublishing\"\n              :disabled=\"!state.workflow.name.trim()\"\n              variant=\"accent\"\n              class=\"ml-2 w-full\"\n              @click=\"publishWorkflow\"\n            >\n              Publish\n            </ui-button>\n          </div>\n          <ui-button\n            :disabled=\"state.isPublishing\"\n            class=\"mt-2 w-full\"\n            @click=\"$emit('close')\"\n          >\n            Cancel\n          </ui-button>\n        </template>\n        <ui-select\n          v-model=\"state.workflow.category\"\n          class=\"mt-4 w-full\"\n          :label=\"t('common.category')\"\n        >\n          <option value=\"\">(none)</option>\n          <option\n            v-for=\"(category, id) in workflowCategories\"\n            :key=\"id\"\n            :value=\"id\"\n          >\n            {{ category }}\n          </option>\n        </ui-select>\n        <span class=\"ml-2 mt-5 block text-sm text-gray-600 dark:text-gray-200\">\n          Environment\n        </span>\n        <ui-tabs v-model=\"state.workflow.tag\" type=\"fill\" fill>\n          <ui-tab value=\"stage\"> Stage </ui-tab>\n          <ui-tab value=\"production\"> Production </ui-tab>\n        </ui-tabs>\n      </div>\n    </div>\n  </ui-card>\n</template>\n<script setup>\nimport SharedWysiwyg from '@/components/newtab/shared/SharedWysiwyg.vue';\nimport { useTeamWorkflowStore } from '@/stores/teamWorkflow';\nimport { useUserStore } from '@/stores/user';\nimport { fetchApi } from '@/utils/api';\nimport { debounce, parseJSON } from '@/utils/helper';\nimport { workflowCategories } from '@/utils/shared';\nimport { convertWorkflow } from '@/utils/workflowData';\nimport { registerWorkflowTrigger } from '@/utils/workflowTrigger';\nimport cloneDeep from 'lodash.clonedeep';\nimport { onMounted, reactive, watch } from 'vue';\nimport { useI18n } from 'vue-i18n';\nimport { useToast } from 'vue-toastification';\nimport browser from 'webextension-polyfill';\n\nconst props = defineProps({\n  workflow: {\n    type: Object,\n    default: () => ({}),\n  },\n  isUpdate: Boolean,\n});\nconst emit = defineEmits(['close', 'publish', 'change', 'update']);\n\nconst { t } = useI18n();\nconst toast = useToast();\nconst userStore = useUserStore();\nconst teamWorkflowStore = useTeamWorkflowStore();\n\nconst state = reactive({\n  userTeams: [],\n  activeTeam: null,\n  contentLength: 0,\n  isPublishing: false,\n  workflow: JSON.parse(JSON.stringify(props.workflow)),\n});\n\nasync function publishWorkflow() {\n  try {\n    state.isPublishing = true;\n\n    const workflow = convertWorkflow(state.workflow, ['id', 'category']);\n    workflow.name = workflow.name || 'unnamed';\n    workflow.tag = state.workflow.tag || 'stage';\n    workflow.content = state.workflow.content || null;\n    workflow.description = state.workflow.description.slice(0, 300);\n    workflow.drawflow = parseJSON(workflow.drawflow, workflow.drawflow);\n\n    delete workflow.extVersion;\n\n    const response = await fetchApi(`/teams/${state.activeTeam}/workflows`, {\n      auth: true,\n      method: 'POST',\n      body: JSON.stringify({ workflow }),\n    });\n    const result = await response.json();\n\n    if (!response.ok) {\n      const error = new Error(response.statusText);\n      error.data = result.data;\n\n      throw error;\n    }\n\n    workflow.id = result.id;\n    workflow.teamId = result.teamId;\n    workflow.createdAt = Date.now();\n    workflow.drawflow = props.workflow.drawflow;\n\n    await teamWorkflowStore.insert(state.activeTeam, cloneDeep(workflow));\n    state.isPublishing = false;\n\n    const triggerBlock = workflow.drawflow.nodes?.find(\n      (node) => node.label === 'trigger'\n    );\n    if (triggerBlock) {\n      await registerWorkflowTrigger(workflow.id, triggerBlock);\n    }\n\n    toast('Share the workflow with your team successfully.');\n\n    emit('publish');\n  } catch (error) {\n    let errorMessage = t('message.somethingWrong');\n\n    if (error.data && error.data.show) {\n      errorMessage = error.message;\n    }\n\n    toast.error(errorMessage);\n    console.error(error);\n\n    state.isPublishing = false;\n  }\n}\nfunction saveDraft() {\n  const key = `draft-team:${props.workflow.id}`;\n  browser.storage.local.set({\n    [key]: {\n      name: state.workflow.name,\n      tag: state.tag,\n      content: state.workflow.content,\n      category: state.workflow.category,\n      description: state.workflow.description,\n    },\n  });\n}\n\nwatch(\n  () => state.workflow,\n  debounce(() => {\n    emit('change', state.workflow);\n  }, 200),\n  { deep: true }\n);\n\nonMounted(() => {\n  if (!props.isUpdate) {\n    const key = `draft-team:${props.workflow.id}`;\n    browser.storage.local.get(key).then((data) => {\n      Object.assign(state.workflow, data[key]);\n\n      if (!state.workflow.tag) {\n        state.workflow.tag = 'stage';\n      }\n    });\n\n    state.userTeams = userStore.user.teams.filter((team) =>\n      team.access.some((item) => ['owner', 'create'].includes(item))\n    );\n    if (state.userTeams[0]) state.activeTeam = state.userTeams[0].id;\n  }\n});\n</script>\n<style scoped>\n.share-workflow {\n  min-height: 500px;\n  max-height: calc(100vh - 4rem);\n}\n.editor-menu-btn {\n  @apply p-1 rounded-md transition;\n}\n</style>\n<style>\n.content-editor .ProseMirror {\n  min-height: 200px;\n}\n.content-editor .ProseMirror :first-child {\n  margin-top: 0 !important;\n}\n</style>\n"
  },
  {
    "path": "src/components/newtab/workflow/WorkflowSharedActions.vue",
    "content": "<template>\n  <ui-card padding=\"p-1\">\n    <ui-input\n      v-tooltip=\"t('workflow.share.url')\"\n      prepend-icon=\"riLinkM\"\n      :model-value=\"`https://extension.automa.site/workflow/${workflow.id}`\"\n      readonly\n      @click=\"$event.target.select()\"\n    />\n  </ui-card>\n  <ui-card padding=\"p-1 ml-4\">\n    <button\n      v-if=\"data.hasLocal\"\n      v-tooltip.group=\"t('workflow.share.fetchLocal')\"\n      class=\"hoverable rounded-lg p-2\"\n      @click=\"$emit('fetchLocal')\"\n    >\n      <v-remixicon name=\"riRefreshLine\" />\n    </button>\n    <button\n      v-if=\"!data.hasLocal\"\n      v-tooltip.group=\"t('workflow.share.download')\"\n      class=\"hoverable rounded-lg p-2\"\n      @click=\"$emit('insertLocal')\"\n    >\n      <v-remixicon name=\"riDownloadLine\" />\n    </button>\n    <button\n      v-tooltip.group=\"t('workflow.share.edit')\"\n      class=\"hoverable rounded-lg p-2\"\n      @click=\"state.showModal = true\"\n    >\n      <v-remixicon name=\"riFileEditLine\" />\n    </button>\n  </ui-card>\n  <ui-card padding=\"p-1 flex ml-4\">\n    <button\n      v-tooltip.group=\"t('workflow.share.unpublish')\"\n      class=\"hoverable relative mr-2 rounded-lg p-2\"\n      @click=\"$emit('unpublish')\"\n    >\n      <ui-spinner\n        v-if=\"data.isUnpublishing\"\n        color=\"text-accent\"\n        class=\"absolute top-2 left-2\"\n      />\n      <v-remixicon\n        name=\"riLock2Line\"\n        :class=\"{ 'opacity-75': data.isUnpublishing }\"\n      />\n    </button>\n    <ui-button\n      :loading=\"data.isUpdating\"\n      :disabled=\"data.isUnpublishing\"\n      variant=\"accent\"\n      @click=\"$emit('save')\"\n    >\n      <span\n        v-if=\"data.isChanged\"\n        class=\"absolute top-0 left-0 -ml-1 -mt-1 flex h-3 w-3\"\n      >\n        <span\n          class=\"absolute inline-flex h-full w-full animate-ping rounded-full bg-primary opacity-75\"\n        ></span>\n        <span\n          class=\"relative inline-flex h-3 w-3 rounded-full bg-blue-600\"\n        ></span>\n      </span>\n      {{ t('workflow.share.update') }}\n    </ui-button>\n  </ui-card>\n  <ui-modal v-model=\"state.showModal\" custom-content @close=\"updateDescription\">\n    <workflow-share\n      :workflow=\"workflow\"\n      is-update\n      @change=\"onDescriptionUpdated\"\n    >\n      <template #prepend>\n        <div class=\"mb-6 flex justify-between\">\n          <p>{{ t('workflow.share.edit') }}</p>\n          <v-remixicon\n            name=\"riCloseLine\"\n            class=\"cursor-pointer\"\n            @click=\"\n              state.showModal = false;\n              updateDescription();\n            \"\n          />\n        </div>\n      </template>\n    </workflow-share>\n  </ui-modal>\n</template>\n<script setup>\nimport WorkflowShare from '@/components/newtab/workflow/WorkflowShare.vue';\nimport { useGroupTooltip } from '@/composable/groupTooltip';\nimport { shallowReactive } from 'vue';\nimport { useI18n } from 'vue-i18n';\n\nconst props = defineProps({\n  workflow: {\n    type: Object,\n    default: () => ({}),\n  },\n  data: {\n    type: Object,\n    default: () => ({}),\n  },\n});\nconst emit = defineEmits([\n  'insertLocal',\n  'fetchLocal',\n  'update',\n  'save',\n  'unpublish',\n]);\n\nuseGroupTooltip();\nconst { t } = useI18n();\n\nconst state = shallowReactive({\n  showModal: false,\n  isChanged: false,\n  name: props.workflow.name,\n  content: props.workflow.content,\n  category: props.workflow.category,\n  description: props.workflow.description,\n});\n\nfunction onDescriptionUpdated({ name, description, content, category }) {\n  state.isChanged = true;\n\n  state.name = name;\n  state.content = content;\n  state.category = category;\n  state.description = description;\n}\nfunction updateDescription() {\n  if (!state.isChanged) return;\n\n  emit('update', {\n    name: state.name,\n    content: state.content,\n    description: state.description,\n  });\n  state.isChanged = false;\n}\n</script>\n"
  },
  {
    "path": "src/components/newtab/workflow/edit/BlockSetting/BlockSettingGeneral.vue",
    "content": "<template>\n  <div class=\"block-setting-general\">\n    <ui-list>\n      <div v-if=\"props.data.id !== 'delay'\" class=\"flex items-center\">\n        <div class=\"flex-1 overflow-hidden\">\n          <p class=\"text-overflow\">\n            {{ t('workflow.blocks.base.settings.blockTimeout.title') }}\n          </p>\n          <p class=\"line-clamp leading-tight text-gray-600 dark:text-gray-300\">\n            {{ t('workflow.blocks.base.settings.blockTimeout.description') }}\n          </p>\n        </div>\n        <ui-input\n          v-model.number=\"state.blockTimeout\"\n          placeholder=\"0\"\n          class=\"w-24\"\n        />\n      </div>\n      <ui-list-item v-if=\"isDebugSupported\" small class=\"mt-4\">\n        <div class=\"flex-1 overflow-hidden\">\n          <p class=\"text-overflow\">\n            {{ t('workflow.blocks.debugMode.title') }}\n          </p>\n          <p\n            class=\"text-overflow leading-tight text-gray-600 dark:text-gray-300\"\n          >\n            {{ t('workflow.blocks.debugMode.description') }}\n          </p>\n        </div>\n        <ui-switch v-model=\"state.debugMode\" class=\"mr-4\" />\n      </ui-list-item>\n    </ui-list>\n  </div>\n</template>\n<script setup>\nimport { reactive, watch, onMounted } from 'vue';\nimport { useI18n } from 'vue-i18n';\n\nconst props = defineProps({\n  data: {\n    type: Object,\n    default: () => ({}),\n  },\n  block: {\n    type: Object,\n    default: () => ({}),\n  },\n});\nconst emit = defineEmits(['change']);\n\nconst supportedDebugBlocks = [\n  'forms',\n  'event-click',\n  'trigger-event',\n  'press-key',\n];\nconst browserType = BROWSER_TYPE;\nconst isDebugSupported =\n  browserType !== 'firefox' && supportedDebugBlocks.includes(props.block.id);\n\nconst { t } = useI18n();\nconst state = reactive({ blockTimeout: 0 });\n\nwatch(\n  () => state,\n  (onError) => {\n    emit('change', onError);\n  },\n  { deep: true }\n);\n\nonMounted(() => {\n  Object.assign(state, props.data);\n});\n</script>\n"
  },
  {
    "path": "src/components/newtab/workflow/edit/BlockSetting/BlockSettingLines.vue",
    "content": "<template>\n  <div class=\"block-lines max-w-xl\">\n    <ui-select\n      v-model=\"state.activeEdge\"\n      :placeholder=\"t('workflow.blocks.base.settings.line.select')\"\n      class=\"w-full\"\n    >\n      <option v-for=\"edge in state.edges\" :key=\"edge.id\" :value=\"edge.id\">\n        {{ edge.name }}\n      </option>\n    </ui-select>\n    <div v-if=\"activeEdge\" class=\"mt-4\">\n      <ui-input\n        :model-value=\"activeEdge.label\"\n        :label=\"t('workflow.blocks.base.settings.line.label')\"\n        placeholder=\"A label\"\n        class=\"w-full\"\n        @change=\"updateActiveEdge('label', $event)\"\n      />\n      <div class=\"mt-4 flex items-center\">\n        <label class=\"mr-4 block flex items-center\">\n          <ui-switch\n            :model-value=\"activeEdge.animated\"\n            @change=\"updateActiveEdge('animated', $event)\"\n          />\n          <span class=\"ml-2\">\n            {{ t('workflow.blocks.base.settings.line.animated') }}\n          </span>\n        </label>\n        <div class=\"w-32\" />\n        <label class=\"flex items-center\">\n          <input\n            :value=\"activeEdge.style?.stroke ?? null\"\n            type=\"color\"\n            name=\"color\"\n            class=\"bg-input h-10 w-10 rounded-lg p-1\"\n            @input=\"updateActiveEdge('style', { stroke: $event.target.value })\"\n          />\n          <span class=\"ml-2\">\n            {{ t('workflow.blocks.base.settings.line.lineColor') }}\n          </span>\n        </label>\n      </div>\n    </div>\n  </div>\n</template>\n<script setup>\nimport { inject, onMounted, reactive, computed } from 'vue';\nimport { useI18n } from 'vue-i18n';\nimport { debounce } from '@/utils/helper';\n\nconst props = defineProps({\n  blockId: {\n    type: String,\n    default: '',\n  },\n});\n\nconst { t } = useI18n();\n\nconst editor = inject('workflow-editor');\nconst state = reactive({\n  retrieved: false,\n  edges: {},\n  activeEdge: '',\n});\n\nconst activeEdge = computed(() => state.edges[state.activeEdge]);\n\nconst updateActiveEdge = debounce((name, value) => {\n  const edge = editor.value.getEdge.value(state.activeEdge);\n\n  edge[name] = value;\n  state.edges[state.activeEdge][name] = value;\n}, 250);\n\nonMounted(() => {\n  state.edges = editor.value.getEdges.value.reduce(\n    (acc, { id, source, targetNode, label, animated, labelStyle, style }) => {\n      if (source !== props.blockId) return acc;\n\n      let name = t('workflow.blocks.base.settings.line.to', {\n        name: t(`workflow.blocks.${targetNode.label}.name`),\n      });\n      if (targetNode.data.description) {\n        name += ` (${targetNode.data.description.slice(0, 32)})`;\n      }\n\n      acc[id] = {\n        name,\n        id,\n        label: `${label || ''}`,\n        animated: animated ?? false,\n        labelStyle: labelStyle || '',\n      };\n\n      if (style) acc[id].style = style;\n\n      return acc;\n    },\n    {}\n  );\n});\n</script>\n"
  },
  {
    "path": "src/components/newtab/workflow/edit/BlockSetting/BlockSettingOnError.vue",
    "content": "<template>\n  <div\n    class=\"on-block-error scroll overflow-auto\"\n    style=\"max-height: calc(100vh - 13rem)\"\n  >\n    <div\n      class=\"flex items-start rounded-lg bg-green-200 p-4 text-black dark:bg-green-300\"\n    >\n      <v-remixicon name=\"riInformationLine\" />\n      <p class=\"ml-4 flex-1\">\n        {{ t('workflow.blocks.base.onError.info') }}\n      </p>\n    </div>\n    <div class=\"mt-4\">\n      <label class=\"inline-flex\">\n        <ui-switch v-model=\"state.enable\" />\n        <span class=\"ml-2\">\n          {{ t('common.enable') }}\n        </span>\n      </label>\n      <template v-if=\"state.enable\">\n        <div class=\"mt-4\">\n          <label class=\"inline-flex\">\n            <ui-switch v-model=\"state.retry\" />\n            <span class=\"ml-2\">\n              {{ t('workflow.blocks.base.onError.retry') }}\n            </span>\n          </label>\n        </div>\n        <transition-expand>\n          <div v-if=\"state.retry\" class=\"mt-2\">\n            <div class=\"inline-flex items-center\">\n              <span>\n                {{ t('workflow.blocks.base.onError.times.name') }}\n              </span>\n              <v-remixicon\n                :title=\"t('workflow.blocks.base.onError.times.description')\"\n                name=\"riInformationLine\"\n                size=\"20\"\n                class=\"mr-2\"\n              />\n              <ui-input\n                v-model.number=\"state.retryTimes\"\n                type=\"number\"\n                min=\"0\"\n                class=\"w-20\"\n              />\n            </div>\n            <div class=\"ml-12 inline-flex items-center\">\n              <span>\n                {{ t('workflow.blocks.base.onError.interval.name') }}\n              </span>\n              <v-remixicon\n                :title=\"t('workflow.blocks.base.onError.interval.description')\"\n                name=\"riInformationLine\"\n                size=\"20\"\n                class=\"mr-2\"\n              />\n              <ui-input\n                v-model.number=\"state.retryInterval\"\n                type=\"number\"\n                min=\"0\"\n                class=\"w-20\"\n              />\n              <span class=\"ml-1\">\n                {{ t('workflow.blocks.base.onError.interval.second') }}\n              </span>\n            </div>\n          </div>\n        </transition-expand>\n        <ui-select v-model=\"state.toDo\" class=\"mt-2 w-56\">\n          <option\n            v-for=\"type in toDoTypes\"\n            :key=\"type\"\n            :value=\"type\"\n            :disabled=\"type === 'fallback' && data.isInGroup ? true : null\"\n            class=\"to-do-type\"\n          >\n            {{ t(`workflow.blocks.base.onError.toDo.${type}`) }}\n          </option>\n        </ui-select>\n        <ui-input\n          v-if=\"state.toDo === 'error'\"\n          v-model=\"state.errorMessage\"\n          :placeholder=\"t(`workflow.blocks.workflow-state.error.message`)\"\n          :title=\"t(`workflow.blocks.workflow-state.error.message`)\"\n          class=\"mt-1 ml-2 w-56\"\n        />\n        <div class=\"mt-4 flex items-center justify-between\">\n          <label class=\"inline-flex\">\n            <ui-switch v-model=\"state.insertData\" />\n            <span class=\"ml-2\">\n              {{ t('workflow.blocks.base.onError.insertData.name') }}\n            </span>\n          </label>\n          <ui-button\n            v-if=\"state.insertData\"\n            class=\"text-sm\"\n            @click=\"addDataToInsert\"\n          >\n            Add item\n          </ui-button>\n        </div>\n        <transition-expand>\n          <table v-if=\"state.insertData\" class=\"mt-2 w-full\">\n            <thead>\n              <tr class=\"text-left text-sm\">\n                <th>Type</th>\n                <th>Name</th>\n                <th>Value</th>\n                <th></th>\n              </tr>\n            </thead>\n            <tbody>\n              <tr v-for=\"(item, index) in state.dataToInsert\" :key=\"index\">\n                <td>\n                  <ui-select v-model=\"item.type\">\n                    <option value=\"table\">\n                      {{ t('workflow.table.title') }}\n                    </option>\n                    <option value=\"variable\">\n                      {{ t('workflow.variables.title') }}\n                    </option>\n                  </ui-select>\n                </td>\n                <td>\n                  <ui-select\n                    v-if=\"item.type === 'table'\"\n                    v-model=\"item.name\"\n                    placeholder=\"Select column\"\n                    class=\"mt-1 w-full\"\n                  >\n                    <option\n                      v-for=\"column in workflow.columns.value\"\n                      :key=\"column.id\"\n                      :value=\"column.id\"\n                    >\n                      {{ column.name }}\n                    </option>\n                  </ui-select>\n                  <ui-input\n                    v-else\n                    v-model=\"item.name\"\n                    placeholder=\"Variable name\"\n                  />\n                </td>\n                <td>\n                  <ui-input v-model=\"item.value\" placeholder=\"EMPTY\" />\n                </td>\n                <td>\n                  <v-remixicon\n                    name=\"riCloseLine\"\n                    class=\"cursor-pointer text-gray-600 dark:text-gray-200\"\n                    @click=\"state.dataToInsert.splice(index, 1)\"\n                  />\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        </transition-expand>\n      </template>\n    </div>\n  </div>\n</template>\n<script setup>\nimport { reactive, watch, onMounted, inject } from 'vue';\nimport { useI18n } from 'vue-i18n';\n\nconst props = defineProps({\n  data: {\n    type: Object,\n    default: () => ({}),\n  },\n});\nconst emit = defineEmits(['change']);\n\nconst toDoTypes = ['error', 'continue', 'fallback'];\n\nconst { t } = useI18n();\nconst state = reactive({});\n\nconst workflow = inject('workflow', {});\n\nfunction addDataToInsert() {\n  if (!state.dataToInsert) state.dataToInsert = [];\n\n  state.dataToInsert.push({\n    type: 'table',\n    name: '',\n    value: '',\n  });\n}\n\nwatch(\n  () => state,\n  (onError) => {\n    emit('change', onError);\n  },\n  { deep: true }\n);\n\nonMounted(() => {\n  Object.assign(state, props.data);\n});\n</script>\n<style scoped>\ntable th,\ntable,\ntd {\n  font-weight: normal;\n  @apply p-1;\n}\n\n.to-do-type.is-active {\n  @apply bg-accent dark:text-black text-gray-100 !important;\n}\n</style>\n"
  },
  {
    "path": "src/components/newtab/workflow/edit/EditAiWorkflow.vue",
    "content": "<template>\n  <div>\n    <ui-button\n      variant=\"accent\"\n      class=\"text-sm w-full\"\n      @click=\"state.showAIPowerTokenModal = true\"\n    >\n      <span class=\"flex justify-between items-center w-full\">\n        <span class=\"flex items-center space-x-1\">\n          <v-remixicon name=\"riKey\" size=\"16\"></v-remixicon>\n          <span>Configure AI Power Token</span>\n        </span>\n        <v-remixicon name=\"riArrowRightLine\" size=\"16\"></v-remixicon>\n      </span>\n    </ui-button>\n\n    <template v-if=\"aiPowerToken\">\n      <ui-paginated-select\n        :key=\"aiPowerToken\"\n        :model-value=\"data.flowUuid\"\n        :initial-label=\"data.flowLabel\"\n        :load-options=\"loadWorkflows\"\n        option-value-key=\"flowUuid\"\n        option-label-key=\"name\"\n        class=\"mt-4 w-full\"\n        label=\"Select Workflows\"\n        placeholder=\"Select a workflow\"\n        search-placeholder=\"Search workflows...\"\n        @change=\"onFlowChange\"\n      >\n        <template #footer>\n          <ui-button class=\"w-full\" @click=\"createNewWorkflow\">\n            <v-remixicon name=\"riAddLine\" class=\"mr-2\" />\n            New AI Workflow\n          </ui-button>\n        </template>\n      </ui-paginated-select>\n\n      <div\n        class=\"w-full my-6 relative flex items-center justify-center bg-[#e4e4e7] h-[1px]\"\n      ></div>\n\n      <div class=\"my-4\">\n        <p class=\"font-semibold\">Workflow Inputs</p>\n        <template v-if=\"data.inputs && data.inputs.length\">\n          <div\n            v-for=\"(item, index) in data.inputs\"\n            :key=\"`${data.flowUuid}-${item.name}`\"\n          >\n            <component\n              :is=\"getComponent(item.type)\"\n              :label=\"`${item.label} (${item.type})`\"\n              :placeholder=\"item.name || null\"\n              :model-value=\"item.value\"\n              :accept=\"item.accept\"\n              :max-size=\"item.maxSize\"\n              :on-upload=\"handleUploadFile\"\n              class=\"w-full my-2\"\n              @change=\"onInputParamsChange(item, index, $event)\"\n            />\n          </div>\n        </template>\n        <template v-else>\n          <p class=\"text-sm text-gray-500\">No inputs</p>\n        </template>\n      </div>\n\n      <div class=\"my-4\">\n        <p class=\"font-semibold\">Workflow Outputs(view only)</p>\n        <template v-if=\"data.outputs && data.outputs.length\">\n          <ui-input\n            v-for=\"(item, index) in data.outputs\"\n            :key=\"index\"\n            :label=\"`${item.label} (${item.type})`\"\n            :placeholder=\"item.name || null\"\n            readonly\n            class=\"w-full my-2\"\n          />\n        </template>\n        <template v-else>\n          <p class=\"text-sm text-gray-500\">No outputs</p>\n        </template>\n      </div>\n\n      <div class=\"my-4\">\n        <insert-workflow-data :data=\"data\" variables @update=\"updateData\" />\n      </div>\n\n      <span class=\"text-sm text-gray-500 block text-center mt-10\"\n        >Powered by Automa\n        <a href=\"https://aipower.automa.site/\">AI Power</a></span\n      >\n    </template>\n\n    <ui-modal\n      v-model=\"state.showAIPowerTokenModal\"\n      title=\"Configure AI Power Token\"\n    >\n      <div class=\"mb-6\">\n        <p>\n          <span class=\"text-gray-500 text-[14px] leading-[24px]\"\n            >Enter your AI Power token to enable AI Workflow features</span\n          >\n        </p>\n      </div>\n\n      <div\n        class=\"bg-[#f2f2f2] dark:bg-gray-900 mb-6 p-6 rounded-lg w-full space-y-4\"\n      >\n        <p\n          class=\"font-semibold text-[16px] dark:text-gray-300 leading-[24px] flex items-center\"\n        >\n          <v-remixicon name=\"riKey\" size=\"16\" class=\"mr-1\"></v-remixicon>\n          How to get your AI Power Token\n        </p>\n\n        <ol\n          class=\"space-y-2 list-decimal list-inside text-sm text-gray-600 dark:text-gray-400\"\n        >\n          <li>Go to Settings → Authorizations in your AI Power dashboard</li>\n          <li>Navigate to \"AI Power Authorization\" section</li>\n          <li>Click \"Generate New Token\" to create a new token</li>\n          <li>Copy the generated token and paste it below</li>\n        </ol>\n\n        <ui-button variant=\"default\" @click=\"goToAIPowerSettings\">\n          <span class=\"text-[14px] leading-[24px]\">Open AI Power Settings</span>\n          <v-remixicon name=\"riArrowRightUpLine\" size=\"16\"></v-remixicon>\n        </ui-button>\n      </div>\n\n      <div class=\"flex flex-col space-y-4 mb-4\">\n        <span class=\"text-sm text-gray-500 font-semibold\">AI Power Token</span>\n        <ui-input\n          :model-value=\"aiPowerToken\"\n          class=\"w-full\"\n          placeholder=\"Enter your AI Power Token...\"\n          @change=\"updateAIPowerToken\"\n        />\n      </div>\n\n      <div class=\"flex justify-end space-x-2\">\n        <ui-button\n          variant=\"default\"\n          @click=\"state.showAIPowerTokenModal = false\"\n          >Cancel</ui-button\n        >\n        <ui-button variant=\"accent\" @click=\"saveAIPowerToken\">Save</ui-button>\n      </div>\n    </ui-modal>\n  </div>\n</template>\n\n<script setup>\nimport UiFileInput from '@/components/ui/UiFileInput.vue';\nimport UiInput from '@/components/ui/UiInput.vue';\nimport UiPaginatedSelect from '@/components/ui/UiPaginatedSelect.vue';\nimport { useWorkflowStore } from '@/stores/workflow';\nimport {\n  getAPFlowList,\n  getAPWorkflowDetail,\n  postUploadFile,\n} from '@/utils/getAIPoweredInfo';\nimport cloneDeep from 'lodash.clonedeep';\nimport secrets from 'secrets';\nimport {\n  computed,\n  defineEmits,\n  defineProps,\n  shallowReactive,\n  watch,\n} from 'vue';\nimport { useRoute } from 'vue-router';\nimport { useToast } from 'vue-toastification';\nimport browser from 'webextension-polyfill';\nimport InsertWorkflowData from './InsertWorkflowData.vue';\n\nconst toast = useToast();\n\nconst props = defineProps({\n  data: {\n    type: Object,\n    default: () => ({}),\n  },\n});\n\nconst emit = defineEmits(['update:data']);\n\nfunction updateData(value) {\n  emit('update:data', { ...props.data, ...value });\n}\n\nconst getComponent = (type) => {\n  const uploadTypes = ['VIDEO', 'IMAGE', 'AUDIO', 'FILE'];\n  if (uploadTypes.includes(type)) {\n    return UiFileInput;\n  }\n  return UiInput;\n};\n\nconst state = shallowReactive({\n  showAIPowerTokenModal: false,\n});\n\nconst { id: workflowId } = useRoute().params;\nconst workflowStore = useWorkflowStore();\nconst currentWorkflow = workflowStore.getById(workflowId);\n\nconst aiPowerToken = computed(() => {\n  return currentWorkflow?.settings?.aipowerToken;\n});\n\nconst handleUploadFile = async (file) => {\n  try {\n    const res = await postUploadFile(file, aiPowerToken.value);\n    if (res.success) {\n      return {\n        url: res.data.fileReadUrl,\n        filename: file.name,\n      };\n    }\n    throw new Error(res.msg || 'File upload failed');\n  } catch (error) {\n    console.error(error);\n    throw error;\n  }\n};\n\nconst createNewWorkflow = () => {\n  browser.tabs.create({\n    url: secrets.apCreateWorkflowUrl,\n  });\n};\n\nconst clearInputsAndOutputs = () => {\n  updateData({\n    inputs: [],\n    outputs: [],\n    flowUuid: '',\n    flowLabel: '',\n  });\n};\n\nconst loadWorkflows = async ({ query, page }) => {\n  try {\n    const pageSize = 10;\n    const res = await getAPFlowList(\n      { page, size: pageSize, name: query },\n      aiPowerToken.value\n    );\n\n    if (res.success) {\n      return {\n        data: res.data,\n        hasMore: res.page.pages > res.page.page,\n      };\n    }\n    toast.error(`Failed to fetch AI Power workflows: ${res.msg}`);\n    return { data: [], hasMore: false };\n  } catch (err) {\n    console.error(err);\n    toast.error(`${err.message}`);\n    return { data: [], hasMore: false };\n  }\n};\nconst goToAIPowerSettings = () => {\n  const url = `${secrets.apHomeUrl}/authorization`;\n\n  window.open(url, '_blank');\n};\n\nconst updateAIPowerToken = (value) => {\n  state.aipowerToken = value;\n};\n\nconst saveAIPowerToken = () => {\n  const oldToken = currentWorkflow.settings.aipowerToken;\n  const newToken = state.aipowerToken;\n\n  // Do nothing if token hasn't changed.\n  if (newToken === oldToken) {\n    state.showAIPowerTokenModal = false;\n    return;\n  }\n\n  const newSettings = {\n    ...currentWorkflow.settings,\n    aipowerToken: newToken,\n  };\n\n  workflowStore.update({\n    id: workflowId,\n    data: {\n      ...currentWorkflow,\n      settings: newSettings,\n    },\n  });\n  state.showAIPowerTokenModal = false;\n\n  // When token changes, the previous selection is no longer valid.\n  // Clearing it will also reset the inputs/outputs.\n  // The UiPaginatedSelect component will re-initialize because its `key` has changed.\n  clearInputsAndOutputs();\n};\n\nconst onFlowChange = (value, label) => {\n  updateData({ flowUuid: value, flowLabel: label });\n};\n\nconst onInputParamsChange = (item, index, value) => {\n  const newInputs = cloneDeep(props.data.inputs);\n  newInputs[index].value = value;\n  updateData({ inputs: newInputs });\n};\n\nwatch(\n  () => props.data.flowUuid,\n  (newVal, oldVal) => {\n    if (!newVal) {\n      updateData({\n        inputs: [],\n        outputs: [],\n      });\n      return;\n    }\n\n    if (newVal === oldVal) return;\n\n    getAPWorkflowDetail(newVal, aiPowerToken.value).then((res) => {\n      if (res.success) {\n        updateData({\n          inputs: res.data.inputs,\n          outputs: res.data.outputs,\n        });\n      } else {\n        clearInputsAndOutputs();\n        toast.error(`Failed to fetch AI Power workflow detail: ${res.msg}`);\n      }\n    });\n  }\n);\n</script>\n"
  },
  {
    "path": "src/components/newtab/workflow/edit/EditAttributeValue.vue",
    "content": "<template>\n  <edit-interaction-base v-bind=\"{ data }\" @change=\"updateData\">\n    <hr />\n    <ui-select\n      :label=\"t('common.action')\"\n      :model-value=\"data.action || 'get'\"\n      class=\"mt-2 w-full\"\n      @change=\"updateData({ action: $event })\"\n    >\n      <option v-for=\"action in ['get', 'set']\" :key=\"action\" :value=\"action\">\n        {{ t(`workflow.blocks.attribute-value.forms.action.${action}`) }}\n      </option>\n    </ui-select>\n    <edit-autocomplete class=\"mt-2\">\n      <ui-input\n        :model-value=\"data.attributeName\"\n        :label=\"t('workflow.blocks.attribute-value.forms.name')\"\n        autocomplete=\"off\"\n        placeholder=\"name\"\n        class=\"w-full\"\n        @change=\"updateData({ attributeName: $event })\"\n      />\n    </edit-autocomplete>\n    <edit-autocomplete v-if=\"data.action === 'set'\" class=\"mt-2\">\n      <ui-input\n        :model-value=\"data.attributeValue\"\n        :label=\"t('workflow.blocks.attribute-value.forms.value')\"\n        autocomplete=\"off\"\n        placeholder=\"value\"\n        class=\"w-full\"\n        @change=\"updateData({ attributeValue: $event })\"\n      />\n    </edit-autocomplete>\n    <insert-workflow-data\n      v-else\n      :data=\"data\"\n      extra-row\n      variables\n      @update=\"updateData\"\n    />\n  </edit-interaction-base>\n</template>\n<script setup>\nimport { useI18n } from 'vue-i18n';\nimport EditInteractionBase from './EditInteractionBase.vue';\nimport InsertWorkflowData from './InsertWorkflowData.vue';\nimport EditAutocomplete from './EditAutocomplete.vue';\n\nconst props = defineProps({\n  data: {\n    type: Object,\n    default: () => ({}),\n  },\n});\nconst emit = defineEmits(['update:data']);\n\nconst { t } = useI18n();\n\nfunction updateData(value) {\n  emit('update:data', { ...props.data, ...value });\n}\n</script>\n"
  },
  {
    "path": "src/components/newtab/workflow/edit/EditAutocomplete.vue",
    "content": "<template>\n  <ui-autocomplete\n    :items=\"autocompleteList\"\n    :trigger-char=\"['{{', '}}']\"\n    :custom-filter=\"autocompleteFilter\"\n    :replace-after=\"['@', '.']\"\n    block\n    @search=\"onSearch\"\n  >\n    <slot />\n  </ui-autocomplete>\n</template>\n<script setup>\nimport { inject, shallowReactive, computed } from 'vue';\nimport objectPath from 'object-path';\n\ndefineProps({\n  disabled: Boolean,\n});\n\nconst autocompleteData = inject('autocompleteData', {});\nconst state = shallowReactive({\n  path: '',\n  pathLen: -1,\n});\n\nfunction autocompleteFilter({ text, item }) {\n  if (!text) return true;\n\n  const query = text.replace('@', '.').split('.').pop();\n  return item.toLocaleLowerCase().includes(query);\n}\nfunction onSearch(value) {\n  const pathArr = (value ?? '').replace('@', '.').split('.');\n\n  state.path = (pathArr.length > 1 ? pathArr.slice(0, -1) : pathArr).join('.');\n  state.pathLen = pathArr.length;\n}\n\nconst autocompleteList = computed(() => {\n  const data =\n    !state.path || state.pathLen <= 1\n      ? autocompleteData.value\n      : objectPath.get(autocompleteData.value, state.path);\n\n  const list = typeof data === 'string' ? [] : Object.keys(data || {});\n\n  return list;\n});\n</script>\n"
  },
  {
    "path": "src/components/newtab/workflow/edit/EditBlockSettings.vue",
    "content": "<template>\n  <ui-tabs v-model=\"state.activeTab\" class=\"-mt-2\">\n    <ui-tab v-for=\"tab in tabs\" :key=\"tab.id\" :value=\"tab.id\">\n      {{ tab.name }}\n    </ui-tab>\n  </ui-tabs>\n  <ui-tab-panels v-if=\"state.retrieved\" v-model=\"state.activeTab\" class=\"mt-4\">\n    <ui-tab-panel value=\"general\">\n      <block-setting-general\n        v-model:data=\"state.settings\"\n        :block=\"data\"\n        @change=\"onDataChange('settings', $event)\"\n      />\n    </ui-tab-panel>\n    <ui-tab-panel value=\"on-error\">\n      <slot name=\"on-error\">\n        <block-setting-on-error\n          :data=\"state.onError\"\n          @change=\"onDataChange('onError', $event)\"\n        />\n      </slot>\n    </ui-tab-panel>\n    <ui-tab-panel value=\"lines\">\n      <block-setting-lines :block-id=\"data.blockId\" />\n    </ui-tab-panel>\n  </ui-tab-panels>\n</template>\n<script setup>\nimport { reactive, onMounted } from 'vue';\nimport { useI18n } from 'vue-i18n';\nimport defu from 'defu';\nimport { excludeOnError } from '@/utils/shared';\nimport BlockSettingLines from './BlockSetting/BlockSettingLines.vue';\nimport BlockSettingOnError from './BlockSetting/BlockSettingOnError.vue';\nimport BlockSettingGeneral from './BlockSetting/BlockSettingGeneral.vue';\n\nconst props = defineProps({\n  data: {\n    type: Object,\n    default: () => ({}),\n  },\n  onErrorLabel: {\n    type: String,\n    default: '',\n  },\n  show: Boolean,\n});\nconst emit = defineEmits(['change', 'close']);\n\nconst { t } = useI18n();\n\nconst currActiveTab = 'general';\nconst isOnErrorSupported = !excludeOnError.includes(props.data.id);\nconst tabs = [\n  { id: 'general', name: t('settings.menu.general') },\n  {\n    id: 'on-error',\n    name: props.onErrorLabel || t('workflow.blocks.base.onError.button'),\n  },\n  { id: 'lines', name: t('workflow.blocks.base.settings.line.title') },\n];\n\nif (props.data?.itemId) {\n  tabs.pop();\n}\nif (!isOnErrorSupported) {\n  const onErrorTabIndex = tabs.findIndex((tab) => tab.id === 'on-error');\n  tabs.splice(onErrorTabIndex, 1);\n}\n\nconst defaultSettings = {\n  onError: {\n    retry: false,\n    enable: false,\n    retryTimes: 1,\n    retryInterval: 2,\n    toDo: 'error',\n    errorMessage: '',\n    insertData: false,\n    dataToInsert: [],\n  },\n  general: {\n    debugMode: false,\n  },\n};\n\nconst state = reactive({\n  retrieved: false,\n  activeTab: currActiveTab,\n  onError: defaultSettings.onError,\n  settings: defaultSettings.general,\n});\n\nfunction onDataChange(key, data) {\n  if (!state.retrieved) return;\n\n  state[key] = data;\n  emit('change', { [key]: data });\n}\n\nonMounted(() => {\n  const onErrorSetting = defu(\n    props.data.data.onError || {},\n    defaultSettings.onError\n  );\n  state.onError = onErrorSetting;\n\n  const generalSettings = defu(\n    props.data.data.settings,\n    defaultSettings.general\n  );\n  state.settings = generalSettings;\n\n  setTimeout(() => {\n    state.retrieved = true;\n  }, 200);\n});\n</script>\n<style>\n.modal-block-settings {\n  min-height: 500px;\n}\n</style>\n"
  },
  {
    "path": "src/components/newtab/workflow/edit/EditBrowserEvent.vue",
    "content": "<template>\n  <div>\n    <ui-textarea\n      :model-value=\"data.description\"\n      class=\"w-full\"\n      :placeholder=\"t('common.description')\"\n      @change=\"updateData({ description: $event })\"\n    />\n    <ui-input\n      :model-value=\"data.timeout\"\n      :label=\"t('workflow.blocks.browser-event.timeout')\"\n      type=\"number\"\n      class=\"w-full\"\n      @change=\"updateData({ timeout: +$event })\"\n    />\n    <ui-select\n      :placeholder=\"t('workflow.blocks.browser-event.events')\"\n      :model-value=\"data.eventName\"\n      class=\"mt-2 w-full\"\n      @change=\"updateData({ eventName: $event })\"\n    >\n      <optgroup\n        v-for=\"(events, label) in browserEvents\"\n        :key=\"label\"\n        :label=\"label\"\n      >\n        <option v-for=\"event in events\" :key=\"event.id\" :value=\"event.id\">\n          {{ event.name }}\n        </option>\n      </optgroup>\n    </ui-select>\n    <template v-if=\"data.eventName === 'tab:loaded'\">\n      <ui-input\n        v-if=\"!data.activeTabLoaded\"\n        :model-value=\"data.tabLoadedUrl\"\n        type=\"url\"\n        class=\"mt-1 w-full\"\n        placeholder=\"https://example.org/*\"\n        @change=\"updateData({ tabLoadedUrl: $event })\"\n      >\n        <template #label>\n          <span>Match pattern</span>\n          <a\n            href=\"https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Match_patterns#examples\"\n            target=\"_blank\"\n            rel=\"noopener\"\n            title=\"Examples\"\n          >\n            <v-remixicon\n              class=\"ml-1 inline-block\"\n              name=\"riInformationLine\"\n              size=\"18\"\n            />\n          </a>\n        </template>\n      </ui-input>\n      <ui-checkbox\n        :model-value=\"data.activeTabLoaded\"\n        class=\"mt-1\"\n        @change=\"updateData({ activeTabLoaded: $event })\"\n      >\n        {{ t('workflow.blocks.browser-event.activeTabLoaded') }}\n      </ui-checkbox>\n    </template>\n    <template v-if=\"['tab:create', 'window:create'].includes(data.eventName)\">\n      <ui-input\n        :model-value=\"data.tabUrl\"\n        type=\"url\"\n        label=\"Filter\"\n        class=\"mt-1 w-full\"\n        placeholder=\"URL or Regex\"\n        @change=\"updateData({ tabUrl: $event })\"\n      />\n      <ui-checkbox\n        :model-value=\"data.setAsActiveTab\"\n        class=\"mt-1\"\n        @change=\"updateData({ setAsActiveTab: $event })\"\n      >\n        {{ t('workflow.blocks.browser-event.setAsActiveTab') }}\n      </ui-checkbox>\n    </template>\n  </div>\n</template>\n<script setup>\nimport { useI18n } from 'vue-i18n';\n\nconst props = defineProps({\n  data: {\n    type: Object,\n    default: () => ({}),\n  },\n});\nconst emit = defineEmits(['update:data']);\n\nconst { t } = useI18n();\n\nconst browserEvents = {\n  Tab: [\n    { id: 'tab:close', name: 'Tab closed' },\n    { id: 'tab:loaded', name: 'Tab loaded' },\n    { id: 'tab:create', name: 'Tab created' },\n  ],\n  Window: [\n    { id: 'window:create', name: 'Window created' },\n    { id: 'window:close', name: 'Window closed' },\n  ],\n};\n\nfunction updateData(value) {\n  emit('update:data', { ...props.data, ...value });\n}\n</script>\n"
  },
  {
    "path": "src/components/newtab/workflow/edit/EditClipboard.vue",
    "content": "<template>\n  <div>\n    <ui-textarea\n      :model-value=\"data.description\"\n      class=\"w-full\"\n      :placeholder=\"t('common.description')\"\n      @change=\"updateData({ description: $event })\"\n    />\n    <template v-if=\"hasAllPermissions\">\n      <ui-select\n        :model-value=\"data.type\"\n        class=\"mt-4 w-full\"\n        @change=\"updateData({ type: $event })\"\n      >\n        <option v-for=\"type in types\" :key=\"type\" :value=\"type\">\n          {{ t(`workflow.blocks.clipboard.types.${type}`) }}\n        </option>\n      </ui-select>\n      <insert-workflow-data\n        v-if=\"data.type === 'get'\"\n        :data=\"data\"\n        variables\n        @update=\"updateData\"\n      />\n      <template v-else>\n        <ui-textarea\n          v-if=\"!data.copySelectedText\"\n          :model-value=\"data.dataToCopy\"\n          placeholder=\"Text\"\n          class=\"mt-4\"\n          @change=\"updateData({ dataToCopy: $event })\"\n        />\n        <ui-checkbox\n          :model-value=\"data.copySelectedText\"\n          class=\"mt-2\"\n          @change=\"updateData({ copySelectedText: $event })\"\n        >\n          {{ t('workflow.blocks.clipboard.copySelection') }}\n        </ui-checkbox>\n      </template>\n    </template>\n    <template v-else>\n      <p class=\"mt-4\">\n        {{ t('workflow.blocks.clipboard.noPermission') }}\n      </p>\n      <ui-button variant=\"accent\" class=\"mt-2\" @click=\"permission.request\">\n        {{ t('workflow.blocks.clipboard.grantPermission') }}\n      </ui-button>\n    </template>\n  </div>\n</template>\n<script setup>\nimport { computed } from 'vue';\nimport { useI18n } from 'vue-i18n';\nimport { useHasPermissions } from '@/composable/hasPermissions';\nimport InsertWorkflowData from './InsertWorkflowData.vue';\n\nconst props = defineProps({\n  data: {\n    type: Object,\n    default: () => ({}),\n  },\n});\nconst emit = defineEmits(['update:data']);\n\nconst types = ['get', 'insert'];\nconst permissions = ['clipboardRead'];\nconst isFirefox = BROWSER_TYPE === 'firefox';\n\nif (isFirefox) {\n  permissions.push('clipboardWrite');\n}\n\nconst { t } = useI18n();\nconst permission = useHasPermissions(permissions);\n\nconst hasAllPermissions = computed(() => {\n  if (isFirefox)\n    return permission.has.clipboardRead && permission.has.clipboardWrite;\n\n  return permission.has.clipboardRead;\n});\n\nfunction updateData(value) {\n  emit('update:data', { ...props.data, ...value });\n}\n</script>\n"
  },
  {
    "path": "src/components/newtab/workflow/edit/EditCloseTab.vue",
    "content": "<template>\n  <div class=\"mb-2 mt-4\">\n    <ui-textarea\n      :model-value=\"data.description\"\n      :placeholder=\"t('common.description')\"\n      class=\"w-full\"\n      @change=\"updateData({ description: $event })\"\n    />\n    <ui-select\n      :model-value=\"data.closeType\"\n      :placeholder=\"Close\"\n      class=\"mt-2 w-full\"\n      @change=\"updateData({ closeType: $event })\"\n    >\n      <option\n        v-for=\"type in types\"\n        :key=\"type\"\n        :value=\"type\"\n        class=\"capitalize\"\n      >\n        {{ type }}\n      </option>\n    </ui-select>\n    <template v-if=\"data.closeType === 'tab'\">\n      <div class=\"mt-1\">\n        <ui-checkbox\n          :model-value=\"data.activeTab\"\n          @change=\"updateData({ activeTab: $event })\"\n        >\n          {{ t('workflow.blocks.close-tab.activeTab') }}\n        </ui-checkbox>\n      </div>\n      <edit-autocomplete v-if=\"!data.activeTab\">\n        <ui-input\n          :model-value=\"data.url\"\n          class=\"mt-1 w-full\"\n          placeholder=\"http://example.com/*\"\n          @change=\"updateData({ url: $event })\"\n        >\n          <template #label>\n            {{ t('workflow.blocks.close-tab.url') }}\n            <a\n              :title=\"t('common.example', 2)\"\n              rel=\"noopener\"\n              target=\"_blank\"\n              href=\"https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Match_patterns#examples\"\n            >\n              <v-remixicon\n                name=\"riInformationLine\"\n                size=\"18\"\n                class=\"inline-block\"\n              />\n            </a>\n          </template>\n        </ui-input>\n      </edit-autocomplete>\n    </template>\n    <ui-checkbox\n      v-else\n      class=\"mt-1\"\n      :model-value=\"data.allWindows\"\n      @change=\"updateData({ allWindows: $event })\"\n    >\n      {{ t('workflow.blocks.close-tab.allWindows') }}\n    </ui-checkbox>\n  </div>\n</template>\n<script setup>\nimport { useI18n } from 'vue-i18n';\nimport EditAutocomplete from './EditAutocomplete.vue';\n\nconst props = defineProps({\n  data: {\n    type: Object,\n    default: () => ({}),\n  },\n});\nconst emit = defineEmits(['update:data']);\n\nconst { t } = useI18n();\nconst types = ['tab', 'window'];\n\nfunction updateData(value) {\n  emit('update:data', { ...props.data, ...value });\n}\n</script>\n"
  },
  {
    "path": "src/components/newtab/workflow/edit/EditConditions.vue",
    "content": "<template>\n  <div>\n    <ui-textarea\n      :model-value=\"data.description\"\n      class=\"w-full\"\n      :placeholder=\"t('common.description')\"\n      @change=\"updateData({ description: $event })\"\n    />\n    <div class=\"my-4 flex items-center space-x-2\">\n      <p v-if=\"state.showSettings\" class=\"font-semibold\">\n        {{ t('common.settings') }}\n      </p>\n      <ui-button\n        v-else\n        :disabled=\"conditions.length >= 20\"\n        variant=\"accent\"\n        class=\"mr-2\"\n        @click=\"addCondition\"\n      >\n        {{ t('workflow.blocks.conditions.add') }}\n      </ui-button>\n      <div class=\"grow\"></div>\n      <ui-button\n        v-tooltip:bottom=\"t('common.settings')\"\n        icon\n        @click=\"state.showSettings = !state.showSettings\"\n      >\n        <v-remixicon\n          :name=\"state.showSettings ? 'riCloseLine' : 'riSettings3Line'\"\n        />\n      </ui-button>\n    </div>\n    <template v-if=\"state.showSettings\">\n      <label class=\"mt-6 flex items-center\">\n        <ui-switch\n          :model-value=\"data.retryConditions\"\n          @change=\"updateData({ retryConditions: $event })\"\n        />\n        <span class=\"ml-2 leading-tight\">\n          {{ t('workflow.blocks.conditions.retryConditions') }}\n        </span>\n      </label>\n      <div v-if=\"data.retryConditions\" class=\"mt-2\">\n        <ui-input\n          :model-value=\"data.retryCount\"\n          :title=\"t('workflow.blocks.element-exists.tryFor.title')\"\n          :label=\"t('workflow.blocks.element-exists.tryFor.label')\"\n          class=\"mb-1 w-full\"\n          type=\"number\"\n          min=\"1\"\n          @change=\"updateData({ retryCount: +$event })\"\n        />\n        <ui-input\n          :model-value=\"data.retryTimeout\"\n          :label=\"t('workflow.blocks.element-exists.timeout.label')\"\n          :title=\"t('workflow.blocks.element-exists.timeout.title')\"\n          class=\"w-full\"\n          type=\"number\"\n          min=\"200\"\n          @change=\"updateData({ retryTimeout: +$event })\"\n        />\n      </div>\n    </template>\n    <draggable\n      v-else\n      v-model=\"conditions\"\n      item-key=\"id\"\n      tag=\"ui-list\"\n      class=\"space-y-1\"\n      @end=\"onEnd\"\n    >\n      <template #item=\"{ element, index }\">\n        <ui-list-item class=\"group cursor-move\">\n          <v-remixicon name=\"riGuideLine\" size=\"20\" class=\"mr-2 -ml-1\" />\n          <p class=\"text-overflow flex-1\" :title=\"element.name\">\n            {{ element.name }}\n          </p>\n          <v-remixicon\n            class=\"invisible cursor-pointer group-hover:visible\"\n            name=\"riPencilLine\"\n            size=\"20\"\n            @click=\"editCondition(index)\"\n          />\n          <v-remixicon\n            name=\"riDeleteBin7Line\"\n            size=\"20\"\n            class=\"ml-2 -mr-1 cursor-pointer\"\n            @click=\"deleteCondition(index, element.id)\"\n          />\n        </ui-list-item>\n      </template>\n    </draggable>\n    <ui-modal v-model=\"state.showModal\" custom-content>\n      <ui-card padding=\"p-0\" class=\"w-full max-w-3xl\">\n        <div class=\"flex items-center px-4 pt-4\">\n          <p class=\"flex-1\">\n            {{ t('workflow.conditionBuilder.title') }}\n          </p>\n          <v-remixicon\n            name=\"riCloseLine\"\n            class=\"cursor-pointer\"\n            @click=\"state.showModal = false\"\n          />\n        </div>\n        <div\n          class=\"scroll overflow-auto p-4\"\n          style=\"height: calc(100vh - 8rem)\"\n        >\n          <input\n            v-model=\"conditions[state.conditionsIndex].name\"\n            class=\"mb-4 bg-transparent text-xl font-semibold focus:ring-0\"\n          />\n          <shared-condition-builder\n            :model-value=\"conditions[state.conditionsIndex].conditions\"\n            @change=\"conditions[state.conditionsIndex].conditions = $event\"\n          />\n        </div>\n      </ui-card>\n    </ui-modal>\n  </div>\n</template>\n<script setup>\nimport { ref, watch, onMounted, shallowReactive } from 'vue';\nimport { useI18n } from 'vue-i18n';\nimport { nanoid } from 'nanoid';\nimport Draggable from 'vuedraggable';\nimport { debounce } from '@/utils/helper';\nimport SharedConditionBuilder from '@/components/newtab/shared/SharedConditionBuilder/index.vue';\n\nconst props = defineProps({\n  data: {\n    type: Object,\n    default: () => ({}),\n  },\n  editor: {\n    type: Object,\n    default: () => ({}),\n  },\n  fullData: {\n    type: Object,\n    default: () => ({}),\n  },\n  blockId: {\n    type: String,\n    default: '',\n  },\n});\nconst emit = defineEmits(['update:data']);\n\nconst conditionTypes = {\n  '==': 'eq',\n  '!=': 'nq',\n  '>': 'gt',\n  '>=': 'gte',\n  '<': 'lt',\n  '<=': 'lte',\n  '()': 'cnt',\n};\nconst { t } = useI18n();\n\nconst conditions = ref(props.data.conditions);\nconst state = shallowReactive({\n  showModal: false,\n  conditionsIndex: 0,\n  showSettings: false,\n});\n\nfunction editCondition(index) {\n  state.conditionsIndex = index;\n  state.showModal = true;\n}\nfunction addCondition() {\n  if (conditions.value.length >= 20) return;\n\n  conditions.value.push({\n    id: nanoid(),\n    name: `Path ${conditions.value.length + 1}`,\n    conditions: [],\n  });\n}\nfunction deleteCondition(index, id) {\n  conditions.value.splice(index, 1);\n\n  props.editor.removeEdges((edges) => {\n    return edges.filter(\n      (edge) => edge.sourceHandle === `${props.blockId}-output-${id}`\n    );\n  });\n}\nfunction updateData(value) {\n  emit('update:data', { ...props.data, ...value });\n}\n\nconst onEnd = debounce(() => {\n  props.editor.updateNodeInternals([props.blockId]);\n}, 500);\n\nwatch(\n  conditions,\n  () => {\n    updateData({ conditions: conditions.value });\n  },\n  { deep: true }\n);\n\nonMounted(() => {\n  if (props.fullData?.editCondition) {\n    const index = props.data.conditions.findIndex(\n      (item) => item.id === props.fullData.editCondition\n    );\n    if (index !== -1) editCondition(index);\n  }\n\n  const condition = props.data.conditions[0];\n  if (condition && condition.conditions) return;\n\n  const generateConditionItem = (type, data) => {\n    if (type === 'value') {\n      return {\n        id: nanoid(),\n        type: 'value',\n        category: 'value',\n        data: { value: data },\n      };\n    }\n\n    return { id: nanoid(), category: 'compare', type: data };\n  };\n  conditions.value = conditions.value.map((item, index) => {\n    const items = [\n      generateConditionItem('value', item.compareValue),\n      generateConditionItem('compare', conditionTypes[item.type]),\n      generateConditionItem('value', item.value),\n    ];\n\n    return {\n      id: nanoid(),\n      name: `Path ${index + 1}`,\n      conditions: [\n        {\n          id: nanoid(),\n          conditions: [{ id: nanoid(), items }],\n        },\n      ],\n    };\n  });\n});\n</script>\n"
  },
  {
    "path": "src/components/newtab/workflow/edit/EditCookie.vue",
    "content": "<template>\n  <div>\n    <ui-textarea\n      :model-value=\"data.description\"\n      class=\"w-full\"\n      :placeholder=\"t('common.description')\"\n      @change=\"updateData({ description: $event })\"\n    />\n    <template v-if=\"permission.has.cookies\">\n      <ui-select\n        :model-value=\"data.type\"\n        class=\"mt-4 w-full\"\n        @change=\"updateData({ type: $event })\"\n      >\n        <option v-for=\"type in types\" :key=\"type\" :value=\"type\">\n          {{ t(`workflow.blocks.cookie.types.${type}`) }}\n        </option>\n      </ui-select>\n      <ui-checkbox\n        v-if=\"data.type === 'get'\"\n        :model-value=\"data.getAll\"\n        class=\"mt-1\"\n        @change=\"updateData({ getAll: $event })\"\n      >\n        {{ t('workflow.blocks.cookie.types.getAll') }}\n      </ui-checkbox>\n      <ui-checkbox\n        :model-value=\"data.useJson\"\n        block\n        class=\"mt-1\"\n        @change=\"updateData({ useJson: $event })\"\n      >\n        {{ t('workflow.blocks.cookie.useJson') }}\n      </ui-checkbox>\n      <template v-if=\"data.useJson\">\n        <shared-codemirror\n          :model-value=\"data.jsonCode\"\n          :extensions=\"codemirrorExts\"\n          lang=\"json\"\n          class=\"cookie-editor mt-4\"\n          @change=\"updateData({ jsonCode: $event })\"\n        />\n        <a\n          :href=\"`https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/cookies/${\n            data.type === 'get' && data.getAll ? 'getAll' : data.type\n          }`\"\n          rel=\"noopener\"\n          class=\"mt-2 inline-block underline\"\n          target=\"_blank\"\n        >\n          See all available properties\n        </a>\n      </template>\n      <template v-else>\n        <ui-input\n          :model-value=\"data.url\"\n          class=\"mt-2 w-full\"\n          type=\"url\"\n          label=\"URL\"\n          placeholder=\"https://example.com/\"\n          @change=\"updateData({ url: $event })\"\n        />\n        <ui-input\n          :model-value=\"data.name\"\n          :label=\"`Name ${\n            data.type === 'get' && !data.getAll ? '' : '(optional)'\n          }`\"\n          class=\"mt-2 w-full\"\n          placeholder=\"site-cookie\"\n          @change=\"updateData({ name: $event })\"\n        />\n        <ui-input\n          v-if=\"data.type === 'set'\"\n          :model-value=\"data.value\"\n          label=\"Value (optional)\"\n          class=\"mt-2 w-full\"\n          placeholder=\"value\"\n          @change=\"updateData({ value: $event })\"\n        />\n        <ui-input\n          :model-value=\"data.path\"\n          class=\"mt-2 w-full\"\n          label=\"Path (optional)\"\n          placeholder=\"/\"\n          @change=\"updateData({ path: $event })\"\n        />\n        <ui-input\n          v-if=\"isGetOrSet\"\n          :model-value=\"data.domain\"\n          class=\"mt-2 w-full\"\n          label=\"Domain (optional)\"\n          placeholder=\".example.com\"\n          @change=\"updateData({ domain: $event })\"\n        />\n        <ui-input\n          v-if=\"data.type === 'set'\"\n          :model-value=\"data.sameSite\"\n          class=\"mt-2 w-full\"\n          label=\"sameSite (optional)\"\n          placeholder=\"lax\"\n          @change=\"updateData({ sameSite: $event })\"\n        />\n        <ui-input\n          v-if=\"data.type === 'set'\"\n          :model-value=\"data.expirationDate\"\n          class=\"mt-2 w-full\"\n          label=\"expirationDate (seconds) (optional)\"\n          placeholder=\"3600\"\n          @change=\"updateData({ expirationDate: $event })\"\n        />\n        <div\n          v-if=\"data.type === 'set' || (data.type === 'get' && data.getAll)\"\n          class=\"mt-4\"\n        >\n          <ui-checkbox\n            v-if=\"data.type === 'set'\"\n            :model-value=\"data.httpOnly\"\n            class=\"mr-4\"\n            @change=\"updateData({ httpOnly: $event })\"\n          >\n            httpOnly\n          </ui-checkbox>\n          <ui-checkbox\n            :model-value=\"data.secure\"\n            @change=\"updateData({ secure: $event })\"\n          >\n            secure\n          </ui-checkbox>\n        </div>\n      </template>\n      <div v-if=\"data.type === 'get'\" class=\"cookie-data mt-4 border-t pt-4\">\n        <insert-workflow-data :data=\"data\" variables @update=\"updateData\" />\n      </div>\n    </template>\n    <template v-else>\n      <p class=\"mt-4\">\n        This block requires \"Cookies\" permission to work properly\n      </p>\n      <ui-button variant=\"accent\" class=\"mt-2\" @click=\"permission.request\">\n        {{ t('workflow.blocks.clipboard.grantPermission') }}\n      </ui-button>\n    </template>\n  </div>\n</template>\n<script setup>\nimport { computed, defineAsyncComponent } from 'vue';\nimport { useI18n } from 'vue-i18n';\nimport { autocompletion } from '@codemirror/autocomplete';\nimport { syntaxTree } from '@codemirror/language';\nimport { useHasPermissions } from '@/composable/hasPermissions';\nimport InsertWorkflowData from './InsertWorkflowData.vue';\n\nconst SharedCodemirror = defineAsyncComponent(() =>\n  import('@/components/newtab/shared/SharedCodemirror.vue')\n);\n\nconst props = defineProps({\n  data: {\n    type: Object,\n    default: () => ({}),\n  },\n});\nconst emit = defineEmits(['update:data']);\n\nconst { t } = useI18n();\nconst permission = useHasPermissions(['cookies']);\n\nconst types = ['get', 'set', 'remove'];\nconst methodProps = {\n  name: { label: 'name', type: 'text' },\n  url: { label: 'url', type: 'text' },\n  path: { label: 'path', type: 'text' },\n  session: { label: 'session', type: 'text' },\n  secure: { label: 'secure', type: 'text' },\n  domain: { label: 'domain', type: 'text' },\n  sameSite: { label: 'sameSite', type: 'text' },\n  httpOnly: { label: 'httpOnly', type: 'text' },\n};\n\nconst isGetOrSet = computed(\n  () =>\n    (props.data.type === 'get' && props.data.getAll) ||\n    props.data.type === 'set'\n);\n\nfunction cookieOptionsAutocomplete(context) {\n  const word = context.matchBefore(/\\w*/);\n  const nodeBefore = syntaxTree(context.state).resolveInner(context.pos, -1);\n\n  if (\n    nodeBefore.name !== 'PropertyName' ||\n    (word.from === word.to && !context.explicit)\n  )\n    return null;\n\n  let options = [];\n\n  if (props.data.type === 'get') {\n    if (props.data.getAll) {\n      options = [\n        methodProps.domain,\n        methodProps.name,\n        methodProps.path,\n        methodProps.secure,\n        methodProps.url,\n      ];\n    } else {\n      options = [methodProps.name, methodProps.url];\n    }\n  } else if (props.data.type === 'set') {\n    options = Object.values(methodProps);\n  } else if (props.data.type === 'remove') {\n    options = [methodProps.name, methodProps.url];\n  }\n\n  return {\n    options,\n    from: word.from,\n  };\n}\nfunction updateData(value) {\n  emit('update:data', { ...props.data, ...value });\n}\n\nconst codemirrorExts = [\n  autocompletion({\n    override: [cookieOptionsAutocomplete],\n  }),\n];\n</script>\n<style>\n.cookie-data .block-variable {\n  margin-top: 0;\n}\n\n.cookie-editor .cm-tooltip-autocomplete {\n  margin-left: 0px !important;\n  margin-top: -5px !important;\n}\n</style>\n"
  },
  {
    "path": "src/components/newtab/workflow/edit/EditCreateElement.vue",
    "content": "<template>\n  <edit-interaction-base\n    :data=\"blockData\"\n    hide-mark-el\n    hide-multiple\n    @change=\"updateSelector\"\n  >\n    <ui-select\n      v-model=\"blockData.insertAt\"\n      :label=\"$t('workflow.blocks.create-element.insertEl.title')\"\n      class=\"mt-4 w-full\"\n    >\n      <option v-for=\"item in insertOptions\" :key=\"item\" :value=\"item\">\n        {{ $t(`workflow.blocks.create-element.insertEl.items.${item}`) }}\n      </option>\n    </ui-select>\n    <ui-checkbox\n      :model-value=\"data.runBeforeLoad\"\n      class=\"mt-2\"\n      @change=\"updateData({ runBeforeLoad: $event })\"\n    >\n      Run before page loaded\n    </ui-checkbox>\n    <ui-button\n      variant=\"accent\"\n      class=\"mt-4 w-full\"\n      @click=\"state.showModal = true\"\n    >\n      {{ $t('workflow.blocks.create-element.edit') }}\n    </ui-button>\n    <ui-modal\n      v-model=\"state.showModal\"\n      content-class=\"max-w-3xl create-element-modal\"\n      padding=\"p-0\"\n    >\n      <template #header>\n        <ui-tabs v-model=\"state.activeTab\" class=\"space-x-1 border-none\">\n          <ui-tab v-for=\"tab in tabs\" :key=\"tab.id\" :value=\"tab.id\">\n            {{ tab.name }}\n          </ui-tab>\n          <ui-tab value=\"preloadScript\">\n            {{ $t('workflow.blocks.javascript-code.modal.tabs.preloadScript') }}\n          </ui-tab>\n        </ui-tabs>\n      </template>\n      <ui-tab-panels\n        :model-value=\"state.activeTab\"\n        class=\"scroll mb-4 overflow-auto px-4\"\n        style=\"height: calc(100vh - 12rem)\"\n      >\n        <ui-tab-panel value=\"html\" class=\"h-full\">\n          <shared-codemirror\n            v-model=\"blockData.html\"\n            lang=\"html\"\n            class=\"h-full\"\n          />\n        </ui-tab-panel>\n        <ui-tab-panel value=\"css\" class=\"h-full\">\n          <shared-codemirror\n            v-model=\"blockData.css\"\n            lang=\"css\"\n            class=\"h-full\"\n          />\n        </ui-tab-panel>\n        <ui-tab-panel value=\"javascript\" class=\"h-full\">\n          <div class=\"mb-4\">\n            <span class=\"text-sm text-gray-500 dark:text-gray-300\">\n              Available functions\n            </span>\n            <div class=\"flex items-center space-x-2\">\n              <a\n                v-for=\"func in availableFuncs\"\n                :key=\"func.id\"\n                :href=\"`https://docs.extension.automa.site/blocks/javascript-code.html#${func.id}`\"\n                target=\"_blank\"\n                rel=\"noopener\"\n                class=\"bg-box-transparent inline-block rounded-md p-1 text-sm\"\n              >\n                <code>\n                  {{ func.name }}\n                </code>\n              </a>\n            </div>\n          </div>\n          <shared-codemirror\n            v-model=\"blockData.javascript\"\n            :extensions=\"codemirrorExts\"\n            lang=\"javascript\"\n            class=\"h-full\"\n          />\n        </ui-tab-panel>\n        <ui-tab-panel value=\"preloadScript\">\n          <ul class=\"my-1 space-y-2\">\n            <li\n              v-for=\"(item, index) in blockData.preloadScripts\"\n              :key=\"index\"\n              class=\"flex items-center space-x-2\"\n            >\n              <ui-select v-model=\"item.type\" placeholder=\"Type\">\n                <option value=\"style\">Style</option>\n                <option value=\"script\">Script</option>\n              </ui-select>\n              <ui-input\n                v-model=\"item.src\"\n                :placeholder=\"`https://example.com/${\n                  item.type === 'style' ? 'style.css' : 'script.js'\n                }`\"\n                type=\"url\"\n                class=\"flex-1\"\n              />\n              <v-remixicon\n                name=\"riDeleteBin7Line\"\n                class=\"cursor-pointer\"\n                @click=\"blockData.preloadScripts.splice(index, 1)\"\n              />\n            </li>\n          </ul>\n          <ui-button class=\"mt-4\" @click=\"addPreloadScript\">\n            Add script\n          </ui-button>\n        </ui-tab-panel>\n      </ui-tab-panels>\n    </ui-modal>\n  </edit-interaction-base>\n</template>\n<script setup>\nimport {\n  automaFuncsCompletion,\n  automaFuncsSnippets,\n  completeFromGlobalScope,\n} from '@/utils/codeEditorAutocomplete';\nimport { autocompletion } from '@codemirror/autocomplete';\nimport cloneDeep from 'lodash.clonedeep';\nimport { defineAsyncComponent, reactive, watch } from 'vue';\nimport EditInteractionBase from './EditInteractionBase.vue';\n\nconst SharedCodemirror = defineAsyncComponent(() =>\n  import('@/components/newtab/shared/SharedCodemirror.vue')\n);\n\nconst props = defineProps({\n  data: {\n    type: Object,\n    default: () => ({}),\n  },\n  blockId: {\n    type: String,\n    default: '',\n  },\n});\nconst emit = defineEmits(['update:data']);\n\nconst availableFuncs = [\n  { name: 'automaRefData(keyword, path?)', id: 'automarefdata-keyword-path' },\n  { name: 'automaExecWorkflow(options)', id: 'automaexecworkflow-options' },\n];\nconst insertOptions = [\n  'before',\n  'after',\n  'prev-sibling',\n  'next-sibling',\n  'replace',\n];\nconst tabs = [\n  { id: 'html', name: 'HTML' },\n  { id: 'css', name: 'CSS' },\n  { id: 'javascript', name: 'JavaScript' },\n];\n\nconst autocompleteList = [\n  automaFuncsSnippets.automaExecWorkflow,\n  automaFuncsSnippets.automaRefData,\n];\nconst codemirrorExts = [\n  autocompletion({\n    override: [\n      automaFuncsCompletion(autocompleteList),\n      completeFromGlobalScope,\n    ],\n  }),\n];\n\nconst blockData = reactive(cloneDeep(props.data));\nconst state = reactive({\n  showModal: false,\n  activeTab: 'html',\n});\n\nfunction updateData(value) {\n  emit('update:data', { ...props.data, ...value });\n}\nfunction addPreloadScript() {\n  blockData.preloadScripts.push({\n    src: '',\n    type: 'script',\n  });\n}\nfunction updateSelector(data) {\n  Object.assign(blockData, data);\n}\n\nwatch(blockData, (newValue) => {\n  updateData(newValue);\n});\n</script>\n<style>\n.create-element-modal .modal-ui__content-header {\n  @apply p-4 mb-0;\n}\n</style>\n"
  },
  {
    "path": "src/components/newtab/workflow/edit/EditDataMapping.vue",
    "content": "<template>\n  <div>\n    <ui-textarea\n      :model-value=\"data.description\"\n      :placeholder=\"t('common.description')\"\n      class=\"w-full\"\n      @change=\"updateData({ description: $event })\"\n    />\n    <ui-select\n      :label=\"t('workflow.blocks.data-mapping.dataSource')\"\n      :model-value=\"data.dataSource\"\n      class=\"mt-4 w-full\"\n      @change=\"updateData({ dataSource: $event })\"\n    >\n      <option v-for=\"source in dataSources\" :key=\"source.id\" :value=\"source.id\">\n        {{ source.name }}\n      </option>\n    </ui-select>\n    <ui-input\n      v-if=\"data.dataSource === 'variable'\"\n      :model-value=\"data.varSourceName\"\n      :placeholder=\"t('workflow.variables.name')\"\n      :title=\"t('workflow.variables.name')\"\n      class=\"mt-2 w-full\"\n      @change=\"updateData({ varSourceName: $event })\"\n    />\n    <ui-button\n      variant=\"accent\"\n      class=\"mt-4 w-full\"\n      @click=\"state.showModal = true\"\n    >\n      {{ t('workflow.blocks.data-mapping.edit') }}\n    </ui-button>\n    <insert-workflow-data :data=\"data\" variables @update=\"updateData\" />\n    <ui-modal\n      v-model=\"state.showModal\"\n      :title=\"t('workflow.blocks.data-mapping.edit')\"\n      content-class=\"max-w-2xl data-map\"\n    >\n      <div\n        class=\"scroll my-4 overflow-auto px-4\"\n        style=\"min-height: 400px; max-height: calc(100vh - 12rem)\"\n      >\n        <table class=\"w-full\">\n          <thead>\n            <tr class=\"bg-box-transparent\">\n              <th class=\"w-6/12 rounded-l-lg\">\n                {{ t('workflow.blocks.data-mapping.source') }}\n              </th>\n              <th class=\"w-6/12 rounded-r-lg\">\n                {{ t('workflow.blocks.data-mapping.destination') }}\n              </th>\n            </tr>\n          </thead>\n          <tbody class=\"divide-y\">\n            <tr v-for=\"(source, index) in state.sources\" :key=\"source.id\">\n              <td class=\"group relative pr-4 align-baseline\">\n                <div class=\"flex items-center space-x-2\">\n                  <ui-autocomplete\n                    :items=\"state.autocompleteItems\"\n                    :disabled=\"data.dataSource !== 'table'\"\n                  >\n                    <ui-input\n                      :model-value=\"source.name\"\n                      class=\"flex-1\"\n                      placeholder=\"Source property\"\n                      @blur=\"updateSource({ index, source, event: $event })\"\n                    />\n                  </ui-autocomplete>\n                  <v-remixicon\n                    name=\"riDeleteBin7Line\"\n                    class=\"invisible cursor-pointer group-hover:visible\"\n                    @click=\"state.sources.splice(index, 1)\"\n                  />\n                  <v-remixicon\n                    name=\"riArrowLeftLine\"\n                    rotate=\"180\"\n                    class=\"absolute -right-2 top-4 text-gray-600 dark:text-gray-300\"\n                  />\n                </div>\n              </td>\n              <td class=\"pl-4 align-baseline\">\n                <ul class=\"space-y-1\">\n                  <li\n                    v-for=\"(destination, destIndex) in source.destinations\"\n                    :key=\"destination.id\"\n                    class=\"group flex items-center space-x-2\"\n                  >\n                    <ui-input\n                      :model-value=\"destination.name\"\n                      class=\"flex-1\"\n                      placeholder=\"Destination property\"\n                      @blur=\"\n                        updateDestination({\n                          index,\n                          destIndex,\n                          destination,\n                          event: $event,\n                        })\n                      \"\n                    />\n                    <v-remixicon\n                      name=\"riDeleteBin7Line\"\n                      class=\"invisible cursor-pointer group-hover:visible\"\n                      @click=\"\n                        state.sources[index].destinations.splice(destIndex, 1)\n                      \"\n                    />\n                  </li>\n                </ul>\n                <ui-button\n                  icon\n                  class=\"mt-2 text-sm\"\n                  @click=\"addDestination(index)\"\n                >\n                  {{ t('workflow.blocks.data-mapping.addDestination') }}\n                </ui-button>\n              </td>\n            </tr>\n            <tr>\n              <td>\n                <ui-button class=\"text-sm\" @click=\"addSource\">\n                  {{ t('workflow.blocks.data-mapping.addSource') }}\n                </ui-button>\n              </td>\n            </tr>\n          </tbody>\n        </table>\n      </div>\n    </ui-modal>\n  </div>\n</template>\n<script setup>\nimport { reactive, onMounted, inject, watch } from 'vue';\nimport { useI18n } from 'vue-i18n';\nimport { nanoid } from 'nanoid';\nimport { debounce } from '@/utils/helper';\nimport InsertWorkflowData from './InsertWorkflowData.vue';\n\nconst props = defineProps({\n  data: {\n    type: Object,\n    default: () => ({}),\n  },\n});\nconst emit = defineEmits(['update:data']);\n\nconst { t } = useI18n();\nconst dataSources = [\n  { id: 'table', name: t('workflow.table.title') },\n  { id: 'variable', name: t('workflow.variables.title') },\n];\n\nconst workflow = inject('workflow');\n\nconst state = reactive({\n  query: '',\n  showModal: false,\n  autocompleteItems: [],\n  sources: [...props.data.sources],\n});\n\nfunction isNameDuplicate({ items, currItem, newName, event }) {\n  const isDuplicate = items.some(\n    (item) => currItem.id !== item.id && item.name === newName\n  );\n\n  if (isDuplicate || !newName) {\n    event.target.value = currItem.name;\n    return true;\n  }\n\n  return false;\n}\nfunction updateSource({ index, source, event }) {\n  const newName = event.target.value.trim();\n  const isDuplicate = isNameDuplicate({\n    event,\n    newName,\n    currItem: source,\n    items: state.sources,\n  });\n\n  if (isDuplicate) return;\n\n  state.sources[index].name = newName;\n}\nfunction updateDestination({ index, destIndex, destination, event }) {\n  const newName = event.target.value.trim();\n  const sourceDests = state.sources[index].destinations;\n  const isDuplicate = isNameDuplicate({\n    event,\n    newName,\n    items: sourceDests,\n    currItem: destination,\n  });\n\n  if (isDuplicate) return;\n\n  sourceDests[destIndex].name = newName;\n}\nfunction addSource() {\n  const id = nanoid(4);\n\n  state.sources.push({\n    id,\n    destinations: [],\n    name: `source_${id}`,\n  });\n}\nfunction addDestination(sourceIndex) {\n  const id = nanoid(4);\n\n  state.sources[sourceIndex].destinations.push({\n    id,\n    name: `dest_${id}`,\n  });\n}\nfunction updateData(value) {\n  emit('update:data', { ...props.data, ...value });\n}\n\nwatch(\n  () => state.sources,\n  debounce(() => {\n    updateData({ sources: state.sources });\n  }, 200),\n  { deep: true }\n);\n\nonMounted(() => {\n  state.autocompleteItems = workflow.columns.value.map(({ name }) => name);\n});\n</script>\n<style>\n.data-map {\n  padding: 0 !important;\n  .modal-ui__content-header {\n    @apply px-4 pt-4;\n  }\n}\n</style>\n"
  },
  {
    "path": "src/components/newtab/workflow/edit/EditDelay.vue",
    "content": "<template>\n  <div class=\"space-y-2\">\n    <ui-input\n      :model-value=\"data.time\"\n      label=\"Delay time (millisecond)\"\n      class=\"w-full\"\n      type=\"text\"\n      @change=\"updateData({ time: $event })\"\n    />\n  </div>\n</template>\n<script setup>\nconst props = defineProps({\n  data: {\n    type: Object,\n    default: () => ({}),\n  },\n});\nconst emit = defineEmits(['update:data']);\n\nfunction updateData(value) {\n  emit('update:data', { ...props.data, ...value });\n}\n</script>\n"
  },
  {
    "path": "src/components/newtab/workflow/edit/EditDeleteData.vue",
    "content": "<template>\n  <div>\n    <ui-textarea\n      :model-value=\"data.description\"\n      class=\"w-full\"\n      :placeholder=\"t('common.description')\"\n      @change=\"updateData({ description: $event })\"\n    />\n    <ul class=\"delete-list mt-4\">\n      <li\n        v-for=\"(item, index) in deleteList\"\n        :key=\"item.id\"\n        class=\"mb-2 border-b pb-4\"\n      >\n        <div class=\"flex items-end space-x-2\">\n          <ui-select\n            v-model=\"deleteList[index].type\"\n            :label=\"t('workflow.blocks.delete-data.from')\"\n            class=\"flex-1\"\n          >\n            <option v-for=\"type in types\" :key=\"type.id\" :value=\"type.id\">\n              {{ type.name }}\n            </option>\n          </ui-select>\n          <ui-button icon @click=\"deleteList.splice(index, 1)\">\n            <v-remixicon name=\"riDeleteBin7Line\" />\n          </ui-button>\n        </div>\n        <ui-input\n          v-if=\"item.type === 'variable'\"\n          v-model=\"deleteList[index].variableName\"\n          :placeholder=\"t('workflow.variables.name')\"\n          :title=\"t('workflow.variables.name')\"\n          autocomplete=\"off\"\n          class=\"mt-2 w-full\"\n        />\n        <ui-select\n          v-else\n          v-model=\"deleteList[index].columnId\"\n          :label=\"t('workflow.table.select')\"\n          class=\"mt-1 w-full\"\n        >\n          <option value=\"[all]\">\n            {{ t('workflow.blocks.delete-data.allColumns') }}\n          </option>\n          <option value=\"column\">Column</option>\n          <option\n            v-for=\"column in workflow.columns.value\"\n            :key=\"column.id\"\n            :value=\"column.id\"\n          >\n            {{ column.name }}\n          </option>\n        </ui-select>\n      </li>\n    </ul>\n    <ui-button class=\"my-4\" variant=\"accent\" @click=\"addItem\">\n      {{ t('common.add') }}\n    </ui-button>\n  </div>\n</template>\n<script setup>\nimport { inject, ref, watch } from 'vue';\nimport { useI18n } from 'vue-i18n';\nimport cloneDeep from 'lodash.clonedeep';\n\nconst props = defineProps({\n  data: {\n    type: Object,\n    default: () => ({}),\n  },\n});\nconst emit = defineEmits(['update:data']);\n\nconst { t } = useI18n();\n\nconst workflow = inject('workflow', {});\nconst deleteList = ref(cloneDeep(props.data.deleteList));\n\nconst types = [\n  { id: 'table', name: t('workflow.table.title') },\n  { id: 'variable', name: t('workflow.variables.title') },\n];\n\nfunction updateData(value) {\n  emit('update:data', { ...props.data, ...value });\n}\nfunction addItem() {\n  deleteList.value.push({\n    type: 'table',\n    variableName: '',\n    columnId: '[all]',\n  });\n}\n\nwatch(\n  deleteList,\n  (value) => {\n    updateData({ deleteList: value });\n  },\n  { deep: true }\n);\n</script>\n<style scoped>\n.delete-list li:last-child {\n  padding-bottom: 0;\n  margin-bottom: 0;\n  border-bottom: 0;\n}\n</style>\n"
  },
  {
    "path": "src/components/newtab/workflow/edit/EditElementExists.vue",
    "content": "<template>\n  <div>\n    <ui-textarea\n      :model-value=\"data.description\"\n      :placeholder=\"t('common.description')\"\n      class=\"w-full\"\n      @change=\"updateData({ description: $event })\"\n    />\n    <ui-select\n      :model-value=\"data.findBy || 'cssSelector'\"\n      :placeholder=\"t('workflow.blocks.base.findElement.placeholder')\"\n      class=\"mb-1 mt-4 w-full\"\n      @change=\"updateData({ findBy: $event })\"\n    >\n      <option v-for=\"type in selectorTypes\" :key=\"type\" :value=\"type\">\n        {{ t(`workflow.blocks.base.findElement.options.${type}`) }}\n      </option>\n    </ui-select>\n    <edit-autocomplete class=\"mb-1\">\n      <ui-input\n        :model-value=\"data.selector\"\n        :label=\"t('workflow.blocks.element-exists.selector')\"\n        :placeholder=\"data.findBy === 'xpath' ? '//element' : '.element'\"\n        autocomplete=\"off\"\n        class=\"w-full\"\n        @change=\"updateData({ selector: $event })\"\n      />\n    </edit-autocomplete>\n    <ui-input\n      :model-value=\"data.tryCount\"\n      :title=\"t('workflow.blocks.element-exists.tryFor.title')\"\n      :label=\"t('workflow.blocks.element-exists.tryFor.label')\"\n      class=\"mb-1 w-full\"\n      type=\"number\"\n      min=\"1\"\n      @change=\"updateData({ tryCount: +$event })\"\n    />\n    <ui-input\n      :model-value=\"data.timeout\"\n      :label=\"t('workflow.blocks.element-exists.timeout.label')\"\n      :title=\"t('workflow.blocks.element-exists.timeout.title')\"\n      class=\"w-full\"\n      type=\"number\"\n      min=\"200\"\n      @change=\"updateData({ timeout: +$event })\"\n    />\n    <label class=\"mt-4 flex items-center\">\n      <ui-switch\n        :model-value=\"data.throwError\"\n        class=\"mr-2\"\n        @change=\"updateData({ throwError: $event })\"\n      />\n      <span>{{ t('workflow.blocks.element-exists.throwError') }}</span>\n    </label>\n  </div>\n</template>\n<script setup>\nimport { onMounted } from 'vue';\nimport { useI18n } from 'vue-i18n';\nimport EditAutocomplete from './EditAutocomplete.vue';\n\nconst props = defineProps({\n  data: {\n    type: Object,\n    default: () => ({}),\n  },\n});\nconst emit = defineEmits(['update:data']);\n\nconst { t } = useI18n();\n\nconst selectorTypes = ['cssSelector', 'xpath'];\n\nfunction updateData(value) {\n  emit('update:data', { ...props.data, ...value });\n}\n\nonMounted(() => {\n  if (!props.data.findBy) {\n    updateData({ findBy: 'cssSelector' });\n  }\n});\n</script>\n"
  },
  {
    "path": "src/components/newtab/workflow/edit/EditExecuteWorkflow.vue",
    "content": "<template>\n  <div class=\"mb-12\">\n    <ui-textarea\n      :model-value=\"data.description\"\n      :placeholder=\"t('common.description')\"\n      autoresize\n      class=\"mb-2 w-full\"\n      @change=\"updateData({ description: $event })\"\n    />\n    <ui-select\n      :model-value=\"data.workflowId\"\n      :placeholder=\"t('workflow.blocks.execute-workflow.select')\"\n      class=\"mb-2 w-full\"\n      @change=\"updateData({ workflowId: $event })\"\n    >\n      <optgroup label=\"Local\">\n        <option\n          v-for=\"workflow in workflows\"\n          :key=\"workflow.id\"\n          :value=\"workflow.id\"\n        >\n          {{ workflow.name }}\n        </option>\n      </optgroup>\n      <optgroup v-if=\"route.params.teamId\" label=\"Team\">\n        <option\n          v-for=\"workflow in teamWorkflows\"\n          :key=\"workflow.id\"\n          :value=\"workflow.id\"\n        >\n          {{ workflow.name }}\n        </option>\n      </optgroup>\n    </ui-select>\n    <ui-input\n      :model-value=\"data.executeId\"\n      :label=\"t('workflow.blocks.execute-workflow.executeId')\"\n      :title=\"t('workflow.blocks.execute-workflow.executeId')\"\n      placeholder=\"abc123\"\n      class=\"w-full\"\n      @change=\"updateData({ executeId: $event })\"\n    />\n    <p class=\"mt-4 ml-1 mb-1 text-sm text-gray-600 dark:text-gray-200\">\n      {{ t('common.globalData') }}\n    </p>\n    <ui-checkbox\n      :model-value=\"data.insertAllGlobalData\"\n      class=\"mb-4 leading-tight text-sm\"\n      @change=\"updateData({ insertAllGlobalData: $event })\"\n    >\n      {{ t('workflow.blocks.execute-workflow.insertAllGlobalData') }}\n    </ui-checkbox>\n    <pre\n      v-if=\"!state.showGlobalData\"\n      class=\"max-h-80 overflow-auto rounded-lg bg-gray-900 p-4 text-gray-200\"\n      @click=\"state.showGlobalData = true\"\n      v-text=\"data.globalData || '____'\"\n    />\n    <ui-checkbox\n      :model-value=\"data.insertAllVars\"\n      class=\"mt-4 leading-tight\"\n      @change=\"updateData({ insertAllVars: $event })\"\n    >\n      {{ t('workflow.blocks.execute-workflow.insertAllVars') }}\n    </ui-checkbox>\n    <template v-if=\"!data.insertAllVars\">\n      <label class=\"mt-4 block\">\n        <span class=\"ml-1 block text-sm text-gray-600 dark:text-gray-200\">\n          {{ t('workflow.blocks.execute-workflow.insertVars') }}\n        </span>\n        <ui-textarea\n          :model-value=\"data.insertVars\"\n          placeholder=\"varA,varB,varC\"\n          @change=\"updateData({ insertVars: $event })\"\n        />\n      </label>\n      <span\n        class=\"ml-1 block text-sm leading-tight text-gray-600 dark:text-gray-200\"\n      >\n        {{ t('workflow.blocks.execute-workflow.useCommas') }}\n      </span>\n    </template>\n    <ui-modal\n      v-model=\"state.showGlobalData\"\n      title=\"Global data\"\n      content-class=\"max-w-xl\"\n    >\n      <p>{{ t('workflow.blocks.execute-workflow.overwriteNote') }}</p>\n      <shared-codemirror\n        :model-value=\"data.globalData\"\n        lang=\"json\"\n        class=\"scroll w-full\"\n        style=\"height: calc(100vh - 10rem)\"\n        @change=\"updateData({ globalData: $event })\"\n      />\n    </ui-modal>\n  </div>\n</template>\n<script setup>\nimport { computed, shallowReactive, defineAsyncComponent } from 'vue';\nimport { useI18n } from 'vue-i18n';\nimport { useRoute } from 'vue-router';\nimport { useWorkflowStore } from '@/stores/workflow';\nimport { useTeamWorkflowStore } from '@/stores/teamWorkflow';\n\nconst SharedCodemirror = defineAsyncComponent(() =>\n  import('@/components/newtab/shared/SharedCodemirror.vue')\n);\n\nconst props = defineProps({\n  data: {\n    type: Object,\n    default: () => ({}),\n  },\n  hideBase: {\n    type: Boolean,\n    default: false,\n  },\n});\nconst emit = defineEmits(['update:data']);\n\nconst { t } = useI18n();\nconst route = useRoute();\nconst workflowStore = useWorkflowStore();\nconst teamWorkflowStore = useTeamWorkflowStore();\n\nconst state = shallowReactive({\n  showGlobalData: false,\n});\n\nconst filterWorkflows = (workflows) =>\n  workflows\n    .filter(({ id, drawflow }) => {\n      const flow =\n        typeof drawflow === 'string' ? drawflow : JSON.stringify(drawflow);\n\n      return id !== route.params.id && !flow.includes(route.params.id);\n    })\n    .sort((a, b) => (a.name > b.name ? 1 : -1));\n\nconst workflows = computed(() => filterWorkflows(workflowStore.getWorkflows));\nconst teamWorkflows = computed(() => {\n  const { teamId } = route.params;\n  if (!teamId) return [];\n\n  return filterWorkflows(teamWorkflowStore.getByTeam(teamId));\n});\n\nfunction updateData(value) {\n  emit('update:data', { ...props.data, ...value });\n}\n</script>\n"
  },
  {
    "path": "src/components/newtab/workflow/edit/EditExportData.vue",
    "content": "<template>\n  <div>\n    <ui-textarea\n      :model-value=\"data.description\"\n      class=\"w-full\"\n      :placeholder=\"t('common.description')\"\n      @change=\"updateData({ description: $event })\"\n    />\n    <template v-if=\"!permission.has.downloads\">\n      <p class=\"mt-4\">\n        {{ t('workflow.blocks.handle-download.noPermission') }}\n      </p>\n      <ui-button variant=\"accent\" class=\"mt-2\" @click=\"permission.request\">\n        {{ t('workflow.blocks.clipboard.grantPermission') }}\n      </ui-button>\n    </template>\n    <template v-else>\n      <ui-select\n        :model-value=\"data.dataToExport\"\n        :label=\"t('workflow.blocks.export-data.dataToExport.placeholder')\"\n        class=\"mt-2 w-full\"\n        @change=\"updateData({ dataToExport: $event })\"\n      >\n        <option v-for=\"option in dataToExport\" :key=\"option\" :value=\"option\">\n          {{ t(`workflow.blocks.export-data.dataToExport.options.${option}`) }}\n        </option>\n      </ui-select>\n      <ui-input\n        v-if=\"data.dataToExport === 'google-sheets'\"\n        :model-value=\"data.refKey\"\n        :title=\"t('workflow.blocks.export-data.refKey')\"\n        :placeholder=\"t('workflow.blocks.export-data.refKey')\"\n        class=\"mt-2 w-full\"\n        @change=\"updateData({ refKey: $event })\"\n      />\n      <ui-input\n        v-if=\"data.dataToExport === 'variable'\"\n        :model-value=\"data.variableName\"\n        :title=\"t('workflow.variables.name')\"\n        :placeholder=\"t('workflow.variables.name')\"\n        class=\"mt-2 w-full\"\n        @change=\"updateData({ variableName: $event })\"\n      />\n      <edit-autocomplete class=\"mt-2\">\n        <ui-input\n          :model-value=\"data.name\"\n          autocomplete=\"off\"\n          label=\"File name\"\n          class=\"w-full\"\n          placeholder=\"unnamed\"\n          @change=\"updateData({ name: $event })\"\n        />\n      </edit-autocomplete>\n      <ui-select\n        :model-value=\"data.onConflict\"\n        :label=\"t('workflow.blocks.handle-download.onConflict')\"\n        class=\"mt-2 w-full\"\n        @change=\"updateData({ onConflict: $event })\"\n      >\n        <option v-for=\"item in onConflict\" :key=\"item\" :value=\"item\">\n          {{ t(`workflow.blocks.base.downloads.onConflict.${item}`) }}\n        </option>\n      </ui-select>\n      <ui-select\n        :model-value=\"data.type\"\n        :label=\"t('workflow.blocks.export-data.exportAs')\"\n        class=\"mt-2 w-full\"\n        @change=\"updateData({ type: $event })\"\n      >\n        <option v-for=\"type in dataExportTypes\" :key=\"type.id\" :value=\"type.id\">\n          {{ type.name }}\n        </option>\n      </ui-select>\n      <ui-expand\n        v-if=\"data.type === 'csv'\"\n        hide-header-icon\n        header-class=\"flex items-center focus:ring-0 w-full\"\n      >\n        <template #header=\"{ show }\">\n          <v-remixicon\n            :rotate=\"show ? 270 : 180\"\n            name=\"riArrowLeftSLine\"\n            class=\"text-gray-600 transition-transform dark:text-gray-300\"\n          />\n          {{ t('common.options') }}\n        </template>\n        <div class=\"mt-1 pl-6\">\n          <ui-checkbox\n            v-if=\"data.type === 'csv'\"\n            :model-value=\"data.addBOMHeader\"\n            @change=\"updateData({ addBOMHeader: $event })\"\n          >\n            {{ t('workflow.blocks.export-data.bomHeader') }}\n          </ui-checkbox>\n          <ui-input\n            :model-value=\"data.csvDelimiter\"\n            label=\"Delimiter\"\n            class=\"mt-1\"\n            placeholder=\",\"\n            @change=\"updateData({ csvDelimiter: $event })\"\n          />\n        </div>\n      </ui-expand>\n    </template>\n  </div>\n</template>\n<script setup>\nimport { useI18n } from 'vue-i18n';\nimport { dataExportTypes } from '@/utils/shared';\nimport { useHasPermissions } from '@/composable/hasPermissions';\nimport EditAutocomplete from './EditAutocomplete.vue';\n\nconst props = defineProps({\n  data: {\n    type: Object,\n    default: () => ({}),\n  },\n});\nconst emit = defineEmits(['update:data']);\n\nconst dataToExport = ['data-columns', 'google-sheets', 'variable'];\nconst onConflict = ['uniquify', 'overwrite', 'prompt'];\n\nconst { t } = useI18n();\nconst permission = useHasPermissions(['downloads']);\n\nfunction updateData(value) {\n  emit('update:data', { ...props.data, ...value });\n}\n</script>\n"
  },
  {
    "path": "src/components/newtab/workflow/edit/EditForms.vue",
    "content": "<template>\n  <edit-interaction-base v-bind=\"{ data, hide: hideBase }\" @change=\"updateData\">\n    <hr />\n    <ui-checkbox\n      :model-value=\"data.getValue\"\n      @change=\"updateData({ getValue: $event })\"\n    >\n      {{ t('workflow.blocks.forms.getValue') }}\n    </ui-checkbox>\n    <insert-workflow-data\n      v-if=\"data.getValue && !hideBase\"\n      :data=\"data\"\n      variables\n      @update=\"updateData\"\n    />\n    <template v-else>\n      <ui-select\n        :model-value=\"data.type\"\n        class=\"mb-2 mt-4 block w-full\"\n        :placeholder=\"t('workflow.blocks.forms.type')\"\n        @change=\"updateData({ type: $event })\"\n      >\n        <option v-for=\"form in forms\" :key=\"form\" :value=\"form\">\n          {{ t(`workflow.blocks.forms.${form}.name`) }}\n        </option>\n      </ui-select>\n      <ui-checkbox\n        v-if=\"data.type === 'checkbox' || data.type === 'radio'\"\n        :model-value=\"data.selected\"\n        @change=\"updateData({ selected: $event })\"\n      >\n        {{ t('workflow.blocks.forms.selected') }}\n      </ui-checkbox>\n      <template v-if=\"data.type === 'text-field'\">\n        <edit-autocomplete class=\"mb-1 w-full\">\n          <ui-textarea\n            :model-value=\"data.value\"\n            :placeholder=\"t('workflow.blocks.forms.text-field.value')\"\n            class=\"w-full\"\n            @change=\"updateData({ value: $event })\"\n          />\n        </edit-autocomplete>\n        <ui-checkbox\n          :model-value=\"data.clearValue\"\n          @change=\"updateData({ clearValue: $event })\"\n        >\n          {{ t('workflow.blocks.forms.text-field.clearValue') }}\n        </ui-checkbox>\n      </template>\n      <template v-if=\"data.type === 'select'\">\n        <ui-select\n          :model-value=\"data.selectOptionBy\"\n          label=\"Select an option by\"\n          class=\"w-full\"\n          @change=\"updateData({ selectOptionBy: $event })\"\n        >\n          <option value=\"value\">The value</option>\n          <optgroup label=\"The position\">\n            <option value=\"first-option\">First option</option>\n            <option value=\"last-option\">Last option</option>\n            <option value=\"custom-position\">Custom</option>\n          </optgroup>\n        </ui-select>\n        <div v-if=\"data.selectOptionBy === 'value'\" class=\"mt-2\">\n          <edit-autocomplete class=\"mb-1 w-full\">\n            <ui-textarea\n              :model-value=\"data.value\"\n              :placeholder=\"t('workflow.blocks.forms.text-field.value')\"\n              class=\"w-full\"\n              @change=\"updateData({ value: $event })\"\n            />\n          </edit-autocomplete>\n          <ui-checkbox\n            :model-value=\"data.clearValue\"\n            @change=\"updateData({ clearValue: $event })\"\n          >\n            {{ t('workflow.blocks.forms.text-field.clearValue') }}\n          </ui-checkbox>\n        </div>\n        <ui-input\n          v-else-if=\"data.selectOptionBy === 'custom-position'\"\n          :model-value=\"data.optionPosition\"\n          label=\"Option position\"\n          placeholder=\"0\"\n          class=\"mt-2 w-full\"\n          @change=\"updateData({ optionPosition: $event })\"\n        />\n      </template>\n      <ui-input\n        v-if=\"data.type === 'text-field'\"\n        :model-value=\"data.delay\"\n        :label=\"t('workflow.blocks.forms.text-field.delay.label')\"\n        :placeholder=\"t('workflow.blocks.forms.text-field.delay.placeholder')\"\n        class=\"mt-1 w-full\"\n        min=\"0\"\n        @change=\"updateData({ delay: $event })\"\n      />\n    </template>\n  </edit-interaction-base>\n</template>\n<script setup>\nimport { useI18n } from 'vue-i18n';\nimport InsertWorkflowData from './InsertWorkflowData.vue';\nimport EditInteractionBase from './EditInteractionBase.vue';\nimport EditAutocomplete from './EditAutocomplete.vue';\n\nconst props = defineProps({\n  data: {\n    type: Object,\n    default: () => ({}),\n  },\n  hideBase: {\n    type: Boolean,\n    default: false,\n  },\n});\nconst emit = defineEmits(['update:data']);\n\nconst { t } = useI18n();\n\nconst forms = ['text-field', 'select', 'checkbox', 'radio'];\n\nfunction updateData(value) {\n  emit('update:data', { ...props.data, ...value });\n}\n</script>\n"
  },
  {
    "path": "src/components/newtab/workflow/edit/EditGetText.vue",
    "content": "<template>\n  <edit-interaction-base v-bind=\"{ data }\" @change=\"updateData\">\n    <hr />\n    <div class=\"bg-input flex items-center rounded-lg px-4 transition\">\n      <span>/</span>\n      <input\n        :value=\"data.regex\"\n        placeholder=\"Regex\"\n        class=\"w-11/12 bg-transparent p-2 focus:ring-0\"\n        @input=\"updateData({ regex: $event.target.value })\"\n      />\n      <ui-popover>\n        <template #trigger>\n          <button>/{{ regexExp.join('') || 'flags' }}</button>\n        </template>\n        <p class=\"mb-2 text-gray-600 dark:text-gray-200\">Expression flags</p>\n        <div class=\"space-y-1\">\n          <div v-for=\"item in exps\" :key=\"item.id\">\n            <ui-checkbox\n              :model-value=\"regexExp.includes(item.id)\"\n              @change=\"handleExpCheckbox(item.id, $event)\"\n            >\n              {{ item.name }}\n            </ui-checkbox>\n          </div>\n        </div>\n      </ui-popover>\n    </div>\n    <div class=\"mt-2 flex space-x-2\">\n      <edit-autocomplete class=\"w-full\">\n        <ui-input\n          :model-value=\"data.prefixText\"\n          :title=\"t('workflow.blocks.get-text.prefixText.title')\"\n          :label=\"t('workflow.blocks.get-text.prefixText.placeholder')\"\n          autocomplete=\"off\"\n          placeholder=\"Text\"\n          class=\"w-full\"\n          @change=\"updateData({ prefixText: $event })\"\n        />\n      </edit-autocomplete>\n      <edit-autocomplete class=\"w-full\">\n        <ui-input\n          :model-value=\"data.suffixText\"\n          :title=\"t('workflow.blocks.get-text.suffixText.title')\"\n          :label=\"t('workflow.blocks.get-text.suffixText.placeholder')\"\n          autocomplete=\"off\"\n          placeholder=\"Text\"\n          class=\"w-full\"\n          @change=\"updateData({ suffixText: $event })\"\n        />\n      </edit-autocomplete>\n    </div>\n    <ui-checkbox\n      :model-value=\"data.includeTags\"\n      class=\"mt-4\"\n      @change=\"updateData({ includeTags: $event })\"\n    >\n      {{ t('workflow.blocks.get-text.includeTags') }}\n    </ui-checkbox>\n    <ui-checkbox\n      :model-value=\"data.useTextContent\"\n      class=\"mt-2\"\n      @change=\"updateData({ useTextContent: $event })\"\n    >\n      Use <code>textContent</code>\n    </ui-checkbox>\n    <hr />\n    <insert-workflow-data\n      :data=\"data\"\n      variables\n      extra-row\n      @update=\"updateData\"\n    />\n  </edit-interaction-base>\n</template>\n<script setup>\nimport { ref } from 'vue';\nimport { useI18n } from 'vue-i18n';\nimport InsertWorkflowData from './InsertWorkflowData.vue';\nimport EditInteractionBase from './EditInteractionBase.vue';\nimport EditAutocomplete from './EditAutocomplete.vue';\n\nconst props = defineProps({\n  data: {\n    type: Object,\n    default: () => ({}),\n  },\n});\nconst emit = defineEmits(['update:data']);\n\nconst { t } = useI18n();\n\nconst regexData = Array.isArray(props.data.regexExp)\n  ? props.data.regexExp\n  : Object.values(props.data.regexExp);\nconst regexExp = ref([...new Set(regexData)]);\n\nconst exps = [\n  { id: 'g', name: 'global' },\n  { id: 'i', name: 'ignore case' },\n  { id: 'm', name: 'multiline' },\n];\n\nfunction updateData(value) {\n  emit('update:data', { ...props.data, ...value });\n}\nfunction handleExpCheckbox(id, value) {\n  if (value) {\n    regexExp.value.push(id);\n  } else {\n    regexExp.value.splice(regexExp.value.indexOf(id), 1);\n  }\n\n  updateData({ regexExp: regexExp.value });\n}\n</script>\n"
  },
  {
    "path": "src/components/newtab/workflow/edit/EditGoogleDrive.vue",
    "content": "<template>\n  <div>\n    <div v-if=\"!store.integrations.googleDrive\">\n      <p>\n        You haven't\n        <a\n          href=\"https://docs.extension.automa.site/integrations/google-drive.html\"\n          target=\"_blank\"\n          class=\"underline\"\n          >connected Automa to Google Drive</a\n        >.\n      </p>\n    </div>\n    <template v-else>\n      <ui-textarea\n        :model-value=\"data.description\"\n        class=\"w-full\"\n        :placeholder=\"t('common.description')\"\n        @change=\"updateData({ description: $event })\"\n      />\n      <ui-select\n        :model-value=\"data.action\"\n        class=\"w-full mt-4\"\n        @change=\"updateData({ action: $event })\"\n      >\n        <option v-for=\"action in actions\" :key=\"action\" :value=\"action\">\n          {{ t(`workflow.blocks.google-drive.actions.${action}`) }}\n        </option>\n      </ui-select>\n      <div class=\"mt-4\">\n        <ul class=\"space-y-2\">\n          <li\n            v-for=\"(item, index) in filePaths\"\n            :key=\"item.id\"\n            class=\"p-2 border rounded-lg\"\n          >\n            <div class=\"flex items-center\">\n              <ui-select\n                v-model=\"item.type\"\n                class=\"grow mr-2\"\n                placeholder=\"File location\"\n              >\n                <option value=\"url\">URL</option>\n                <option value=\"local\" :disabled=\"!hasFileAccess\">\n                  Local computer\n                </option>\n                <option\n                  value=\"downloadId\"\n                  :disabled=\"!permissions.has.downloads\"\n                >\n                  Download id\n                </option>\n              </ui-select>\n              <ui-button icon @click=\"filePaths.splice(index, 1)\">\n                <v-remixicon name=\"riDeleteBin7Line\" />\n              </ui-button>\n            </div>\n            <edit-autocomplete>\n              <ui-input\n                v-model=\"item.name\"\n                placeholder=\"Filename (optional)\"\n                class=\"w-full mt-2\"\n              />\n            </edit-autocomplete>\n            <edit-autocomplete>\n              <ui-input\n                v-model=\"item.path\"\n                :placeholder=\"placeholders[item.type]\"\n                title=\"File location\"\n                class=\"w-full mt-2\"\n              />\n            </edit-autocomplete>\n          </li>\n        </ul>\n        <ui-button class=\"mt-4\" variant=\"accent\" @click=\"addFile\">\n          Add file\n        </ui-button>\n      </div>\n    </template>\n  </div>\n</template>\n<script setup>\nimport { useHasPermissions } from '@/composable/hasPermissions';\nimport { useStore } from '@/stores/main';\nimport cloneDeep from 'lodash.clonedeep';\nimport { nanoid } from 'nanoid/non-secure';\nimport { ref, watch } from 'vue';\nimport { useI18n } from 'vue-i18n';\nimport browser from 'webextension-polyfill';\nimport EditAutocomplete from './EditAutocomplete.vue';\n\nconst props = defineProps({\n  data: {\n    type: Object,\n    default: () => ({}),\n  },\n  hideBase: {\n    type: Boolean,\n    default: false,\n  },\n});\nconst emit = defineEmits(['update:data']);\n\nconst { t } = useI18n();\nconst store = useStore();\nstore.checkGDriveIntegration();\n\nconst actions = ['upload'];\nconst placeholders = {\n  downloadId: '0',\n  local: 'C:\\\\file.zip',\n  url: 'https://example.com/file.zip',\n};\n\nconst permissions = useHasPermissions(['downloads']);\n\nconst filePaths = ref(cloneDeep(props.data.filePaths));\nconst hasFileAccess = ref(true);\n\nfunction updateData(value) {\n  emit('update:data', { ...props.data, ...value });\n}\nfunction addFile() {\n  filePaths.value.push({ path: '', type: 'url', name: '', id: nanoid(5) });\n}\n\nbrowser.extension.isAllowedFileSchemeAccess().then((value) => {\n  hasFileAccess.value = value;\n});\n\nwatch(\n  filePaths,\n  (paths) => {\n    updateData({ filePaths: paths });\n  },\n  { deep: true }\n);\n</script>\n"
  },
  {
    "path": "src/components/newtab/workflow/edit/EditGoogleSheets.vue",
    "content": "<template>\n  <div class=\"mb-10\">\n    <ui-textarea\n      :model-value=\"data.description\"\n      class=\"mb-2 w-full\"\n      :placeholder=\"t('common.description')\"\n      @change=\"updateData({ description: $event })\"\n    />\n    <ui-select\n      :model-value=\"data.type\"\n      class=\"mb-2 w-full\"\n      @change=\"onActionChange\"\n    >\n      <option v-for=\"action in actions\" :key=\"action\" :value=\"action\">\n        {{ t(`workflow.blocks.google-sheets.select.${action}`) }}\n      </option>\n    </ui-select>\n    <slot />\n    <edit-autocomplete\n      v-if=\"\n        !googleDrive ||\n        (data.inputSpreadsheetId === 'manually' && data.type !== 'create')\n      \"\n    >\n      <ui-input\n        :model-value=\"data.spreadsheetId\"\n        class=\"w-full\"\n        placeholder=\"abcd123\"\n        @change=\"updateData({ spreadsheetId: $event }), checkPermission($event)\"\n      >\n        <template #label>\n          {{ t('workflow.blocks.google-sheets.spreadsheetId.label') }}*\n          <a\n            href=\"https://docs.extension.automa.site/blocks/google-sheets.html#spreadsheet-id\"\n            target=\"_blank\"\n            rel=\"noopener\"\n            :title=\"t('workflow.blocks.google-sheets.spreadsheetId.link')\"\n          >\n            <v-remixicon name=\"riInformationLine\" size=\"18\" class=\"inline\" />\n          </a>\n        </template>\n      </ui-input>\n    </edit-autocomplete>\n    <a\n      v-if=\"!state.havePermission\"\n      href=\"https://docs.extension.automa.site/blocks/google-sheets.html#access-to-spreadsheet\"\n      target=\"_blank\"\n      rel=\"noopener\"\n      class=\"ml-1 inline-block text-sm leading-tight\"\n    >\n      Automa doesn't have access to the spreadsheet.\n      <a\n        href=\"https://docs.extension.automa.site/blocks/google-sheets.html#access-to-spreadsheet\"\n        target=\"_blank\"\n        rel=\"noopener\"\n      >\n        Click here to read more.\n        <v-remixicon name=\"riInformationLine\" size=\"18\" class=\"inline\" />\n      </a>\n    </a>\n    <edit-autocomplete v-if=\"!['create', 'add-sheet'].includes(data.type)\">\n      <ui-input\n        :model-value=\"data.range\"\n        class=\"mt-1 w-full\"\n        placeholder=\"Sheet1!A1:B2\"\n        :class=\"{\n          'border-red-500':\n            !data.range && !['create', 'add-sheet'].includes(data.type),\n        }\"\n        @change=\"updateData({ range: $event })\"\n      >\n        <template #label>\n          {{ t('workflow.blocks.google-sheets.range.label') }}*\n          <a\n            href=\"https://docs.extension.automa.site/blocks/google-sheets.html#range\"\n            target=\"_blank\"\n            rel=\"noopener\"\n            :title=\"t('workflow.blocks.google-sheets.range.link')\"\n          >\n            <v-remixicon name=\"riInformationLine\" size=\"18\" class=\"inline\" />\n          </a>\n        </template>\n        <template\n          v-if=\"!data.range && !['create', 'add-sheet'].includes(data.type)\"\n          #footer\n        >\n          <span class=\"text-red-500 text-sm\">{{\n            t(\n              'workflow.blocks.google-sheets.range.required',\n              'Range is required'\n            )\n          }}</span>\n        </template>\n      </ui-input>\n    </edit-autocomplete>\n    <template v-if=\"data.type === 'get'\">\n      <ui-input\n        :model-value=\"data.refKey\"\n        :label=\"t('workflow.blocks.google-sheets.refKey.label')\"\n        :placeholder=\"t('workflow.blocks.google-sheets.refKey.placeholder')\"\n        class=\"mt-1 w-full\"\n        @change=\"updateData({ refKey: $event })\"\n      />\n      <ui-checkbox\n        :model-value=\"data.firstRowAsKey\"\n        class=\"mt-3\"\n        @change=\"updateData({ firstRowAsKey: $event })\"\n      >\n        {{ t('workflow.blocks.google-sheets.firstRow') }}\n      </ui-checkbox>\n      <ui-button\n        :loading=\"previewDataState.status === 'loading'\"\n        variant=\"accent\"\n        class=\"mt-3\"\n        @click=\"previewData\"\n      >\n        {{ t('workflow.blocks.google-sheets.previewData') }}\n      </ui-button>\n      <p v-if=\"previewDataState.status === 'error'\" class=\"text-red-500\">\n        {{ previewDataState.errorMessage }}\n      </p>\n    </template>\n    <template v-else-if=\"['getRange', 'create'].includes(data.type)\">\n      <p class=\"mt-4\">\n        {{ t('workflow.blocks.google-sheets.spreadsheetId.label') }}\n      </p>\n      <insert-workflow-data :data=\"data\" variables @update=\"updateData\" />\n      <ui-button\n        v-if=\"data.type === 'getRange'\"\n        :loading=\"previewDataState.status === 'loading'\"\n        variant=\"accent\"\n        class=\"mt-4\"\n        @click=\"previewData\"\n      >\n        {{ t('workflow.blocks.google-sheets.previewData') }}\n      </ui-button>\n    </template>\n    <template v-else-if=\"['update', 'append'].includes(data.type)\">\n      <ui-select\n        :model-value=\"data.valueInputOption\"\n        class=\"mt-2 w-full\"\n        @change=\"updateData({ valueInputOption: $event })\"\n      >\n        <template #label>\n          {{ t('workflow.blocks.google-sheets.valueInputOption') }}\n          <a\n            href=\"https://developers.google.com/sheets/api/reference/rest/v4/ValueInputOption\"\n            target=\"_blank\"\n            rel=\"noopener\"\n          >\n            <v-remixicon name=\"riInformationLine\" size=\"18\" class=\"inline\" />\n          </a>\n        </template>\n        <option\n          v-for=\"option in valueInputOptions\"\n          :key=\"option\"\n          :value=\"option\"\n        >\n          {{ option }}\n        </option>\n      </ui-select>\n      <ui-select\n        v-if=\"data.type === 'append'\"\n        :model-value=\"data.insertDataOption || 'INSERT_ROWS'\"\n        class=\"mt-2 w-full\"\n        @change=\"updateData({ insertDataOption: $event })\"\n      >\n        <template #label>\n          {{ t('workflow.blocks.google-sheets.insertDataOption') }}\n          <a\n            href=\"https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets.values/append#InsertDataOption\"\n            target=\"_blank\"\n            rel=\"noopener\"\n          >\n            <v-remixicon name=\"riInformationLine\" size=\"18\" class=\"inline\" />\n          </a>\n        </template>\n        <option\n          v-for=\"option in insertDataOptions\"\n          :key=\"option\"\n          :value=\"option\"\n        >\n          {{ option }}\n        </option>\n      </ui-select>\n      <ui-select\n        :model-value=\"data.dataFrom\"\n        :label=\"t('workflow.blocks.google-sheets.dataFrom.label')\"\n        class=\"mt-2 w-full\"\n        @change=\"updateData({ dataFrom: $event })\"\n      >\n        <option v-for=\"item in dataFrom\" :key=\"item\" :value=\"item\">\n          {{ t(`workflow.blocks.google-sheets.dataFrom.options.${item}`) }}\n        </option>\n      </ui-select>\n      <ui-checkbox\n        v-if=\"data.dataFrom === 'data-columns'\"\n        :model-value=\"data.keysAsFirstRow\"\n        class=\"mt-2\"\n        @change=\"updateData({ keysAsFirstRow: $event })\"\n      >\n        {{ t('workflow.blocks.google-sheets.keysAsFirstRow') }}\n      </ui-checkbox>\n      <ui-button\n        v-else\n        class=\"mt-2 w-full\"\n        variant=\"accent\"\n        @click=\"customDataState.showModal = true\"\n      >\n        {{ t('workflow.blocks.google-sheets.insertData') }}\n      </ui-button>\n    </template>\n    <shared-codemirror\n      v-if=\"\n        previewDataState.data &&\n        previewDataState.status !== 'error' &&\n        data.type !== 'update'\n      \"\n      :model-value=\"previewDataState.data\"\n      :line-numbers=\"false\"\n      readonly\n      hide-lang\n      class=\"scroll mt-4 max-h-96\"\n    />\n    <ui-modal\n      v-model=\"customDataState.showModal\"\n      title=\"Custom data\"\n      content-class=\"max-w-xl\"\n    >\n      <shared-codemirror\n        v-model=\"customDataState.data\"\n        style=\"height: calc(100vh - 10rem)\"\n        lang=\"json\"\n        @change=\"updateData({ customData: $event })\"\n      />\n    </ui-modal>\n  </div>\n</template>\n<script setup>\nimport { fetchApi } from '@/utils/api';\nimport googleSheetsApi from '@/utils/googleSheetsApi';\nimport { convert2DArrayToArrayObj, debounce } from '@/utils/helper';\nimport { defineAsyncComponent, shallowReactive } from 'vue';\nimport { useI18n } from 'vue-i18n';\nimport { useToast } from 'vue-toastification';\nimport EditAutocomplete from './EditAutocomplete.vue';\nimport InsertWorkflowData from './InsertWorkflowData.vue';\n\nconst SharedCodemirror = defineAsyncComponent(() =>\n  import('@/components/newtab/shared/SharedCodemirror.vue')\n);\n\nconst props = defineProps({\n  data: {\n    type: Object,\n    default: () => ({}),\n  },\n  googleDrive: Boolean,\n  additionalActions: {\n    type: Array,\n    default: () => [],\n  },\n});\nconst emit = defineEmits(['update:data']);\n\nconst { t } = useI18n();\nconst toast = useToast();\n\nconst actions = [\n  'get',\n  'getRange',\n  'update',\n  'append',\n  'clear',\n  ...props.additionalActions,\n];\nconst dataFrom = ['data-columns', 'custom'];\nconst valueInputOptions = ['RAW', 'USER_ENTERED'];\nconst insertDataOptions = ['OVERWRITE', 'INSERT_ROWS'];\n\nconst previewDataState = shallowReactive({\n  data: '',\n  status: 'idle',\n  errorMessage: '',\n});\nconst customDataState = shallowReactive({\n  showModal: false,\n  data: props.data.customData,\n});\nconst state = shallowReactive({\n  lastSheetId: null,\n  havePermission: true,\n});\n\nconst checkPermission = debounce(async (value) => {\n  try {\n    if (!value.trim()) {\n      toast.error('Spreadsheet id is empty');\n      return;\n    }\n\n    if (\n      value.includes('://') ||\n      value.startsWith('http') ||\n      value.startsWith('www.') ||\n      value.includes('docs.google.com')\n    ) {\n      toast.error('Spreadsheet id is invalid');\n      return;\n    }\n\n    if (state.lastSheetId === value) return;\n\n    const response = await fetchApi(\n      `/services/google-sheets/meta?spreadsheetId=${value}`\n    );\n\n    state.havePermission = response.status !== 403;\n    state.lastSheetId = value;\n  } catch (error) {\n    console.error(error);\n  }\n}, 1000);\n\nfunction updateData(value) {\n  emit('update:data', { ...props.data, ...value });\n}\nasync function previewData() {\n  try {\n    previewDataState.status = 'loading';\n\n    const isGetValues = props.data.type === 'get';\n    const params = {\n      range: props.data.range,\n      spreadsheetId: props.data.spreadsheetId,\n    };\n\n    if (!props.data.spreadsheetId.trim()) {\n      toast.error(\n        props.googleDrive\n          ? 'No spreadsheet is selected'\n          : 'Spreadsheet id is empty'\n      );\n      previewDataState.status = 'idle';\n      return;\n    }\n\n    if (\n      props.data.spreadsheetId.includes('http') ||\n      props.data.spreadsheetId.includes('spreadsheets')\n    ) {\n      toast.error('Spreadsheet Id is invalid, please check it');\n      previewDataState.status = 'idle';\n      return;\n    }\n\n    if (!props.data.range) {\n      toast.error('Spreadsheet range is empty');\n      previewDataState.status = 'idle';\n      return;\n    }\n\n    const response = await (isGetValues\n      ? googleSheetsApi(props.googleDrive).getValues(params)\n      : googleSheetsApi(props.googleDrive).getRange(params));\n\n    let result = props.googleDrive ? response : await response.json();\n\n    if (!response.ok && !props.googleDrive) {\n      throw new Error(result.message || response.statusText);\n    }\n\n    if (isGetValues) {\n      const values = result?.values ?? [];\n      result = props.data.firstRowAsKey\n        ? convert2DArrayToArrayObj(values)\n        : values;\n    } else {\n      result = {\n        tableRange: result.tableRange || null,\n        lastRange: result.updates.updatedRange,\n      };\n    }\n\n    previewDataState.data = JSON.stringify(result, null, 2);\n    previewDataState.status = 'idle';\n  } catch (error) {\n    console.error(error);\n    previewDataState.data = '';\n    previewDataState.status = 'error';\n    previewDataState.errorMessage = error.message;\n  }\n}\nfunction onActionChange(value) {\n  updateData({ type: value });\n\n  previewDataState.data = '';\n  previewDataState.status = '';\n  previewDataState.errorMessage = '';\n}\n</script>\n"
  },
  {
    "path": "src/components/newtab/workflow/edit/EditGoogleSheetsDrive.vue",
    "content": "<template>\n  <div v-if=\"!store.integrations.googleDrive\">\n    <p>\n      You haven't\n      <a\n        href=\"https://docs.automa.site/integrations/google-drive.html\"\n        target=\"_blank\"\n        class=\"underline\"\n        >connected Automa to Google Drive</a\n      >.\n    </p>\n  </div>\n  <edit-google-sheets\n    v-else\n    google-drive\n    :data=\"data\"\n    :additional-actions=\"['create', 'add-sheet']\"\n    @update:data=\"updateData\"\n  >\n    <ui-tabs\n      v-if=\"data.type !== 'create'\"\n      small\n      :model-value=\"data.inputSpreadsheetId\"\n      fill\n      class=\"w-full my-2\"\n      type=\"fill\"\n      @change=\"updateData({ inputSpreadsheetId: $event })\"\n    >\n      <ui-tab value=\"connected\"> Connected </ui-tab>\n      <ui-tab value=\"manually\"> Manually </ui-tab>\n    </ui-tabs>\n    <div\n      v-if=\"data.type !== 'create' && data.inputSpreadsheetId === 'connected'\"\n      class=\"flex items-end\"\n    >\n      <ui-select\n        :model-value=\"data.spreadsheetId\"\n        :label=\"t('workflow.blocks.google-sheets-drive.connected')\"\n        :placeholder=\"t('workflow.blocks.google-sheets-drive.select')\"\n        class=\"w-full\"\n        @change=\"updateData({ spreadsheetId: $event })\"\n      >\n        <option\n          v-for=\"sheet in store.connectedSheets\"\n          :key=\"sheet.id\"\n          :value=\"sheet.id\"\n        >\n          {{ sheet.name }}\n        </option>\n      </ui-select>\n      <ui-button\n        v-tooltip=\"t('workflow.blocks.google-sheets-drive.connect')\"\n        icon\n        class=\"ml-2\"\n        @click=\"connectSheet\"\n      >\n        <v-remixicon name=\"riLink\" />\n      </ui-button>\n    </div>\n    <ui-input\n      v-if=\"['create', 'add-sheet'].includes(data.type)\"\n      :model-value=\"data.sheetName\"\n      label=\"Sheet name\"\n      placeholder=\"A Spreadsheet\"\n      class=\"w-full\"\n      @change=\"updateData({ sheetName: $event })\"\n    />\n  </edit-google-sheets>\n</template>\n<script setup>\nimport { useStore } from '@/stores/main';\nimport { openGDrivePickerPopup } from '@/utils/openGDriveFilePicker';\nimport { useI18n } from 'vue-i18n';\nimport { useToast } from 'vue-toastification';\nimport browser from 'webextension-polyfill';\nimport EditGoogleSheets from './EditGoogleSheets.vue';\n\nconst props = defineProps({\n  data: {\n    type: Object,\n    default: () => ({}),\n  },\n});\nconst emit = defineEmits(['update:data']);\n\nconst { t } = useI18n();\nconst toast = useToast();\nconst store = useStore();\nstore.getConnectedSheets();\n\nfunction updateData(value) {\n  emit('update:data', { ...props.data, ...value });\n}\n\nasync function connectSheet() {\n  // 1. 获取当前 access_token\n  const { sessionToken } = await browser.storage.local.get('sessionToken');\n  if (!sessionToken?.access) {\n    toast.error('未获取到 Google 授权');\n    return;\n  }\n  try {\n    // 2. 弹出 Picker 让用户选择文件\n    const file = await openGDrivePickerPopup(sessionToken.access);\n    if (!file) return;\n    if (file.mimeType !== 'application/vnd.google-apps.spreadsheet') {\n      toast.error('File is not a google spreadsheet');\n      return;\n    }\n    const sheetExists = store.connectedSheets.some(\n      (sheet) => sheet.id === file.id\n    );\n    if (sheetExists) return;\n    // 3. 加入已连接列表\n    store.connectedSheets.push({ name: file.name, id: file.id });\n  } catch (e) {\n    toast.error('未选择文件或授权失败');\n  }\n}\n\n// function connectSheet() {\n//   openGDriveFilePicker().then((sheets) => {\n//     if (!Array.isArray(sheets) || sheets.length === 0) {\n//       toast.error('未获取到 Google Sheets 文件');\n//       return;\n//     }\n//     // 弹窗/下拉选择，用户选择后加入 store.connectedSheets\n//     // 这里用 window.prompt 简化，实际可用自定义弹窗组件\n//     const options = sheets.map((s, i) => `${i + 1}. ${s.name}`).join('\\n');\n//     const idx = window.prompt(`请选择要连接的 Google Sheet:\\n${options}`);\n//     const index = Number(idx) - 1;\n//     if (Number.isNaN(index) || !sheets[index]) return;\n//     const { name, id, mimeType } = sheets[index];\n//     if (mimeType !== 'application/vnd.google-apps.spreadsheet') {\n//       toast.error('File is not a google spreadsheet');\n//       return;\n//     }\n//     const sheetExists = store.connectedSheets.some((sheet) => sheet.id === id);\n//     if (sheetExists) return;\n//     store.connectedSheets.push({ name, id });\n//   });\n// }\n</script>\n"
  },
  {
    "path": "src/components/newtab/workflow/edit/EditHandleDialog.vue",
    "content": "<template>\n  <div>\n    <ui-textarea\n      :model-value=\"data.description\"\n      class=\"w-full\"\n      :placeholder=\"t('common.description')\"\n      @change=\"updateData({ description: $event })\"\n    />\n    <ui-checkbox\n      :model-value=\"data.accept\"\n      block\n      class=\"mt-4\"\n      @change=\"updateData({ accept: $event })\"\n    >\n      {{ t('workflow.blocks.handle-dialog.accept') }}\n    </ui-checkbox>\n    <edit-autocomplete v-if=\"data.accept\" class=\"mt-1\">\n      <ui-input\n        :model-value=\"data.promptText\"\n        :label=\"t('workflow.blocks.handle-dialog.promptText.label')\"\n        :title=\"t('workflow.blocks.handle-dialog.promptText.description')\"\n        autocomplete=\"off\"\n        placeholder=\"Text\"\n        class=\"w-full\"\n        @change=\"updateData({ promptText: $event })\"\n      />\n    </edit-autocomplete>\n  </div>\n</template>\n<script setup>\nimport { useI18n } from 'vue-i18n';\nimport EditAutocomplete from './EditAutocomplete.vue';\n\nconst props = defineProps({\n  data: {\n    type: Object,\n    default: () => ({}),\n  },\n});\nconst emit = defineEmits(['update:data']);\n\nconst { t } = useI18n();\n\nfunction updateData(value) {\n  emit('update:data', { ...props.data, ...value });\n}\n</script>\n"
  },
  {
    "path": "src/components/newtab/workflow/edit/EditHandleDownload.vue",
    "content": "<template>\n  <div>\n    <template v-if=\"permission.has.downloads\">\n      <ui-textarea\n        :model-value=\"data.description\"\n        class=\"w-full\"\n        :placeholder=\"t('common.description')\"\n        @change=\"updateData({ description: $event })\"\n      />\n      <ui-input\n        :model-value=\"data.timeout\"\n        :label=\"t('workflow.blocks.handle-download.timeout')\"\n        placeholder=\"1000\"\n        type=\"number\"\n        class=\"mt-2 w-full\"\n        @change=\"updateData({ timeout: +$event || 1000 })\"\n      />\n      <ui-input\n        :model-value=\"data.downloadId\"\n        :label=\"t('workflow.blocks.handle-download.downloadId')\"\n        class=\"mt-2 w-full\"\n        placeholder=\"0\"\n        @change=\"updateData({ downloadId: $event })\"\n      />\n      <template v-if=\"!data.downloadId?.trim()\">\n        <ui-input\n          :model-value=\"data.filename\"\n          :label=\"`${t('common.fileName')} (${t('common.optional')})`\"\n          placeholder=\"file\"\n          class=\"mt-2 w-full\"\n          @change=\"updateData({ filename: $event })\"\n        />\n        <ui-select\n          :model-value=\"data.onConflict\"\n          :label=\"t('workflow.blocks.handle-download.onConflict')\"\n          class=\"mt-2 w-full\"\n          @change=\"updateData({ onConflict: $event })\"\n        >\n          <option v-for=\"item in onConflict\" :key=\"item\" :value=\"item\">\n            {{ t(`workflow.blocks.base.downloads.onConflict.${item}`) }}\n          </option>\n        </ui-select>\n      </template>\n      <ui-checkbox\n        :model-value=\"data.waitForDownload\"\n        class=\"mt-4\"\n        @change=\"updateData({ waitForDownload: $event })\"\n      >\n        {{ t('workflow.blocks.handle-download.waitFile') }}\n      </ui-checkbox>\n      <template v-if=\"data.waitForDownload\">\n        <hr class=\"my-4 w-full\" />\n        <p class=\"text-sm text-gray-600 dark:text-gray-300\">\n          {{ t('workflow.blocks.handle-download.filePath') }}\n        </p>\n        <insert-workflow-data :data=\"data\" variables @update=\"updateData\" />\n      </template>\n    </template>\n    <template v-else>\n      <p class=\"mt-4\">\n        {{ t('workflow.blocks.handle-download.noPermission') }}\n      </p>\n      <ui-button variant=\"accent\" class=\"mt-2\" @click=\"permission.request\">\n        {{ t('workflow.blocks.clipboard.grantPermission') }}\n      </ui-button>\n    </template>\n  </div>\n</template>\n<script setup>\nimport { useI18n } from 'vue-i18n';\nimport { useHasPermissions } from '@/composable/hasPermissions';\nimport InsertWorkflowData from './InsertWorkflowData.vue';\n\nconst props = defineProps({\n  data: {\n    type: Object,\n    default: () => ({}),\n  },\n});\nconst emit = defineEmits(['update:data']);\n\nconst permission = useHasPermissions(['downloads']);\nconst onConflict = ['uniquify', 'overwrite', 'prompt'];\n\nconst { t } = useI18n();\n\nfunction updateData(value) {\n  emit('update:data', { ...props.data, ...value });\n}\n</script>\n"
  },
  {
    "path": "src/components/newtab/workflow/edit/EditIncreaseVariable.vue",
    "content": "<template>\n  <div>\n    <ui-textarea\n      :model-value=\"data.description\"\n      :placeholder=\"t('common.description')\"\n      class=\"w-full\"\n      @change=\"updateData({ description: $event })\"\n    />\n    <ui-input\n      :model-value=\"data.variableName\"\n      :label=\"t('workflow.variables.name')\"\n      :title=\"t('workflow.variables.name')\"\n      class=\"mt-2 w-full\"\n      @change=\"updateData({ variableName: $event })\"\n    />\n    <ui-input\n      :model-value=\"data.increaseBy\"\n      :label=\"t('workflow.blocks.increase-variable.increase')\"\n      placeholder=\"0\"\n      type=\"number\"\n      class=\"mt-2 w-full\"\n      @change=\"updateData({ increaseBy: +$event })\"\n    />\n  </div>\n</template>\n<script setup>\nimport { useI18n } from 'vue-i18n';\n\nconst props = defineProps({\n  data: {\n    type: Object,\n    default: () => ({}),\n  },\n});\nconst emit = defineEmits(['update:data']);\n\nconst { t } = useI18n();\n\nfunction updateData(value) {\n  emit('update:data', { ...props.data, ...value });\n}\n</script>\n<style>\n.log-data .block-variable {\n  margin-top: 4px;\n}\n</style>\n"
  },
  {
    "path": "src/components/newtab/workflow/edit/EditInsertData.vue",
    "content": "<template>\n  <div>\n    <ui-textarea\n      :model-value=\"data.description\"\n      :placeholder=\"t('common.description')\"\n      class=\"w-full\"\n      @change=\"updateData({ description: $event })\"\n    />\n    <ui-button\n      class=\"mt-4 mb-2 w-full\"\n      variant=\"accent\"\n      @click=\"showModal = !showModal\"\n    >\n      Insert data ({{ dataList.length }})\n    </ui-button>\n    <ui-modal\n      v-model=\"showModal\"\n      title=\"Insert data\"\n      padding=\"p-0\"\n      content-class=\"max-w-3xl insert-data-modal\"\n    >\n      <ul\n        class=\"data-list scroll mt-4 overflow-auto px-4 pb-4\"\n        style=\"max-height: calc(100vh - 13rem)\"\n      >\n        <li\n          v-for=\"(item, index) in dataList\"\n          :key=\"index\"\n          class=\"mb-4 rounded-lg border\"\n        >\n          <div class=\"flex items-center border-b p-2\">\n            <ui-select\n              :model-value=\"item.type\"\n              class=\"mr-2 shrink-0\"\n              @change=\"changeItemType(index, $event)\"\n            >\n              <option value=\"table\">\n                {{ t('workflow.table.title') }}\n              </option>\n              <option value=\"variable\">\n                {{ t('workflow.variables.title') }}\n              </option>\n            </ui-select>\n            <ui-input\n              v-if=\"item.type === 'variable'\"\n              v-model=\"item.name\"\n              :placeholder=\"t('workflow.variables.name')\"\n              :title=\"t('workflow.variables.name')\"\n              class=\"flex-1\"\n            />\n            <ui-select\n              v-else\n              v-model=\"item.name\"\n              :placeholder=\"t('workflow.table.select')\"\n            >\n              <option\n                v-for=\"column in workflow.columns.value\"\n                :key=\"column.id\"\n                :value=\"column.id\"\n              >\n                {{ column.name }}\n              </option>\n            </ui-select>\n            <div class=\"grow\" />\n            <v-remixicon\n              name=\"riDeleteBin7Line\"\n              class=\"cursor-pointer\"\n              @click=\"removeItem(index)\"\n            />\n          </div>\n          <div class=\"p-2\">\n            <div v-if=\"hasFileAccess && item.isFile\" class=\"flex items-end\">\n              <edit-autocomplete class=\"w-full\">\n                <ui-input\n                  v-model=\"item.filePath\"\n                  class=\"w-full\"\n                  :placeholder=\"\n                    isFirefox ? 'File URL' : 'File absolute path/File URL'\n                  \"\n                />\n              </edit-autocomplete>\n              <template\n                v-if=\"\n                  /.xlsx?$/.test(item.filePath) &&\n                  (item.action || item.csvAction)?.includes?.('json')\n                \"\n              >\n                <ui-input\n                  v-model=\"item.xlsSheet\"\n                  label=\"Sheet (optional)\"\n                  class=\"ml-2\"\n                  placeholder=\"Sheet1\"\n                />\n                <ui-input\n                  v-model=\"item.xlsRange\"\n                  label=\"Range (optional)\"\n                  class=\"ml-2\"\n                  placeholder=\"A1:C10\"\n                />\n              </template>\n            </div>\n            <edit-autocomplete v-else class=\"w-full\">\n              <ui-textarea\n                v-model=\"item.value\"\n                placeholder=\"value\"\n                title=\"value\"\n                class=\"w-full\"\n              />\n            </edit-autocomplete>\n            <div class=\"mt-2 flex items-center\">\n              <ui-button\n                v-tooltip=\"\n                  hasFileAccess\n                    ? 'Import file'\n                    : 'Don\\'t have access, click to learn more'\n                \"\n                :class=\"{ 'text-primary': item.isFile }\"\n                icon\n                @click=\"setAsFile(item)\"\n              >\n                <v-remixicon name=\"riFileLine\" />\n              </ui-button>\n              <template v-if=\"hasFileAccess && item.isFile\">\n                <ui-button class=\"ml-2\" @click=\"previewData(index, item)\">\n                  Preview data\n                </ui-button>\n                <ui-button\n                  v-if=\"previewState.itemId === index\"\n                  v-tooltip=\"'Clear preview'\"\n                  class=\"ml-2\"\n                  icon\n                  @click=\"clearPreview\"\n                >\n                  <v-remixicon name=\"riBrush2Line\" />\n                </ui-button>\n                <div class=\"grow\" />\n                <ui-select\n                  :model-value=\"item.action || item.csvAction\"\n                  placeholder=\"File Action\"\n                  @change=\"item.action = $event\"\n                >\n                  <option value=\"default\">Default</option>\n                  <option value=\"base64\">Read as base64</option>\n                  <optgroup\n                    v-if=\"/.(csv|xlsx?)$/.test(item.filePath)\"\n                    label=\"CSV/Excel File\"\n                  >\n                    <option value=\"json\">Read as JSON</option>\n                    <option value=\"json-header\">\n                      Read as JSON with headers\n                    </option>\n                  </optgroup>\n                </ui-select>\n              </template>\n            </div>\n            <shared-codemirror\n              v-if=\"previewState.itemId === index\"\n              :model-value=\"previewState.data\"\n              readonly\n              hide-lang\n              class=\"mt-4 w-full\"\n              lang=\"json\"\n              style=\"max-height: 500px\"\n            />\n          </div>\n        </li>\n        <ui-button class=\"mt-4 w-24\" variant=\"accent\" @click=\"addItem\">\n          {{ t('common.add') }}\n        </ui-button>\n      </ul>\n    </ui-modal>\n  </div>\n</template>\n<script setup>\nimport getFile, { readFileAsBase64 } from '@/utils/getFile';\nimport Papa from 'papaparse';\nimport { defineAsyncComponent, inject, ref, shallowReactive, watch } from 'vue';\nimport { useI18n } from 'vue-i18n';\nimport { useToast } from 'vue-toastification';\nimport browser from 'webextension-polyfill';\nimport { read as readXlsx, utils as utilsXlsx } from 'xlsx';\nimport EditAutocomplete from './EditAutocomplete.vue';\n\nconst SharedCodemirror = defineAsyncComponent(() =>\n  import('@/components/newtab/shared/SharedCodemirror.vue')\n);\n\nconst props = defineProps({\n  data: {\n    type: Object,\n    default: () => ({}),\n  },\n});\nconst emit = defineEmits(['update:data']);\n\nconst isFirefox = BROWSER_TYPE === 'firefox';\n\nconst { t } = useI18n();\nconst toast = useToast();\n\nconst workflow = inject('workflow', {});\n\nconst showModal = ref(false);\nconst hasFileAccess = ref(false);\nconst dataList = ref(JSON.parse(JSON.stringify(props.data.dataList)));\n\nconst previewState = shallowReactive({\n  data: '',\n  itemId: '',\n});\n\nfunction clearPreview() {\n  previewState.itemId = '';\n  previewState.data = '';\n}\nfunction removeItem(index) {\n  dataList.value.splice(index, 1);\n  clearPreview();\n}\nfunction updateData(value) {\n  emit('update:data', { ...props.data, ...value });\n}\nfunction addItem() {\n  dataList.value.push({\n    type: 'table',\n    name: '',\n    value: '',\n    filePath: '',\n    isFile: false,\n    action: 'default',\n  });\n}\nfunction changeItemType(index, type) {\n  dataList.value[index] = {\n    ...dataList.value[index],\n    type,\n    name: '',\n  };\n}\nfunction setAsFile(item) {\n  if (!hasFileAccess.value) {\n    window.open(\n      'https://docs.extension.automa.site/blocks/insert-data.html#import-file'\n    );\n    return;\n  }\n\n  item.isFile = !item.isFile;\n}\nasync function previewData(index, item) {\n  try {\n    const path = item.filePath || '';\n    const isExcel = /.xlsx?$/.test(path);\n    const isJSON = path.endsWith('.json');\n\n    const action = item.action || item.csvAction || 'default';\n    let responseType = 'text';\n\n    if (isJSON) responseType = 'json';\n    else if (action === 'base64' || (isExcel && action !== 'default'))\n      responseType = 'blob';\n\n    let result = await getFile(path, {\n      responseType,\n      returnValue: true,\n    });\n\n    const readAsJson = action.includes('json');\n\n    if (action === 'base64') {\n      result = await readFileAsBase64(result);\n    } else if (result && path.endsWith('.csv') && readAsJson) {\n      const parsedCSV = Papa.parse(result, {\n        header: action.includes('header'),\n      });\n      result = JSON.stringify(parsedCSV.data || [], null, 2);\n    } else if (isJSON) {\n      result = JSON.stringify(result, null, 2);\n    } else if (isExcel && readAsJson) {\n      const base64Xls = await readFileAsBase64(result);\n      const wb = readXlsx(base64Xls.slice(base64Xls.indexOf(',')), {\n        type: 'base64',\n      });\n\n      const inputtedSheet = (item.xlsSheet || '').trim();\n      const sheetName = wb.SheetNames.includes(inputtedSheet)\n        ? inputtedSheet\n        : wb.SheetNames[0];\n\n      const options = {};\n      if (item.xlsRange) options.range = item.xlsRange;\n      if (!action.includes('header')) options.header = 1;\n\n      const sheetData = utilsXlsx.sheet_to_json(wb.Sheets[sheetName], options);\n      result = JSON.stringify(sheetData, null, 2);\n    }\n\n    previewState.itemId = index;\n    previewState.data = result;\n  } catch (error) {\n    console.error(error);\n    toast.error(error.message);\n  }\n}\n\nbrowser.extension.isAllowedFileSchemeAccess().then((value) => {\n  hasFileAccess.value = isFirefox ? true : value;\n});\n\nwatch(\n  dataList,\n  (value) => {\n    updateData({ dataList: value });\n  },\n  { deep: true }\n);\n</script>\n<style>\n.insert-data-modal .modal-ui__content-header {\n  @apply p-4;\n}\n</style>\n"
  },
  {
    "path": "src/components/newtab/workflow/edit/EditInteractionBase.vue",
    "content": "<template>\n  <div>\n    <slot name=\"prepend\" />\n    <template v-if=\"!hide\">\n      <ui-textarea\n        v-if=\"!hideDescription\"\n        :model-value=\"data.description\"\n        :placeholder=\"t('common.description')\"\n        class=\"mb-2 w-full\"\n        @change=\"updateData({ description: $event })\"\n      />\n      <slot name=\"prepend:selector\" />\n      <div v-if=\"!hideSelector\" class=\"mb-2 flex items-center\">\n        <ui-select\n          :model-value=\"data.findBy || 'cssSelector'\"\n          :placeholder=\"t('workflow.blocks.base.findElement.placeholder')\"\n          class=\"mr-2 flex-1\"\n          @change=\"updateData({ findBy: $event })\"\n        >\n          <option v-for=\"type in selectorTypes\" :key=\"type\" :value=\"type\">\n            {{ t(`workflow.blocks.base.findElement.options.${type}`) }}\n          </option>\n        </ui-select>\n        <SharedElSelectorActions\n          :find-by=\"data.findBy\"\n          :selector=\"data.selector\"\n          :multiple=\"data.multiple\"\n          @update:selector=\"updateData({ selector: $event })\"\n        />\n      </div>\n      <edit-autocomplete v-if=\"!hideSelector\" class=\"mb-1\">\n        <ui-textarea\n          v-if=\"!hideSelector\"\n          :model-value=\"data.selector\"\n          :placeholder=\"t('workflow.blocks.base.selector')\"\n          autoresize\n          class=\"w-full\"\n          @change=\"updateData({ selector: $event })\"\n        />\n      </edit-autocomplete>\n      <ui-expand\n        v-if=\"!hideSelector\"\n        hide-header-icon\n        header-class=\"flex items-center w-full focus:ring-0\"\n      >\n        <template #header=\"{ show }\">\n          <v-remixicon\n            name=\"riArrowLeftSLine\"\n            :rotate=\"show ? 270 : 180\"\n            class=\"mr-1 -ml-1 transition-transform\"\n          />\n          {{ t('workflow.blocks.base.selectorOptions') }}\n        </template>\n        <div class=\"mt-1\">\n          <ui-checkbox\n            v-if=\"!data.disableMultiple && !hideMultiple\"\n            :title=\"t('workflow.blocks.base.multiple.title')\"\n            :model-value=\"data.multiple\"\n            class=\"mr-6\"\n            @change=\"updateData({ multiple: $event })\"\n          >\n            {{ t('workflow.blocks.base.multiple.text') }}\n          </ui-checkbox>\n          <ui-checkbox\n            v-if=\"\n              !hideMarkEl && (data.findBy || 'cssSelector') === 'cssSelector'\n            \"\n            :model-value=\"data.markEl\"\n            :title=\"t('workflow.blocks.base.markElement.title')\"\n            @change=\"updateData({ markEl: $event })\"\n          >\n            {{ t('workflow.blocks.base.markElement.text') }}\n          </ui-checkbox>\n        </div>\n        <ui-checkbox\n          :model-value=\"data.waitForSelector\"\n          block\n          class=\"mt-1\"\n          @change=\"updateData({ waitForSelector: $event })\"\n        >\n          {{ t('workflow.blocks.base.waitSelector.title') }}\n        </ui-checkbox>\n        <ui-input\n          v-if=\"data.waitForSelector\"\n          :model-value=\"data.waitSelectorTimeout\"\n          :label=\"t('workflow.blocks.base.waitSelector.timeout')\"\n          class=\"mt-1 w-full\"\n          @change=\"updateData({ waitSelectorTimeout: +$event })\"\n        />\n      </ui-expand>\n    </template>\n    <slot></slot>\n  </div>\n</template>\n<script setup>\nimport { onMounted } from 'vue';\nimport { useI18n } from 'vue-i18n';\nimport SharedElSelectorActions from '@/components/newtab/shared/SharedElSelectorActions.vue';\nimport EditAutocomplete from './EditAutocomplete.vue';\n\nconst props = defineProps({\n  data: {\n    type: Object,\n    default: () => ({}),\n  },\n  hide: {\n    type: Boolean,\n    default: false,\n  },\n  hideMarkEl: {\n    type: Boolean,\n    default: false,\n  },\n  hideSelector: {\n    type: Boolean,\n    default: false,\n  },\n  hideMultiple: {\n    type: Boolean,\n    default: false,\n  },\n  hideDescription: Boolean,\n});\nconst emit = defineEmits(['update:data', 'change']);\n\nconst { t } = useI18n();\n\nconst selectorTypes = ['cssSelector', 'xpath'];\n\nfunction updateData(value) {\n  const payload = { ...props.data, ...value };\n\n  emit('update:data', payload);\n  emit('change', payload);\n}\n\nonMounted(() => {\n  if (!props.data.findBy) {\n    updateData({ findBy: 'cssSelector' });\n  }\n});\n</script>\n"
  },
  {
    "path": "src/components/newtab/workflow/edit/EditJavascriptCode.vue",
    "content": "<template>\n  <div class=\"mb-2 mt-4\">\n    <ui-textarea\n      :model-value=\"data.description\"\n      autoresize\n      :placeholder=\"t('common.description')\"\n      class=\"mb-1 w-full\"\n      @change=\"updateData({ description: $event })\"\n    />\n    <template v-if=\"!data.everyNewTab\">\n      <ui-input\n        :model-value=\"data.timeout\"\n        :label=\"t('workflow.blocks.javascript-code.timeout.placeholder')\"\n        :title=\"t('workflow.blocks.javascript-code.timeout.title')\"\n        type=\"number\"\n        class=\"mb-2 w-full\"\n        @change=\"updateData({ timeout: +$event })\"\n      />\n      <ui-select\n        v-if=\"\n          !isFirefox &&\n          (!workflow?.data?.value.settings?.execContext ||\n            workflow?.data?.value.settings?.execContext === 'popup')\n        \"\n        :model-value=\"data.context\"\n        :label=\"t('workflow.blocks.javascript-code.context.name')\"\n        class=\"mb-2 w-full\"\n        @change=\"updateData({ context: $event })\"\n      >\n        <option\n          v-for=\"item in ['website', 'background']\"\n          :key=\"item\"\n          :value=\"item\"\n        >\n          {{ t(`workflow.blocks.javascript-code.context.items.${item}`) }}\n        </option>\n      </ui-select>\n    </template>\n    <p class=\"ml-1 text-sm text-gray-600 dark:text-gray-200\">\n      {{ t('workflow.blocks.javascript-code.name') }}\n    </p>\n    <pre\n      v-if=\"!state.showCodeModal\"\n      class=\"max-h-80 overflow-auto rounded-lg bg-gray-900 p-4 text-gray-200\"\n      @click=\"state.showCodeModal = true\"\n      v-text=\"data.code\"\n    />\n    <template v-if=\"isFirefox || data.context !== 'background'\">\n      <ui-checkbox\n        :model-value=\"data.everyNewTab\"\n        class=\"mt-2\"\n        @change=\"updateData({ everyNewTab: $event })\"\n      >\n        {{ t('workflow.blocks.javascript-code.everyNewTab') }}\n      </ui-checkbox>\n      <ui-checkbox\n        :model-value=\"data.runBeforeLoad\"\n        class=\"mt-2\"\n        @change=\"updateData({ runBeforeLoad: $event })\"\n      >\n        Run before page loaded\n      </ui-checkbox>\n    </template>\n    <ui-modal v-model=\"state.showCodeModal\" content-class=\"max-w-4xl\">\n      <template #header>\n        <ui-tabs v-model=\"state.activeTab\" class=\"border-none\">\n          <ui-tab value=\"code\">\n            {{ t('workflow.blocks.javascript-code.modal.tabs.code') }}\n          </ui-tab>\n          <ui-tab value=\"preloadScript\">\n            {{ t('workflow.blocks.javascript-code.modal.tabs.preloadScript') }}\n          </ui-tab>\n        </ui-tabs>\n      </template>\n      <ui-tab-panels\n        v-model=\"state.activeTab\"\n        class=\"overflow-auto\"\n        style=\"height: calc(100vh - 9rem)\"\n      >\n        <ui-tab-panel value=\"code\" class=\"h-full\">\n          <shared-codemirror\n            v-model=\"state.code\"\n            :extensions=\"codemirrorExts\"\n            :style=\"{ height: data.everyNewTab ? '100%' : '87%' }\"\n            class=\"overflow-auto\"\n          />\n          <template v-if=\"!data.everyNewTab\">\n            <p class=\"mt-1 flex justify-between text-sm\">\n              <span>{{\n                t('workflow.blocks.javascript-code.availabeFuncs')\n              }}</span>\n              <span>\n                <span\n                  class=\"cursor-pointer select-none underline\"\n                  @click=\"modifyWhiteSpace\"\n                  >wrap line</span\n                >\n              </span>\n            </p>\n            <p\n              class=\"scroll space-x-1 overflow-x-auto overflow-y-hidden whitespace-nowrap pb-1\"\n            >\n              <a\n                v-for=\"func in availableFuncs\"\n                :key=\"func.id\"\n                :href=\"`https://docs.extension.automa.site/blocks/javascript-code.html#${func.id}`\"\n                target=\"_blank\"\n                rel=\"noopener\"\n                class=\"inline-block\"\n              >\n                <code>\n                  {{ func.name }}\n                </code>\n              </a>\n            </p>\n          </template>\n        </ui-tab-panel>\n        <ui-tab-panel value=\"preloadScript\">\n          <div\n            v-for=\"(script, index) in state.preloadScripts\"\n            :key=\"index\"\n            class=\"mt-4 flex items-center\"\n          >\n            <v-remixicon\n              name=\"riDeleteBin7Line\"\n              class=\"mr-2 cursor-pointer\"\n              @click=\"state.preloadScripts.splice(index, 1)\"\n            />\n            <ui-input\n              v-model=\"state.preloadScripts[index].src\"\n              placeholder=\"http://example.com/script.js\"\n              class=\"mr-4 flex-1\"\n            />\n            <ui-checkbox\n              v-if=\"\n                (!data.everyNewTab || data.context !== 'website') && !isFirefox\n              \"\n              v-model=\"state.preloadScripts[index].removeAfterExec\"\n            >\n              {{ t('workflow.blocks.javascript-code.removeAfterExec') }}\n            </ui-checkbox>\n          </div>\n          <ui-button variant=\"accent\" class=\"mt-4 w-20\" @click=\"addScript\">\n            {{ t('common.add') }}\n          </ui-button>\n        </ui-tab-panel>\n      </ui-tab-panels>\n    </ui-modal>\n  </div>\n</template>\n<script setup>\nimport {\n  automaFuncsCompletion,\n  automaFuncsSnippets,\n  completeFromGlobalScope,\n} from '@/utils/codeEditorAutocomplete';\nimport { autocompletion } from '@codemirror/autocomplete';\nimport { defineAsyncComponent, inject, reactive, watch } from 'vue';\nimport { useI18n } from 'vue-i18n';\nimport { store } from '../../settings/jsBlockWrap';\n\nfunction modifyWhiteSpace() {\n  if (store.whiteSpace === 'pre') {\n    store.whiteSpace = 'pre-wrap';\n  } else {\n    store.whiteSpace = 'pre';\n  }\n}\n\nconst SharedCodemirror = defineAsyncComponent(() =>\n  import('@/components/newtab/shared/SharedCodemirror.vue')\n);\n\nconst props = defineProps({\n  data: {\n    type: Object,\n    default: () => ({}),\n  },\n});\nconst emit = defineEmits(['update:data']);\n\nconst { t } = useI18n();\n\nconst isFirefox = BROWSER_TYPE === 'firefox';\nconst availableFuncs = [\n  { name: 'automaNextBlock(data, insert?)', id: 'automanextblock-data' },\n  { name: 'automaRefData(keyword, path?)', id: 'automarefdata-keyword-path' },\n  {\n    name: 'automaSetVariable(name, value)',\n    id: 'automasetvariable-name-value',\n  },\n  {\n    name: 'automaFetch(type, resource)',\n    id: 'automasetvariable-type-resource',\n  },\n  { name: 'automaResetTimeout()', id: 'automaresettimeout' },\n];\nconst autocompleteList = Object.values(automaFuncsSnippets).slice(0, 4);\n\nconst workflow = inject('workflow');\n\nconst state = reactive({\n  activeTab: 'code',\n  code: `${props.data.code}`,\n  preloadScripts: [...Object.values(props.data.preloadScripts || [])],\n  showCodeModal: false,\n});\n\nfunction updateData(value) {\n  emit('update:data', { ...props.data, ...value });\n}\nfunction addScript() {\n  state.preloadScripts.push({ src: '', removeAfterExec: true });\n}\n\nconst codemirrorExts = [\n  autocompletion({\n    override: [\n      automaFuncsCompletion(autocompleteList),\n      completeFromGlobalScope,\n    ],\n  }),\n];\n\nwatch(\n  () => state.code,\n  (value) => {\n    updateData({ code: value });\n  }\n);\nwatch(\n  () => state.preloadScripts,\n  (value) => {\n    updateData({ preloadScripts: value });\n  },\n  { deep: true }\n);\n</script>\n<style scoped>\ncode {\n  @apply bg-gray-900 text-sm text-white p-1 rounded-md;\n}\n</style>\n"
  },
  {
    "path": "src/components/newtab/workflow/edit/EditLink.vue",
    "content": "<template>\n  <edit-interaction-base v-bind=\"{ data }\" @change=\"updateData\">\n    <ui-checkbox\n      :model-value=\"data.openInNewTab\"\n      class=\"mt-4\"\n      @change=\"updateData({ openInNewTab: $event })\"\n    >\n      {{ t('workflow.blocks.link.openInNewTab') }}\n    </ui-checkbox>\n  </edit-interaction-base>\n</template>\n<script setup>\nimport { useI18n } from 'vue-i18n';\nimport EditInteractionBase from './EditInteractionBase.vue';\n\nconst props = defineProps({\n  data: {\n    type: Object,\n    default: () => ({}),\n  },\n});\nconst emit = defineEmits(['update:data']);\n\nconst { t } = useI18n();\n\nfunction updateData(value) {\n  emit('update:data', { ...props.data, ...value });\n}\n</script>\n"
  },
  {
    "path": "src/components/newtab/workflow/edit/EditLogData.vue",
    "content": "<template>\n  <div>\n    <ui-textarea\n      :model-value=\"data.description\"\n      :placeholder=\"t('common.description')\"\n      class=\"w-full\"\n      @change=\"updateData({ description: $event })\"\n    />\n    <ui-select\n      :model-value=\"data.workflowId\"\n      :placeholder=\"t('workflow.blocks.execute-workflow.select')\"\n      class=\"mt-4 w-full\"\n      @change=\"updateData({ workflowId: $event })\"\n    >\n      <option\n        v-for=\"workflow in workflows\"\n        :key=\"workflow.id\"\n        :value=\"workflow.id\"\n      >\n        {{ workflow.name }}\n      </option>\n    </ui-select>\n    <div class=\"log-data mb-8\">\n      <template v-if=\"data.workflowId\">\n        <p class=\"mt-4 mb-2\">\n          {{ t('workflow.blocks.log-data.data') }}\n        </p>\n        <insert-workflow-data :data=\"data\" variables @update=\"updateData\" />\n      </template>\n    </div>\n  </div>\n</template>\n<script setup>\nimport { useI18n } from 'vue-i18n';\nimport { useWorkflowStore } from '@/stores/workflow';\nimport InsertWorkflowData from './InsertWorkflowData.vue';\n\nconst props = defineProps({\n  data: {\n    type: Object,\n    default: () => ({}),\n  },\n});\nconst emit = defineEmits(['update:data']);\n\nconst { t } = useI18n();\nconst workflowStore = useWorkflowStore();\n\nconst workflows = workflowStore.getWorkflows.sort((a, b) =>\n  a.name > b.name ? 1 : -1\n);\n\nfunction updateData(value) {\n  emit('update:data', { ...props.data, ...value });\n}\n</script>\n<style>\n.log-data .block-variable {\n  margin-top: 4px;\n}\n</style>\n"
  },
  {
    "path": "src/components/newtab/workflow/edit/EditLoopData.vue",
    "content": "<template>\n  <div>\n    <ui-textarea\n      :model-value=\"data.description\"\n      :placeholder=\"t('common.description')\"\n      class=\"mb-1 w-full\"\n      @change=\"updateData({ description: $event })\"\n    />\n    <ui-input\n      :model-value=\"data.loopId\"\n      class=\"mb-2 w-full\"\n      :label=\"t('workflow.blocks.loop-data.loopId')\"\n      :placeholder=\"t('workflow.blocks.loop-data.loopId')\"\n      @change=\"updateLoopID\"\n    />\n    <ui-select\n      :model-value=\"data.loopThrough\"\n      :label=\"t('workflow.blocks.loop-data.loopThrough.placeholder')\"\n      class=\"w-full\"\n      @change=\"updateData({ loopThrough: $event })\"\n    >\n      <option v-for=\"type in loopTypes\" :key=\"type\" :value=\"type\">\n        {{ t(`workflow.blocks.loop-data.loopThrough.options.${type}`) }}\n      </option>\n    </ui-select>\n    <ui-input\n      v-if=\"data.loopThrough === 'google-sheets'\"\n      :model-value=\"data.referenceKey\"\n      :label=\"t('workflow.blocks.loop-data.refKey')\"\n      placeholder=\"abc123\"\n      class=\"mt-2 w-full\"\n      @change=\"updateData({ referenceKey: $event })\"\n    />\n    <ui-input\n      v-else-if=\"data.loopThrough === 'variable'\"\n      :model-value=\"data.variableName\"\n      :label=\"t('workflow.variables.name')\"\n      placeholder=\"abc123\"\n      class=\"mt-2 w-full\"\n      @change=\"updateData({ variableName: $event })\"\n    />\n    <template v-else-if=\"data.loopThrough === 'elements'\">\n      <edit-autocomplete class=\"mt-2\" trigger-class=\"!flex items-end\">\n        <ui-input\n          :model-value=\"data.elementSelector\"\n          :label=\"t('workflow.blocks.base.selector')\"\n          autocomplete=\"off\"\n          placeholder=\"CSS Selector or XPath\"\n          class=\"mr-2 flex-1\"\n          @change=\"updateData({ elementSelector: $event })\"\n        />\n        <shared-el-selector-actions\n          :multiple=\"true\"\n          :selector=\"data.elementSelector\"\n          @update:selector=\"updateData({ elementSelector: $event })\"\n        />\n      </edit-autocomplete>\n      <ui-checkbox\n        :model-value=\"data.waitForSelector\"\n        block\n        class=\"mt-1\"\n        @change=\"updateData({ waitForSelector: $event })\"\n      >\n        {{ t('workflow.blocks.base.waitSelector.title') }}\n      </ui-checkbox>\n      <ui-input\n        v-if=\"data.waitForSelector\"\n        :model-value=\"data.waitSelectorTimeout\"\n        :label=\"t('workflow.blocks.base.waitSelector.timeout')\"\n        class=\"mt-1 w-full\"\n        @change=\"updateData({ waitSelectorTimeout: +$event })\"\n      />\n    </template>\n    <ui-button\n      v-else-if=\"data.loopThrough === 'custom-data'\"\n      class=\"mt-4 w-full\"\n      variant=\"accent\"\n      @click=\"state.showDataModal = true\"\n    >\n      {{ t('workflow.blocks.loop-data.buttons.insert') }}\n    </ui-button>\n    <div\n      v-else-if=\"data.loopThrough === 'numbers'\"\n      class=\"mt-2 flex items-center space-x-2\"\n    >\n      <ui-input\n        :model-value=\"data.fromNumber\"\n        :label=\"t('workflow.blocks.loop-data.loopThrough.fromNumber')\"\n        type=\"number\"\n        @change=\"\n          updateData({\n            fromNumber: +$event >= data.toNumber ? data.toNumber - 1 : +$event,\n          })\n        \"\n      />\n      <ui-input\n        :model-value=\"data.toNumber\"\n        :label=\"t('workflow.blocks.loop-data.loopThrough.toNumber')\"\n        type=\"number\"\n        @change=\"\n          updateData({\n            toNumber:\n              +$event <= data.fromNumber ? data.fromNumber + 1 : +$event,\n          })\n        \"\n      />\n    </div>\n    <template v-if=\"data.loopThrough !== 'numbers'\">\n      <ui-input\n        :model-value=\"data.maxLoop\"\n        :label=\"t('workflow.blocks.loop-data.maxLoop.label')\"\n        :title=\"t('workflow.blocks.loop-data.maxLoop.title')\"\n        class=\"mt-2 w-full\"\n        @change=\"updateData({ maxLoop: $event })\"\n      />\n      <ui-input\n        v-if=\"!data.resumeLastWorkflow\"\n        :model-value=\"data.startIndex\"\n        :label=\"t('workflow.blocks.loop-data.startIndex')\"\n        placeholder=\"0\"\n        class=\"mt-2 w-full\"\n        @change=\"updateData({ startIndex: $event })\"\n      />\n      <ui-checkbox\n        :model-value=\"data.resumeLastWorkflow\"\n        class=\"mt-1\"\n        @change=\"updateData({ resumeLastWorkflow: $event })\"\n      >\n        {{ t('workflow.blocks.loop-data.resumeLastWorkflow') }}\n      </ui-checkbox>\n      <ui-checkbox\n        :model-value=\"data.reverseLoop\"\n        class=\"mt-1\"\n        @change=\"updateData({ reverseLoop: $event })\"\n      >\n        {{ t('workflow.blocks.loop-data.reverse') }}\n      </ui-checkbox>\n    </template>\n    <ui-modal\n      v-model=\"state.showDataModal\"\n      title=\"Data\"\n      content-class=\"max-w-3xl\"\n    >\n      <div class=\"mb-4 flex items-center\">\n        <ui-button variant=\"accent\" @click=\"importFile\">\n          {{ t('workflow.blocks.loop-data.buttons.import') }}\n        </ui-button>\n        <ui-button\n          v-tooltip=\"t('common.options')\"\n          :class=\"{ 'text-primary': state.showOptions }\"\n          icon\n          class=\"ml-2\"\n          @click=\"state.showOptions = !state.showOptions\"\n        >\n          <v-remixicon name=\"riSettings3Line\" />\n        </ui-button>\n        <p class=\"text-overflow mx-4 flex-1\">{{ file.name }}</p>\n        <p>{{ t('workflow.blocks.loop-data.modal.maxFile') }}</p>\n      </div>\n      <div style=\"height: calc(100vh - 11rem)\">\n        <shared-codemirror\n          v-show=\"!state.showOptions\"\n          :model-value=\"data.loopData\"\n          lang=\"json\"\n          class=\"h-full\"\n          @change=\"updateLoopData\"\n        />\n        <div v-show=\"state.showOptions\">\n          <p class=\"mb-2 font-semibold\">CSV</p>\n          <ui-checkbox v-model=\"options.header\">\n            {{ t('workflow.blocks.loop-data.modal.options.firstRow') }}\n          </ui-checkbox>\n        </div>\n      </div>\n    </ui-modal>\n  </div>\n</template>\n<script setup>\nimport { onMounted, shallowReactive, defineAsyncComponent } from 'vue';\nimport { nanoid } from 'nanoid';\nimport { useI18n } from 'vue-i18n';\nimport { useToast } from 'vue-toastification';\nimport Papa from 'papaparse';\nimport { openFilePicker } from '@/utils/helper';\nimport SharedElSelectorActions from '@/components/newtab/shared/SharedElSelectorActions.vue';\nimport EditAutocomplete from './EditAutocomplete.vue';\n\nconst SharedCodemirror = defineAsyncComponent(() =>\n  import('@/components/newtab/shared/SharedCodemirror.vue')\n);\n\nconst props = defineProps({\n  blockId: {\n    type: String,\n    default: '',\n  },\n  data: {\n    type: Object,\n    default: () => ({}),\n  },\n});\nconst emit = defineEmits(['update:data']);\n\nconst { t } = useI18n();\nconst toast = useToast();\n\nconst maxFileSize = 1024 * 1024;\nconst loopTypes = [\n  'data-columns',\n  'numbers',\n  'google-sheets',\n  'variable',\n  'custom-data',\n  'elements',\n];\n\nconst state = shallowReactive({\n  showOptions: false,\n  showDataModal: false,\n  workflowLoopData: {},\n});\nconst options = shallowReactive({\n  header: true,\n});\nconst file = shallowReactive({\n  size: 0,\n  name: '',\n  type: '',\n});\n\nfunction updateData(value) {\n  emit('update:data', { ...props.data, ...value });\n}\nfunction updateLoopData(value) {\n  if (value.length > maxFileSize) {\n    toast.error(t('message.maxSizeExceeded'));\n  }\n\n  updateData({ loopData: value.slice(0, maxFileSize) });\n}\nfunction updateLoopID(id) {\n  let loopId = id.replace(/\\s/g, '');\n\n  if (!loopId) {\n    loopId = nanoid(6);\n  }\n\n  updateData({ loopId });\n}\nfunction importFile() {\n  openFilePicker(['application/json', 'text/csv', 'application/vnd.ms-excel'])\n    .then(async ([fileObj]) => {\n      if (fileObj.size > maxFileSize) {\n        toast.error(t('message.maxSizeExceeded'));\n        return;\n      }\n\n      file.name = fileObj.name;\n      file.type = fileObj.type;\n\n      const csvTypes = ['text/csv', 'application/vnd.ms-excel'];\n\n      const reader = new FileReader();\n\n      reader.onload = ({ target }) => {\n        let loopData;\n\n        if (fileObj.type === 'application/json') {\n          const result = JSON.parse(target.result);\n          loopData = Array.isArray(result) ? result : [result];\n        } else if (csvTypes.includes(fileObj.type)) {\n          loopData = Papa.parse(target.result, options).data;\n        }\n\n        if (Array.isArray(loopData)) {\n          const loopDataStr = JSON.stringify(loopData, null, 2);\n\n          updateData({ loopData: loopDataStr });\n        }\n      };\n\n      reader.readAsText(fileObj);\n    })\n    .catch((error) => {\n      console.error(error);\n      if (error.message.startsWith('invalid')) toast.error(error.message);\n    });\n}\n\nonMounted(() => {\n  if (!props.data.loopId) {\n    updateData({ loopId: nanoid(6) });\n  }\n});\n</script>\n"
  },
  {
    "path": "src/components/newtab/workflow/edit/EditLoopElements.vue",
    "content": "<template>\n  <edit-interaction-base\n    :data=\"data\"\n    hide-multiple\n    hide-mark-el\n    @change=\"updateData\"\n  >\n    <template #prepend:selector>\n      <ui-input\n        :model-value=\"data.loopId\"\n        class=\"mb-4 w-full\"\n        :label=\"t('workflow.blocks.loop-data.loopId')\"\n        :placeholder=\"t('workflow.blocks.loop-data.loopId')\"\n        @change=\"updateLoopId\"\n      />\n    </template>\n    <ui-input\n      :model-value=\"data.maxLoop\"\n      :label=\"t('workflow.blocks.loop-data.maxLoop.label')\"\n      :title=\"t('workflow.blocks.loop-data.maxLoop.title')\"\n      class=\"mt-3 w-full\"\n      @change=\"updateData({ maxLoop: $event })\"\n    />\n    <ui-checkbox\n      :model-value=\"data.reverseLoop\"\n      class=\"mt-2\"\n      @change=\"updateData({ reverseLoop: $event })\"\n    >\n      {{ t('workflow.blocks.loop-data.reverse') }}\n    </ui-checkbox>\n    <div class=\"mt-4 mb-8 border-t pt-4\">\n      <p class=\"text-sm text-gray-600 dark:text-gray-200\">\n        {{ t('workflow.blocks.loop-elements.loadMore') }}\n      </p>\n      <ui-select\n        :model-value=\"data.loadMoreAction\"\n        :label=\"t('common.action')\"\n        class=\"mt-2 w-full\"\n        @change=\"updateData({ loadMoreAction: $event })\"\n      >\n        <option v-for=\"action in actions\" :key=\"action\" :value=\"action\">\n          {{ t(`workflow.blocks.loop-elements.actions.${action}`) }}\n        </option>\n      </ui-select>\n      <edit-autocomplete\n        v-if=\"['click-element', 'click-link'].includes(data.loadMoreAction)\"\n        block\n        class=\"mt-2\"\n        trigger-class=\"!flex items-end\"\n      >\n        <ui-input\n          :model-value=\"data.actionElSelector\"\n          :label=\"t('workflow.blocks.base.selector')\"\n          placeholder=\"CSS Selector or XPath\"\n          class=\"mr-2 flex-1\"\n          autocomplete=\"off\"\n          @change=\"updateData({ actionElSelector: $event })\"\n        />\n        <shared-el-selector-actions\n          :selector=\"data.actionElSelector\"\n          @update:selector=\"updateData({ actionElSelector: $event })\"\n        />\n      </edit-autocomplete>\n      <ui-input\n        v-if=\"\n          ['click-element', 'scroll', 'scroll-up'].includes(data.loadMoreAction)\n        \"\n        :model-value=\"data.actionElMaxWaitTime\"\n        label=\"Max seconds wait for more elements\"\n        class=\"mt-2 w-full\"\n        placeholder=\"0\"\n        type=\"number\"\n        @change=\"updateData({ actionElMaxWaitTime: +$event })\"\n      />\n      <ui-checkbox\n        v-if=\"data.loadMoreAction.includes('scroll')\"\n        :model-value=\"data.scrollToBottom\"\n        class=\"mt-4\"\n        @change=\"updateData({ scrollToBottom: $event })\"\n      >\n        {{\n          t(\n            `workflow.blocks.loop-elements.${\n              data.loadMoreAction === 'scroll-up'\n                ? 'scrollToTop'\n                : 'scrollToBottom'\n            }`\n          )\n        }}\n      </ui-checkbox>\n      <ui-input\n        v-if=\"data.loadMoreAction === 'click-link'\"\n        :model-value=\"data.actionPageMaxWaitTime\"\n        label=\"Max seconds wait for the page to load\"\n        class=\"mt-2 w-full\"\n        placeholder=\"0\"\n        type=\"number\"\n        @change=\"updateData({ actionPageMaxWaitTime: +$event })\"\n      />\n    </div>\n  </edit-interaction-base>\n</template>\n<script setup>\nimport { onMounted } from 'vue';\nimport { useI18n } from 'vue-i18n';\nimport { nanoid } from 'nanoid/non-secure';\nimport SharedElSelectorActions from '@/components/newtab/shared/SharedElSelectorActions.vue';\nimport EditAutocomplete from './EditAutocomplete.vue';\nimport EditInteractionBase from './EditInteractionBase.vue';\n\nconst props = defineProps({\n  data: {\n    type: Object,\n    default: () => ({}),\n  },\n});\nconst emit = defineEmits(['update:data']);\n\nconst actions = ['none', 'click-element', 'click-link', 'scroll', 'scroll-up'];\n\nconst { t } = useI18n();\n\nfunction updateData(value) {\n  emit('update:data', { ...props.data, ...value });\n}\nfunction updateLoopId(id) {\n  let loopId = id.replace(/\\s/g, '');\n\n  if (!loopId) {\n    loopId = nanoid(6);\n  }\n\n  updateData({ loopId });\n}\n\nonMounted(() => {\n  if (!props.data.loopId) {\n    updateData({ loopId: nanoid(6) });\n  }\n});\n</script>\n"
  },
  {
    "path": "src/components/newtab/workflow/edit/EditNewTab.vue",
    "content": "<template>\n  <div>\n    <ui-textarea\n      :model-value=\"data.description\"\n      :placeholder=\"t('common.description')\"\n      class=\"w-full\"\n      @change=\"updateData({ description: $event })\"\n    />\n    <edit-autocomplete v-if=\"!data.activeTab\" class=\"mt-2\">\n      <label for=\"new-tab-url\" class=\"input-label\">\n        {{ t('workflow.blocks.new-tab.url') }}\n      </label>\n      <div class=\"flex items-center gap-2 w-full\">\n        <ui-select v-model=\"state.protocol\" class=\"w-28\">\n          <option\n            v-for=\"proto in PROTOCOLS\"\n            :key=\"proto.value\"\n            :value=\"proto.value\"\n          >\n            {{ proto.label }}\n          </option>\n        </ui-select>\n        <ui-input\n          id=\"new-tab-url\"\n          v-model=\"state.urlPath\"\n          placeholder=\"example.com/\"\n          class=\"flex-1\"\n          autocomplete=\"off\"\n          type=\"text\"\n        />\n      </div>\n    </edit-autocomplete>\n    <ui-checkbox\n      :model-value=\"data.updatePrevTab\"\n      class=\"mt-2 leading-tight\"\n      :title=\"t('workflow.blocks.new-tab.updatePrevTab.title')\"\n      @change=\"updateData({ updatePrevTab: $event })\"\n    >\n      {{ t('workflow.blocks.new-tab.updatePrevTab.text') }}\n    </ui-checkbox>\n    <ui-checkbox\n      :model-value=\"data.waitTabLoaded\"\n      class=\"mt-2 leading-tight\"\n      :title=\"t('workflow.blocks.new-tab.waitTabLoaded')\"\n      @change=\"updateData({ waitTabLoaded: $event })\"\n    >\n      {{ t('workflow.blocks.new-tab.waitTabLoaded') }}\n    </ui-checkbox>\n    <ui-checkbox\n      :model-value=\"data.active\"\n      class=\"my-2\"\n      @change=\"updateData({ active: $event })\"\n    >\n      {{ t('workflow.blocks.new-tab.activeTab') }}\n    </ui-checkbox>\n    <template v-if=\"browserType === 'chrome'\">\n      <ui-checkbox\n        :model-value=\"data.inGroup\"\n        @change=\"updateData({ inGroup: $event })\"\n      >\n        {{ t('workflow.blocks.new-tab.tabToGroup') }}\n      </ui-checkbox>\n      <ui-checkbox\n        :model-value=\"data.customUserAgent\"\n        block\n        class=\"mt-2\"\n        @change=\"updateData({ customUserAgent: $event })\"\n      >\n        {{ t('workflow.blocks.new-tab.customUserAgent') }}\n      </ui-checkbox>\n    </template>\n    <ui-input\n      v-if=\"data.customUserAgent\"\n      :model-value=\"data.userAgent\"\n      placeholder=\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36\"\n      class=\"mt-1 w-full\"\n      @change=\"updateData({ userAgent: $event })\"\n    />\n    <div class=\"mt-4\">\n      <p>{{ t('workflow.blocks.new-tab.tab-zoom') }}</p>\n      <vue-slider\n        :min=\"0.25\"\n        :max=\"4.5\"\n        :interval=\"0.25\"\n        :model-value=\"data.tabZoom || 1\"\n        @change=\"updateData({ tabZoom: $event })\"\n      />\n    </div>\n  </div>\n</template>\n<script setup>\nimport UiInput from '@/components/ui/UiInput.vue';\nimport UiSelect from '@/components/ui/UiSelect.vue';\nimport { reactive, watch } from 'vue';\nimport { useI18n } from 'vue-i18n';\nimport VueSlider from 'vue-slider-component';\nimport 'vue-slider-component/theme/default.css';\nimport EditAutocomplete from './EditAutocomplete.vue';\n\nconst props = defineProps({\n  data: {\n    type: Object,\n    default: () => ({}),\n  },\n});\nconst emit = defineEmits(['update:data']);\n\nconst { t } = useI18n();\nconst browserType = BROWSER_TYPE;\n\nconst PROTOCOLS = [\n  { value: 'https://', label: 'HTTPS' },\n  { value: 'http://', label: 'HTTP' },\n  { value: 'ftp://', label: 'FTP' },\n  { value: 'file://', label: 'FILE' },\n  { value: 'mailto:', label: 'MAILTO' },\n];\n\nfunction isTemplateVariable(str) {\n  if (!str || typeof str !== 'string') return false;\n  return str.includes('{{');\n}\n\nfunction parseUrl(url) {\n  if (!url || typeof url !== 'string') {\n    return { protocol: 'https://', path: '' };\n  }\n\n  if (isTemplateVariable(url)) {\n    return { protocol: 'https://', path: url };\n  }\n\n  const protocolMatch = url.match(/^(https?:|ftp:|file:|mailto:)(\\/\\/)?/i);\n  if (protocolMatch) {\n    const protocolBase = protocolMatch[1].toLowerCase();\n    const protocol = protocolBase + (protocolBase === 'mailto:' ? '' : '//');\n    const path = url.slice(protocolMatch[0].length);\n    return { protocol, path };\n  }\n\n  return { protocol: 'https://', path: url };\n}\n\nfunction cleanProtocol(path) {\n  if (!path || typeof path !== 'string') return path;\n  return path.replace(/^(https?:|ftp:|file:|mailto:)(\\/\\/)?/i, '');\n}\n\nconst state = reactive({\n  protocol: 'https://',\n  urlPath: '',\n});\n\nconst parsed = parseUrl(props.data.url || '');\nstate.protocol = parsed.protocol;\nstate.urlPath = parsed.path;\n\nfunction updateData(value) {\n  emit('update:data', { ...props.data, ...value });\n}\n\nwatch(\n  () => [state.protocol, state.urlPath],\n  ([newProtocol, newPath]) => {\n    const cleanPath = cleanProtocol(newPath || '');\n    const fullUrl = cleanPath ? newProtocol + cleanPath : newProtocol;\n    updateData({ url: fullUrl });\n  },\n  { deep: true }\n);\n\nwatch(\n  () => props.data.url,\n  (newUrl) => {\n    const urlData = parseUrl(newUrl || '');\n    if (urlData.protocol !== state.protocol || urlData.path !== state.urlPath) {\n      state.protocol = urlData.protocol;\n      state.urlPath = urlData.path;\n    }\n  }\n);\n</script>\n"
  },
  {
    "path": "src/components/newtab/workflow/edit/EditNewWindow.vue",
    "content": "<template>\n  <div class=\"mb-2 mt-4\">\n    <ui-textarea\n      :model-value=\"data.description\"\n      class=\"w-full\"\n      :placeholder=\"t('common.description')\"\n      @change=\"updateData({ description: $event })\"\n    />\n    <ui-select\n      :model-value=\"data.type\"\n      class=\"mt-4 w-full\"\n      label=\"Type\"\n      @change=\"updateData({ type: $event })\"\n    >\n      <option v-for=\"type in windowType\" :key=\"type\" :value=\"type\">\n        {{ type }}\n      </option>\n    </ui-select>\n    <ui-input\n      :model-value=\"data.url\"\n      class=\"mt-2 w-full\"\n      label=\"URL (optional)\"\n      placeholder=\"https://example.com\"\n      @change=\"updateData({ url: $event })\"\n    />\n    <ui-select\n      :model-value=\"data.windowState\"\n      class=\"mt-2 w-full\"\n      :label=\"t('workflow.blocks.new-window.windowState.placeholder')\"\n      @change=\"updateData({ windowState: $event })\"\n    >\n      <option v-for=\"state in windowStates\" :key=\"state\" :value=\"state\">\n        {{ t(`workflow.blocks.new-window.windowState.options.${state}`) }}\n      </option>\n    </ui-select>\n    <ui-checkbox\n      :model-value=\"data.incognito\"\n      :disabled=\"!allowInIncognito\"\n      class=\"mt-1\"\n      @change=\"updateData({ incognito: $event })\"\n    >\n      {{ t('workflow.blocks.new-window.incognito.text') }}\n      <span :title=\"t('workflow.blocks.new-window.incognito.note')\">\n        &#128712;\n      </span>\n    </ui-checkbox>\n    <div v-if=\"data.windowState === 'normal'\" class=\"mt-2\">\n      <div\n        :title=\"t('workflow.blocks.new-window.position')\"\n        class=\"mb-1 flex items-center space-x-2\"\n      >\n        <ui-input\n          :model-value=\"data.top\"\n          :label=\"t('workflow.blocks.new-window.top')\"\n          @change=\"updateData({ top: +$event })\"\n        />\n        <ui-input\n          :model-value=\"data.left\"\n          :label=\"t('workflow.blocks.new-window.left')\"\n          @change=\"updateData({ left: +$event })\"\n        />\n      </div>\n      <div\n        :title=\"t('workflow.blocks.new-window.size')\"\n        class=\"flex items-center space-x-2\"\n      >\n        <ui-input\n          :model-value=\"data.height\"\n          :label=\"t('workflow.blocks.new-window.height')\"\n          @change=\"updateData({ height: +$event })\"\n        />\n        <ui-input\n          :model-value=\"data.width\"\n          :label=\"t('workflow.blocks.new-window.width')\"\n          @change=\"updateData({ width: +$event })\"\n        />\n      </div>\n      <p class=\"mt-2 text-gray-600 dark:text-gray-200\">\n        {{ t('workflow.blocks.new-window.note') }}\n      </p>\n    </div>\n  </div>\n</template>\n<script setup>\nimport { ref, onMounted } from 'vue';\nimport { useI18n } from 'vue-i18n';\nimport browser from 'webextension-polyfill';\n\nconst props = defineProps({\n  data: {\n    type: Object,\n    default: () => ({}),\n  },\n});\nconst emit = defineEmits(['update:data']);\n\nconst { t } = useI18n();\n\nconst windowType = ['normal', 'popup', 'panel'];\nconst windowStates = ['normal', 'minimized', 'maximized', 'fullscreen'];\nconst allowInIncognito = ref(false);\n\nfunction updateData(value) {\n  emit('update:data', { ...props.data, ...value });\n}\n\nonMounted(async () => {\n  allowInIncognito.value = await browser.extension.isAllowedIncognitoAccess();\n\n  if (!allowInIncognito.value) {\n    updateData({ incognito: false });\n  }\n});\n</script>\n"
  },
  {
    "path": "src/components/newtab/workflow/edit/EditNotification.vue",
    "content": "<template>\n  <div>\n    <ui-textarea\n      :model-value=\"data.description\"\n      :placeholder=\"t('common.description')\"\n      class=\"mb-2 w-full\"\n      @change=\"updateData({ description: $event })\"\n    />\n    <template v-if=\"permission.has.notifications\">\n      <edit-autocomplete class=\"mb-2\">\n        <ui-input\n          :model-value=\"data.title\"\n          :label=\"t('workflow.blocks.notification.title')\"\n          placeholder=\"Hello world!\"\n          class=\"w-full\"\n          @change=\"updateData({ title: $event })\"\n        />\n      </edit-autocomplete>\n      <label class=\"input-label\" for=\"notification-message\">\n        {{ t('workflow.blocks.notification.message') }}\n      </label>\n      <edit-autocomplete>\n        <ui-textarea\n          id=\"notification-message\"\n          :model-value=\"data.message\"\n          placeholder=\"Notification message\"\n          class=\"w-full\"\n          @change=\"updateData({ message: $event })\"\n        />\n      </edit-autocomplete>\n      <edit-autocomplete\n        v-for=\"type in ['iconUrl', 'imageUrl']\"\n        :key=\"type\"\n        class=\"mt-2\"\n      >\n        <ui-input\n          :model-value=\"data[type]\"\n          :label=\"t(`workflow.blocks.notification.${type}`)\"\n          class=\"w-full\"\n          placeholder=\"https://example.com/image.png\"\n          @change=\"updateData({ [type]: $event })\"\n        />\n      </edit-autocomplete>\n    </template>\n    <template v-else>\n      <p class=\"mt-4\">\n        {{ t('workflow.blocks.base.noPermission') }}\n      </p>\n      <ui-button\n        variant=\"accent\"\n        class=\"mt-2\"\n        @click=\"permission.request(true)\"\n      >\n        {{ t('workflow.blocks.base.grantPermission') }}\n      </ui-button>\n    </template>\n  </div>\n</template>\n<script setup>\nimport { useI18n } from 'vue-i18n';\nimport { useHasPermissions } from '@/composable/hasPermissions';\nimport EditAutocomplete from './EditAutocomplete.vue';\n\nconst props = defineProps({\n  data: {\n    type: Object,\n    default: () => ({}),\n  },\n});\nconst emit = defineEmits(['update:data']);\n\nconst { t } = useI18n();\nconst permission = useHasPermissions(['notifications']);\n\nfunction updateData(value) {\n  emit('update:data', { ...props.data, ...value });\n}\n</script>\n"
  },
  {
    "path": "src/components/newtab/workflow/edit/EditParameterPrompt.vue",
    "content": "<template>\n  <div>\n    <ui-textarea\n      :model-value=\"data.description\"\n      :placeholder=\"t('common.description')\"\n      class=\"w-full\"\n      @change=\"updateData({ description: $event })\"\n    />\n    <ui-input\n      :model-value=\"data.timeout\"\n      type=\"number\"\n      label=\"Timeout (millisecond) (0 to disable)\"\n      class=\"mt-2 w-full\"\n      @change=\"updateData({ timeout: +$event })\"\n    />\n    <ui-button\n      class=\"mt-4 w-full\"\n      variant=\"accent\"\n      @click=\"showModal = !showModal\"\n    >\n      Insert Parameters\n    </ui-button>\n    <ui-modal v-model=\"showModal\" title=\"Parameters\" content-class=\"max-w-4xl\">\n      <edit-workflow-parameters\n        :data=\"data.parameters\"\n        hide-prefer-tab\n        @update=\"updateData({ parameters: $event })\"\n      />\n    </ui-modal>\n  </div>\n</template>\n<script setup>\nimport { ref } from 'vue';\nimport { useI18n } from 'vue-i18n';\nimport EditWorkflowParameters from './EditWorkflowParameters.vue';\n\nconst props = defineProps({\n  data: {\n    type: Object,\n    default: () => ({}),\n  },\n});\nconst emit = defineEmits(['update:data']);\n\nconst { t } = useI18n();\n\nconst showModal = ref(false);\n\nfunction updateData(value) {\n  emit('update:data', { ...props.data, ...value });\n}\n</script>\n"
  },
  {
    "path": "src/components/newtab/workflow/edit/EditPressKey.vue",
    "content": "<template>\n  <div>\n    <ui-textarea\n      :model-value=\"data.description\"\n      class=\"w-full\"\n      :placeholder=\"t('common.description')\"\n      @change=\"updateData({ description: $event })\"\n    />\n    <edit-autocomplete class=\"mt-2\" trigger-class=\"!flex items-end\">\n      <ui-input\n        :model-value=\"data.selector\"\n        class=\"mr-2 flex-1\"\n        autocomplete=\"off\"\n        label=\"Target element (Optional)\"\n        placeholder=\"CSS Selector or XPath\"\n        @change=\"updateData({ selector: $event })\"\n      />\n      <shared-el-selector-actions\n        :selector=\"data.selector\"\n        @update:selector=\"updateData({ selector: $event })\"\n      />\n    </edit-autocomplete>\n    <ui-select\n      :model-value=\"data.action || 'press-key'\"\n      :label=\"t('workflow.blocks.base.action')\"\n      class=\"mt-2 w-full\"\n      @change=\"updateData({ action: $event })\"\n    >\n      <option\n        v-for=\"action in ['press-key', 'multiple-keys']\"\n        :key=\"action\"\n        :value=\"action\"\n      >\n        {{ t(`workflow.blocks.press-key.actions.${action}`) }}\n      </option>\n    </ui-select>\n    <div\n      v-if=\"!data.action || data.action === 'press-key'\"\n      class=\"flex items-end\"\n    >\n      <ui-autocomplete\n        :items=\"keysList\"\n        :model-value=\"dataKeys\"\n        hide-empty\n        block\n        class=\"mt-2\"\n      >\n        <ui-input\n          :label=\"t('workflow.blocks.press-key.key')\"\n          :model-value=\"dataKeys\"\n          :disabled=\"isRecordingKey\"\n          placeholder=\"(Enter, Esc, a, b, ...)\"\n          autocomplete=\"off\"\n          class=\"w-full\"\n          @change=\"updateKeys\"\n        />\n      </ui-autocomplete>\n      <ui-button\n        v-tooltip=\"\n          isRecordingKey\n            ? t('common.cancel')\n            : t('workflow.blocks.press-key.detect')\n        \"\n        icon\n        class=\"ml-2\"\n        @click=\"toggleRecordKeys\"\n      >\n        <v-remixicon :name=\"isRecordingKey ? 'riCloseLine' : 'riFocus3Line'\" />\n      </ui-button>\n    </div>\n    <ui-textarea\n      v-else\n      :model-value=\"data.keysToPress\"\n      class=\"mt-2 w-full\"\n      placeholder=\"keys\"\n      @change=\"updateData({ keysToPress: $event })\"\n    />\n    <ui-input\n      :model-value=\"data.pressTime || 0\"\n      :label=\"t('workflow.blocks.press-key.press-time')\"\n      class=\"w-full mt-2\"\n      :placeholder=\"t('common.millisecond')\"\n      @change=\"updateData({ pressTime: $event })\"\n    />\n  </div>\n</template>\n<script setup>\nimport SharedElSelectorActions from '@/components/newtab/shared/SharedElSelectorActions.vue';\nimport { keyDefinitions } from '@/utils/USKeyboardLayout';\nimport { recordPressedKey } from '@/utils/recordKeys';\nimport { onBeforeUnmount, ref } from 'vue';\nimport { useI18n } from 'vue-i18n';\nimport EditAutocomplete from './EditAutocomplete.vue';\n\nconst props = defineProps({\n  data: {\n    type: Object,\n    default: () => ({}),\n  },\n});\nconst emit = defineEmits(['update:data']);\n\nconst spaceKeys = ['\\r', '\\n', ' ', '\\u0000'];\n\nconst includedKeys = ['Enter', 'Control', 'Meta', 'Shift', 'Alt', 'Space'];\nconst filteredDefinitions = Object.keys(keyDefinitions)\n  .filter((key) => key.trim().length <= 1 || key.startsWith('Arrow'))\n  .filter((key) => !spaceKeys.includes(key));\nconst keysList = filteredDefinitions.concat(includedKeys);\n\nconst { t } = useI18n();\n\nconst isRecordingKey = ref(false);\nconst dataKeys = ref(`${props.data.keys}`);\n\nfunction updateData(value) {\n  emit('update:data', { ...props.data, ...value });\n}\nfunction updateKeys(value) {\n  dataKeys.value = value;\n  updateData({ keys: value });\n}\nfunction onKeydown(event) {\n  event.preventDefault();\n  event.stopPropagation();\n\n  recordPressedKey(event, (keys) => {\n    updateKeys(keys.join('+'));\n  });\n}\nfunction onKeyup() {\n  isRecordingKey.value = false;\n\n  /* eslint-disable-next-line */\n  detachKeyEvents();\n}\nfunction attachKeyEvents() {\n  window.addEventListener('keyup', onKeyup);\n  window.addEventListener('keydown', onKeydown);\n}\nfunction detachKeyEvents() {\n  window.removeEventListener('keyup', onKeyup);\n  window.removeEventListener('keydown', onKeydown);\n}\nfunction toggleRecordKeys() {\n  isRecordingKey.value = !isRecordingKey.value;\n\n  if (isRecordingKey.value) {\n    attachKeyEvents();\n  } else {\n    detachKeyEvents();\n  }\n}\n\nonBeforeUnmount(() => {\n  detachKeyEvents();\n});\n</script>\n"
  },
  {
    "path": "src/components/newtab/workflow/edit/EditProxy.vue",
    "content": "<template>\n  <div>\n    <ui-textarea\n      :model-value=\"data.description\"\n      :placeholder=\"t('common.description')\"\n      class=\"mb-2 w-full\"\n      @change=\"updateData({ description: $event })\"\n    />\n    <ui-input\n      :model-value=\"data.host\"\n      placeholder=\"socks5://1.2.3.4:1080\"\n      class=\"mb-2 w-full\"\n      @change=\"updateData({ host: $event })\"\n    >\n      <template #label>\n        <span class=\"input-label\"> Host </span>\n        <v-remixicon\n          title=\"Supported protocols: http, https, socks4, and socks5\"\n          name=\"riInformationLine\"\n          class=\"inline-block\"\n          size=\"18\"\n        />\n      </template>\n    </ui-input>\n    <ui-input\n      :model-value=\"data.port\"\n      label=\"Port\"\n      placeholder=\"443\"\n      class=\"mb-2 w-full\"\n      @change=\"updateData({ port: $event })\"\n    />\n    <label for=\"input-bypass\" class=\"input-label\">\n      {{ t('workflow.blocks.proxy.bypass.label') }}\n      <a\n        href=\"https://developer.chrome.com/docs/extensions/reference/proxy/#bypass-list\"\n        target=\"_blank\"\n        rel=\"noopener\"\n      >\n        &#128712;\n      </a>\n    </label>\n    <ui-textarea\n      id=\"input-bypass\"\n      :model-value=\"data.bypassList\"\n      placeholder=\"example1.com, example2.org\"\n      class=\"w-full\"\n      @change=\"updateData({ bypassList: $event })\"\n    >\n    </ui-textarea>\n    <p class=\"text-sm text-gray-600 dark:text-gray-200\">\n      {{ t('workflow.blocks.proxy.bypass.note') }}\n    </p>\n    <ui-checkbox\n      :model-value=\"data.clearProxy\"\n      class=\"mt-4\"\n      @change=\"updateData({ clearProxy: $event })\"\n    >\n      {{ t('workflow.blocks.proxy.clear') }}\n    </ui-checkbox>\n  </div>\n</template>\n<script setup>\nimport { useI18n } from 'vue-i18n';\n\nconst props = defineProps({\n  data: {\n    type: Object,\n    default: () => ({}),\n  },\n});\nconst emit = defineEmits(['update:data']);\n\nconst { t } = useI18n();\n\nfunction updateData(value) {\n  emit('update:data', { ...props.data, ...value });\n}\n</script>\n"
  },
  {
    "path": "src/components/newtab/workflow/edit/EditRegexVariable.vue",
    "content": "<template>\n  <div>\n    <ui-textarea\n      :model-value=\"data.description\"\n      :placeholder=\"t('common.description')\"\n      class=\"w-full\"\n      @change=\"updateData({ description: $event })\"\n    />\n    <ui-input\n      :model-value=\"data.variableName\"\n      :label=\"t('workflow.variables.name')\"\n      :title=\"t('workflow.variables.name')\"\n      class=\"mt-2 w-full\"\n      @change=\"updateData({ variableName: $event })\"\n    />\n    <ui-select\n      :model-value=\"data.method\"\n      label=\"Method\"\n      class=\"mt-2 w-full\"\n      @change=\"updateData({ method: $event })\"\n    >\n      <option v-for=\"method in methods\" :key=\"method.id\" :value=\"method.id\">\n        {{ method.name }}\n      </option>\n    </ui-select>\n    <ui-input\n      v-if=\"data.method === 'replace'\"\n      :model-value=\"data.replaceVal\"\n      label=\"Replace with\"\n      placeholder=\"(empty)\"\n      class=\"mt-2 w-full\"\n      @change=\"updateData({ replaceVal: $event })\"\n    />\n    <div class=\"mt-3 flex items-end\">\n      <div class=\"mr-2 flex-1\">\n        <label\n          class=\"ml-1 block text-sm text-gray-600 dark:text-gray-200\"\n          for=\"var-expression\"\n        >\n          RegEx\n        </label>\n        <div\n          class=\"bg-input flex items-center rounded-lg px-4 transition-colors\"\n        >\n          <span>/</span>\n          <input\n            id=\"var-expression\"\n            :value=\"data.expression\"\n            placeholder=\"Expression\"\n            class=\"w-11/12 bg-transparent py-2 px-1 focus:ring-0\"\n            @input=\"updateData({ expression: $event.target.value })\"\n          />\n          <span class=\"text-right\">/</span>\n        </div>\n      </div>\n      <ui-popover>\n        <template #trigger>\n          <button class=\"bg-input rounded-lg p-2\" title=\"Flags\">\n            {{ data.flag.length === 0 ? 'flags' : data.flag.join('') }}\n          </button>\n        </template>\n        <p>Flags</p>\n        <ul class=\"mt-2 space-y-1\">\n          <li v-for=\"flag in flags\" :key=\"flag.id\">\n            <ui-checkbox\n              :model-value=\"data.flag.includes(flag.id)\"\n              @change=\"updateFlag($event, flag.id)\"\n            >\n              {{ flag.name }}\n            </ui-checkbox>\n          </li>\n        </ul>\n      </ui-popover>\n    </div>\n  </div>\n</template>\n<script setup>\nimport { useI18n } from 'vue-i18n';\n\nconst props = defineProps({\n  data: {\n    type: Object,\n    default: () => ({}),\n  },\n});\nconst emit = defineEmits(['update:data']);\n\nconst methods = [\n  { id: 'match', name: 'Match value' },\n  { id: 'replace', name: 'Replace value' },\n];\nconst flags = [\n  { id: 'g', name: 'global' },\n  { id: 'i', name: 'ignore case' },\n  { id: 'm', name: 'multiline' },\n];\n\nconst { t } = useI18n();\n\nfunction updateData(value) {\n  emit('update:data', { ...props.data, ...value });\n}\nfunction updateFlag(include, flag) {\n  const copyFlag = [...props.data.flag];\n\n  if (include) {\n    copyFlag.push(flag);\n  } else {\n    const index = copyFlag.indexOf(flag);\n    copyFlag.splice(index, 1);\n  }\n\n  updateData({ flag: copyFlag });\n}\n</script>\n<style>\n.log-data .block-variable {\n  margin-top: 4px;\n}\n</style>\n"
  },
  {
    "path": "src/components/newtab/workflow/edit/EditSaveAssets.vue",
    "content": "<template>\n  <edit-interaction-base\n    :data=\"data\"\n    :hide=\"!permission.has.downloads\"\n    :hide-selector=\"data.type !== 'element'\"\n    @change=\"updateData\"\n  >\n    <template #prepend:selector>\n      <ui-select\n        class=\"mb-4\"\n        :model-value=\"data.type\"\n        :label=\"t('workflow.blocks.save-assets.contentTypes.title')\"\n        @change=\"updateData({ type: $event })\"\n      >\n        <option v-for=\"type in types\" :key=\"type\" :value=\"type\">\n          {{ t(`workflow.blocks.save-assets.contentTypes.${type}`) }}\n        </option>\n      </ui-select>\n    </template>\n    <template #prepend>\n      <template v-if=\"!permission.has.downloads\">\n        <p class=\"mt-4\">\n          {{ t('workflow.blocks.handle-download.noPermission') }}\n        </p>\n        <ui-button variant=\"accent\" class=\"mt-2\" @click=\"permission.request\">\n          {{ t('workflow.blocks.clipboard.grantPermission') }}\n        </ui-button>\n      </template>\n    </template>\n    <edit-autocomplete v-if=\"data.type === 'url'\">\n      <ui-input\n        :model-value=\"data.url\"\n        label=\"URL\"\n        class=\"w-full\"\n        autocomplete=\"off\"\n        placeholder=\"https://example.com/picture.png\"\n        @change=\"updateData({ url: $event })\"\n      />\n    </edit-autocomplete>\n    <template v-if=\"permission.has.downloads\">\n      <edit-autocomplete class=\"mt-4\">\n        <ui-input\n          :model-value=\"data.filename\"\n          :label=\"t('workflow.blocks.save-assets.filename')\"\n          class=\"w-full\"\n          autocomplete=\"off\"\n          placeholder=\"image.jpeg\"\n          @change=\"updateData({ filename: $event })\"\n        />\n      </edit-autocomplete>\n      <ui-select\n        :model-value=\"data.onConflict\"\n        :label=\"t('workflow.blocks.handle-download.onConflict')\"\n        class=\"mt-2 w-full\"\n        @change=\"updateData({ onConflict: $event })\"\n      >\n        <option v-for=\"item in onConflict\" :key=\"item\" :value=\"item\">\n          {{ t(`workflow.blocks.base.downloads.onConflict.${item}`) }}\n        </option>\n      </ui-select>\n      <hr class=\"my-4 w-full\" />\n      <label class=\"flex items-center\">\n        <ui-switch\n          :model-value=\"data.saveDownloadIds\"\n          @change=\"updateData({ saveDownloadIds: $event })\"\n        />\n        <p class=\"ml-2\">\n          {{ t('workflow.blocks.save-assets.saveDownloadIds') }}\n        </p>\n      </label>\n      <insert-workflow-data\n        v-if=\"data.saveDownloadIds\"\n        :data=\"data\"\n        variables\n        @update=\"updateData\"\n      />\n    </template>\n  </edit-interaction-base>\n</template>\n<script setup>\nimport { useI18n } from 'vue-i18n';\nimport { useHasPermissions } from '@/composable/hasPermissions';\nimport EditInteractionBase from './EditInteractionBase.vue';\nimport EditAutocomplete from './EditAutocomplete.vue';\nimport InsertWorkflowData from './InsertWorkflowData.vue';\n\nconst props = defineProps({\n  data: {\n    type: Object,\n    default: () => ({}),\n  },\n});\nconst emit = defineEmits(['update:data']);\n\nconst { t } = useI18n();\nconst permission = useHasPermissions(['downloads']);\n\nconst types = ['element', 'url'];\nconst onConflict = ['uniquify', 'overwrite', 'prompt'];\n\nfunction updateData(value) {\n  emit('update:data', { ...props.data, ...value });\n}\n</script>\n"
  },
  {
    "path": "src/components/newtab/workflow/edit/EditScrollElement.vue",
    "content": "<template>\n  <edit-interaction-base v-bind=\"{ data, hide: hideBase }\" @change=\"updateData\">\n    <div v-if=\"!data.scrollIntoView\" class=\"mt-3 flex items-center space-x-2\">\n      <ui-input\n        :model-value=\"data.scrollX || 0\"\n        :label=\"t('workflow.blocks.element-scroll.scrollX')\"\n        type=\"number\"\n        @change=\"updateData({ scrollX: +$event })\"\n      />\n      <ui-input\n        :model-value=\"data.scrollY || 0\"\n        :label=\"t('workflow.blocks.element-scroll.scrollY')\"\n        type=\"number\"\n        @change=\"updateData({ scrollY: +$event })\"\n      />\n    </div>\n    <div class=\"mt-3 space-y-2\">\n      <ui-checkbox\n        class=\"w-full\"\n        :model-value=\"data.scrollIntoView\"\n        @change=\"updateData({ scrollIntoView: $event })\"\n      >\n        {{ t('workflow.blocks.element-scroll.intoView') }}\n      </ui-checkbox>\n      <ui-checkbox\n        :model-value=\"data.smooth\"\n        @change=\"updateData({ smooth: $event })\"\n      >\n        {{ t('workflow.blocks.element-scroll.smooth') }}\n      </ui-checkbox>\n      <template v-if=\"!data.scrollIntoView\">\n        <ui-checkbox\n          :model-value=\"data.incX\"\n          @change=\"updateData({ incX: $event })\"\n        >\n          {{ t('workflow.blocks.element-scroll.incScrollX') }}\n        </ui-checkbox>\n        <ui-checkbox\n          :model-value=\"data.incY\"\n          @change=\"updateData({ incY: $event })\"\n        >\n          {{ t('workflow.blocks.element-scroll.incScrollY') }}\n        </ui-checkbox>\n      </template>\n    </div>\n  </edit-interaction-base>\n</template>\n<script setup>\nimport { useI18n } from 'vue-i18n';\nimport EditInteractionBase from './EditInteractionBase.vue';\n\nconst props = defineProps({\n  data: {\n    type: Object,\n    default: () => ({}),\n  },\n  hideBase: {\n    type: Boolean,\n    default: false,\n  },\n});\nconst emit = defineEmits(['update:data']);\n\nconst { t } = useI18n();\n\nfunction updateData(value) {\n  emit('update:data', { ...props.data, ...value });\n}\n</script>\n"
  },
  {
    "path": "src/components/newtab/workflow/edit/EditSliceVariable.vue",
    "content": "<template>\n  <div>\n    <ui-textarea\n      :model-value=\"data.description\"\n      :placeholder=\"t('common.description')\"\n      class=\"w-full\"\n      @change=\"updateData({ description: $event })\"\n    />\n    <ui-input\n      :model-value=\"data.variableName\"\n      :label=\"t('workflow.variables.name')\"\n      :title=\"t('workflow.variables.name')\"\n      class=\"mt-2 w-full\"\n      @change=\"updateData({ variableName: $event })\"\n    />\n    <ul class=\"mt-4 space-y-2\">\n      <li v-for=\"param in params\" :key=\"param.id\">\n        <ui-checkbox\n          :model-value=\"data[param.toggleKey]\"\n          @change=\"updateData({ [param.toggleKey]: $event })\"\n        >\n          {{ t(`workflow.blocks.slice-variable.${param.text}`) }}\n        </ui-checkbox>\n        <ui-input\n          v-if=\"data[param.toggleKey]\"\n          :model-value=\"data[param.id]\"\n          placeholder=\"0\"\n          type=\"number\"\n          class=\"mb-2 w-full\"\n          @change=\"updateData({ [param.id]: +$event })\"\n        />\n      </li>\n    </ul>\n  </div>\n</template>\n<script setup>\nimport { useI18n } from 'vue-i18n';\n\nconst props = defineProps({\n  data: {\n    type: Object,\n    default: () => ({}),\n  },\n});\nconst emit = defineEmits(['update:data']);\n\nconst { t } = useI18n();\n\nconst params = [\n  { id: 'startIndex', text: 'start', toggleKey: 'startIdxEnabled' },\n  { id: 'endIndex', text: 'end', toggleKey: 'endIdxEnabled' },\n];\n\nfunction updateData(value) {\n  emit('update:data', { ...props.data, ...value });\n}\n</script>\n<style>\n.log-data .block-variable {\n  margin-top: 4px;\n}\n</style>\n"
  },
  {
    "path": "src/components/newtab/workflow/edit/EditSortData.vue",
    "content": "<template>\n  <div>\n    <ui-textarea\n      :model-value=\"data.description\"\n      :placeholder=\"t('common.description')\"\n      class=\"w-full\"\n      @change=\"updateData({ description: $event })\"\n    />\n    <ui-select\n      :label=\"t('workflow.blocks.data-mapping.dataSource')\"\n      :model-value=\"data.dataSource\"\n      class=\"mt-4 w-full\"\n      @change=\"updateData({ dataSource: $event })\"\n    >\n      <option v-for=\"source in dataSources\" :key=\"source.id\" :value=\"source.id\">\n        {{ source.name }}\n      </option>\n    </ui-select>\n    <ui-input\n      v-if=\"data.dataSource === 'variable'\"\n      :model-value=\"data.varSourceName\"\n      :placeholder=\"t('workflow.variables.name')\"\n      :title=\"t('workflow.variables.name')\"\n      class=\"mt-2 w-full\"\n      @change=\"updateData({ varSourceName: $event })\"\n    />\n    <label class=\"mt-4 flex items-center\">\n      <ui-switch\n        :model-value=\"data.sortByProperty\"\n        @change=\"updateData({ sortByProperty: $event })\"\n      />\n      <span class=\"ml-2\">\n        {{ t('workflow.blocks.sort-data.property') }}\n      </span>\n    </label>\n    <template v-if=\"data.sortByProperty\">\n      <ul\n        v-for=\"(property, index) in properties\"\n        :key=\"index\"\n        class=\"mt-4 space-y-1 divide-y\"\n      >\n        <li class=\"sort-property\">\n          <ui-autocomplete\n            :model-value=\"property.name\"\n            :items=\"columns\"\n            :disabled=\"data.dataSource !== 'table'\"\n            class=\"w-full\"\n          >\n            <ui-input\n              v-model=\"property.name\"\n              autocomplete=\"off\"\n              :placeholder=\"`Property ${index + 1}`\"\n              class=\"w-full\"\n            />\n          </ui-autocomplete>\n          <div class=\"mt-2 flex items-center\">\n            <ui-select v-model=\"property.order\" class=\"flex-1\">\n              <option value=\"asc\">Ascending</option>\n              <option value=\"desc\">Descending</option>\n            </ui-select>\n            <ui-button class=\"ml-2\" icon @click=\"properties.splice(index, 1)\">\n              <v-remixicon name=\"riDeleteBin7Line\" />\n            </ui-button>\n          </div>\n        </li>\n      </ul>\n      <ui-button\n        v-if=\"properties.length < 3\"\n        variant=\"accent\"\n        class=\"mt-4 text-sm\"\n        @click=\"addProperty\"\n      >\n        {{ t('workflow.blocks.sort-data.addProperty') }}\n      </ui-button>\n    </template>\n    <div class=\"mt-6\">\n      <insert-workflow-data :data=\"data\" variables @update=\"updateData\" />\n    </div>\n  </div>\n</template>\n<script setup>\nimport { inject, ref, watch } from 'vue';\nimport { useI18n } from 'vue-i18n';\nimport cloneDeep from 'lodash.clonedeep';\nimport InsertWorkflowData from './InsertWorkflowData.vue';\n\nconst props = defineProps({\n  data: {\n    type: Object,\n    default: () => ({}),\n  },\n});\nconst emit = defineEmits(['update:data']);\n\nconst { t } = useI18n();\nconst dataSources = [\n  { id: 'table', name: t('workflow.table.title') },\n  { id: 'variable', name: t('workflow.variables.title') },\n];\n\nconst workflow = inject('workflow');\nconst columns = workflow.columns.value.map(({ name }) => name);\n\nconst properties = ref(cloneDeep(props.data.itemProperties));\n\nfunction updateData(value) {\n  emit('update:data', { ...props.data, ...value });\n}\nfunction addProperty() {\n  properties.value.push({\n    name: '',\n    order: 'asc',\n  });\n}\n\nwatch(\n  properties,\n  (value) => {\n    updateData({ itemProperties: value });\n  },\n  { deep: true }\n);\n</script>\n<style>\n.sort-property .ui-popover__trigger {\n  width: 100%;\n}\n</style>\n"
  },
  {
    "path": "src/components/newtab/workflow/edit/EditSwitchTab.vue",
    "content": "<template>\n  <div>\n    <ui-textarea\n      :model-value=\"data.description\"\n      :placeholder=\"t('common.description')\"\n      class=\"w-full\"\n      @change=\"updateData({ description: $event })\"\n    />\n    <ui-select\n      :model-value=\"data.findTabBy\"\n      label=\"Find tab by\"\n      class=\"mb-2 mt-3 w-full\"\n      @change=\"updateData({ findTabBy: $event })\"\n    >\n      <option v-for=\"type in types\" :key=\"type.id\" :value=\"type.id\">\n        {{ type.name }}\n      </option>\n    </ui-select>\n    <template v-if=\"['match-patterns', 'tab-title'].includes(data.findTabBy)\">\n      <edit-autocomplete v-if=\"data.findTabBy === 'match-patterns'\">\n        <ui-input\n          :model-value=\"data.matchPattern\"\n          placeholder=\"https://example.com/*\"\n          class=\"w-full\"\n          @change=\"updateData({ matchPattern: $event })\"\n        >\n          <template #label>\n            {{ t('workflow.blocks.switch-tab.matchPattern') }}\n            <a\n              :title=\"t('common.example', 2)\"\n              href=\"https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Match_patterns#examples\"\n              target=\"_blank\"\n              rel=\"noopener\"\n              class=\"ml-1 inline-block\"\n            >\n              <v-remixicon\n                name=\"riInformationLine\"\n                class=\"inline-block\"\n                size=\"18\"\n              />\n            </a>\n          </template>\n        </ui-input>\n      </edit-autocomplete>\n      <edit-autocomplete v-else-if=\"data.findTabBy === 'tab-title'\">\n        <ui-input\n          :model-value=\"data.tabTitle\"\n          label=\"Tab title\"\n          class=\"w-full\"\n          @change=\"updateData({ tabTitle: $event })\"\n        />\n      </edit-autocomplete>\n      <ui-checkbox\n        :model-value=\"data.createIfNoMatch\"\n        class=\"mt-1\"\n        @change=\"updateData({ createIfNoMatch: $event })\"\n      >\n        {{ t('workflow.blocks.switch-tab.createIfNoMatch') }}\n      </ui-checkbox>\n      <edit-autocomplete v-if=\"data.createIfNoMatch\" class=\"mt-2\">\n        <ui-input\n          :model-value=\"data.url\"\n          :label=\"t('workflow.blocks.switch-tab.url')\"\n          placeholder=\"https://example.com\"\n          class=\"w-full\"\n          @change=\"updateData({ url: $event })\"\n        />\n      </edit-autocomplete>\n    </template>\n    <ui-input\n      v-else-if=\"data.findTabBy === 'tab-index'\"\n      :model-value=\"data.tabIndex\"\n      label=\"Index\"\n      type=\"number\"\n      class=\"w-full\"\n      min=\"0\"\n      @change=\"updateData({ tabIndex: +$event })\"\n    />\n    <ui-checkbox\n      :model-value=\"data.activeTab\"\n      class=\"my-2\"\n      @change=\"updateData({ activeTab: $event })\"\n    >\n      {{ t('workflow.blocks.new-tab.activeTab') }}\n    </ui-checkbox>\n  </div>\n</template>\n<script setup>\nimport { useI18n } from 'vue-i18n';\nimport EditAutocomplete from './EditAutocomplete.vue';\n\nconst props = defineProps({\n  data: {\n    type: Object,\n    default: () => ({}),\n  },\n});\nconst emit = defineEmits(['update:data']);\n\nconst { t } = useI18n();\nconst types = [\n  { id: 'match-patterns', name: 'Match patterns' },\n  { id: 'tab-title', name: 'Tab title' },\n  { id: 'next-tab', name: 'Next tab' },\n  { id: 'prev-tab', name: 'Previous tab' },\n  { id: 'tab-index', name: 'Tab index' },\n];\n\nfunction updateData(value) {\n  emit('update:data', { ...props.data, ...value });\n}\n</script>\n"
  },
  {
    "path": "src/components/newtab/workflow/edit/EditSwitchTo.vue",
    "content": "<template>\n  <div class=\"space-y-2\">\n    <ui-textarea\n      :model-value=\"data.description\"\n      :placeholder=\"t('common.description')\"\n      autoresize\n      class=\"w-full\"\n      @change=\"updateData({ description: $event })\"\n    />\n    <ui-select\n      :model-value=\"data.windowType\"\n      class=\"w-full\"\n      @change=\"updateData({ windowType: $event })\"\n    >\n      <option value=\"main-window\">\n        {{ t('workflow.blocks.switch-to.windowTypes.main') }}\n      </option>\n      <option value=\"iframe\">\n        {{ t('workflow.blocks.switch-to.windowTypes.iframe') }}\n      </option>\n    </ui-select>\n    <edit-autocomplete\n      v-if=\"data.windowType === 'iframe'\"\n      :trigger-char=\"['{{', '}}']\"\n      block\n      hide-empty\n      class=\"mt-2\"\n      trigger-class=\"!flex items-end\"\n    >\n      <ui-input\n        :model-value=\"data.selector\"\n        :label=\"t('workflow.blocks.switch-to.iframeSelector')\"\n        placeholder=\"CSS Selector or XPath\"\n        autocomplete=\"off\"\n        class=\"mr-2 w-full\"\n        @change=\"updateData({ selector: $event })\"\n      />\n      <shared-el-selector-actions\n        :selector=\"data.selector\"\n        @update:selector=\"updateData({ selector: $event })\"\n      />\n    </edit-autocomplete>\n  </div>\n</template>\n<script setup>\nimport { useI18n } from 'vue-i18n';\nimport SharedElSelectorActions from '@/components/newtab/shared/SharedElSelectorActions.vue';\nimport EditAutocomplete from './EditAutocomplete.vue';\n\nconst props = defineProps({\n  data: {\n    type: Object,\n    default: () => ({}),\n  },\n});\nconst emit = defineEmits(['update:data']);\n\nconst { t } = useI18n();\n\nfunction updateData(value) {\n  emit('update:data', { ...props.data, ...value });\n}\n</script>\n"
  },
  {
    "path": "src/components/newtab/workflow/edit/EditTabURL.vue",
    "content": "<template>\n  <div>\n    <ui-textarea\n      :model-value=\"data.description\"\n      :placeholder=\"t('common.description')\"\n      class=\"w-full\"\n      @change=\"updateData({ description: $event })\"\n    />\n    <ui-select\n      :model-value=\"data.type\"\n      :label=\"t('workflow.blocks.tab-url.select')\"\n      class=\"mt-4 w-full\"\n      @change=\"updateData({ type: $event })\"\n    >\n      <option v-for=\"type in types\" :key=\"type\" :value=\"type\">\n        {{ t(`workflow.blocks.tab-url.types.${type}`) }}\n      </option>\n    </ui-select>\n    <div v-if=\"data.type === 'all'\" class=\"mt-4 rounded-lg border p-2\">\n      <p class=\"text-sm text-gray-600\">\n        {{ t('workflow.blocks.tab-url.query.title') }}\n      </p>\n      <ui-input\n        :model-value=\"data.qMatchPatterns\"\n        class=\"mt-2 w-full\"\n        placeholder=\"https://example.com/*\"\n        @change=\"updateData({ qMatchPatterns: $event })\"\n      >\n        <template #label>\n          {{ t('workflow.blocks.tab-url.query.matchPatterns') }}\n        </template>\n      </ui-input>\n      <ui-input\n        :model-value=\"data.qTitle\"\n        :label=\"t('workflow.blocks.tab-url.query.tabTitle')\"\n        class=\"mt-2 w-full\"\n        @change=\"updateData({ qTitle: $event })\"\n      />\n    </div>\n    <insert-workflow-data :data=\"data\" variables @update=\"updateData\" />\n  </div>\n</template>\n<script setup>\nimport { useI18n } from 'vue-i18n';\nimport InsertWorkflowData from './InsertWorkflowData.vue';\n\nconst props = defineProps({\n  data: {\n    type: Object,\n    default: () => ({}),\n  },\n});\nconst emit = defineEmits(['update:data']);\n\nconst types = ['active-tab', 'all'];\nconst { t } = useI18n();\n\nfunction updateData(value) {\n  emit('update:data', { ...props.data, ...value });\n}\n</script>\n<style>\n.log-data .block-variable {\n  margin-top: 4px;\n}\n</style>\n"
  },
  {
    "path": "src/components/newtab/workflow/edit/EditTakeScreenshot.vue",
    "content": "<template>\n  <div class=\"take-screenshot\">\n    <ui-textarea\n      :model-value=\"data.description\"\n      :placeholder=\"t('common.description')\"\n      class=\"w-full\"\n      @change=\"updateData({ description: $event })\"\n    />\n    <ui-select\n      :model-value=\"data.type\"\n      :label=\"t('workflow.blocks.take-screenshot.types.title')\"\n      class=\"mt-2 w-full\"\n      @change=\"updateData({ type: $event })\"\n    >\n      <option v-for=\"type in types\" :key=\"type\" :value=\"type\">\n        {{ t(`workflow.blocks.take-screenshot.types.${type}`) }}\n      </option>\n    </ui-select>\n    <ui-input\n      v-if=\"data.type === 'element'\"\n      :model-value=\"data.selector\"\n      :label=\"t(`workflow.blocks.base.findElement.options.cssSelector`)\"\n      class=\"mt-2 w-full\"\n      placeholder=\".element\"\n      @change=\"updateData({ selector: $event })\"\n    />\n    <template v-if=\"data.ext === 'jpeg'\">\n      <p class=\"ml-2 mt-4 text-sm text-gray-600 dark:text-gray-200\">\n        {{ t('workflow.blocks.take-screenshot.imageQuality') }}\n      </p>\n      <div class=\"bg-box-transparent flex items-center rounded-lg px-4 py-2\">\n        <input\n          :value=\"data.quality\"\n          :title=\"t('workflow.blocks.take-screenshot.imageQuality')\"\n          class=\"flex-1 focus:outline-none\"\n          type=\"range\"\n          min=\"0\"\n          max=\"100\"\n          @change=\"updateQuality\"\n        />\n        <span class=\"w-12 text-right\">{{ data.quality }}%</span>\n      </div>\n    </template>\n    <ui-checkbox\n      :model-value=\"data.saveToComputer\"\n      class=\"mt-4\"\n      @change=\"updateData({ saveToComputer: $event })\"\n    >\n      {{ t('workflow.blocks.take-screenshot.saveToComputer') }}\n    </ui-checkbox>\n    <div v-if=\"data.saveToComputer\" class=\"mt-1 flex items-center\">\n      <edit-autocomplete class=\"mr-2 flex-1\">\n        <ui-input\n          :model-value=\"data.fileName\"\n          :placeholder=\"t('common.fileName')\"\n          autocomplete=\"off\"\n          class=\"mr-2 flex-1\"\n          title=\"File name\"\n          @change=\"updateData({ fileName: $event })\"\n        />\n      </edit-autocomplete>\n      <ui-select\n        :model-value=\"data.ext || 'png'\"\n        placeholder=\"Type\"\n        @change=\"updateData({ ext: $event })\"\n      >\n        <option value=\"png\">PNG</option>\n        <option value=\"jpeg\">JPEG</option>\n      </ui-select>\n    </div>\n    <ui-checkbox\n      :model-value=\"data.saveToColumn\"\n      class=\"mt-4\"\n      @change=\"updateData({ saveToColumn: $event })\"\n    >\n      {{ t('workflow.blocks.take-screenshot.saveToColumn') }}\n    </ui-checkbox>\n    <ui-select\n      v-if=\"data.saveToColumn\"\n      :model-value=\"data.dataColumn\"\n      placeholder=\"Select column\"\n      class=\"mt-1 w-full\"\n      @change=\"updateData({ dataColumn: $event })\"\n    >\n      <option\n        v-for=\"column in workflow.columns.value\"\n        :key=\"column.id || column.name\"\n        :value=\"column.id || column.name\"\n      >\n        {{ column.name }}\n      </option>\n    </ui-select>\n    <ui-checkbox\n      :model-value=\"data.assignVariable\"\n      block\n      class=\"mt-4\"\n      @change=\"updateData({ assignVariable: $event })\"\n    >\n      {{ t('workflow.variables.assign') }}\n    </ui-checkbox>\n    <ui-input\n      v-if=\"data.assignVariable\"\n      :model-value=\"data.variableName\"\n      :placeholder=\"t('workflow.variables.name')\"\n      :title=\"t('workflow.variables.name')\"\n      class=\"mt-1 w-full\"\n      @change=\"updateData({ variableName: $event })\"\n    />\n  </div>\n</template>\n<script setup>\n/* eslint-disable no-unused-expressions */\nimport { inject, onMounted } from 'vue';\nimport { useI18n } from 'vue-i18n';\nimport { objectHasKey } from '@/utils/helper';\nimport EditAutocomplete from './EditAutocomplete.vue';\n\nconst props = defineProps({\n  data: {\n    type: Object,\n    default: () => ({}),\n  },\n});\nconst emit = defineEmits(['update:data']);\n\nconst { t } = useI18n();\nconst workflow = inject('workflow');\n\nconst types = ['page', 'fullpage', 'element'];\n\nfunction updateData(value) {\n  emit('update:data', { ...props.data, ...value });\n}\nfunction updateQuality({ target }) {\n  let quality = +target.value;\n\n  if (quality <= 0) quality = 0;\n  if (quality >= 100) quality = 100;\n\n  updateData({ quality });\n}\n\nonMounted(() => {\n  if (!objectHasKey(props.data, 'saveToComputer')) {\n    updateData({ saveToComputer: true, saveToColumn: false });\n  }\n\n  if (!objectHasKey(props.data, 'type')) {\n    const type = 'page';\n\n    if (props.data.fullPage) {\n      type === 'fullpage';\n    }\n\n    updateData({ type, fullPage: false });\n  }\n});\n</script>\n"
  },
  {
    "path": "src/components/newtab/workflow/edit/EditTrigger.vue",
    "content": "<template>\n  <div class=\"trigger\">\n    <ui-textarea\n      :model-value=\"data.description\"\n      autoresize\n      :placeholder=\"t('common.description')\"\n      class=\"mb-2 w-full\"\n      @change=\"updateData({ description: $event })\"\n    />\n    <ui-button\n      variant=\"accent\"\n      class=\"mt-4 w-full\"\n      @click=\"state.showTriggersModal = true\"\n    >\n      Edit Triggers\n    </ui-button>\n    <ui-button class=\"mt-4\" @click=\"state.showParamModal = true\">\n      <v-remixicon name=\"riCommandLine\" class=\"mr-2 -ml-1\" />\n      <span>Parameters</span>\n    </ui-button>\n    <ui-modal\n      v-model=\"state.showParamModal\"\n      title=\"Parameters\"\n      content-class=\"max-w-4xl\"\n    >\n      <edit-workflow-parameters\n        :prefer-tab=\"data.preferParamsInTab\"\n        :data=\"data.parameters\"\n        @update=\"updateData({ parameters: $event })\"\n        @update:prefer-tab=\"updateData({ preferParamsInTab: $event })\"\n      />\n    </ui-modal>\n    <ui-modal\n      v-model=\"state.showTriggersModal\"\n      title=\"Workflow Triggers\"\n      content-class=\"max-w-2xl\"\n    >\n      <shared-workflow-triggers\n        :triggers=\"state.triggers\"\n        @update=\"updateWorkflow\"\n      />\n    </ui-modal>\n  </div>\n</template>\n<script setup>\nimport { onMounted, reactive } from 'vue';\nimport { useI18n } from 'vue-i18n';\nimport { nanoid } from 'nanoid/non-secure';\nimport SharedWorkflowTriggers from '@/components/newtab/shared/SharedWorkflowTriggers.vue';\nimport EditWorkflowParameters from './EditWorkflowParameters.vue';\n\nconst props = defineProps({\n  data: {\n    type: Object,\n    default: () => ({}),\n  },\n});\nconst emit = defineEmits(['update:data']);\n\nconst { t } = useI18n();\n\nconst state = reactive({\n  showParamModal: false,\n  showTriggersModal: false,\n  triggers: [...(props.data?.triggers || [])],\n});\n\nfunction updateData(value) {\n  emit('update:data', { ...props.data, ...value });\n}\nfunction updateWorkflow(triggers) {\n  state.triggers = triggers;\n  updateData({ triggers });\n}\n\nonMounted(() => {\n  if (props.data.triggers) return;\n\n  state.triggers = [\n    { type: props.data.type, data: { ...props.data }, id: nanoid(5) },\n  ];\n});\n</script>\n<style>\n.trigger-item > button {\n  @apply focus:ring-0;\n  text-align: left;\n  .delete-btn {\n    visibility: hidden;\n  }\n  &:hover .delete-btn {\n    visibility: visible;\n  }\n}\n</style>\n"
  },
  {
    "path": "src/components/newtab/workflow/edit/EditTriggerEvent.vue",
    "content": "<template>\n  <edit-interaction-base v-bind=\"{ data, hide: hideBase }\" @change=\"updateData\">\n    <ui-select\n      :model-value=\"data.eventName\"\n      :placeholder=\"t('workflow.blocks.trigger-event.selectEvent')\"\n      class=\"mt-4 w-full\"\n      @change=\"handleSelectChange\"\n    >\n      <option v-for=\"event in eventList\" :key=\"event.id\" :value=\"event.id\">\n        {{ event.name }}\n      </option>\n    </ui-select>\n    <button\n      class=\"mb-2 mt-1 block flex w-full items-center text-left focus:ring-0\"\n      @click=\"showOptions = !showOptions\"\n    >\n      <v-remixicon\n        name=\"riArrowLeftSLine\"\n        class=\"mr-1 -ml-1 transition-transform\"\n        :rotate=\"showOptions ? 270 : 180\"\n      />\n      <span class=\"flex-1\">{{ t('common.options') }}</span>\n      <a\n        v-if=\"data.eventName\"\n        :href=\"getEventDetailsUrl()\"\n        rel=\"noopener\"\n        target=\"_blank\"\n        @click.stop\n      >\n        <v-remixicon name=\"riInformationLine\" size=\"20\" />\n      </a>\n    </button>\n    <transition-expand>\n      <div v-if=\"showOptions\">\n        <div class=\"mb-4 grid grid-cols-2 gap-2\">\n          <ui-checkbox\n            :model-value=\"params.bubbles\"\n            @change=\"updateParams({ ...params, bubbles: $event })\"\n          >\n            Bubbles\n          </ui-checkbox>\n          <ui-checkbox\n            :model-value=\"params.cancelable\"\n            @change=\"updateParams({ ...params, cancelable: $event })\"\n          >\n            Cancelable\n          </ui-checkbox>\n        </div>\n        <component\n          :is=\"eventComponents[data.eventType]\"\n          v-if=\"eventComponents[data.eventType]\"\n          :key=\"data.eventName\"\n          :params=\"params\"\n          @update=\"updateParams({ ...params, ...$event })\"\n        />\n      </div>\n    </transition-expand>\n  </edit-interaction-base>\n</template>\n<script setup>\nimport { ref } from 'vue';\nimport { useI18n } from 'vue-i18n';\nimport { eventList } from '@/utils/shared';\nimport { toCamelCase } from '@/utils/helper';\nimport EditInteractionBase from './EditInteractionBase.vue';\nimport TriggerEventMouse from './TriggerEvent/TriggerEventMouse.vue';\nimport TriggerEventTouch from './TriggerEvent/TriggerEventTouch.vue';\nimport TriggerEventWheel from './TriggerEvent/TriggerEventWheel.vue';\nimport TriggerEventInput from './TriggerEvent/TriggerEventInput.vue';\nimport TriggerEventKeyboard from './TriggerEvent/TriggerEventKeyboard.vue';\n\nconst props = defineProps({\n  data: {\n    type: Object,\n    default: () => ({}),\n  },\n  hideBase: {\n    type: Boolean,\n    default: false,\n  },\n});\nconst emit = defineEmits(['update:data']);\n\nconst { t } = useI18n();\n\nconst eventComponents = {\n  'mouse-event': TriggerEventMouse,\n  'focus-event': '',\n  event: '',\n  'touch-event': TriggerEventTouch,\n  'keyboard-event': TriggerEventKeyboard,\n  'wheel-event': TriggerEventWheel,\n  'input-event': TriggerEventInput,\n};\n\nconst params = ref(props.data.eventParams);\nconst showOptions = ref(false);\n\nfunction getEventDetailsUrl() {\n  const eventType = toCamelCase(props.data.eventType);\n\n  return `https://developer.mozilla.org/en-US/docs/Web/API/${eventType}/${eventType}`;\n}\nfunction updateData(value) {\n  emit('update:data', { ...props.data, ...value });\n}\nfunction updateParams(value) {\n  params.value = value;\n  updateData({ eventParams: value });\n}\nfunction handleSelectChange(value) {\n  const eventType = eventList.find(({ id }) => id === value).type;\n  const payload = { eventName: value, eventType };\n\n  if (eventType !== props.eventType) {\n    const defaultParams = { bubbles: true, cancelable: true };\n\n    payload.eventParams = defaultParams;\n    params.value = defaultParams;\n  }\n\n  updateData(payload);\n}\n</script>\n"
  },
  {
    "path": "src/components/newtab/workflow/edit/EditUploadFile.vue",
    "content": "<template>\n  <edit-interaction-base\n    class=\"mb-8\"\n    v-bind=\"{ data, hide: hideBase }\"\n    @change=\"updateData\"\n  >\n    <template v-if=\"hasFileAccess || browserType === 'firefox'\">\n      <div\n        v-if=\"browserType === 'firefox'\"\n        class=\"mt-4 flex items-start rounded-lg bg-primary p-2 text-white\"\n      >\n        <v-remixicon name=\"riErrorWarningLine\" size=\"20\" />\n        <div class=\"ml-2 flex-1 text-sm leading-tight\">\n          <p>{{ t('workflow.blocks.upload-file.onlyURL') }}</p>\n        </div>\n      </div>\n      <div class=\"mt-4 space-y-2\">\n        <div\n          v-for=\"(path, index) in filePaths\"\n          :key=\"index\"\n          class=\"group flex items-center\"\n        >\n          <edit-autocomplete class=\"mr-2\">\n            <ui-input\n              v-model=\"filePaths[index]\"\n              placeholder=\"URL/File path/base64\"\n              autocomplete=\"off\"\n              class=\"w-full\"\n            />\n          </edit-autocomplete>\n          <v-remixicon\n            name=\"riDeleteBin7Line\"\n            class=\"invisible cursor-pointer group-hover:visible\"\n            @click=\"filePaths.splice(index, 1)\"\n          />\n        </div>\n      </div>\n      <ui-button variant=\"accent\" class=\"mt-2\" @click=\"filePaths.push('')\">\n        {{ t('workflow.blocks.upload-file.addFile') }}\n      </ui-button>\n    </template>\n    <template v-else>\n      <div\n        class=\"mt-4 flex items-start rounded-lg bg-red-200 p-2 dark:bg-red-400\"\n      >\n        <v-remixicon name=\"riErrorWarningLine\" />\n        <div class=\"ml-2 flex-1 leading-tight\">\n          <p>{{ t('workflow.blocks.upload-file.noFileAccess') }}</p>\n        </div>\n      </div>\n      <a\n        href=\"https://docs.extension.automa.site/blocks/upload-file.html#requirements\"\n        target=\"_blank\"\n        rel=\"noopener\"\n        class=\"mt-2 inline-block leading-tight text-primary\"\n      >\n        {{ t('workflow.blocks.upload-file.requirement') }}\n      </a>\n    </template>\n  </edit-interaction-base>\n</template>\n<script setup>\nimport { ref, watch } from 'vue';\nimport { useI18n } from 'vue-i18n';\nimport browser from 'webextension-polyfill';\nimport EditAutocomplete from './EditAutocomplete.vue';\nimport EditInteractionBase from './EditInteractionBase.vue';\n\nconst props = defineProps({\n  data: {\n    type: Object,\n    default: () => ({}),\n  },\n  hideBase: {\n    type: Boolean,\n    default: false,\n  },\n});\nconst emit = defineEmits(['update:data']);\n\nconst { t } = useI18n();\nconst browserType = BROWSER_TYPE;\n\nconst filePaths = ref([...props.data.filePaths]);\nconst hasFileAccess = ref(true);\n\nfunction updateData(value) {\n  emit('update:data', { ...props.data, ...value });\n}\n\nbrowser.extension.isAllowedFileSchemeAccess().then((value) => {\n  hasFileAccess.value = value;\n});\n\nwatch(\n  filePaths,\n  (paths) => {\n    updateData({ filePaths: paths });\n  },\n  { deep: true }\n);\n</script>\n"
  },
  {
    "path": "src/components/newtab/workflow/edit/EditWaitConnections.vue",
    "content": "<template>\n  <div class=\"mb-4\">\n    <ui-textarea\n      :model-value=\"data.description\"\n      class=\"w-full\"\n      :placeholder=\"t('common.description')\"\n      @change=\"updateData({ description: $event })\"\n    />\n    <ui-input\n      :model-value=\"data.timeout\"\n      :label=\"t('workflow.blocks.base.timeout')\"\n      placeholder=\"10000\"\n      type=\"number\"\n      class=\"mt-1 w-full\"\n      @change=\"updateData({ timeout: +$event })\"\n    />\n    <ui-checkbox\n      :model-value=\"data.specificFlow\"\n      class=\"mt-4\"\n      @change=\"updateData({ specificFlow: $event })\"\n    >\n      {{ t('workflow.blocks.wait-connections.specificFlow') }}\n    </ui-checkbox>\n    <ui-select\n      v-if=\"data.specificFlow\"\n      :model-value=\"data.flowBlockId\"\n      :label=\"t('workflow.blocks.wait-connections.selectFlow')\"\n      class=\"mt-1 w-full\"\n      @change=\"updateData({ flowBlockId: $event })\"\n    >\n      <option v-for=\"item in connections\" :key=\"item.id\" :value=\"item.id\">\n        {{ item.name }}\n      </option>\n    </ui-select>\n  </div>\n</template>\n<script setup>\nimport { onMounted } from 'vue';\nimport { useI18n } from 'vue-i18n';\n\nconst props = defineProps({\n  data: {\n    type: Object,\n    default: () => ({}),\n  },\n  connections: {\n    type: Array,\n    default: () => [],\n  },\n});\nconst emit = defineEmits(['update:data']);\n\nconst { t } = useI18n();\n\nfunction updateData(value) {\n  emit('update:data', { ...props.data, ...value });\n}\n\nonMounted(() => {\n  if (props.data.flowBlockId) return;\n\n  updateData({\n    flowBlockId: props.connections[0]?.id || '',\n  });\n});\n</script>\n"
  },
  {
    "path": "src/components/newtab/workflow/edit/EditWebhook.vue",
    "content": "<template>\n  <div class=\"mb-2 mt-4\">\n    <ui-textarea\n      :model-value=\"data.description\"\n      :placeholder=\"t('common.description')\"\n      class=\"mb-2 w-full\"\n      @change=\"updateData({ description: $event })\"\n    />\n    <ui-select\n      :model-value=\"data.method || 'POST'\"\n      :label=\"t('workflow.blocks.webhook.method')\"\n      class=\"mb-2 w-full\"\n      @change=\"updateMethod\"\n    >\n      <option v-for=\"method in methods\" :key=\"method\" :value=\"method\">\n        {{ method }}\n      </option>\n    </ui-select>\n    <edit-autocomplete class=\"mb-2\">\n      <ui-textarea\n        :model-value=\"data.url\"\n        :label=\"`${t('workflow.blocks.webhook.url')}*`\"\n        placeholder=\"http://api.example.com\"\n        class=\"w-full\"\n        rows=\"1\"\n        autocomplete=\"off\"\n        required\n        type=\"url\"\n        @change=\"updateData({ url: $event })\"\n      />\n    </edit-autocomplete>\n    <ui-select\n      :model-value=\"data.contentType\"\n      :label=\"t('workflow.blocks.webhook.contentType')\"\n      class=\"mb-2 w-full\"\n      @change=\"updateData({ contentType: $event })\"\n    >\n      <option\n        v-for=\"type in contentTypes\"\n        :key=\"type.value\"\n        :value=\"type.value\"\n      >\n        {{ type.name }}\n      </option>\n    </ui-select>\n    <ui-input\n      :model-value=\"data.timeout\"\n      :label=\"\n        t('workflow.blocks.webhook.timeout.placeholder') +\n        ` (${t('common.0disable')})`\n      \"\n      :title=\"t('workflow.blocks.webhook.timeout.title')\"\n      class=\"mb-2 w-full\"\n      type=\"number\"\n      @change=\"updateData({ timeout: +$event })\"\n    />\n    <ui-tabs v-model=\"activeTab\" fill>\n      <ui-tab value=\"headers\">\n        {{ t('workflow.blocks.webhook.tabs.headers') }}\n      </ui-tab>\n      <ui-tab v-if=\"!notHaveBody.includes(data.method)\" value=\"body\">\n        {{ t('workflow.blocks.webhook.tabs.body') }}\n      </ui-tab>\n      <ui-tab value=\"response\">\n        {{ t('workflow.blocks.webhook.tabs.response') }}\n      </ui-tab>\n    </ui-tabs>\n    <ui-tab-panels v-model=\"activeTab\">\n      <ui-tab-panel\n        value=\"headers\"\n        class=\"mt-4 grid grid-cols-7 justify-items-center gap-2\"\n      >\n        <template v-for=\"(items, index) in headers\" :key=\"index\">\n          <ui-input\n            v-model=\"items.name\"\n            :title=\"items.name\"\n            :placeholder=\"`Header ${index + 1}`\"\n            type=\"text\"\n            class=\"col-span-3\"\n          />\n          <ui-input\n            v-model=\"items.value\"\n            :title=\"items.value\"\n            placeholder=\"Value\"\n            type=\"text\"\n            class=\"col-span-3\"\n          />\n          <button @click=\"removeHeader(index)\">\n            <v-remixicon name=\"riCloseCircleLine\" size=\"20\" />\n          </button>\n        </template>\n        <ui-button class=\"col-span-4 mt-4 block w-full\" @click=\"addHeader\">\n          <span> {{ t('workflow.blocks.webhook.buttons.header') }} </span>\n        </ui-button>\n      </ui-tab-panel>\n      <ui-tab-panel value=\"body\" class=\"mt-4\">\n        <pre\n          v-if=\"!showBodyModal\"\n          class=\"max-h-80 overflow-auto rounded-lg bg-gray-900 p-4 text-gray-200\"\n          @click=\"showBodyModal = true\"\n          v-text=\"data.body\"\n        />\n      </ui-tab-panel>\n      <ui-tab-panel value=\"response\" class=\"mt-2\">\n        <ui-select\n          :model-value=\"data.responseType\"\n          label=\"Response type\"\n          class=\"w-full\"\n          @change=\"updateData({ responseType: $event })\"\n        >\n          <option value=\"json\">JSON</option>\n          <option value=\"text\">Text</option>\n          <option value=\"base64\">Base64</option>\n        </ui-select>\n        <ui-input\n          v-if=\"data.responseType === 'json'\"\n          :model-value=\"data.dataPath\"\n          placeholder=\"path.to.data\"\n          label=\"Data path\"\n          class=\"mt-2 w-full\"\n          @change=\"updateData({ dataPath: $event })\"\n        />\n        <insert-workflow-data\n          :data=\"data\"\n          :columns=\"[{ name: '[Assign columns]', id: '$assignColumns' }]\"\n          variables\n          @update=\"updateData\"\n        />\n      </ui-tab-panel>\n    </ui-tab-panels>\n    <ui-modal\n      v-model=\"showBodyModal\"\n      content-class=\"max-w-3xl\"\n      :title=\"t('workflow.blocks.webhook.tabs.body')\"\n    >\n      <shared-codemirror\n        :model-value=\"data.body\"\n        lang=\"json\"\n        style=\"height: calc(100vh - 10rem)\"\n        @change=\"updateData({ body: $event })\"\n      />\n      <div class=\"mt-3\">\n        <a\n          href=\"https://docs.extension.automa.site/workflow/expressions.html\"\n          rel=\"noopener\"\n          class=\"border-b text-primary\"\n          target=\"_blank\"\n        >\n          {{ t('message.useDynamicData') }}\n        </a>\n      </div>\n    </ui-modal>\n  </div>\n</template>\n<script setup>\nimport { contentTypes } from '@/utils/shared';\nimport { defineAsyncComponent, ref, watch } from 'vue';\nimport { useI18n } from 'vue-i18n';\nimport EditAutocomplete from './EditAutocomplete.vue';\nimport InsertWorkflowData from './InsertWorkflowData.vue';\n\nconst SharedCodemirror = defineAsyncComponent(() =>\n  import('@/components/newtab/shared/SharedCodemirror.vue')\n);\n\nconst props = defineProps({\n  data: {\n    type: Object,\n    default: () => ({}),\n  },\n});\nconst emit = defineEmits(['update:data']);\n\nconst { t } = useI18n();\n\nconst methods = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD'];\nconst notHaveBody = ['GET', 'HEAD'];\nconst copyHeaders = JSON.parse(JSON.stringify(props.data.headers));\n\nconst activeTab = ref('headers');\nconst showBodyModal = ref(false);\nconst headers = ref(Array.isArray(copyHeaders) ? copyHeaders : []);\n\nfunction updateData(value) {\n  emit('update:data', { ...props.data, ...value });\n}\nfunction removeHeader(index) {\n  headers.value.splice(index, 1);\n}\nfunction addHeader() {\n  headers.value.push({ name: '', value: '' });\n}\nfunction updateMethod(method) {\n  if (notHaveBody.includes(method) && activeTab.value === 'body') {\n    activeTab.value = 'headers';\n  }\n\n  updateData({ method });\n}\n\nwatch(\n  headers,\n  (value) => {\n    updateData({ headers: value });\n  },\n  { deep: true }\n);\n</script>\n<style scoped>\ncode {\n  @apply bg-gray-900 text-sm text-white p-1 rounded-md;\n}\n</style>\n"
  },
  {
    "path": "src/components/newtab/workflow/edit/EditWhileLoop.vue",
    "content": "<template>\n  <div>\n    <ui-textarea\n      :model-value=\"data.description\"\n      :placeholder=\"t('common.description')\"\n      class=\"mb-1 w-full\"\n      @change=\"updateData({ description: $event })\"\n    />\n    <ui-button\n      variant=\"accent\"\n      class=\"mt-4 w-full\"\n      @click=\"showConditionBuilder = true\"\n    >\n      {{ t('workflow.blocks.while-loop.editCondition') }}\n    </ui-button>\n    <ui-modal v-model=\"showConditionBuilder\" custom-content>\n      <ui-card padding=\"p-0\" class=\"w-full max-w-3xl\">\n        <div class=\"flex items-center px-4 pt-4\">\n          <p class=\"flex-1\">\n            {{ t('workflow.conditionBuilder.title') }}\n          </p>\n          <v-remixicon\n            name=\"riCloseLine\"\n            class=\"cursor-pointer\"\n            @click=\"showConditionBuilder = false\"\n          />\n        </div>\n        <shared-condition-builder\n          :model-value=\"data.conditions\"\n          class=\"scroll mt-4 overflow-auto p-4\"\n          style=\"height: calc(100vh - 8rem)\"\n          @change=\"updateData({ conditions: $event })\"\n        />\n      </ui-card>\n    </ui-modal>\n  </div>\n</template>\n<script setup>\nimport { onMounted, ref } from 'vue';\nimport { useI18n } from 'vue-i18n';\nimport { nanoid } from 'nanoid';\nimport SharedConditionBuilder from '@/components/newtab/shared/SharedConditionBuilder/index.vue';\n\nconst props = defineProps({\n  data: {\n    type: Object,\n    default: () => ({}),\n  },\n});\nconst emit = defineEmits(['update:data']);\n\nconst { t } = useI18n();\nconst defaultConditions = () => [\n  {\n    id: nanoid(),\n    conditions: [\n      {\n        id: nanoid(),\n        items: [\n          {\n            id: nanoid(),\n            type: 'value',\n            category: 'value',\n            data: { value: '' },\n          },\n          { id: nanoid(), category: 'compare', type: 'eq' },\n          {\n            id: nanoid(),\n            type: 'value',\n            category: 'value',\n            data: { value: '' },\n          },\n        ],\n      },\n      {\n        id: nanoid(),\n        items: [\n          {\n            id: nanoid(),\n            type: 'value',\n            category: 'value',\n            data: { value: '' },\n          },\n          { id: nanoid(), category: 'compare', type: 'eq' },\n          {\n            id: nanoid(),\n            type: 'value',\n            category: 'value',\n            data: { value: '' },\n          },\n        ],\n      },\n    ],\n  },\n];\n\nconst showConditionBuilder = ref(false);\n\nfunction updateData(value) {\n  emit('update:data', { ...props.data, ...value });\n}\n\nonMounted(() => {\n  if (props.data.conditions === null) {\n    updateData({ conditions: defaultConditions() });\n  }\n});\n</script>\n"
  },
  {
    "path": "src/components/newtab/workflow/edit/EditWorkflowParameters.vue",
    "content": "<template>\n  <div\n    class=\"scroll overflow-auto\"\n    style=\"max-height: calc(100vh - 15rem); min-height: 200px\"\n  >\n    <p\n      v-if=\"state.parameters.length === 0\"\n      class=\"my-4 text-center text-gray-600 dark:text-gray-200\"\n    >\n      No parameters\n    </p>\n    <section v-else class=\"w-full\">\n      <div class=\"grid grid-cols-12 space-x-2 text-sm\">\n        <div class=\"col-span-3\" style=\"padding-left: 28px\">Name</div>\n        <div class=\"col-span-2\">Type</div>\n        <div class=\"col-span-3\">Placeholder</div>\n        <div class=\"col-span-4\">Default Value</div>\n      </div>\n      <draggable\n        v-model=\"state.parameters\"\n        tag=\"div\"\n        item-key=\"id\"\n        handle=\".handle\"\n      >\n        <template #item=\"{ element: param, index }\">\n          <div class=\"mb-4\">\n            <div class=\"grid grid-cols-12 space-x-2\">\n              <div class=\"col-span-3 flex\">\n                <v-remixicon name=\"mdiDrag\" class=\"handle mr-2 cursor-move\" />\n                <ui-input\n                  :model-value=\"param.name\"\n                  placeholder=\"Parameter name\"\n                  @change=\"updateParam(index, $event)\"\n                />\n              </div>\n              <div class=\"col-span-2\">\n                <ui-select\n                  :model-value=\"param.type\"\n                  @change=\"updateParamType(index, $event)\"\n                >\n                  <option\n                    v-for=\"type in paramTypesArr\"\n                    :key=\"type.id\"\n                    :value=\"type.id\"\n                  >\n                    {{ type.name }}\n                  </option>\n                </ui-select>\n              </div>\n              <div class=\"col-span-3\">\n                <ui-input\n                  v-model=\"param.placeholder\"\n                  placeholder=\"A parameter\"\n                />\n              </div>\n              <div class=\"col-span-4 flex items-center\">\n                <component\n                  :is=\"paramTypes[param.type]?.valueComp\"\n                  v-if=\"paramTypes[param.type]?.valueComp\"\n                  v-model=\"param.defaultValue\"\n                  :param-data=\"param\"\n                  :editor=\"true\"\n                  class=\"flex-1\"\n                  style=\"max-width: 232px\"\n                />\n                <ui-input\n                  v-else\n                  v-model=\"param.defaultValue\"\n                  :type=\"param.type === 'number' ? 'number' : 'text'\"\n                  placeholder=\"NULL\"\n                />\n                <ui-button\n                  icon\n                  class=\"ml-2\"\n                  @click=\"state.parameters.splice(index, 1)\"\n                >\n                  <v-remixicon name=\"riDeleteBin7Line\" />\n                </ui-button>\n              </div>\n            </div>\n            <div class=\"w-full\">\n              <ui-expand\n                hide-header-icon\n                header-class=\"flex items-center focus:ring-0 w-full\"\n              >\n                <template #header=\"{ show }\">\n                  <v-remixicon\n                    :rotate=\"show ? 270 : 180\"\n                    name=\"riArrowLeftSLine\"\n                    class=\"mr-2 -ml-1 transition-transform\"\n                  />\n                  <span>Options</span>\n                </template>\n                <div class=\"mt-2 mb-4 pl-[28px]\">\n                  <div class=\"mb-2 flex items-start\">\n                    <ui-textarea\n                      v-model=\"param.description\"\n                      placeholder=\"Description\"\n                      title=\"Description\"\n                      style=\"max-width: 400px\"\n                    />\n                    <ui-checkbox\n                      v-if=\"['string', 'number'].includes(param.type)\"\n                      :model-value=\"param.data?.required\"\n                      class=\"ml-6\"\n                      @change=\"param.data.required = $event\"\n                    >\n                      Parameter required\n                    </ui-checkbox>\n                  </div>\n                  <component\n                    :is=\"paramTypes[param.type].options\"\n                    v-if=\"paramTypes[param.type].options\"\n                    v-model=\"param.data\"\n                    :default-value=\"paramTypes[param.type].data\"\n                  />\n                </div>\n              </ui-expand>\n            </div>\n          </div>\n        </template>\n      </draggable>\n    </section>\n  </div>\n  <div class=\"mt-4 flex items-center\">\n    <ui-button variant=\"accent\" @click=\"addParameter\">\n      {{ $t('workflow.parameters.add') }}\n    </ui-button>\n    <div class=\"grow\" />\n    <ui-checkbox\n      v-if=\"!hidePreferTab\"\n      :model-value=\"preferTab\"\n      @change=\"$emit('update:preferTab', $event)\"\n    >\n      {{ $t('workflow.parameters.preferInTab') }}\n    </ui-checkbox>\n  </div>\n</template>\n<script setup>\nimport workflowParameters from '@business/parameters';\nimport cloneDeep from 'lodash.clonedeep';\nimport { nanoid } from 'nanoid/non-secure';\nimport { reactive, watch } from 'vue';\nimport Draggable from 'vuedraggable';\nimport ParameterCheckboxValue from './Parameter/ParameterCheckboxValue.vue';\nimport ParameterInputOptions from './Parameter/ParameterInputOptions.vue';\nimport ParameterInputValue from './Parameter/ParameterInputValue.vue';\nimport ParameterJsonValue from './Parameter/ParameterJsonValue.vue';\n\nconst props = defineProps({\n  data: {\n    type: Array,\n    default: () => [],\n  },\n  preferTab: Boolean,\n  hidePreferTab: Boolean,\n});\nconst emit = defineEmits(['update', 'update:preferTab']);\n\nconst customParameters = workflowParameters();\n\nconst paramTypes = {\n  string: {\n    id: 'string',\n    name: 'Input (string)',\n    options: ParameterInputOptions,\n    valueComp: ParameterInputValue,\n    data: {\n      masks: [],\n      required: false,\n      useMask: false,\n      unmaskValue: false,\n    },\n  },\n  number: {\n    id: 'number',\n    name: 'Input (number)',\n    data: {\n      required: false,\n    },\n  },\n  json: {\n    id: 'json',\n    name: 'Input (JSON)',\n    valueComp: ParameterJsonValue,\n    data: {\n      required: false,\n    },\n  },\n  checkbox: {\n    id: 'checkbox',\n    name: 'Checkbox',\n    valueComp: ParameterCheckboxValue,\n    data: {\n      required: false,\n    },\n  },\n  ...customParameters,\n};\nconst paramTypesArr = Object.values(paramTypes)\n  .filter((item) => item.id)\n  .sort((a, b) => (a.name > b.name ? 1 : -1));\n\nconst state = reactive({\n  parameters: cloneDeep(props.data || []).map((item) => {\n    item.id = nanoid(4);\n\n    return item;\n  }),\n});\n\nfunction addParameter() {\n  state.parameters.push({\n    name: 'param',\n    type: 'string',\n    description: '',\n    defaultValue: '',\n    placeholder: 'Text',\n    data: paramTypes.string.data,\n  });\n}\nfunction updateParam(index, value) {\n  state.parameters[index].name = value.replace(/\\s/g, '_');\n}\nfunction updateParamType(index, type) {\n  const param = state.parameters[index];\n\n  param.type = type;\n  param.data = paramTypes[type].data || {};\n}\n\nwatch(\n  () => state.parameters,\n  (parameters) => {\n    emit('update', parameters);\n  },\n  { deep: true }\n);\n</script>\n<style scoped>\ntable th,\ntable td {\n  @apply p-1 font-normal;\n}\n</style>\n"
  },
  {
    "path": "src/components/newtab/workflow/edit/EditWorkflowState.vue",
    "content": "<template>\n  <div>\n    <ui-textarea\n      :model-value=\"data.description\"\n      class=\"w-full\"\n      :placeholder=\"t('common.description')\"\n      @change=\"updateData({ description: $event })\"\n    />\n    <ui-select\n      :model-value=\"data.type\"\n      label=\"Action\"\n      class=\"mt-4 w-full\"\n      @change=\"updateData({ type: $event })\"\n    >\n      <optgroup v-for=\"action in actions\" :key=\"action.id\" :label=\"action.name\">\n        <option\n          v-for=\"item in actionsItems[action.id]\"\n          :key=\"item.id\"\n          :value=\"item.id\"\n        >\n          {{ item.name }}\n        </option>\n      </optgroup>\n    </ui-select>\n    <ui-checkbox\n      v-if=\"includeExceptions.includes(data.type)\"\n      :model-value=\"data.exceptCurrent\"\n      class=\"mt-2\"\n      @change=\"updateData({ exceptCurrent: $event })\"\n    >\n      Execpt for the current workflow\n    </ui-checkbox>\n    <!-- 停止当前工作流 是否抛出错误及自定义错误信息 -->\n    <template v-if=\"data.type === 'stop-current'\">\n      <ui-checkbox\n        :model-value=\"data.throwError\"\n        block\n        class=\"block-variable mt-4\"\n        @change=\"updateData({ throwError: $event })\"\n      >\n        {{ t(`workflow.blocks.workflow-state.error.throwError`) }}\n      </ui-checkbox>\n      <ui-input\n        v-if=\"data.throwError\"\n        :model-value=\"data.errorMessage\"\n        :placeholder=\"t(`workflow.blocks.workflow-state.error.message`)\"\n        :title=\"t(`workflow.blocks.workflow-state.error.message`)\"\n        class=\"mt-2 w-full\"\n        @change=\"updateData({ errorMessage: $event })\"\n      />\n    </template>\n    <div\n      v-if=\"data.type === 'stop-specific'\"\n      class=\"bg-input focus-within:bg-box-transparent-2 mt-4 rounded-lg transition\"\n    >\n      <div\n        v-if=\"data.workflowsToStop.length > 0\"\n        class=\"scroll overflow-auto px-4 py-2\"\n        style=\"max-height: 114px\"\n      >\n        <div\n          v-for=\"item in data.workflowsToStop\"\n          :key=\"item\"\n          class=\"bg-box-transparent mb-1 mr-1 inline-flex items-center rounded-md p-1 text-sm\"\n        >\n          <span class=\"flex-1\">\n            {{ selectedWorkflows[item] }}\n          </span>\n          <v-remixicon\n            name=\"riCloseLine\"\n            class=\"cursor-pointer text-gray-600 dark:text-gray-300\"\n            size=\"20\"\n            @click=\"removeSelectedItem(item)\"\n          />\n        </div>\n      </div>\n      <ui-autocomplete\n        :model-value=\"query\"\n        :items=\"workflows\"\n        item-key=\"id\"\n        item-label=\"name\"\n        block\n        @selected=\"onItemSelected\"\n      >\n        <input\n          v-model=\"query\"\n          type=\"text\"\n          placeholder=\"Select a workflow\"\n          class=\"w-full rounded-lg bg-transparent py-2 px-4\"\n        />\n      </ui-autocomplete>\n    </div>\n  </div>\n</template>\n<script setup>\nimport { computed, inject, ref } from 'vue';\nimport { useI18n } from 'vue-i18n';\nimport { useWorkflowStore } from '@/stores/workflow';\nimport { useTeamWorkflowStore } from '@/stores/teamWorkflow';\n\nconst props = defineProps({\n  data: {\n    type: Object,\n    default: () => ({}),\n  },\n});\nconst emit = defineEmits(['update:data']);\n\nconst { t } = useI18n();\nconst workflowStore = useWorkflowStore();\nconst teamWorkflowStore = useTeamWorkflowStore();\n\nconst includeExceptions = ['stop-all'];\nconst actions = [\n  { id: 'stop', name: t('workflow.blocks.workflow-state.actions.stop') },\n];\nconst actionsItems = {\n  stop: [\n    { id: 'stop-all', name: 'Stop all workflows' },\n    { id: 'stop-current', name: 'Stop current workflow' },\n    { id: 'stop-specific', name: 'Stop specific workflows' },\n  ],\n};\n\nconst query = ref('');\nconst selectedWorkflows = ref({});\nconst currentWorkflow = inject('workflow', {});\n\nconst workflows = computed(() => {\n  let workflowsList = [];\n  const workflow = currentWorkflow.data.value;\n\n  if (workflow.id.startsWith('team')) {\n    workflowsList = teamWorkflowStore.getByTeam(workflow.teamId) || [];\n  } else {\n    workflowsList = workflowStore.getWorkflows;\n  }\n\n  return workflowsList.filter((item) => {\n    const selected = props.data.workflowsToStop.includes(item.id);\n    if (selected) selectedWorkflows.value[item.id] = item.name;\n\n    return !selected;\n  });\n});\n\nfunction updateData(value) {\n  emit('update:data', { ...props.data, ...value });\n}\nfunction onItemSelected({ item }) {\n  const copy = [...props.data.workflowsToStop];\n  copy.push(item.id);\n\n  selectedWorkflows.value[item.id] = item.name;\n\n  updateData({ workflowsToStop: copy });\n\n  query.value = '';\n}\nfunction removeSelectedItem(itemId) {\n  const copy = [...props.data.workflowsToStop];\n  const index = props.data.workflowsToStop.indexOf(itemId);\n  copy.splice(index, 1);\n\n  updateData({ workflowsToStop: copy });\n\n  delete selectedWorkflows.value[itemId];\n}\n</script>\n"
  },
  {
    "path": "src/components/newtab/workflow/edit/InsertWorkflowData.vue",
    "content": "<template>\n  <template v-if=\"variables\">\n    <ui-checkbox\n      :model-value=\"data.assignVariable\"\n      block\n      class=\"block-variable mt-4\"\n      @change=\"updateData({ assignVariable: $event })\"\n    >\n      {{ t('workflow.variables.assign') }}\n    </ui-checkbox>\n    <ui-input\n      v-if=\"data.assignVariable\"\n      :model-value=\"data.variableName\"\n      :placeholder=\"t('workflow.variables.name')\"\n      :title=\"t('workflow.variables.name')\"\n      class=\"mt-2 w-full\"\n      @change=\"updateData({ variableName: $event })\"\n    />\n  </template>\n  <template v-if=\"table && !workflow?.isPackage && workflow.columns?.value\">\n    <ui-checkbox\n      :model-value=\"data.saveData\"\n      block\n      class=\"mt-4\"\n      @change=\"updateData({ saveData: $event })\"\n    >\n      {{ t('workflow.blocks.base.table.checkbox') }}\n    </ui-checkbox>\n    <ui-select\n      v-if=\"data.saveData\"\n      :model-value=\"data.dataColumn\"\n      :placeholder=\"t('workflow.blocks.base.table.select')\"\n      class=\"mt-2 w-full\"\n      @change=\"updateData({ dataColumn: $event })\"\n    >\n      <option\n        v-for=\"column in [...columns, ...workflow.columns.value]\"\n        :key=\"column.id\"\n        :value=\"column.id\"\n      >\n        {{ column.name }}\n      </option>\n    </ui-select>\n  </template>\n  <template v-if=\"extraRow\">\n    <ui-checkbox\n      :model-value=\"data.addExtraRow\"\n      class=\"mt-4\"\n      block\n      @change=\"updateData({ addExtraRow: $event })\"\n    >\n      {{ t('workflow.blocks.base.table.extraRow.checkbox') }}\n    </ui-checkbox>\n    <template v-if=\"data.addExtraRow\">\n      <ui-input\n        :model-value=\"data.extraRowValue\"\n        :title=\"t('workflow.blocks.base.table.extraRow.title')\"\n        :placeholder=\"t('workflow.blocks.base.table.extraRow.placeholder')\"\n        class=\"my-2 w-full\"\n        @change=\"updateData({ extraRowValue: $event })\"\n      />\n      <ui-select\n        :model-value=\"data.extraRowDataColumn\"\n        placeholder=\"Select column\"\n        class=\"mt-1 w-full\"\n        @change=\"updateData({ extraRowDataColumn: $event })\"\n      >\n        <option\n          v-for=\"column in [...columns, ...workflow.columns.value]\"\n          :key=\"column.id\"\n          :value=\"column.id\"\n        >\n          {{ column.name }}\n        </option>\n      </ui-select>\n    </template>\n  </template>\n</template>\n<script setup>\nimport { inject } from 'vue';\nimport { useI18n } from 'vue-i18n';\n\ndefineProps({\n  data: {\n    type: Object,\n    default: () => ({}),\n  },\n  table: {\n    type: Boolean,\n    default: true,\n  },\n  extraRow: Boolean,\n  variables: Boolean,\n  columns: {\n    type: Array,\n    default: () => [],\n  },\n});\nconst emit = defineEmits(['update']);\n\nconst { t } = useI18n();\nconst workflow = inject('workflow', {});\n\nfunction updateData(data) {\n  emit('update', data);\n}\n</script>\n"
  },
  {
    "path": "src/components/newtab/workflow/edit/Parameter/ParameterCheckboxValue.vue",
    "content": "<template>\n  <ui-checkbox\n    :model-value=\"Boolean(modelValue)\"\n    type=\"text\"\n    class=\"w-full\"\n    :placeholder=\"paramData.placeholder\"\n    @change=\"$emit('update:modelValue', $event)\"\n  >\n    {{ paramData.placeholder || paramData.name }}\n  </ui-checkbox>\n</template>\n<script setup>\ndefineProps({\n  modelValue: {\n    type: Boolean,\n    default: false,\n  },\n  paramData: {\n    type: Object,\n    default: () => ({}),\n  },\n  editor: Boolean,\n});\ndefineEmits(['update:modelValue']);\n</script>\n"
  },
  {
    "path": "src/components/newtab/workflow/edit/Parameter/ParameterInputOptions.vue",
    "content": "<template>\n  <div class=\"flex items-center\">\n    <label class=\"flex items-center\">\n      <ui-switch v-model=\"options.useMask\" />\n      <span class=\"ml-2\"> Use input masking </span>\n    </label>\n    <v-remixicon\n      v-tooltip=\"{ content: maskInfo, allowHTML: true }\"\n      name=\"riInformationLine\"\n      class=\"ml-1 text-gray-600 dark:text-gray-200\"\n      size=\"20\"\n    />\n    <label v-if=\"false\" class=\"ml-4 flex items-center\">\n      <ui-switch v-model=\"options.unmaskValue\" />\n      <span class=\"ml-2\">Return unmask value</span>\n    </label>\n  </div>\n  <div v-if=\"options.useMask\" class=\"mt-2\">\n    <p>Masks</p>\n    <div class=\"space-y-2\">\n      <div\n        v-for=\"(mask, index) in options.masks\"\n        :key=\"index\"\n        class=\"flex items-center\"\n      >\n        <ui-input\n          v-model=\"options.masks[index].mask\"\n          placeholder=\"aaa-aaa-aaa\"\n        />\n        <ui-checkbox v-model=\"mask.isRegex\" class=\"ml-4\">\n          Is RegEx\n        </ui-checkbox>\n        <div class=\"grow\" />\n        <v-remixicon\n          name=\"riDeleteBin7Line\"\n          class=\"ml-1 shrink-0 cursor-pointer\"\n          @click=\"options.masks.splice(index, 1)\"\n        />\n      </div>\n    </div>\n    <template v-if=\"false\">\n      <p>Custom tokens</p>\n      <div class=\"grid grid-cols-2 gap-4\">\n        <div\n          v-for=\"(token, index) in options.customTokens\"\n          :key=\"index\"\n          class=\"flex items-center\"\n        >\n          <ui-input\n            v-model=\"token.symbol\"\n            placeholder=\"Symbol\"\n            style=\"width: 120px\"\n          />\n          <ui-input\n            v-model=\"token.regex\"\n            placeholder=\"RegEx\"\n            class=\"ml-2 flex-1\"\n          />\n          <v-remixicon\n            name=\"riDeleteBin7Line\"\n            class=\"ml-1 shrink-0 cursor-pointer\"\n            @click=\"options.customTokens.splice(index, 1)\"\n          />\n        </div>\n      </div>\n      <ui-button class=\"mt-4\" @click=\"addToken\"> Add token </ui-button>\n    </template>\n  </div>\n</template>\n<script setup>\nimport { reactive, watch, onMounted } from 'vue';\nimport cloneDeep from 'lodash.clonedeep';\n\nconst props = defineProps({\n  modelValue: {\n    type: [Object, String],\n    default: () => ({}),\n  },\n  defaultValue: {\n    type: Object,\n    default: () => ({}),\n  },\n});\nconst emit = defineEmits(['update:modelValue']);\n\nconst maskInfo = `\nAdd mask to the input field\n<p class=\"mt-2\">Supported patterns</p>\n<table class=\"tokens\">\n\t<tbody>\n\t\t<tr>\n\t\t\t<td>0</td>\n\t\t\t<td>Any digit</td>\n\t\t</tr>\n\t\t<tr>\n\t\t\t<td>a</td>\n\t\t\t<td>Any letter</td>\n\t\t</tr>\n\t\t<tr>\n\t\t\t<td>*</td>\n\t\t\t<td>Any char</td>\n\t\t</tr>\n\t\t<tr>\n\t\t\t<td>[]</td>\n\t\t\t<td>Make input optional</td>\n\t\t</tr>\n\t\t<tr>\n\t\t\t<td>{}</td>\n\t\t\t<td>Include fixed part in unmasked value</td>\n\t\t</tr>\n\t\t<tr>\n\t\t\t<td>\\`</td>\n\t\t\t<td>Prevent symbols shift back</td>\n\t\t</tr>\n    <tr>\n      <td>!</td>\n      <td>Escape char</td>\n    </tr>\n\t<tbody>\n</table>\n`;\n\nconst cloneData = cloneDeep(props.modelValue || {});\nconst options = reactive({\n  ...(props.defaultValue || {}),\n  ...cloneData,\n});\n\nfunction addMask() {\n  options.masks.push({\n    isRegex: false,\n    mask: '',\n    lazy: false,\n  });\n}\nfunction addToken() {\n  options.customTokens.push({\n    symbol: '',\n    regex: '',\n  });\n}\n\nwatch(\n  options,\n  () => {\n    emit('update:modelValue', options);\n  },\n  { deep: true }\n);\n\nonMounted(() => {\n  if (options.masks.length === 0) {\n    addMask();\n  }\n});\n</script>\n"
  },
  {
    "path": "src/components/newtab/workflow/edit/Parameter/ParameterInputValue.vue",
    "content": "<template>\n  <ui-input\n    :model-value=\"modelValue\"\n    :mask=\"mask\"\n    type=\"text\"\n    class=\"w-full\"\n    :placeholder=\"paramData.placeholder\"\n    @change=\"$emit('update:modelValue', $event)\"\n    @keyup.enter=\"$emit('execute')\"\n  />\n</template>\n<script setup>\nimport { computed } from 'vue';\n\nconst props = defineProps({\n  modelValue: {\n    type: String,\n    default: '',\n  },\n  paramData: {\n    type: Object,\n    default: () => ({}),\n  },\n});\ndefineEmits(['update:modelValue', 'execute']);\n\nconst mask = computed(() => {\n  const options = props.paramData.data;\n  if (!options || !options.useMask) return null;\n\n  const masks = options.masks.map((item) => {\n    const cloneMask = { ...item };\n    if (cloneMask.isRegex) cloneMask.mask = new RegExp(cloneMask.mask);\n    else cloneMask.mask = cloneMask.mask.replaceAll('!', '\\\\');\n\n    delete cloneMask.isRegex;\n\n    return cloneMask;\n  });\n\n  if (masks.length === 1) return masks[0];\n\n  return {\n    mask: masks,\n  };\n});\n</script>\n"
  },
  {
    "path": "src/components/newtab/workflow/edit/Parameter/ParameterJsonValue.vue",
    "content": "<template>\n  <label>\n    <span v-if=\"!editor\" class=\"ml-1 text-sm text-gray-600 dark:text-gray-200\">\n      {{ paramData.name }}\n    </span>\n    <ui-textarea\n      :model-value=\"modelValue\"\n      type=\"text\"\n      class=\"w-full\"\n      :placeholder=\"paramData.placeholder\"\n      @change=\"$emit('update:modelValue', $event)\"\n    />\n  </label>\n</template>\n<script setup>\ndefineProps({\n  modelValue: {\n    type: String,\n    default: '',\n  },\n  paramData: {\n    type: Object,\n    default: () => ({}),\n  },\n  editor: Boolean,\n});\ndefineEmits(['update:modelValue']);\n</script>\n"
  },
  {
    "path": "src/components/newtab/workflow/edit/Trigger/TriggerContextMenu.vue",
    "content": "<template>\n  <div>\n    <template v-if=\"!permission.has[permissionName]\">\n      <p>\n        {{ t('workflow.blocks.trigger.contextMenus.noPermission') }}\n      </p>\n      <ui-button class=\"mt-2\" @click=\"permission.request(true)\">\n        {{ t('workflow.blocks.trigger.contextMenus.grantPermission') }}\n      </ui-button>\n    </template>\n    <template v-else-if=\"workflow.data\">\n      <ui-input\n        :label=\"t('workflow.blocks.trigger.contextMenus.contextName')\"\n        :placeholder=\"workflow.data.value.name\"\n        :model-value=\"data.contextMenuName\"\n        class=\"w-full\"\n        @change=\"$emit('update', { contextMenuName: $event })\"\n      />\n      <ui-popover\n        :options=\"{ animation: null }\"\n        trigger-width\n        class=\"mt-2 w-full\"\n        trigger-class=\"w-full\"\n      >\n        <template #trigger>\n          <span class=\"ml-1 text-sm text-gray-600 dark:text-gray-200\">\n            {{ t('workflow.blocks.trigger.contextMenus.appearIn') }}\n          </span>\n          <ui-button class=\"w-full\">\n            <p class=\"text-overflow mr-2 flex-1 text-left\">\n              {{ data.contextTypes.join(', ') || 'All' }}\n            </p>\n            <v-remixicon\n              size=\"28\"\n              name=\"riArrowDropDownLine\"\n              class=\"-mr-2 text-gray-600 dark:text-gray-200\"\n            />\n          </ui-button>\n        </template>\n        <div class=\"grid grid-cols-2 gap-2\">\n          <ui-checkbox\n            v-for=\"type in types\"\n            :key=\"type\"\n            :model-value=\"data.contextTypes?.includes(type)\"\n            @change=\"onSelectContextType($event, type)\"\n          >\n            <span class=\"capitalize\">{{ type }}</span>\n          </ui-checkbox>\n        </div>\n      </ui-popover>\n    </template>\n  </div>\n</template>\n<script setup>\nimport { onMounted, inject } from 'vue';\nimport { useI18n } from 'vue-i18n';\nimport { useHasPermissions } from '@/composable/hasPermissions';\n\nconst props = defineProps({\n  data: {\n    type: Object,\n    default: () => ({}),\n  },\n});\nconst emit = defineEmits(['update']);\n\nconst types = [\n  'audio',\n  'editable',\n  'image',\n  'link',\n  'page',\n  'password',\n  'selection',\n  'video',\n];\nconst permissionName = BROWSER_TYPE === 'firefox' ? 'menus' : 'contextMenus';\n\nconst { t } = useI18n();\nconst permission = useHasPermissions([permissionName]);\n\nconst workflow = inject('workflow', {});\n\nfunction onSelectContextType(selected, type) {\n  const contextTypes = [...props.data.contextTypes];\n\n  if (selected) {\n    contextTypes.push(type);\n  } else {\n    const index = contextTypes.indexOf(type);\n    contextTypes.splice(index, 1);\n  }\n\n  emit('update', { contextTypes });\n}\n\nonMounted(() => {\n  if (props.data.contextMenuName.trim() || !workflow?.data) return;\n\n  emit('update', { contextMenuName: workflow.data.value.name });\n});\n</script>\n"
  },
  {
    "path": "src/components/newtab/workflow/edit/Trigger/TriggerCronJob.vue",
    "content": "<template>\n  <ui-input\n    :model-value=\"data.expression\"\n    :label=\"t('workflow.blocks.trigger.forms.cron-expression')\"\n    class=\"-mt-2 w-full\"\n    placeholder=\"0 15 10 ? * *\"\n    @change=\"updateCronExpression($event, true)\"\n  />\n  <p\n    class=\"ml-1 mt-1 leading-tight\"\n    :class=\"{ 'text-red-400 dark:text-red-500': state.isError }\"\n  >\n    {{ state.nextSchedule }}\n  </p>\n</template>\n<script setup>\nimport { shallowReactive, onMounted } from 'vue';\nimport { useI18n } from 'vue-i18n';\nimport cronParser from 'cron-parser';\nimport { debounce } from '@/utils/helper';\nimport { readableCron } from '@/lib/cronstrue';\nimport dayjs from '@/lib/dayjs';\n\nconst props = defineProps({\n  data: {\n    type: Object,\n    default: () => ({}),\n  },\n});\nconst emit = defineEmits(['update']);\n\nconst { t } = useI18n();\n\nconst state = shallowReactive({\n  isError: false,\n  readableCron: '',\n  nextSchedule: '',\n});\n\nconst updateCronExpression = debounce((expression, update = false) => {\n  try {\n    const cronExpression = cronParser.parseExpression(expression);\n\n    state.isError = false;\n    state.nextSchedule = `${readableCron(expression)} - ${t(\n      'scheduledWorkflow.nextRun'\n    )}: ${dayjs(cronExpression.next()).format('DD MMM YYYY, HH:mm:ss')}`;\n\n    if (update) emit('update', { expression });\n  } catch (error) {\n    state.isError = true;\n    state.nextSchedule = error.message;\n  }\n}, 100);\n\nonMounted(() => {\n  updateCronExpression(props.data.expression);\n});\n</script>\n"
  },
  {
    "path": "src/components/newtab/workflow/edit/Trigger/TriggerDate.vue",
    "content": "<template>\n  <div>\n    <ui-input\n      :model-value=\"data.date\"\n      :max=\"maxDate\"\n      :min=\"minDate\"\n      :placeholder=\"t('workflow.blocks.trigger.forms.date')\"\n      class=\"w-full\"\n      type=\"date\"\n      @change=\"updateDate({ date: $event })\"\n    />\n    <ui-input\n      :model-value=\"data.time\"\n      :placeholder=\"t('workflow.blocks.trigger.forms.time')\"\n      type=\"time\"\n      step=\"1\"\n      class=\"mt-2 w-full\"\n      @change=\"$emit('update', { time: $event || '00:00' })\"\n    />\n  </div>\n</template>\n<script setup>\nimport { useI18n } from 'vue-i18n';\nimport dayjs from '@/lib/dayjs';\n\ndefineProps({\n  data: {\n    type: Object,\n    default: () => ({}),\n  },\n});\nconst emit = defineEmits(['update']);\n\nconst { t } = useI18n();\nconst maxDate = dayjs().add(30, 'day').format('YYYY-MM-DD');\nconst minDate = dayjs().format('YYYY-MM-DD');\n\nfunction updateDate(value) {\n  if (!value) return;\n\n  let date = value?.date ?? minDate;\n\n  if (dayjs(minDate).isAfter(date)) date = minDate;\n  else if (dayjs(maxDate).isBefore(date)) date = maxDate;\n\n  emit('update', { date });\n}\n</script>\n"
  },
  {
    "path": "src/components/newtab/workflow/edit/Trigger/TriggerElementChange.vue",
    "content": "<template>\n  <div>\n    <ui-input\n      v-model=\"observeDetail.matchPattern\"\n      :label=\"t('workflow.blocks.trigger.element-change.target')\"\n      class=\"w-full\"\n      placeholder=\"https://web.telegram.org/*\"\n    >\n      <template #label>\n        {{ t('workflow.blocks.switch-tab.matchPattern') }}\n        <a\n          :title=\"t('workflow.blocks.trigger.element-change.targetWebsite')\"\n          href=\"https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Match_patterns#examples\"\n          target=\"_blank\"\n          rel=\"noopener\"\n          class=\"inline-block\"\n        >\n          <v-remixicon name=\"riInformationLine\" class=\"info-icon\" size=\"18\" />\n        </a>\n      </template>\n    </ui-input>\n    <ui-input\n      v-model=\"observeDetail.baseSelector\"\n      class=\"mt-4 w-full\"\n      placeholder=\"CSS Selector or XPath\"\n    >\n      <template #label>\n        <span>\n          {{ t('workflow.blocks.trigger.element-change.baseEl.title') }}\n        </span>\n        <v-remixicon\n          :title=\"\n            t('workflow.blocks.trigger.element-change.baseEl.description')\n          \"\n          name=\"riInformationLine\"\n          class=\"info-icon\"\n          size=\"18\"\n        />\n      </template>\n    </ui-input>\n    <ui-expand header-class=\"w-full group flex items-center focus:ring-0\">\n      <template #header>\n        {{ t('common.options') }}\n      </template>\n      <trigger-element-options v-model=\"observeDetail.baseElOptions\" />\n    </ui-expand>\n    <ui-input\n      v-model=\"observeDetail.selector\"\n      :label=\"t('workflow.blocks.trigger.element-change.target')\"\n      class=\"mt-4 w-full\"\n      placeholder=\"CSS Selector or XPath\"\n    />\n    <ui-expand header-class=\"w-full flex items-center focus:ring-0 group\">\n      <template #header>\n        {{ t('common.options') }}\n        <v-remixicon\n          :title=\"t('workflow.blocks.trigger.element-change.optionsInfo')\"\n          class=\"info-icon invisible group-hover:visible\"\n          size=\"18\"\n          name=\"riInformationLine\"\n        />\n      </template>\n      <trigger-element-options\n        v-model=\"observeDetail.targetOptions\"\n        show-desc\n      />\n    </ui-expand>\n  </div>\n</template>\n<script setup>\nimport { onMounted, reactive, watch } from 'vue';\nimport { useI18n } from 'vue-i18n';\nimport cloneDeep from 'lodash.clonedeep';\nimport TriggerElementOptions from './TriggerElementOptions.vue';\n\nconst props = defineProps({\n  data: {\n    type: Object,\n    default: () => ({}),\n  },\n});\nconst emit = defineEmits(['update']);\n\nconst { t } = useI18n();\n\nconst state = reactive({\n  retrieved: false,\n});\nconst observeDetail = reactive({\n  selector: '',\n  baseSelector: '',\n  matchPattern: '',\n  targetOptions: {\n    subtree: false,\n    childList: true,\n    attributes: false,\n    attributeFilter: [],\n    characterData: false,\n  },\n  baseElOptions: {\n    subtree: false,\n    childList: true,\n    attributes: false,\n    attributeFilter: [],\n    characterData: false,\n  },\n});\n\nwatch(\n  observeDetail,\n  (value) => {\n    if (!state.retrieved) return;\n\n    emit('update', { observeElement: value });\n  },\n  { deep: true }\n);\n\nonMounted(() => {\n  Object.assign(observeDetail, cloneDeep(props.data.observeElement));\n  setTimeout(() => {\n    state.retrieved = true;\n  }, 100);\n});\n</script>\n<style>\n.info-icon {\n  @apply text-gray-600 dark:text-gray-300 inline-block;\n}\n</style>\n"
  },
  {
    "path": "src/components/newtab/workflow/edit/Trigger/TriggerElementOptions.vue",
    "content": "<template>\n  <ul class=\"space-y-2\">\n    <li v-for=\"option in types\" :key=\"option\" class=\"group\">\n      <ui-checkbox\n        :model-value=\"modelValue[option]\"\n        @change=\"\n          $emit('update:modelValue', { ...modelValue, [option]: $event })\n        \"\n      >\n        {{ t(`workflow.blocks.trigger.element-change.${option}.title`) }}\n        <v-remixicon\n          :title=\"\n            t(`workflow.blocks.trigger.element-change.${option}.description`)\n          \"\n          class=\"info-icon invisible group-hover:visible\"\n          size=\"18\"\n          name=\"riInformationLine\"\n        />\n      </ui-checkbox>\n      <template v-if=\"option === 'attributes' && modelValue.attributes\">\n        <ui-input\n          :model-value=\"modelValue.attributeFilter.join(',')\"\n          :label=\"\n            t('workflow.blocks.trigger.element-change.subtree.description')\n          \"\n          class=\"block w-full\"\n          placeholder=\"id,label,class\"\n          @change=\"onAttrFilterChange\"\n        >\n          <template #label>\n            {{\n              t('workflow.blocks.trigger.element-change.attributeFilter.title')\n            }}\n            <v-remixicon\n              :title=\"\n                t(\n                  'workflow.blocks.trigger.element-change.attributeFilter.description'\n                )\n              \"\n              class=\"info-icon\"\n              size=\"18\"\n              name=\"riInformationLine\"\n            />\n          </template>\n        </ui-input>\n        <span class=\"text-sm text-gray-600 dark:text-gray-200\">\n          {{\n            t('workflow.blocks.trigger.element-change.attributeFilter.separate')\n          }}\n        </span>\n      </template>\n    </li>\n  </ul>\n</template>\n<script setup>\nimport { useI18n } from 'vue-i18n';\nimport { debounce } from '@/utils/helper';\n\nconst props = defineProps({\n  modelValue: {\n    type: Object,\n    default: () => ({}),\n  },\n});\nconst emit = defineEmits(['update:modelValue']);\n\nconst types = ['subtree', 'childList', 'attributes', 'characterData'];\nconst { t } = useI18n();\n\nconst onAttrFilterChange = debounce((value) => {\n  emit('update:modelValue', {\n    ...props.modelValue,\n    attributeFilter: value.split(','),\n  });\n}, 200);\n</script>\n"
  },
  {
    "path": "src/components/newtab/workflow/edit/Trigger/TriggerInterval.vue",
    "content": "<template>\n  <div class=\"flex items-center\">\n    <ui-input\n      :model-value=\"data.interval\"\n      :label=\"t('workflow.blocks.trigger.forms.interval')\"\n      type=\"number\"\n      class=\"w-full\"\n      placeholder=\"1-360\"\n      min=\"1\"\n      max=\"360\"\n      @change=\"\n        updateIntervalInput($event, { key: 'interval', min: 1, max: 360 })\n      \"\n    />\n    <ui-input\n      v-if=\"!data.fixedDelay\"\n      :model-value=\"data.delay\"\n      type=\"number\"\n      class=\"ml-2 w-full\"\n      :label=\"t('workflow.blocks.trigger.forms.delay')\"\n      min=\"0\"\n      max=\"20\"\n      placeholder=\"0-20\"\n      @change=\"updateIntervalInput($event, { key: 'delay', min: 0, max: 20 })\"\n    />\n  </div>\n  <ui-checkbox\n    :model-value=\"data.fixedDelay\"\n    block\n    class=\"mt-2\"\n    @change=\"emit('update', { fixedDelay: $event })\"\n  >\n    {{ t('workflow.blocks.trigger.fixedDelay') }}\n  </ui-checkbox>\n</template>\n<script setup>\nimport { useI18n } from 'vue-i18n';\n\ndefineProps({\n  data: {\n    type: Object,\n    default: () => ({}),\n  },\n});\nconst emit = defineEmits(['update']);\n\nconst { t } = useI18n();\n\nfunction updateIntervalInput(value, { key, min, max }) {\n  let num = +value;\n\n  if (num < min) num = min;\n  else if (num > max) num = max;\n\n  emit('update', { [key]: num });\n}\n</script>\n"
  },
  {
    "path": "src/components/newtab/workflow/edit/Trigger/TriggerKeyboardShortcut.vue",
    "content": "<template>\n  <div>\n    <div class=\"mb-2 flex items-center\">\n      <ui-input\n        :model-value=\"getReadableShortcut(recordKeys.keys)\"\n        readonly\n        class=\"mr-2 flex-1\"\n        :placeholder=\"t('workflow.blocks.trigger.forms.shortcut')\"\n      />\n      <ui-button\n        v-tooltip=\"\n          t(\n            `workflow.blocks.trigger.shortcut.${\n              recordKeys.isRecording ? 'stopRecord' : 'tooltip'\n            }`\n          )\n        \"\n        icon\n        @click=\"toggleRecordKeys\"\n      >\n        <v-remixicon\n          :name=\"recordKeys.isRecording ? 'riStopLine' : 'riRecordCircleLine'\"\n        />\n      </ui-button>\n    </div>\n    <ui-checkbox\n      :model-value=\"data.activeInInput\"\n      class=\"mb-1\"\n      :title=\"t('workflow.blocks.trigger.shortcut.checkboxTitle')\"\n      @change=\"$emit('update', { activeInInput: $event })\"\n    >\n      {{ t('workflow.blocks.trigger.shortcut.checkbox') }}\n    </ui-checkbox>\n    <p class=\"mt-4 leading-tight text-gray-600 dark:text-gray-200\">\n      {{ t('workflow.blocks.trigger.shortcut.note') }}\n    </p>\n  </div>\n</template>\n<script setup>\nimport { reactive, onBeforeUnmount, onDeactivated } from 'vue';\nimport { useI18n } from 'vue-i18n';\nimport { recordShortcut } from '@/utils/recordKeys';\nimport { getReadableShortcut } from '@/composable/shortcut';\n\nconst props = defineProps({\n  data: {\n    type: Object,\n    default: () => ({}),\n  },\n});\nconst emit = defineEmits(['update']);\n\nconst { t } = useI18n();\n\nconst recordKeys = reactive({\n  isRecording: false,\n  keys: `${props.data.shortcut}`,\n});\n\nfunction onKeydown(event) {\n  event.preventDefault();\n  event.stopPropagation();\n\n  recordShortcut(event, (keys) => {\n    recordKeys.keys = keys.join('+');\n    emit('update', { shortcut: recordKeys.keys });\n  });\n}\nfunction attachKeyEvents() {\n  window.addEventListener('keydown', onKeydown);\n  /* eslint-disable-next-line */\n  window.addEventListener('keyup', detachKeyEvents);\n}\nfunction detachKeyEvents() {\n  recordKeys.isRecording = false;\n\n  window.removeEventListener('keydown', onKeydown);\n  window.removeEventListener('keyup', detachKeyEvents);\n}\nfunction toggleRecordKeys() {\n  recordKeys.isRecording = !recordKeys.isRecording;\n\n  if (recordKeys.isRecording) {\n    attachKeyEvents();\n  } else {\n    detachKeyEvents();\n  }\n}\n\nonDeactivated(detachKeyEvents);\nonBeforeUnmount(detachKeyEvents);\n</script>\n"
  },
  {
    "path": "src/components/newtab/workflow/edit/Trigger/TriggerSpecificDay.vue",
    "content": "<template>\n  <div>\n    <ui-popover\n      :options=\"{ animation: null }\"\n      trigger-width\n      class=\"mb-2 w-full\"\n      trigger-class=\"w-full\"\n    >\n      <template #trigger>\n        <ui-button class=\"w-full\">\n          <p class=\"text-overflow mr-2 flex-1 text-left\">\n            {{\n              tempDate.days.length === 0\n                ? t('workflow.blocks.trigger.selectDay')\n                : getDaysText(tempDate.days)\n            }}\n          </p>\n          <v-remixicon\n            size=\"28\"\n            name=\"riArrowDropDownLine\"\n            class=\"-mr-2 text-gray-600 dark:text-gray-200\"\n          />\n        </ui-button>\n      </template>\n      <div class=\"grid grid-cols-2 gap-2\">\n        <ui-checkbox\n          v-for=\"(day, id) in days\"\n          :key=\"id\"\n          :model-value=\"data.days?.includes(id)\"\n          @change=\"onSelectDayChange($event, id)\"\n        >\n          {{ t(`workflow.blocks.trigger.days.${id}`) }}\n        </ui-checkbox>\n      </div>\n    </ui-popover>\n    <div class=\"flex items-center\">\n      <ui-input\n        v-model=\"tempDate.time\"\n        type=\"time\"\n        class=\"mr-2 flex-1\"\n        step=\"1\"\n      />\n      <ui-button variant=\"accent\" @click=\"addTime\">\n        {{ t('workflow.blocks.trigger.addTime') }}\n      </ui-button>\n    </div>\n    <div class=\"mt-4 grid grid-cols-2 gap-x-4 gap-y-2\">\n      <ui-expand\n        v-for=\"day in sortedDaysArr\"\n        :key=\"day.id\"\n        header-class=\"focus:ring-0 flex items-center w-full group text-left\"\n        type=\"time\"\n        class=\"w-full\"\n      >\n        <template #header>\n          <p class=\"flex-1\">\n            {{ t(`workflow.blocks.trigger.days.${day.id}`) }}\n          </p>\n          <span class=\"text-gray-600 dark:text-gray-200\">\n            <v-remixicon\n              name=\"riDeleteBin7Line\"\n              class=\"group invisible mr-1 inline-block group-hover:visible\"\n              @click=\"removeDay(day.id)\"\n            />\n            {{ day.times.length }}x\n          </span>\n        </template>\n        <div class=\"mb-1 grid grid-cols-2 gap-1\">\n          <div\n            v-for=\"(time, timeIndex) in day.times\"\n            :key=\"day.id + time\"\n            class=\"group flex items-center rounded-lg border p-2\"\n          >\n            <span class=\"flex-1\"> {{ formatTime(time) }} </span>\n            <v-remixicon\n              name=\"riDeleteBin7Line\"\n              class=\"cursor-pointer\"\n              size=\"18\"\n              @click.stop=\"removeDayTime(day.id, timeIndex)\"\n            />\n          </div>\n        </div>\n      </ui-expand>\n    </div>\n  </div>\n</template>\n<script setup>\nimport { reactive, computed, ref, watch, onMounted } from 'vue';\nimport { useI18n } from 'vue-i18n';\nimport { useToast } from 'vue-toastification';\nimport dayjs from '@/lib/dayjs';\nimport { isObject } from '@/utils/helper';\n\nconst props = defineProps({\n  data: {\n    type: Object,\n    default: () => ({}),\n  },\n});\nconst emit = defineEmits(['update']);\n\nconst days = {\n  0: 'Sunday',\n  1: 'Monday',\n  2: 'Tuesday',\n  3: 'Wednesday',\n  4: 'Thursday',\n  5: 'Friday',\n  6: 'Saturday',\n};\n\nconst { t } = useI18n();\nconst toast = useToast();\n\nconst daysArr = ref(null);\nconst tempDate = reactive({\n  days: [],\n  time: '00:00',\n});\n\nconst sortedDaysArr = computed(() =>\n  daysArr.value ? daysArr.value.slice().sort((a, b) => a.id - b.id) : []\n);\n\nfunction formatTime(time) {\n  const [hour, minute, seconds] = time.split(':');\n\n  return dayjs()\n    .hour(hour)\n    .minute(minute)\n    .second(seconds || 0)\n    .format('hh:mm:ss A');\n}\nfunction removeDay(dayId) {\n  const dayIndex = daysArr.value.findIndex((day) => day.id === dayId);\n  if (dayIndex === -1) return;\n\n  daysArr.value.splice(dayIndex, 1);\n}\nfunction removeDayTime(dayId, timeIndex) {\n  const dayIndex = daysArr.value.findIndex((day) => day.id === dayId);\n  if (dayIndex === -1) return;\n\n  daysArr.value[dayIndex].times.splice(timeIndex, 1);\n\n  if (daysArr.value[dayIndex].times.length === 0) {\n    daysArr.value.splice(dayIndex, 1);\n  }\n}\nfunction addTime() {\n  tempDate.days.forEach((dayId) => {\n    const dayIndex = daysArr.value.findIndex(({ id }) => id === dayId);\n\n    if (dayIndex === -1) {\n      daysArr.value.push({\n        id: dayId,\n        times: [tempDate.time],\n      });\n    } else {\n      const isTimeExist = daysArr.value[dayIndex].times.includes(tempDate.time);\n\n      if (isTimeExist) {\n        const message = t('workflow.blocks.trigger.timeExist', {\n          time: formatTime(tempDate.time),\n          day: t(`workflow.blocks.trigger.days.${dayId}`),\n        });\n\n        toast.error(message);\n\n        return;\n      }\n\n      daysArr.value[dayIndex].times.push(tempDate.time);\n    }\n  });\n}\nfunction onSelectDayChange(value, id) {\n  if (value) tempDate.days.push(+id);\n  else tempDate.days.splice(tempDate.days.indexOf(+id), 1);\n}\nfunction getDaysText(dayIds) {\n  return dayIds\n    .map((day) => t(`workflow.blocks.trigger.days.${day}`))\n    .join(', ');\n}\n\nwatch(\n  daysArr,\n  (value, oldValue) => {\n    if (!oldValue) return;\n\n    emit('update', { days: value });\n  },\n  { deep: true }\n);\n\nonMounted(() => {\n  const isStringDay =\n    props.data.days.length > 0 && !isObject(props.data.days[0]);\n  daysArr.value = isStringDay\n    ? props.data.days.map((day) => ({ id: day, times: [props.data.time] }))\n    : props.data.days;\n});\n</script>\n"
  },
  {
    "path": "src/components/newtab/workflow/edit/Trigger/TriggerVisitWeb.vue",
    "content": "<template>\n  <div>\n    <ui-input\n      :model-value=\"data.url\"\n      :placeholder=\"t('workflow.blocks.trigger.forms.url')\"\n      class=\"w-full\"\n      @change=\"$emit('update', { url: $event })\"\n    />\n    <ui-checkbox\n      :model-value=\"data.isUrlRegex\"\n      class=\"mt-1\"\n      @change=\"$emit('update', { isUrlRegex: $event })\"\n    >\n      {{ t('workflow.blocks.trigger.useRegex') }}\n    </ui-checkbox>\n    <ui-checkbox\n      :model-value=\"data.supportSPA\"\n      class=\"ml-6\"\n      @change=\"$emit('update', { supportSPA: $event })\"\n    >\n      Support SPA website\n    </ui-checkbox>\n  </div>\n</template>\n<script setup>\nimport { useI18n } from 'vue-i18n';\n\ndefineProps({\n  data: {\n    type: Object,\n    default: () => ({}),\n  },\n});\ndefineEmits(['update']);\n\nconst { t } = useI18n();\n</script>\n"
  },
  {
    "path": "src/components/newtab/workflow/edit/TriggerEvent/TriggerEventInput.vue",
    "content": "<template>\n  <div class=\"grid grid-cols-2 gap-2\">\n    <ui-input v-model=\"defaultParams.data\" label=\"Data\" />\n    <ui-input v-model=\"defaultParams.inputType\" label=\"Input type\" />\n  </div>\n</template>\n<script setup>\nimport { shallowReactive, watch, onMounted } from 'vue';\nimport { objectHasKey } from '@/utils/helper';\n\nconst props = defineProps({\n  params: {\n    type: Object,\n    default: () => ({}),\n  },\n});\nconst emit = defineEmits(['update']);\n\nconst defaultParams = shallowReactive({\n  data: '',\n  inputType: 'insertText',\n});\n\nwatch(\n  defaultParams,\n  (value) => {\n    emit('update', value);\n  },\n  { deep: true }\n);\n\nonMounted(() => {\n  Object.entries(props.params).forEach(([key, value]) => {\n    if (objectHasKey(defaultParams, key)) defaultParams[key] = value;\n  });\n});\n</script>\n"
  },
  {
    "path": "src/components/newtab/workflow/edit/TriggerEvent/TriggerEventKeyboard.vue",
    "content": "<template>\n  <div class=\"grid grid-cols-2 gap-2\">\n    <ui-checkbox\n      v-for=\"item in ['altKey', 'ctrlKey', 'metaKey', 'shiftKey']\"\n      :key=\"item\"\n      v-model=\"defaultParams[item]\"\n    >\n      {{ item }}\n    </ui-checkbox>\n  </div>\n  <ui-input\n    v-model=\"defaultParams.key\"\n    class=\"mt-2 w-full\"\n    label=\"key\"\n    placeholder=\"a\"\n    @change=\"findKeyDefintion\"\n  />\n  <div class=\"mt-1 flex items-center space-x-2\">\n    <ui-input\n      v-model=\"defaultParams.code\"\n      class=\"flex-1\"\n      label=\"code\"\n      placeholder=\"KeyA\"\n    />\n    <ui-input\n      v-model.number=\"defaultParams.keyCode\"\n      type=\"number\"\n      class=\"flex-1\"\n      label=\"keyCode\"\n    />\n  </div>\n  <ui-checkbox v-model=\"defaultParams.repeat\" class=\"mt-4\">\n    Repeat\n  </ui-checkbox>\n</template>\n<script setup>\nimport { shallowReactive, watch, onMounted } from 'vue';\nimport { objectHasKey } from '@/utils/helper';\nimport { keyDefinitions } from '@/utils/USKeyboardLayout';\n\nconst props = defineProps({\n  params: {\n    type: Object,\n    default: () => ({}),\n  },\n});\nconst emit = defineEmits(['update']);\n\nconst defaultParams = shallowReactive({\n  altKey: false,\n  ctrlKey: false,\n  metaKey: false,\n  shiftKey: false,\n  code: '',\n  key: '',\n  keyCode: 0,\n  repeat: false,\n});\n\nfunction findKeyDefintion(value) {\n  const keyDefinition = keyDefinitions[value];\n\n  if (!keyDefinition) return;\n\n  defaultParams.code = keyDefinitions[value].code;\n  defaultParams.keyCode = keyDefinitions[value].keyCode;\n}\n\nwatch(\n  defaultParams,\n  (value) => {\n    emit('update', value);\n  },\n  { deep: true }\n);\n\nonMounted(() => {\n  Object.entries(props.params).forEach(([key, value]) => {\n    if (objectHasKey(defaultParams, key)) defaultParams[key] = value;\n  });\n});\n</script>\n"
  },
  {
    "path": "src/components/newtab/workflow/edit/TriggerEvent/TriggerEventMouse.vue",
    "content": "<template>\n  <div class=\"grid grid-cols-2 gap-2\">\n    <ui-checkbox\n      v-for=\"item in ['altKey', 'ctrlKey', 'metaKey', 'shiftKey']\"\n      :key=\"item\"\n      v-model=\"defaultParams[item]\"\n    >\n      {{ item }}\n    </ui-checkbox>\n  </div>\n  <ui-select\n    v-model.number=\"defaultParams.button\"\n    class=\"mt-2 w-full\"\n    label=\"Button\"\n  >\n    <option v-for=\"button in buttons\" :key=\"button.id\" :value=\"button.id\">\n      {{ button.name }}\n    </option>\n  </ui-select>\n  <div\n    v-for=\"items in posGroups\"\n    :key=\"items[0]\"\n    class=\"mt-2 flex items-center space-x-2\"\n  >\n    <template v-if=\"items[0].startsWith('client')\">\n      <ui-input\n        v-for=\"item in items\"\n        :key=\"item\"\n        :model-value=\"defaultParams[item]\"\n        :label=\"item\"\n        class=\"flex-1\"\n        @change=\"defaultParams[item] = +$event || $event\"\n      />\n    </template>\n    <template v-else>\n      <ui-input\n        v-for=\"item in items\"\n        :key=\"item\"\n        v-model.number=\"defaultParams[item]\"\n        type=\"number\"\n        class=\"flex-1\"\n        :label=\"item\"\n      />\n    </template>\n  </div>\n</template>\n<script setup>\nimport { shallowReactive, watch, onMounted } from 'vue';\nimport { objectHasKey } from '@/utils/helper';\n\nconst props = defineProps({\n  params: {\n    type: Object,\n    default: () => ({}),\n  },\n});\nconst emit = defineEmits(['update']);\n\nconst buttons = [\n  { id: 0, name: 'Left click' },\n  { id: 1, name: 'Middle click' },\n  { id: 2, name: 'Right click' },\n];\nconst posGroups = [\n  ['clientX', 'clientY'],\n  ['movementX', 'movementY'],\n  ['offsetX', 'offsetY'],\n  ['pageX', 'pageY'],\n  ['screenX', 'screenY'],\n];\n\nconst defaultParams = shallowReactive({\n  altKey: false,\n  button: 0,\n  clientX: 0,\n  clientY: 0,\n  ctrlKey: false,\n  metaKey: false,\n  shiftKey: false,\n  movementX: 0,\n  movementY: 0,\n  offsetX: 0,\n  offsetY: 0,\n  pageX: 0,\n  pageY: 0,\n  screenX: 0,\n  screenY: 0,\n});\n\nwatch(\n  defaultParams,\n  (value) => {\n    emit('update', value);\n  },\n  { deep: true }\n);\n\nonMounted(() => {\n  Object.entries(props.params).forEach(([key, value]) => {\n    if (objectHasKey(defaultParams, key)) defaultParams[key] = value;\n  });\n});\n</script>\n"
  },
  {
    "path": "src/components/newtab/workflow/edit/TriggerEvent/TriggerEventTouch.vue",
    "content": "<template>\n  <div class=\"grid grid-cols-2 gap-2\">\n    <ui-checkbox\n      v-for=\"item in ['altKey', 'ctrlKey', 'metaKey', 'shiftKey']\"\n      :key=\"item\"\n      v-model=\"defaultParams[item]\"\n    >\n      {{ item }}\n    </ui-checkbox>\n  </div>\n</template>\n<script setup>\nimport { shallowReactive, watch, onMounted } from 'vue';\nimport { objectHasKey } from '@/utils/helper';\n\nconst props = defineProps({\n  params: {\n    type: Object,\n    default: () => ({}),\n  },\n});\nconst emit = defineEmits(['update']);\n\nconst defaultParams = shallowReactive({\n  altKey: false,\n  ctrlKey: false,\n  metaKey: false,\n  shiftKey: false,\n});\n\nwatch(\n  defaultParams,\n  (value) => {\n    emit('update', value);\n  },\n  { deep: true }\n);\n\nonMounted(() => {\n  Object.entries(props.params).forEach(([key, value]) => {\n    if (objectHasKey(defaultParams, key)) defaultParams[key] = value;\n  });\n});\n</script>\n"
  },
  {
    "path": "src/components/newtab/workflow/edit/TriggerEvent/TriggerEventWheel.vue",
    "content": "<template>\n  <div class=\"grid grid-cols-2 gap-2\">\n    <ui-input\n      v-model.number=\"defaultParams.deltaX\"\n      type=\"number\"\n      label=\"deltaX\"\n    />\n    <ui-input\n      v-model.number=\"defaultParams.deltaY\"\n      type=\"number\"\n      label=\"deltaY\"\n    />\n    <ui-input\n      v-model.number=\"defaultParams.deltaZ\"\n      type=\"number\"\n      class=\"col-span-2\"\n      label=\"deltaZ\"\n    />\n  </div>\n</template>\n<script setup>\nimport { shallowReactive, watch, onMounted } from 'vue';\nimport { objectHasKey } from '@/utils/helper';\n\nconst props = defineProps({\n  params: {\n    type: Object,\n    default: () => ({}),\n  },\n});\nconst emit = defineEmits(['update']);\n\nconst defaultParams = shallowReactive({\n  deltaX: 0,\n  deltaY: 0,\n  deltaZ: 0,\n});\n\nwatch(\n  defaultParams,\n  (value) => {\n    emit('update', value);\n  },\n  { deep: true }\n);\n\nonMounted(() => {\n  Object.entries(props.params).forEach(([key, value]) => {\n    if (objectHasKey(defaultParams, key)) defaultParams[key] = value;\n  });\n});\n</script>\n"
  },
  {
    "path": "src/components/newtab/workflow/editor/EditorAddPackage.vue",
    "content": "<template>\n  <div class=\"flex items-center\">\n    <ui-popover v-tooltip:bottom=\"t('packages.icon')\" class=\"mr-2\">\n      <template #trigger>\n        <img\n          v-if=\"state.icon.startsWith('http')\"\n          :src=\"state.icon\"\n          width=\"38px\"\n          height=\"38px\"\n          class=\"rounded-lg\"\n        />\n        <span\n          v-else\n          icon\n          class=\"bg-box-transparent inline-block rounded-lg p-2\"\n        >\n          <v-remixicon :name=\"state.icon || 'mdiPackageVariantClosed'\" />\n        </span>\n      </template>\n      <div class=\"w-64\">\n        <p>{{ t('packages.icon') }}</p>\n        <div class=\"mt-4 grid grid-cols-6 gap-2\">\n          <div v-for=\"icon in icons\" :key=\"icon\">\n            <span\n              :class=\"{ 'bg-box-transparent': icon === state.icon }\"\n              class=\"hoverable inline-block cursor-pointer rounded-lg p-2\"\n              @click=\"state.icon = icon\"\n            >\n              <v-remixicon :name=\"icon\" />\n            </span>\n          </div>\n        </div>\n        <ui-input\n          :model-value=\"state.icon.startsWith('http') ? state.icon : ''\"\n          type=\"url\"\n          placeholder=\"http://example.com/img.png\"\n          label=\"Icon URL\"\n          class=\"mt-2 w-full\"\n          @change=\"updatePackageIcon\"\n        />\n      </div>\n    </ui-popover>\n    <ui-input\n      v-model=\"state.name\"\n      :placeholder=\"t('common.name')\"\n      autofocus\n      class=\"w-full\"\n      @keyup.enter=\"$emit('add')\"\n    />\n  </div>\n  <ui-textarea\n    v-model=\"state.description\"\n    :label=\"t('common.description')\"\n    placeholder=\"Description...\"\n    class=\"mt-4 w-full\"\n  />\n  <div class=\"mt-6 flex items-center justify-end space-x-4\">\n    <ui-button @click=\"$emit('cancel')\">\n      {{ t('common.cancel') }}\n    </ui-button>\n    <ui-button variant=\"accent\" class=\"w-20\" @click=\"$emit('add')\">\n      {{ t('common.add') }}\n    </ui-button>\n  </div>\n</template>\n<script setup>\nimport { reactive, watch, onMounted } from 'vue';\nimport { useI18n } from 'vue-i18n';\n\nconst props = defineProps({\n  data: {\n    type: Object,\n    default: () => ({}),\n  },\n});\nconst emit = defineEmits(['update', 'add', 'cancel']);\n\nconst icons = [\n  'mdiPackageVariantClosed',\n  'riGlobalLine',\n  'riFileTextLine',\n  'riEqualizerLine',\n  'riTimerLine',\n  'riCalendarLine',\n  'riFlashlightLine',\n  'riLightbulbFlashLine',\n  'riDatabase2Line',\n  'riWindowLine',\n  'riCursorLine',\n  'riDownloadLine',\n  'riCommandLine',\n];\n\nconst { t } = useI18n();\n\nconst state = reactive({\n  name: '',\n  icon: '',\n  description: '',\n});\n\nfunction updatePackageIcon(value) {\n  if (!value.startsWith('http')) return;\n\n  state.icon = value.slice(0, 1024);\n}\n\nwatch(\n  state,\n  () => {\n    emit('update', state);\n  },\n  { deep: true }\n);\n\nonMounted(() => {\n  Object.assign(state, props.data);\n});\n</script>\n"
  },
  {
    "path": "src/components/newtab/workflow/editor/EditorCustomEdge.vue",
    "content": "<template>\n  <base-edge\n    :id=\"id\"\n    :style=\"style\"\n    :path=\"path[0]\"\n    :marker-end=\"markerEnd\"\n    :label=\"label\"\n    :label-x=\"path[1]\"\n    :label-y=\"path[2]\"\n    :label-style=\"{ fill: 'white' }\"\n    :label-show-bg=\"true\"\n    :label-bg-style=\"{ fill: '#3b82f6' }\"\n    :label-bg-padding=\"[2, 4]\"\n    :label-bg-border-radius=\"2\"\n  />\n</template>\n<script setup>\nimport { computed } from 'vue';\nimport { BaseEdge, getBezierPath, getSmoothStepPath } from '@vue-flow/core';\n\nconst props = defineProps({\n  id: {\n    type: String,\n    required: true,\n  },\n  sourceX: {\n    type: Number,\n    required: true,\n  },\n  sourceY: {\n    type: Number,\n    required: true,\n  },\n  targetX: {\n    type: Number,\n    required: true,\n  },\n  targetY: {\n    type: Number,\n    required: true,\n  },\n  sourcePosition: {\n    type: String,\n    required: true,\n  },\n  targetPosition: {\n    type: String,\n    required: true,\n  },\n  data: {\n    type: Object,\n    required: false,\n    default: () => ({}),\n  },\n  markerEnd: {\n    type: String,\n    required: false,\n    default: '',\n  },\n  label: {\n    type: String,\n    required: false,\n    default: '',\n  },\n  style: {\n    type: Object,\n    required: false,\n    default: null,\n  },\n});\n\nconst path = computed(() => {\n  const options = {\n    sourceX: props.sourceX,\n    sourceY: props.sourceY,\n    sourcePosition: props.sourcePosition,\n    targetX: props.targetX,\n    targetY: props.targetY,\n    targetPosition: props.targetPosition,\n  };\n\n  if (props.sourceX > props.targetX) {\n    return getSmoothStepPath(options);\n  }\n\n  return getBezierPath(options);\n});\n</script>\n<script>\nexport default {\n  inheritAttrs: false,\n};\n</script>\n"
  },
  {
    "path": "src/components/newtab/workflow/editor/EditorDebugging.vue",
    "content": "<template>\n  <ui-card\n    v-if=\"workflowState?.state\"\n    class=\"shadow-xl flex items-start fixed bottom-8 z-50 left-1/2 -translate-x-1/2\"\n  >\n    <div class=\"mr-4 w-52\">\n      <div class=\"flex items-center gap-2\">\n        <ui-button\n          :disabled=\"workflowState.state.nextBlockBreakpoint\"\n          variant=\"accent\"\n          class=\"flex-1\"\n          @click=\"toggleExecution\"\n        >\n          <v-remixicon\n            :name=\"\n              workflowState.status === 'breakpoint'\n                ? 'riPlayLine'\n                : 'riPauseLine'\n            \"\n            class=\"mr-2 -ml-1\"\n          />\n          <span>\n            {{\n              t(\n                `common.${\n                  workflowState.status === 'breakpoint' ? 'resume' : 'pause'\n                }`\n              )\n            }}\n          </span>\n        </ui-button>\n        <ui-button\n          v-tooltip=\"t('workflow.testing.nextBlock')\"\n          :disabled=\"workflowState.status !== 'breakpoint'\"\n          icon\n          @click=\"nextBlock\"\n        >\n          <v-remixicon name=\"riArrowLeftSLine\" rotate=\"180\" />\n        </ui-button>\n        <ui-button\n          v-tooltip=\"t('common.stop')\"\n          icon\n          class=\"text-red-500 dark:text-red-600\"\n          @click=\"stopWorkflow\"\n        >\n          <v-remixicon name=\"riStopLine\" />\n        </ui-button>\n      </div>\n      <ui-list\n        v-if=\"workflowState.state\"\n        class=\"mt-4 overflow-auto h-[105px] scroll\"\n      >\n        <ui-list-item\n          v-for=\"block in workflowState.state.currentBlock\"\n          :key=\"block.id\"\n          small\n        >\n          <div class=\"text-overflow text-sm w-full\">\n            <div class=\"flex items-center\">\n              <p class=\"flex-1 text-overflow\">\n                {{ getBlockName(block.name) }}\n              </p>\n              <v-remixicon\n                title=\"Go to block\"\n                name=\"riEyeLine\"\n                size=\"18\"\n                class=\"text-gray-600 dark:text-gray-200 cursor-pointer\"\n                @click=\"$emit('goToBlock', block.id)\"\n              />\n            </div>\n            <p\n              class=\"leading-tight text-overflow text-gray-600 dark:text-gray-200\"\n            >\n              {{ t('workflow.testing.startRun') }}:\n              {{ dayjs(block.startedAt).format('HH:mm:ss, SSS') }}\n            </p>\n          </div>\n        </ui-list-item>\n      </ui-list>\n    </div>\n    <div class=\"w-64\">\n      <ui-tabs v-model=\"activeTab\" class=\"-mt-1\">\n        <ui-tab class=\"!py-2\" value=\"workflow-data\">Data</ui-tab>\n        <ui-tab class=\"!py-2\" value=\"workflow-logs\">Logs</ui-tab>\n      </ui-tabs>\n      <ui-tab-panels v-model=\"activeTab\">\n        <ui-tab-panel value=\"workflow-data\">\n          <shared-codemirror\n            :model-value=\"JSON.stringify(workflowData, null, 2)\"\n            :line-numbers=\"false\"\n            hide-lang\n            readonly\n            lang=\"json\"\n            class=\"h-40 scroll breakpoint-data\"\n          />\n        </ui-tab-panel>\n        <ui-tab-panel\n          ref=\"workflowLogsContainer\"\n          value=\"workflow-logs\"\n          class=\"h-40 scroll text-sm overflow-auto\"\n        >\n          <ui-list class=\"mt-2\">\n            <ui-list-item\n              v-for=\"item in (workflowState?.state?.logs ?? [])\n                .slice(-100)\n                .reverse()\"\n              :key=\"item.id\"\n              small\n              class=\"!block\"\n            >\n              <div class=\"flex items-center gap-2 overflow-hidden w-full\">\n                <p class=\"flex-1 text-overflow leading-tight\">\n                  {{ getBlockName(item.name) }}\n                </p>\n                <p\n                  class=\"text-gray-600 leading-tight dark:text-gray-300 tabular-nums\"\n                  :title=\"t('log.duration')\"\n                >\n                  {{ item.duration }}s\n                </p>\n              </div>\n              <p class=\"flex-1 text-gray-600 leading-tight dark:text-gray-300\">\n                {{ item.description }}\n              </p>\n            </ui-list-item>\n          </ui-list>\n        </ui-tab-panel>\n      </ui-tab-panels>\n    </div>\n  </ui-card>\n</template>\n<script setup>\nimport { defineAsyncComponent, computed, watch, shallowRef } from 'vue';\nimport { useI18n } from 'vue-i18n';\nimport dayjs from '@/lib/dayjs';\nimport { tasks } from '@/utils/shared';\nimport { debounce } from '@/utils/helper';\nimport { sendMessage } from '@/utils/message';\n\nconst SharedCodemirror = defineAsyncComponent(() =>\n  import('@/components/newtab/shared/SharedCodemirror.vue')\n);\n\nconst props = defineProps({\n  states: {\n    type: Array,\n    default: () => [],\n  },\n  editor: {\n    type: Object,\n    default: () => ({}),\n  },\n});\ndefineEmits(['goToBlock']);\n\nlet currentRunningEls = [];\n\nconst { t, te } = useI18n();\n\nconst activeTab = shallowRef('workflow-data');\n\nconst workflowState = computed(() => props.states[0]);\nconst workflowData = computed(() => {\n  if (!workflowState.value?.state?.ctxData) return {};\n  const { ctxData, dataSnapshot } = workflowState.value.state.ctxData;\n  const latestData = Object.values(ctxData).at(-1);\n  if (!latestData) return {};\n\n  return {\n    ...latestData,\n    referenceData: {\n      ...latestData.referenceData,\n      loopData: dataSnapshot[latestData.referenceData.loopData] ?? {},\n      variables: dataSnapshot[latestData.referenceData.variables] ?? {},\n    },\n  };\n});\n\nfunction getBlockName(blockId) {\n  const key = `workflow.blocks.${blockId}.name`;\n\n  return te(key) ? t(key) : tasks[blockId].name;\n}\nfunction toggleExecution() {\n  if (!workflowState.value) return;\n\n  if (workflowState.value.status === 'running') {\n    sendMessage('workflow:breakpoint', workflowState.value.id, 'background');\n  } else {\n    sendMessage(\n      'workflow:resume',\n      { id: workflowState.value.id },\n      'background'\n    );\n  }\n}\nfunction stopWorkflow() {\n  if (!workflowState.value) return;\n\n  sendMessage('workflow:stop', workflowState.value.id, 'background');\n}\nfunction nextBlock() {\n  sendMessage(\n    'workflow:resume',\n    { id: workflowState.value.id, nextBlock: true },\n    'background'\n  );\n}\n\nwatch(\n  workflowState,\n  debounce(() => {\n    currentRunningEls.forEach((element) => {\n      element.classList.remove('current-running');\n    });\n\n    if (!workflowState.value?.state?.currentBlock) return;\n\n    const selectors = workflowState.value.state.currentBlock\n      .map((block) => `.vue-flow [data-block-id=\"${block.id}\"]`)\n      .join(',');\n    const elements = document.querySelectorAll(selectors);\n\n    currentRunningEls = elements;\n    elements.forEach((el) => {\n      el.classList.add('current-running');\n    });\n  }, 200),\n  { immediate: true }\n);\n</script>\n<style>\n.breakpoint-data .cm-editor {\n  font-size: 13px;\n  padding-bottom: 0;\n}\n\n.current-running {\n  @apply ring;\n}\n</style>\n"
  },
  {
    "path": "src/components/newtab/workflow/editor/EditorLocalActions.vue",
    "content": "<template>\n  <span\n    v-if=\"isTeam && workflow.tag\"\n    :class=\"tagColors[workflow.tag]\"\n    class=\"mr-2 rounded-md p-1 text-sm capitalize text-black\"\n  >\n    {{ workflow.tag }}\n  </span>\n  <ui-card\n    v-if=\"!isTeam\"\n    padding=\"p-1\"\n    class=\"pointer-events-auto ml-4 flex items-center\"\n  >\n    <ui-popover>\n      <template #trigger>\n        <button\n          v-tooltip.group=\"t('workflow.host.title')\"\n          class=\"hoverable rounded-lg p-2\"\n        >\n          <v-remixicon\n            :class=\"{ 'text-primary': hosted }\"\n            name=\"riBaseStationLine\"\n          />\n        </button>\n      </template>\n      <div :class=\"{ 'text-center': state.isUploadingHost }\" class=\"w-64\">\n        <div class=\"flex items-center text-gray-600 dark:text-gray-200\">\n          <p>\n            {{ t('workflow.host.set') }}\n          </p>\n          <a\n            :title=\"t('common.docs')\"\n            href=\"https://docs.extension.automa.site/workflow/sharing-workflow.html#host-workflow\"\n            target=\"_blank\"\n            class=\"ml-1\"\n          >\n            <v-remixicon name=\"riInformationLine\" size=\"20\" />\n          </a>\n          <div class=\"grow\"></div>\n          <ui-spinner v-if=\"state.isUploadingHost\" color=\"text-accent\" />\n          <ui-switch\n            v-else\n            :model-value=\"Boolean(hosted)\"\n            @change=\"setAsHostWorkflow\"\n          />\n        </div>\n        <transition-expand>\n          <ui-input\n            v-if=\"hosted\"\n            v-tooltip:bottom=\"t('workflow.host.id')\"\n            :model-value=\"hosted.hostId\"\n            prepend-icon=\"riLinkM\"\n            readonly\n            class=\"mt-4 block w-full\"\n            @click=\"$event.target.select()\"\n          />\n        </transition-expand>\n      </div>\n    </ui-popover>\n    <ui-popover :disabled=\"userDontHaveTeamsAccess\">\n      <template #trigger>\n        <button\n          v-tooltip.group=\"t('workflow.share.title')\"\n          :class=\"{ 'text-primary': shared }\"\n          class=\"hoverable rounded-lg p-2\"\n          @click=\"shareWorkflow(!userDontHaveTeamsAccess)\"\n        >\n          <v-remixicon name=\"riShareLine\" />\n        </button>\n      </template>\n      <p class=\"font-semibold\">Share the workflow</p>\n      <ui-list class=\"mt-2 w-56 space-y-1\">\n        <ui-list-item\n          v-close-popover\n          class=\"cursor-pointer\"\n          @click=\"shareWorkflowWithTeam\"\n        >\n          <v-remixicon name=\"riTeamLine\" class=\"-ml-1 mr-2\" />\n          With your team\n        </ui-list-item>\n        <ui-list-item\n          v-close-popover\n          class=\"cursor-pointer\"\n          @click=\"shareWorkflow()\"\n        >\n          <v-remixicon name=\"riGroupLine\" class=\"-ml-1 mr-2\" />\n          With the community\n        </ui-list-item>\n      </ui-list>\n    </ui-popover>\n  </ui-card>\n  <ui-card\n    v-if=\"canEdit\"\n    padding=\"p-1 ml-4 hidden md:block pointer-events-auto\"\n  >\n    <button\n      v-for=\"item in modalActions\"\n      :key=\"item.id\"\n      v-tooltip.group=\"item.name\"\n      class=\"hoverable rounded-lg p-2\"\n      @click=\"$emit('modal', item.id)\"\n    >\n      <v-remixicon :name=\"item.icon\" />\n    </button>\n  </ui-card>\n  <ui-card padding=\"p-1 ml-4 flex items-center pointer-events-auto\">\n    <ui-popover v-if=\"canEdit\" class=\"md:hidden\">\n      <template #trigger>\n        <button class=\"hoverable rounded-lg p-2\">\n          <v-remixicon name=\"riMore2Line\" />\n        </button>\n      </template>\n      <ui-list class=\"cursor-pointer space-y-1\">\n        <ui-list-item\n          v-for=\"item in modalActions\"\n          :key=\"item.id\"\n          v-close-popover\n          @click=\"$emit('modal', item.id)\"\n        >\n          <v-remixicon :name=\"item.icon\" class=\"mr-2 -ml-1\" />\n          {{ item.name }}\n        </ui-list-item>\n      </ui-list>\n    </ui-popover>\n    <template v-if=\"!workflow.isDisabled\">\n      <button\n        v-if=\"canEdit\"\n        v-tooltip.group=\"\n          t(`workflow.testing.${isDataChanged ? 'disabled' : 'title'}`)\n        \"\n        :class=\"[\n          { 'cursor-default': isDataChanged },\n          workflow.testingMode\n            ? 'bg-primary bg-opacity-20 text-primary'\n            : 'hoverable',\n        ]\"\n        class=\"rounded-lg p-2\"\n        @click=\"toggleTestingMode\"\n      >\n        <v-remixicon name=\"riBug2Line\" />\n      </button>\n      <button\n        v-tooltip.group=\"\n          `${t('common.execute')} (${\n            shortcuts['editor:execute-workflow'].readable\n          })`\n        \"\n        class=\"hoverable rounded-lg p-2\"\n        @click=\"executeCurrWorkflow\"\n      >\n        <v-remixicon name=\"riPlayLine\" />\n      </button>\n    </template>\n    <button\n      v-else\n      v-tooltip=\"t('workflow.clickToEnable')\"\n      class=\"p-2\"\n      @click=\"updateWorkflow({ isDisabled: false })\"\n    >\n      {{ t('common.disabled') }}\n    </button>\n  </ui-card>\n  <ui-card padding=\"p-1 ml-4 space-x-1 pointer-events-auto flex items-center\">\n    <button\n      v-if=\"!canEdit\"\n      v-tooltip.group=\"state.triggerText\"\n      class=\"hoverable rounded-lg p-2\"\n    >\n      <v-remixicon name=\"riFlashlightLine\" />\n    </button>\n    <ui-popover>\n      <template #trigger>\n        <button class=\"hoverable rounded-lg p-2\">\n          <v-remixicon name=\"riMore2Line\" />\n        </button>\n      </template>\n      <ui-list style=\"min-width: 9rem\">\n        <ui-list-item\n          v-close-popover\n          class=\"cursor-pointer\"\n          @click=\"copyWorkflowId\"\n        >\n          <v-remixicon name=\"riFileCopyLine\" class=\"mr-2 -ml-1\" />\n          Copy workflow Id\n        </ui-list-item>\n        <ui-list-item\n          v-if=\"isTeam && canEdit\"\n          v-close-popover\n          class=\"cursor-pointer\"\n          @click=\"syncWorkflow\"\n        >\n          <v-remixicon name=\"riRefreshLine\" class=\"mr-2 -ml-1\" />\n          <span>{{ t('workflow.host.sync.title') }}</span>\n        </ui-list-item>\n        <ui-list-item\n          class=\"cursor-pointer\"\n          @click=\"updateWorkflow({ isDisabled: !workflow.isDisabled })\"\n        >\n          <v-remixicon name=\"riToggleLine\" class=\"mr-2 -ml-1\" />\n          {{ t(`common.${workflow.isDisabled ? 'enable' : 'disable'}`) }}\n        </ui-list-item>\n        <ui-list-item\n          v-for=\"item in moreActions\"\n          :key=\"item.id\"\n          v-bind=\"item.attrs || {}\"\n          v-close-popover\n          class=\"cursor-pointer\"\n          @click=\"item.action\"\n        >\n          <v-remixicon :name=\"item.icon\" class=\"mr-2 -ml-1\" />\n          {{ item.name }}\n        </ui-list-item>\n        <ui-list-item\n          v-if=\"\n            isTeam &&\n            canEdit &&\n            userStore.validateTeamAccess(teamId, ['owner', 'create'])\n          \"\n          v-close-popover\n          class=\"cursor-pointer text-red-400 dark:text-red-500\"\n          @click=\"deleteFromTeam\"\n        >\n          <v-remixicon name=\"riDeleteBin7Line\" class=\"mr-2 -ml-1\" />\n          <span>Delete from team</span>\n        </ui-list-item>\n      </ui-list>\n    </ui-popover>\n    <ui-button\n      v-if=\"!isTeam\"\n      :title=\"shortcuts['editor:save'].readable\"\n      variant=\"accent\"\n      class=\"relative px-2 md:px-4\"\n      @click=\"saveWorkflow\"\n    >\n      <span\n        v-if=\"isDataChanged\"\n        class=\"absolute top-0 left-0 -ml-1 -mt-1 flex h-3 w-3\"\n      >\n        <span\n          class=\"absolute inline-flex h-full w-full animate-ping rounded-full bg-primary opacity-75\"\n        ></span>\n        <span\n          class=\"relative inline-flex h-3 w-3 rounded-full bg-blue-600\"\n        ></span>\n      </span>\n      <v-remixicon name=\"riSaveLine\" class=\"my-1 md:-ml-1\" />\n      <span class=\"ml-2 hidden md:block\">{{ t('common.save') }}</span>\n    </ui-button>\n    <ui-button\n      v-else-if=\"!canEdit\"\n      v-tooltip.group=\"'Sync workflow'\"\n      :loading=\"state.loadingSync\"\n      variant=\"accent\"\n      @click=\"syncWorkflow\"\n    >\n      <v-remixicon name=\"riRefreshLine\" class=\"mr-2 -ml-1\" />\n      <span>\n        {{ t('workflow.host.sync.title') }}\n      </span>\n    </ui-button>\n    <template v-else>\n      <ui-button\n        v-tooltip=\"`Save workflow (${shortcuts['editor:save'].readable})`\"\n        class=\"mr-2\"\n        icon\n        @click=\"saveWorkflow\"\n      >\n        <span\n          v-if=\"isDataChanged\"\n          class=\"absolute top-0 left-0 -ml-1 -mt-1 flex h-3 w-3\"\n        >\n          <span\n            class=\"absolute inline-flex h-full w-full animate-ping rounded-full bg-primary opacity-75\"\n          ></span>\n          <span\n            class=\"relative inline-flex h-3 w-3 rounded-full bg-blue-600\"\n          ></span>\n        </span>\n        <v-remixicon name=\"riSaveLine\" />\n      </ui-button>\n      <ui-button\n        v-tooltip=\"'Publish workflow update'\"\n        :loading=\"state.isPublishing\"\n        variant=\"accent\"\n        @click=\"publishWorkflow\"\n      >\n        Publish\n      </ui-button>\n    </template>\n  </ui-card>\n  <ui-modal v-model=\"state.showEditDescription\" persist blur custom-content>\n    <workflow-share-team\n      :workflow=\"workflow\"\n      :is-update=\"true\"\n      @update=\"updateWorkflowDescription\"\n      @close=\"state.showEditDescription = false\"\n    />\n  </ui-modal>\n  <ui-modal v-model=\"renameState.showModal\" title=\"Rename\">\n    <ui-input\n      v-model=\"renameState.name\"\n      :placeholder=\"t('common.name')\"\n      autofocus\n      class=\"mb-4 w-full\"\n      @keyup.enter=\"renameWorkflow\"\n    />\n    <ui-textarea\n      v-model=\"renameState.description\"\n      :placeholder=\"t('common.description')\"\n      height=\"165px\"\n      class=\"w-full dark:text-gray-200\"\n      max=\"300\"\n      style=\"min-height: 140px\"\n    />\n    <p class=\"mb-6 text-right text-gray-600 dark:text-gray-200\">\n      {{ renameState.description.length }}/300\n    </p>\n    <div class=\"flex space-x-2\">\n      <ui-button class=\"w-full\" @click=\"clearRenameModal\">\n        {{ t('common.cancel') }}\n      </ui-button>\n      <ui-button variant=\"accent\" class=\"w-full\" @click=\"renameWorkflow\">\n        {{ t('common.update') }}\n      </ui-button>\n    </div>\n  </ui-modal>\n</template>\n<script setup>\nimport WorkflowShareTeam from '@/components/newtab/workflow/WorkflowShareTeam.vue';\nimport { useDialog } from '@/composable/dialog';\nimport { useGroupTooltip } from '@/composable/groupTooltip';\nimport { getShortcut, useShortcut } from '@/composable/shortcut';\nimport RendererWorkflowService from '@/service/renderer/RendererWorkflowService';\nimport { useStore } from '@/stores/main';\nimport { usePackageStore } from '@/stores/package';\nimport { useSharedWorkflowStore } from '@/stores/sharedWorkflow';\nimport { useTeamWorkflowStore } from '@/stores/teamWorkflow';\nimport { useUserStore } from '@/stores/user';\nimport { useWorkflowStore } from '@/stores/workflow';\nimport { fetchApi } from '@/utils/api';\nimport convertWorkflowData from '@/utils/convertWorkflowData';\nimport { findTriggerBlock, parseJSON } from '@/utils/helper';\nimport { tagColors } from '@/utils/shared';\nimport getTriggerText from '@/utils/triggerText';\nimport { convertWorkflow, exportWorkflow } from '@/utils/workflowData';\nimport { registerWorkflowTrigger } from '@/utils/workflowTrigger';\nimport { computed, reactive } from 'vue';\nimport { useI18n } from 'vue-i18n';\nimport { useRouter } from 'vue-router';\nimport { useToast } from 'vue-toastification';\nimport browser from 'webextension-polyfill';\n\nconst props = defineProps({\n  isDataChanged: {\n    type: Boolean,\n    default: false,\n  },\n  workflow: {\n    type: Object,\n    default: () => ({}),\n  },\n  editor: {\n    type: Object,\n    default: () => ({}),\n  },\n  changedData: {\n    type: Object,\n    default: () => ({}),\n  },\n  canEdit: {\n    type: Boolean,\n    default: true,\n  },\n  isTeam: Boolean,\n  isPackage: Boolean,\n});\nconst emit = defineEmits(['modal', 'change', 'update', 'permission']);\n\nuseGroupTooltip();\n\nconst { t } = useI18n();\nconst toast = useToast();\nconst router = useRouter();\nconst dialog = useDialog();\nconst mainStore = useStore();\nconst userStore = useUserStore();\nconst packageStore = usePackageStore();\nconst workflowStore = useWorkflowStore();\nconst teamWorkflowStore = useTeamWorkflowStore();\nconst sharedWorkflowStore = useSharedWorkflowStore();\nconst shortcuts = useShortcut([\n  /* eslint-disable-next-line */\n  getShortcut('editor:save', saveWorkflow),\n  /* eslint-disable-next-line */\n  getShortcut('editor:execute-workflow', executeCurrWorkflow),\n]);\n\nconst { teamId } = router.currentRoute.value.params;\n\nconst state = reactive({\n  triggerText: '',\n  loadingSync: false,\n  isPublishing: false,\n  isUploadingHost: false,\n  showEditDescription: false,\n});\nconst renameState = reactive({\n  name: '',\n  description: '',\n  showModal: false,\n});\n\nconst shared = computed(() => sharedWorkflowStore.getById(props.workflow.id));\nconst hosted = computed(() => userStore.hostedWorkflows[props.workflow.id]);\nconst userDontHaveTeamsAccess = computed(() => {\n  if (props.isTeam || !userStore.user?.teams) return true;\n\n  return !userStore.user.teams.some((team) =>\n    team.access.some((item) => ['owner', 'create'].includes(item))\n  );\n});\n\nfunction updateWorkflow(data = {}, changedIndicator = false) {\n  let store = null;\n\n  if (props.isTeam) {\n    store = teamWorkflowStore.update({\n      data,\n      teamId,\n      id: props.workflow.id,\n    });\n  } else {\n    store = workflowStore.update({\n      data,\n      id: props.workflow.id,\n    });\n  }\n\n  return store.then((result) => {\n    emit('update', { data, changedIndicator });\n\n    return result;\n  });\n}\nfunction toggleTestingMode() {\n  if (props.isDataChanged) return;\n\n  updateWorkflow({ testingMode: !props.workflow.testingMode });\n}\nfunction copyWorkflowId() {\n  navigator.clipboard.writeText(props.workflow.id).catch((error) => {\n    console.error(error);\n\n    const textarea = document.createElement('textarea');\n    textarea.value = props.workflow.id;\n    textarea.select();\n    document.execCommand('copy');\n    textarea.blur();\n  });\n}\nfunction updateWorkflowDescription(value) {\n  const keys = ['description', 'category', 'content', 'tag', 'name'];\n  const payload = {};\n\n  keys.forEach((key) => {\n    payload[key] = value[key];\n  });\n\n  updateWorkflow(payload);\n  state.showEditDescription = false;\n}\nasync function saveWorkflow() {\n  try {\n    const flow = props.editor.toObject();\n    flow.edges = flow.edges.map((edge) => {\n      delete edge.sourceNode;\n      delete edge.targetNode;\n\n      return edge;\n    });\n\n    const triggerBlock = flow.nodes.find((node) => node.label === 'trigger');\n    if (!triggerBlock) {\n      toast.error(t('message.noTriggerBlock'));\n      return;\n    }\n\n    await updateWorkflow(\n      {\n        drawflow: flow,\n        trigger: triggerBlock.data,\n        version: browser.runtime.getManifest().version,\n      },\n      false\n    );\n    await registerWorkflowTrigger(props.workflow.id, triggerBlock);\n\n    emit('change', { drawflow: flow });\n  } catch (error) {\n    console.error(error);\n  }\n}\nasync function executeCurrWorkflow() {\n  if (mainStore.settings.editor.saveWhenExecute && props.isDataChanged) {\n    saveWorkflow();\n  }\n\n  RendererWorkflowService.executeWorkflow({\n    ...props.workflow,\n    isTesting: props.isDataChanged,\n  });\n}\nasync function setAsHostWorkflow(isHost) {\n  if (!userStore.user) {\n    dialog.custom('auth', {\n      title: t('auth.title'),\n    });\n    return;\n  }\n\n  state.isUploadingHost = true;\n\n  try {\n    let url = '/me/workflows';\n    let payload = {\n      auth: true,\n    };\n\n    if (isHost) {\n      const workflowPaylod = convertWorkflow(props.workflow, ['id']);\n      workflowPaylod.drawflow = parseJSON(\n        props.workflow.drawflow,\n        props.workflow.drawflow\n      );\n      delete workflowPaylod.extVersion;\n\n      url += `/host`;\n      payload = {\n        auth: true,\n        method: 'POST',\n        body: JSON.stringify({\n          workflow: workflowPaylod,\n        }),\n      };\n    } else {\n      url += `?id=${props.workflow.id}&type=host`;\n      payload.method = 'DELETE';\n    }\n\n    const response = await fetchApi(url, payload);\n    const result = await response.json();\n\n    if (!response.ok) {\n      const error = new Error(result.message);\n      error.data = result.data;\n\n      throw error;\n    }\n\n    if (isHost) {\n      userStore.hostedWorkflows[props.workflow.id] = result;\n    } else {\n      delete userStore.hostedWorkflows[props.workflow.id];\n    }\n\n    // Update cache\n    const userWorkflows = parseJSON('user-workflows', {\n      backup: [],\n      hosted: {},\n    });\n    userWorkflows.hosted = userStore.hostedWorkflows;\n    sessionStorage.setItem('user-workflows', JSON.stringify(userWorkflows));\n\n    state.isUploadingHost = false;\n  } catch (error) {\n    console.error(error);\n    state.isUploadingHost = false;\n    toast.error(error.message);\n  }\n}\nfunction shareWorkflowWithTeam() {\n  emit('modal', 'workflow-share-team');\n}\nfunction shareWorkflow(disabled = false) {\n  if (disabled) return;\n  if (shared.value) {\n    router.push(`/workflows/${props.workflow.id}/shared`);\n    return;\n  }\n\n  if (userStore.user) {\n    emit('modal', 'workflow-share');\n  } else {\n    dialog.custom('auth', {\n      title: t('auth.title'),\n    });\n  }\n}\nfunction deleteFromTeam() {\n  dialog.confirm({\n    async: true,\n    title: 'Delete workflow from team',\n    okVariant: 'danger',\n    body: `Are you sure want to delete the \"${props.workflow.name}\" workflow from this team?`,\n    onConfirm: async () => {\n      try {\n        const response = await fetchApi(\n          `/teams/${teamId}/workflows/${props.workflow.id}`,\n          { method: 'DELETE', auth: true }\n        );\n        const result = await response.json();\n\n        if (!response.ok && response.status !== 404)\n          throw new Error(result.message);\n\n        await teamWorkflowStore.delete(teamId, props.workflow.id);\n        router.replace(`/workflows?active=team&teamId=${teamId}`);\n\n        return true;\n      } catch (error) {\n        toast.error('Something went wrong');\n        console.error(error);\n        return false;\n      }\n    },\n  });\n}\nfunction clearRenameModal() {\n  Object.assign(renameState, {\n    id: '',\n    name: '',\n    description: '',\n    showModal: false,\n  });\n}\nasync function publishWorkflow() {\n  if (!props.canEdit) return;\n\n  const workflowPaylod = convertWorkflow(props.workflow, [\n    'id',\n    'tag',\n    'content',\n  ]);\n  workflowPaylod.drawflow = parseJSON(\n    props.workflow.drawflow,\n    props.workflow.drawflow\n  );\n  delete workflowPaylod.id;\n  delete workflowPaylod.extVersion;\n\n  state.isPublishing = true;\n\n  try {\n    const response = await fetchApi(\n      `/teams/${teamId}/workflows/${props.workflow.id}`,\n      {\n        auth: true,\n        method: 'PATCH',\n        body: JSON.stringify({ workflow: workflowPaylod }),\n      }\n    );\n    const result = await response.json();\n\n    if (!response.ok) {\n      if (response.status === 404) {\n        await teamWorkflowStore.delete(teamId, props.workflow.id);\n        router.replace('/');\n        return;\n      }\n\n      throw new Error(result.message);\n    }\n  } catch (error) {\n    console.error(error);\n    toast.error('Something went wrong');\n  } finally {\n    state.isPublishing = false;\n  }\n}\nfunction initRenameWorkflow() {\n  if (props.isTeam) {\n    state.showEditDescription = true;\n    return;\n  }\n\n  Object.assign(renameState, {\n    showModal: true,\n    name: `${props.workflow.name}`,\n    description: `${props.workflow.description}`,\n  });\n}\nfunction renameWorkflow() {\n  updateWorkflow({\n    name: renameState.name,\n    description: renameState.description,\n  });\n  clearRenameModal();\n}\nfunction deleteWorkflow() {\n  dialog.confirm({\n    title: props.isPackage ? t('common.delete') : t('workflow.delete'),\n    okVariant: 'danger',\n    body: props.isPackage\n      ? `Are you sure want to delete \"${props.workflow.name}\" package?`\n      : t('message.delete', { name: props.workflow.name }),\n    onConfirm: async () => {\n      if (props.isPackage) {\n        await packageStore.delete(props.workflow.id);\n      } else if (props.isTeam) {\n        await teamWorkflowStore.delete(teamId, props.workflow.id);\n      } else {\n        await workflowStore.delete(props.workflow.id);\n      }\n\n      router.replace(props.isPackage ? '/packages' : '/');\n    },\n  });\n}\n\nasync function retrieveTriggerText() {\n  if (props.canEdit) return;\n\n  const triggerBlock = findTriggerBlock(props.workflow.drawflow);\n  if (!triggerBlock) return;\n\n  state.triggerText = await getTriggerText(\n    triggerBlock.data,\n    t,\n    router.currentRoute.value.params.id,\n    true\n  );\n}\nasync function fetchSyncWorkflow() {\n  try {\n    const response = await fetchApi(\n      `/teams/${teamId}/workflows/${props.workflow.id}`,\n      { auth: true }\n    );\n    const result = await response.json();\n\n    if (response.status === 404) {\n      await teamWorkflowStore.delete(teamId, props.workflow.id);\n      router.replace(`/workflows?active=team&teamId=${teamId}`);\n      return;\n    }\n    if (!response.ok) throw new Error(result.message);\n\n    await teamWorkflowStore.update({\n      teamId,\n      data: result,\n      id: props.workflow.id,\n    });\n\n    const convertedData = convertWorkflowData(result);\n    props.editor.setNodes(convertedData.drawflow.nodes || []);\n    props.editor.setEdges(convertedData.drawflow.edges || []);\n    props.editor.fitView();\n\n    await retrieveTriggerText();\n\n    const triggerBlock = convertedData.drawflow.nodes.find(\n      (node) => node.label === 'trigger'\n    );\n    registerWorkflowTrigger(props.workflow.id, triggerBlock);\n    emit('permission');\n  } catch (error) {\n    toast.error(error.message);\n    console.error(error);\n  } finally {\n    state.loadingSync = false;\n    toast.dismiss('sync');\n  }\n}\nasync function syncWorkflow() {\n  state.loadingSync = true;\n\n  if (props.canEdit) {\n    dialog.confirm({\n      title: 'Sync workflow',\n      okText: 'Sync',\n      body: 'This action will overwrite the current workflow with the one that stored in cloud',\n      onConfirm: () => {\n        fetchSyncWorkflow();\n        toast('Syncing workflow...', { timeout: false, id: 'sync' });\n      },\n    });\n  } else {\n    fetchSyncWorkflow();\n  }\n}\n\nretrieveTriggerText();\n\nconst modalActions = [\n  {\n    id: 'table',\n    name: t('workflow.table.title'),\n    icon: 'riTable2',\n  },\n  {\n    id: 'global-data',\n    name: t('common.globalData'),\n    icon: 'riDatabase2Line',\n  },\n  {\n    id: 'settings',\n    name: t('common.settings'),\n    icon: 'riSettings3Line',\n  },\n];\nconst moreActions = [\n  {\n    id: 'export',\n    icon: 'riDownloadLine',\n    name: t('common.export'),\n    action: () => exportWorkflow(props.workflow),\n    hasAccess: props.isTeam ? props.canEdit : true,\n  },\n  {\n    id: 'rename',\n    icon: 'riPencilLine',\n    hasAccess: props.isTeam ? props.canEdit : true,\n    name: props.isTeam ? 'Edit detail' : t('common.rename'),\n    action: initRenameWorkflow,\n  },\n  {\n    id: 'delete',\n    hasAccess: true,\n    action: deleteWorkflow,\n    name: t('common.delete'),\n    icon: 'riDeleteBin7Line',\n    attrs: {\n      class: 'text-red-400 dark:text-red-500',\n    },\n  },\n].filter((item) => item.hasAccess);\n</script>\n"
  },
  {
    "path": "src/components/newtab/workflow/editor/EditorLocalCtxMenu.vue",
    "content": "<template>\n  <ui-popover\n    v-model=\"state.show\"\n    :options=\"state.position\"\n    padding=\"p-3\"\n    @close=\"clearContextMenu\"\n  >\n    <ui-list class=\"w-52 space-y-1\">\n      <ui-list-item\n        v-for=\"item in state.items\"\n        :key=\"item.id\"\n        v-close-popover\n        class=\"cursor-pointer justify-between text-sm\"\n        @click=\"item.event\"\n      >\n        <span>\n          {{ item.name }}\n        </span>\n        <span\n          v-if=\"item.shortcut\"\n          class=\"text-sm capitalize text-gray-600 dark:text-gray-200\"\n        >\n          {{ item.shortcut }}\n        </span>\n      </ui-list-item>\n    </ui-list>\n  </ui-popover>\n</template>\n<script setup>\nimport { onMounted, reactive, markRaw } from 'vue';\nimport { useI18n } from 'vue-i18n';\nimport { excludeGroupBlocks } from '@/utils/shared';\nimport { getReadableShortcut, getShortcut } from '@/composable/shortcut';\n\nconst props = defineProps({\n  editor: {\n    type: Object,\n    default: () => ({}),\n  },\n  isteam: Boolean,\n  packageIo: Boolean,\n  isPackage: Boolean,\n});\nconst emit = defineEmits([\n  'copy',\n  'paste',\n  'group',\n  'ungroup',\n  'recording',\n  'saveBlock',\n  'duplicate',\n  'packageIo',\n]);\n\nconst { t } = useI18n();\nconst state = reactive({\n  show: false,\n  items: [],\n  position: {},\n});\n\nlet ctxData = null;\nconst menuItems = {\n  paste: {\n    id: 'paste',\n    name: t('workflow.editor.paste'),\n    icon: 'riFileCopyLine',\n    shortcut: getReadableShortcut('mod+v'),\n    event: () => emit('paste', ctxData?.position),\n  },\n  delete: {\n    id: 'delete',\n    name: t('common.delete'),\n    icon: 'riDeleteBin7Line',\n    shortcut: 'Del',\n    event: () => {\n      props.editor.removeEdges(ctxData.edges);\n      props.editor.removeNodes(ctxData.nodes);\n    },\n  },\n  saveToFolder: {\n    id: 'saveToFolder',\n    name: t('packages.set'),\n    event: () => {\n      emit('saveBlock', ctxData);\n    },\n  },\n  copy: {\n    id: 'copy',\n    name: t('workflow.editor.copy'),\n    icon: 'riFileCopyLine',\n    event: () => emit('copy', ctxData),\n    shortcut: getReadableShortcut('mod+c'),\n  },\n  group: {\n    id: 'group',\n    name: t('workflow.editor.group'),\n    icon: 'riFolderZipLine',\n    event: () => emit('group', ctxData),\n  },\n  ungroup: {\n    id: 'ungroup',\n    name: t('workflow.editor.ungroup'),\n    icon: 'riFolderOpenLine',\n    event: () => emit('ungroup', ctxData),\n  },\n  duplicate: {\n    id: 'duplicate',\n    name: t('workflow.editor.duplicate'),\n    icon: 'riFileCopyLine',\n    event: () => emit('duplicate', ctxData),\n    shortcut: getShortcut('editor:duplicate-block').readable,\n  },\n  startRecording: {\n    id: 'startRecording',\n    name: 'Record from here',\n    event: () => emit('recording', ctxData),\n  },\n  setAsInput: {\n    id: 'setAsInput',\n    name: 'Set as block input',\n    event: () => emit('packageIo', { type: 'inputs', ...ctxData }),\n  },\n  setAsOutput: {\n    id: 'setAsOutput',\n    name: 'Set as block output',\n    event: () => emit('packageIo', { type: 'outputs', ...ctxData }),\n  },\n};\n\n/* eslint-disable-next-line */\nfunction showCtxMenu(items = [], event) {\n  event.preventDefault();\n  const { clientX, clientY } = event;\n\n  if (props.isPackage && items.includes('saveToFolder')) {\n    items.splice(items.indexOf('saveToFolder'), 1);\n  }\n\n  state.items = items.map((key) => markRaw(menuItems[key]));\n  state.items.unshift(markRaw(menuItems.paste));\n\n  state.position = {\n    getReferenceClientRect: () => ({\n      width: 0,\n      height: 0,\n      top: clientY,\n      right: clientX,\n      bottom: clientY,\n      left: clientX,\n    }),\n  };\n  state.show = true;\n}\nfunction clearContextMenu() {\n  state.show = false;\n  state.items = [];\n  state.position = {};\n}\n\nonMounted(() => {\n  props.editor.onNodeContextMenu(({ event, node }) => {\n    const items = ['copy', 'duplicate', 'saveToFolder', 'delete'];\n    if (node.label === 'blocks-group') {\n      items.splice(items.indexOf('saveToFolder'), 0, 'ungroup');\n    } else if (!excludeGroupBlocks.includes(node.label)) {\n      items.splice(items.indexOf('saveToFolder'), 0, 'group');\n    }\n\n    const currCtxData = {\n      edges: [],\n      nodes: [node],\n      position: { clientX: event.clientX, clientY: event.clientY },\n    };\n\n    if (!props.isTeam && event.target.closest('[data-handleid]')) {\n      const { handleid, nodeid } = event.target.dataset;\n\n      currCtxData.nodeId = nodeid;\n      currCtxData.handleId = handleid;\n\n      const isOutput = event.target.classList.contains('source');\n\n      if (props.isPackage && props.packageIo) {\n        items.unshift(isOutput ? 'setAsOutput' : 'setAsInput');\n      } else if (isOutput) {\n        items.splice(3, 0, 'startRecording');\n      }\n    }\n\n    showCtxMenu(items, event);\n    ctxData = currCtxData;\n  });\n  props.editor.onEdgeContextMenu(({ event, edge }) => {\n    showCtxMenu(['delete'], event);\n    ctxData = { nodes: [], edges: [edge] };\n  });\n  props.editor.onPaneContextMenu((event) => {\n    showCtxMenu([], event);\n    ctxData = {\n      nodes: [],\n      edges: [],\n      position: { clientX: event.clientX, clientY: event.clientY },\n    };\n  });\n  props.editor.onSelectionContextMenu(({ event }) => {\n    showCtxMenu(\n      ['copy', 'duplicate', 'saveToFolder', 'group', 'delete'],\n      event\n    );\n    ctxData = {\n      nodes: props.editor.getSelectedNodes.value,\n      edges: props.editor.getSelectedEdges.value,\n      position: { clientX: event.clientX, clientY: event.clientY },\n    };\n  });\n});\n</script>\n"
  },
  {
    "path": "src/components/newtab/workflow/editor/EditorLocalSavedBlocks.vue",
    "content": "<template>\n  <div class=\"absolute bottom-0 z-50 w-full p-4\">\n    <ui-card class=\"h-full w-full\" padding=\"p-0\">\n      <div class=\"flex items-center p-4\">\n        <ui-input\n          v-model=\"state.query\"\n          :placeholder=\"$t('common.search')\"\n          autofocus\n          autocomplete=\"off\"\n          prepend-icon=\"riSearch2Line\"\n        />\n        <div class=\"grow\" />\n        <ui-button icon @click=\"$emit('close')\">\n          <v-remixicon name=\"riCloseLine\" />\n        </ui-button>\n      </div>\n      <div\n        class=\"scroll mx-4 flex space-x-4 overflow-x-auto pb-4\"\n        style=\"min-height: 95px\"\n      >\n        <p\n          v-if=\"packageStore.packages.length === 0\"\n          class=\"w-full py-8 text-center\"\n        >\n          {{ t('message.noData') }}\n        </p>\n        <div\n          v-for=\"item in items\"\n          :key=\"item.id\"\n          draggable=\"true\"\n          class=\"hoverable relative flex shrink-0 cursor-move flex-col rounded-lg border-2 transition\"\n          style=\"width: 288px; height: 125px\"\n          @dragstart=\"\n            $event.dataTransfer.setData('savedBlocks', JSON.stringify(item))\n          \"\n        >\n          <div class=\"flex flex-1 items-start p-4\">\n            <div\n              v-if=\"item.icon\"\n              :class=\"{ 'mr-2': item.icon.startsWith('http') }\"\n              class=\"w-8 shrink-0\"\n            >\n              <img\n                v-if=\"item.icon.startsWith('http')\"\n                :src=\"item.icon\"\n                width=\"38\"\n                height=\"38\"\n                class=\"rounded-lg\"\n              />\n              <v-remixicon\n                v-else\n                :name=\"item.icon || 'mdiPackageVariantClosed'\"\n              />\n            </div>\n            <div class=\"flex-1 overflow-hidden\">\n              <p class=\"text-overflow font-semibold leading-tight\">\n                {{ item.name }}\n              </p>\n              <p\n                class=\"line-clamp leading-tight text-gray-600 dark:text-gray-200\"\n              >\n                {{ item.description }}\n              </p>\n            </div>\n          </div>\n          <div\n            class=\"flex items-center space-x-3 px-4 pb-4 text-gray-600 dark:text-gray-200\"\n          >\n            <span v-if=\"item.author\" class=\"text-overflow\">\n              By {{ item.author }}\n            </span>\n            <div class=\"grow\" />\n            <a\n              v-if=\"item.isExternal\"\n              :href=\"`https://extension.automa.site/packages/${item.id}`\"\n              target=\"_blank\"\n              title=\"Open package page\"\n            >\n              <v-remixicon name=\"riExternalLinkLine\" size=\"18\" />\n            </a>\n            <ui-popover style=\"height: 18px\">\n              <template #trigger>\n                <v-remixicon\n                  size=\"18\"\n                  class=\"cursor-pointer\"\n                  name=\"riMore2Line\"\n                />\n              </template>\n              <ui-list>\n                <ui-list-item\n                  v-close-popover\n                  class=\"cursor-pointer\"\n                  @click=\"updatePackages(item)\"\n                >\n                  Update packages\n                  <v-remixicon\n                    v-tooltip=\"\n                      'Update the current package inside the workflow.'\n                    \"\n                    class=\"ml-2 -mr-1\"\n                    name=\"riInformationLine\"\n                    size=\"20\"\n                  />\n                </ui-list-item>\n                <ui-list-item\n                  v-close-popover\n                  class=\"cursor-pointer text-red-400 dark:text-red-500\"\n                  @click=\"deleteItem(item)\"\n                >\n                  {{ t('common.delete') }}\n                </ui-list-item>\n              </ui-list>\n            </ui-popover>\n          </div>\n        </div>\n      </div>\n    </ui-card>\n  </div>\n</template>\n<script setup>\nimport { useDialog } from '@/composable/dialog';\nimport { usePackageStore } from '@/stores/package';\nimport { computed, inject, reactive } from 'vue';\nimport { useI18n } from 'vue-i18n';\n\ndefineEmits(['close']);\n\nconst { t } = useI18n();\nconst dialog = useDialog();\nconst packageStore = usePackageStore();\n\nconst state = reactive({\n  query: '',\n});\n\nconst editor = inject('workflow-editor');\n\nconst sortedItems = computed(() =>\n  packageStore.packages.slice().sort((a, b) => b.createdAt - a.createdAt)\n);\nconst items = computed(() => {\n  const query = state.query.toLocaleLowerCase();\n\n  return sortedItems.value.filter((item) =>\n    item.name.toLocaleLowerCase().includes(query)\n  );\n});\n\nfunction deleteItem({ id, name }) {\n  dialog.confirm({\n    title: 'Delete package',\n    body: `Are you sure want to delete \"${name}\" package?`,\n    okText: 'Delete',\n    okVariant: 'danger',\n    onConfirm: () => {\n      packageStore.delete(id);\n    },\n  });\n}\nfunction removeConnections({ id, type, oldEdges, newEdges }) {\n  const removedEdges = [];\n  oldEdges.forEach((edge) => {\n    const isNotDeleted = newEdges.find((item) => item.id === edge.id);\n    if (isNotDeleted) return;\n\n    const handleType = type.slice(0, -1);\n\n    removedEdges.push(`${id}-${handleType}-${edge.id}`);\n  });\n\n  const edgesToRemove = editor.value.getEdges.value.filter(\n    ({ sourceHandle, targetHandle }) => {\n      if (type === 'outputs') {\n        return removedEdges.includes(sourceHandle);\n      }\n\n      return removedEdges.includes(targetHandle);\n    }\n  );\n\n  editor.value.removeEdges(edgesToRemove);\n}\nfunction updatePackages(item) {\n  const packageNodes = editor.value.getNodes.value.filter(\n    (node) => node.data.id === item.id\n  );\n  if (packageNodes.length === 0) return;\n\n  packageNodes.forEach((node) => {\n    removeConnections({\n      id: node.id,\n      type: 'inputs',\n      newEdges: item.inputs,\n      oldEdges: node.data.inputs,\n    });\n    removeConnections({\n      id: node.id,\n      type: 'outputs',\n      newEdges: item.outputs,\n      oldEdges: node.data.outputs,\n    });\n\n    node.data = { ...item };\n  });\n}\n</script>\n"
  },
  {
    "path": "src/components/newtab/workflow/editor/EditorLogs.vue",
    "content": "<template>\n  <div\n    v-if=\"(!logs || logs.length === 0) && workflowStates.length === 0\"\n    class=\"text-center\"\n  >\n    <img src=\"@/assets/svg/files-and-folder.svg\" class=\"mx-auto max-w-sm\" />\n    <p class=\"text-xl font-semibold\">{{ t('message.noData') }}</p>\n  </div>\n  <shared-logs-table\n    :logs=\"logs\"\n    :running=\"workflowStates\"\n    hide-select\n    class=\"w-full\"\n  >\n    <template #item-append=\"{ log: itemLog }\">\n      <td class=\"text-right\">\n        <v-remixicon\n          name=\"riDeleteBin7Line\"\n          class=\"inline-block cursor-pointer text-red-500 dark:text-red-400\"\n          @click=\"deleteLog(itemLog.id)\"\n        />\n      </td>\n    </template>\n  </shared-logs-table>\n</template>\n<script setup>\nimport { computed } from 'vue';\nimport { useI18n } from 'vue-i18n';\nimport dbLogs from '@/db/logs';\nimport { useLiveQuery } from '@/composable/liveQuery';\nimport SharedLogsTable from '@/components/newtab/shared/SharedLogsTable.vue';\n\nconst props = defineProps({\n  workflowId: {\n    type: String,\n    default: '',\n  },\n  workflowStates: {\n    type: Array,\n    default: () => [],\n  },\n});\n\nconst { t } = useI18n();\n\nconst logsArr = useLiveQuery(() =>\n  dbLogs.items.where('workflowId').equals(props.workflowId).toArray()\n);\n\nconst logs = computed(() =>\n  (logsArr.value || []).sort((a, b) => b.endedAt - a.endedAt).slice(0, 14)\n);\n\nfunction deleteLog(logId) {\n  dbLogs.items.delete(logId).then(() => {\n    dbLogs.ctxData.where('logId').equals(logId).delete();\n    dbLogs.histories.where('logId').equals(logId).delete();\n    dbLogs.logsData.where('logId').equals(logId).delete();\n  });\n}\n</script>\n"
  },
  {
    "path": "src/components/newtab/workflow/editor/EditorPkgActions.vue",
    "content": "<template>\n  <ui-card\n    v-if=\"userStore.user\"\n    class=\"pointer-events-auto mr-2 space-x-1\"\n    padding=\"p-1\"\n  >\n    <ui-popover>\n      <template #trigger>\n        <ui-button\n          :class=\"{ 'text-primary': isPkgShared }\"\n          icon\n          btn-type=\"transparent\"\n        >\n          <v-remixicon name=\"riShareLine\" />\n        </ui-button>\n      </template>\n      <div class=\"w-64\">\n        <div class=\"flex items-center\">\n          <p class=\"flex-1\">Share package</p>\n          <ui-spinner\n            v-if=\"state.isSharing || state.isLoadData\"\n            color=\"text-accent\"\n          />\n          <ui-switch\n            v-else\n            v-tooltip:bottom=\"\n              isPkgShared ? 'Unpublish package' : 'Share package'\n            \"\n            :model-value=\"isPkgShared\"\n            @change=\"toggleSharePackage\"\n          />\n        </div>\n        <transition-expand>\n          <ui-input\n            v-if=\"isPkgShared\"\n            :model-value=\"`https://extension.automa.site/packages/${data.id}`\"\n            readonly\n            title=\"URL\"\n            type=\"url\"\n            class=\"mt-2 w-full\"\n            @click=\"$event.target.select()\"\n          />\n        </transition-expand>\n      </div>\n    </ui-popover>\n  </ui-card>\n  <ui-card class=\"pointer-events-auto flex items-center\" padding=\"p-1\">\n    <ui-popover>\n      <template #trigger>\n        <ui-button icon btn-type=\"transparent\">\n          <v-remixicon name=\"riMore2Line\" />\n        </ui-button>\n      </template>\n      <ui-list class=\"space-y-1\" style=\"min-width: 9rem\">\n        <ui-list-item\n          v-close-popover\n          class=\"cursor-pointer text-red-400 dark:text-red-500\"\n          @click=\"deletePackage\"\n        >\n          <v-remixicon name=\"riDeleteBin7Line\" class=\"mr-2 -ml-1\" />\n          <span>\n            {{ t('common.delete') }}\n          </span>\n        </ui-list-item>\n      </ui-list>\n    </ui-popover>\n    <ui-button\n      :title=\"shortcuts['editor:save'].readable\"\n      :variant=\"isPkgShared ? 'default' : 'accent'\"\n      class=\"relative ml-1\"\n      @click=\"savePackage\"\n    >\n      <span\n        v-if=\"isDataChanged\"\n        class=\"absolute top-0 left-0 -ml-1 -mt-1 flex h-3 w-3\"\n      >\n        <span\n          class=\"absolute inline-flex h-full w-full animate-ping rounded-full bg-primary opacity-75\"\n        ></span>\n        <span\n          class=\"relative inline-flex h-3 w-3 rounded-full bg-blue-600\"\n        ></span>\n      </span>\n      <v-remixicon name=\"riSaveLine\" class=\"my-1 mr-2 -ml-1\" />\n      {{ $t('common.save') }}\n    </ui-button>\n    <ui-button\n      v-if=\"isPkgShared\"\n      :loading=\"state.isUpdating\"\n      variant=\"accent\"\n      class=\"ml-4\"\n      @click=\"updateSharedPackage\"\n    >\n      {{ $t('common.update') }}\n    </ui-button>\n  </ui-card>\n</template>\n<script setup>\nimport { useDialog } from '@/composable/dialog';\nimport { getShortcut, useShortcut } from '@/composable/shortcut';\nimport { usePackageStore } from '@/stores/package';\nimport { useUserStore } from '@/stores/user';\nimport { fetchApi } from '@/utils/api';\nimport { computed, onMounted, reactive } from 'vue';\nimport { useI18n } from 'vue-i18n';\nimport { useRouter } from 'vue-router';\nimport { useToast } from 'vue-toastification';\nimport browser from 'webextension-polyfill';\n\nconst props = defineProps({\n  isDataChanged: {\n    type: Boolean,\n    default: false,\n  },\n  data: {\n    type: Object,\n    default: () => ({}),\n  },\n  editor: {\n    type: Object,\n    default: () => ({}),\n  },\n});\nconst emit = defineEmits(['update']);\n\nconst { t } = useI18n();\nconst toast = useToast();\nconst dialog = useDialog();\nconst router = useRouter();\nconst userStore = useUserStore();\nconst packageStore = usePackageStore();\nconst shortcuts = useShortcut([\n  /* eslint-disable-next-line */\n  getShortcut('editor:save', savePackage),\n]);\n\nconst state = reactive({\n  isSharing: false,\n  isUpdating: false,\n  isLoadData: false,\n});\n\nconst isPkgShared = computed(() => packageStore.isShared(props.data.id));\n\nfunction deletePackage() {\n  dialog.confirm({\n    okVariant: 'danger',\n    okText: 'Delete',\n    title: 'Delete package',\n    body: `Are you sure want to delete the \"${props.data.name}\" package?`,\n    onConfirm: () => {\n      packageStore.delete(props.data.id);\n      router.replace('/packages');\n    },\n  });\n}\nfunction updatePackage(data = {}, changedIndicator = false) {\n  return packageStore\n    .update({\n      data,\n      id: props.data.id,\n    })\n    .then((result) => {\n      emit('update', { data, changedIndicator });\n\n      return result;\n    });\n}\nfunction savePackage() {\n  const flow = props.editor.toObject();\n  flow.edges = flow.edges.map((edge) => {\n    delete edge.sourceNode;\n    delete edge.targetNode;\n\n    return edge;\n  });\n\n  updatePackage({ data: flow }, false);\n}\nasync function toggleSharePackage() {\n  state.isSharing = true;\n\n  try {\n    if (!isPkgShared.value) {\n      const keys = [\n        'data',\n        'description',\n        'icon',\n        'id',\n        'content',\n        'inputs',\n        'outputs',\n        'name',\n        'settings',\n      ];\n      const payload = { extVersion: browser.runtime.getManifest().version };\n\n      keys.forEach((key) => {\n        payload[key] = props.data[key];\n      });\n\n      const response = await fetchApi('/packages', {\n        auth: true,\n        method: 'POST',\n        body: JSON.stringify({\n          package: payload,\n        }),\n      });\n      const data = await response.json();\n\n      if (!response.ok) throw new Error(data.message);\n\n      packageStore.insertShared(props.data.id);\n    } else {\n      const response = await fetchApi(`/packages/${props.data.id}`, {\n        auth: true,\n        method: 'DELETE',\n      });\n      const result = await response.json();\n\n      if (!response.ok) throw new Error(result.message);\n\n      packageStore.deleteShared(props.data.id);\n    }\n  } catch (error) {\n    console.error(error);\n    toast.error('Something went wrong');\n  } finally {\n    state.isSharing = false;\n  }\n}\nasync function updateSharedPackage() {\n  try {\n    state.isUpdating = true;\n\n    const keys = [\n      'data',\n      'description',\n      'icon',\n      'content',\n      'inputs',\n      'outputs',\n      'name',\n      'settings',\n    ];\n    const payload = { extVersion: browser.runtime.getManifest().version };\n\n    keys.forEach((key) => {\n      payload[key] = props.data[key];\n    });\n\n    const response = await fetchApi(`/packages/${props.data.id}`, {\n      auth: true,\n      method: 'PATCH',\n      body: JSON.stringify({ package: payload }),\n    });\n    const result = await response.json();\n\n    if (!response.ok) throw new Error(result.message);\n  } catch (error) {\n    console.error(error);\n    toast.error('Something went wrong!');\n  } finally {\n    state.isUpdating = false;\n  }\n}\n\nonMounted(async () => {\n  try {\n    state.isLoadData = true;\n    await packageStore.loadShared();\n  } catch (error) {\n    console.error(error);\n  } finally {\n    state.isLoadData = false;\n  }\n});\n</script>\n"
  },
  {
    "path": "src/components/newtab/workflow/editor/EditorSearchBlocks.vue",
    "content": "<template>\n  <div\n    class=\"ml-2 inline-flex items-center rounded-lg bg-white dark:bg-gray-800\"\n  >\n    <button\n      v-tooltip=\"\n        `${t('workflow.searchBlocks.title')} (${\n          shortcut['editor:search-blocks'].readable\n        })`\n      \"\n      class=\"hoverable rounded-lg p-2\"\n      icon\n      @click=\"toggleActiveSearch\"\n    >\n      <v-remixicon name=\"riSearch2Line\" />\n    </button>\n    <ui-autocomplete\n      ref=\"autocompleteEl\"\n      :model-value=\"state.query\"\n      :items=\"state.autocompleteItems\"\n      :custom-filter=\"searchNodes\"\n      item-key=\"id\"\n      item-label=\"name\"\n      @cancel=\"blurInput\"\n      @select=\"onSelectItem\"\n      @selected=\"onItemSelected\"\n    >\n      <input\n        id=\"search-blocks\"\n        v-model=\"state.query\"\n        :placeholder=\"t('common.search')\"\n        :style=\"{ width: state.active ? '250px' : '0px' }\"\n        type=\"search\"\n        autocomplete=\"off\"\n        class=\"rounded-lg bg-transparent py-2 focus:ring-0\"\n        @focus=\"extractBlocks\"\n        @blur=\"clearState\"\n      />\n      <template #item=\"{ item }\">\n        <div class=\"flex-1 overflow-hidden\">\n          <p class=\"text-overflow\">\n            {{ item.name }}\n          </p>\n          <p\n            class=\"text-overflow text-sm leading-none text-gray-600 dark:text-gray-300\"\n          >\n            {{ item.description }}\n          </p>\n        </div>\n        <span\n          title=\"Block id\"\n          class=\"text-overflow text-center bg-box-transparent w-16 rounded-md p-1 text-xs text-gray-600 dark:text-gray-300\"\n        >\n          {{ item.id }}\n        </span>\n      </template>\n    </ui-autocomplete>\n  </div>\n</template>\n<script setup>\n/* eslint-disable vue/no-mutating-props */\nimport { reactive, ref } from 'vue';\nimport { useI18n } from 'vue-i18n';\nimport { useShortcut } from '@/composable/shortcut';\n\nconst props = defineProps({\n  editor: {\n    type: Object,\n    default: () => ({}),\n  },\n});\n\nconst { t } = useI18n();\n\nconst initialState = {\n  rectX: 0,\n  rectY: 0,\n  position: {\n    x: 0,\n    y: 0,\n    zoom: 1,\n  },\n};\n\nconst autocompleteEl = ref(null);\nconst state = reactive({\n  query: '',\n  active: false,\n  selected: false,\n  autocompleteItems: [],\n});\n\nconst shortcut = useShortcut('editor:search-blocks', () => {\n  state.active = true;\n  document.querySelector('#search-blocks')?.focus();\n});\n\nfunction searchNodes({ item, text }) {\n  const isMatch = (str) =>\n    str.toLocaleLowerCase().includes(text.toLocaleLowerCase());\n\n  return isMatch(item.id) || isMatch(item.name) || isMatch(item.description);\n}\nfunction toggleActiveSearch() {\n  state.active = !state.active;\n\n  if (state.active) {\n    document.querySelector('#search-blocks')?.focus();\n  }\n}\nfunction extractBlocks() {\n  const editorContainer = document.querySelector('.vue-flow');\n  editorContainer.classList.add('add-transition');\n  const { width, height } = editorContainer.getBoundingClientRect();\n\n  initialState.rectX = width / 2;\n  initialState.rectY = height / 2;\n  initialState.position = props.editor.getTransform();\n\n  state.autocompleteItems = props.editor.getNodes.value.map(\n    ({ computedPosition, id, data, label }) => ({\n      id,\n      position: computedPosition,\n      description: data.description || '',\n      name: t(`workflow.blocks.${label}.name`),\n    })\n  );\n}\nfunction clearHighlightedNodes() {\n  document.querySelectorAll('.search-select-node').forEach((el) => {\n    el.classList.remove('search-select-node');\n  });\n}\nfunction clearState() {\n  if (!state.selected) {\n    props.editor.setTransform(initialState.position);\n  }\n\n  state.query = '';\n  state.active = false;\n  state.selected = false;\n\n  Object.assign(initialState, {\n    rectX: 0,\n    rectY: 0,\n    position: {\n      x: 0,\n      y: 0,\n      zoom: 1,\n    },\n  });\n\n  autocompleteEl.value.state.showPopover = false;\n  clearHighlightedNodes();\n\n  setTimeout(() => {\n    const editorContainer = document.querySelector('.vue-flow');\n    editorContainer.classList.remove('add-transition');\n  }, 500);\n}\nfunction blurInput() {\n  document.querySelector('#search-blocks')?.blur();\n}\nfunction onSelectItem({ item }) {\n  const { x, y } = item.position;\n  const { rectX, rectY } = initialState;\n\n  clearHighlightedNodes();\n  document\n    .querySelector(`[data-id=\"${item.id}\"]`)\n    ?.classList.add('search-select-node');\n\n  props.editor.setTransform({\n    zoom: 1,\n    x: -(x - rectX),\n    y: -(y - rectY),\n  });\n}\nfunction onItemSelected(event) {\n  state.selected = true;\n\n  const node = props.editor.getNode.value(event.item.id);\n  props.editor.addSelectedNodes([node]);\n\n  onSelectItem(event);\n  blurInput();\n}\n</script>\n<style scoped>\ninput {\n  transition: width 300ms ease;\n}\n</style>\n<style>\n.search-select-node > div {\n  @apply ring-4;\n}\n.vue-flow.add-transition .vue-flow__transformationpane {\n  transition: transform 250ms ease;\n}\n</style>\n"
  },
  {
    "path": "src/components/newtab/workflow/editor/EditorUsedCredentials.vue",
    "content": "<template>\n  <ui-card\n    v-if=\"credentials.length > 0\"\n    padding=\"p-1\"\n    class=\"pointer-events-auto mr-4\"\n  >\n    <ui-popover v-tooltip=\"t('credential.use.title')\" @show=\"checkCredentials\">\n      <template #trigger>\n        <button class=\"hoverable rounded-lg p-2 transition\">\n          <v-remixicon name=\"riKey2Line\" />\n        </button>\n      </template>\n      <div class=\"w-64\">\n        <p class=\"leading-tight\">\n          {{ t('credential.use.description') }}\n        </p>\n        <ui-list class=\"scroll mt-2 overflow-auto\" style=\"max-height: 400px\">\n          <ui-list-item\n            v-for=\"item in credentials\"\n            :key=\"item.nodeId\"\n            style=\"align-items: flex-start\"\n            small\n            class=\"group\"\n          >\n            <div class=\"mr-2 flex-1\">\n              <p\n                title=\"Jump to block\"\n                class=\"cursor-pointer text-sm text-gray-600 dark:text-gray-200\"\n                @click=\"jumpToBlock(item.nodeId)\"\n              >\n                {{ item.nodeName }}\n              </p>\n              <ul v-for=\"name in item.items\" :key=\"name\">\n                <li :title=\"`Credential name: ${name}`\">\n                  <p class=\"text-overflow\">- {{ name }}</p>\n                </li>\n              </ul>\n            </div>\n            <v-remixicon\n              name=\"riArrowGoForwardLine\"\n              size=\"18\"\n              title=\"Jump to block\"\n              class=\"invisible cursor-pointer text-gray-600 group-hover:visible dark:text-gray-200\"\n              @click=\"jumpToBlock(item.nodeId)\"\n            />\n          </ui-list-item>\n        </ui-list>\n      </div>\n    </ui-popover>\n  </ui-card>\n</template>\n<script setup>\nimport { ref, onMounted } from 'vue';\nimport { useI18n } from 'vue-i18n';\nimport objectPath from 'object-path';\nimport { getBlocks } from '@/utils/getSharedData';\n\nconst props = defineProps({\n  editor: {\n    type: Object,\n    default: () => ({}),\n  },\n});\n\nconst blocks = getBlocks();\n\nconst { t } = useI18n();\n\nconst credentials = ref([]);\n\nfunction checkCredentials() {\n  const regex = /\\{\\{\\s*secrets@(.*?)\\}\\}/;\n  const tempCreds = [];\n\n  props.editor.getNodes.value.forEach(({ label, id, data }) => {\n    const keys = blocks[label]?.refDataKeys;\n    if (!keys || !data) return;\n\n    const usedCredentials = new Set();\n\n    keys.forEach((key) => {\n      const str = objectPath.get(data, key);\n      const match = str?.match?.(regex);\n      if (!match || !match[1]) return;\n\n      usedCredentials.add(match[1]);\n    });\n\n    if (usedCredentials.size > 0) {\n      tempCreds.push({\n        nodeId: id,\n        items: Array.from(usedCredentials),\n        nodeName: t(`workflow.blocks.${label}.name`),\n      });\n    }\n  });\n\n  credentials.value = tempCreds;\n}\nfunction jumpToBlock(nodeId) {\n  const node = props.editor.getNode.value(nodeId);\n  if (!node) return;\n\n  const { x, y } = node.computedPosition;\n  const editorContainer = document.querySelector('.vue-flow');\n  const { width, height } = editorContainer.getBoundingClientRect();\n  editorContainer.classList.add('add-transition');\n\n  props.editor.setTransform({\n    zoom: 1,\n    x: -(x - width / 2),\n    y: -(y - height / 2),\n  });\n  node.selected = true;\n\n  setTimeout(() => {\n    editorContainer.classList.remove('add-transition');\n  }, 300);\n}\n\nonMounted(checkCredentials);\n</script>\n"
  },
  {
    "path": "src/components/newtab/workflow/settings/SettingsBlocks.vue",
    "content": "<template>\n  <div class=\"flex items-center\">\n    <div class=\"mr-4 flex-1\">\n      <p>\n        {{ t('workflow.settings.blockDelay.title') }}\n      </p>\n      <p class=\"text-sm leading-tight text-gray-600 dark:text-gray-200\">\n        {{ t('workflow.settings.blockDelay.description') }}\n      </p>\n    </div>\n    <ui-input\n      :model-value=\"settings.blockDelay\"\n      type=\"number\"\n      @change=\"updateSetting('blockDelay', +$event)\"\n    />\n  </div>\n  <div class=\"flex items-center pt-4\">\n    <div class=\"mr-4 flex-1\">\n      <p>\n        {{ t('workflow.settings.tabLoadTimeout.title') }}\n      </p>\n      <p class=\"text-sm leading-tight text-gray-600 dark:text-gray-200\">\n        {{ t('workflow.settings.tabLoadTimeout.description') }}\n      </p>\n    </div>\n    <ui-input\n      :model-value=\"settings.tabLoadTimeout\"\n      type=\"number\"\n      min=\"0\"\n      max=\"60000\"\n      @change=\"updateSetting('tabLoadTimeout', +$event)\"\n    />\n  </div>\n</template>\n<script setup>\nimport { useI18n } from 'vue-i18n';\n\ndefineProps({\n  settings: {\n    type: Object,\n    default: () => ({}),\n  },\n});\nconst emit = defineEmits(['update']);\n\nconst { t } = useI18n();\n\nfunction updateSetting(key, value) {\n  emit('update', { key, value });\n}\n</script>\n"
  },
  {
    "path": "src/components/newtab/workflow/settings/SettingsEvents.vue",
    "content": "<template>\n  <div>\n    <div class=\"flex items-center\">\n      <p class=\"flex-1\">{{ t('workflow.events.description') }}</p>\n      <ui-button variant=\"accent\" @click=\"updateModalState({ show: true })\">\n        {{ t('workflow.events.add-action') }}\n      </ui-button>\n    </div>\n    <ui-list class=\"mt-4 space-y-1\">\n      <ui-list-item\n        v-for=\"action in settings.events\"\n        :key=\"action.id\"\n        class=\"gap-2 group\"\n      >\n        <div class=\"flex-1 overflow-hidden\">\n          <p class=\"text-overflow\">{{ action.name || 'Untitled action' }}</p>\n          <div\n            v-for=\"event in action.events\"\n            :key=\"event\"\n            :class=\"[\n              WORKFLOW_EVENTS_CLASSES[event],\n              'border rounded-md px-2 py-1 text-xs inline-flex items-center mr-0.5',\n            ]\"\n          >\n            {{ t(`workflow.events.types.${event}.name`) }}\n          </div>\n        </div>\n        <v-remixicon\n          name=\"riPencilLine\"\n          class=\"group-hover:visible invisible cursor-pointer\"\n          @click=\"\n            Object.assign(actionModal, {\n              show: true,\n              type: 'edit',\n              data: cloneDeep(action),\n            })\n          \"\n        />\n        <v-remixicon\n          name=\"riDeleteBin7Line\"\n          class=\"group-hover:visible invisible cursor-pointer text-red-500 dark:text-red-400\"\n          @click=\"\n            emit('update', {\n              key: 'events',\n              value: settings.events.filter((item) => item.id !== action.id),\n            })\n          \"\n        />\n      </ui-list-item>\n    </ui-list>\n    <ui-modal\n      v-model=\"actionModal.show\"\n      persist\n      :title=\"t('workflow.events.add-action')\"\n      content-class=\"max-w-xl\"\n      @close=\"updateModalState({})\"\n    >\n      <ui-input\n        v-model=\"actionModal.data.name\"\n        :label=\"t('common.name')\"\n        placeholder=\"Untitled\"\n        autofocus\n        class=\"w-full\"\n      />\n      <p class=\"mt-4\">{{ t('workflow.events.event', 2) }}</p>\n      <div class=\"mt-1 flex flex-wrap items-center space-x-2\">\n        <div\n          v-for=\"(event, index) in actionModal.data.events\"\n          :key=\"event\"\n          :class=\"[\n            WORKFLOW_EVENTS_CLASSES[event],\n            'border rounded-lg px-3 text-sm h-8 inline-flex items-center',\n          ]\"\n        >\n          <p class=\"flex-1\">{{ t(`workflow.events.types.${event}.name`) }}</p>\n          <v-remixicon\n            name=\"riCloseLine\"\n            height=\"20\"\n            width=\"20\"\n            class=\"text-gray-200 dark:text-gray-600 ml-1 -mr-1 cursor-pointer\"\n            @click=\"actionModal.data.events.splice(index, 1)\"\n          />\n        </div>\n        <ui-popover\n          v-if=\"WORKFLOW_EVENTS.length !== actionModal.data.events.length\"\n        >\n          <template #trigger>\n            <ui-button class=\"!h-8 !px-3\">\n              <v-remixicon\n                name=\"riAddLine\"\n                class=\"-ml-1 mr-2\"\n                height=\"20\"\n                width=\"20\"\n              />\n              <p class=\"text-sm\">{{ t('common.add') }}</p>\n            </ui-button>\n          </template>\n          <ui-list>\n            <ui-list-item\n              v-for=\"event in WORKFLOW_EVENTS.filter(\n                (item) => !actionModal.data.events.includes(item)\n              )\"\n              :key=\"event\"\n              small\n              class=\"cursor-pointer !items-stretch\"\n              @click=\"actionModal.data.events.push(event)\"\n            >\n              <div\n                :class=\"[\n                  WORKFLOW_EVENTS_CLASSES[event],\n                  'w-2 flex-shrink-0 rounded-full',\n                ]\"\n              ></div>\n              <div class=\"text-sm ml-2\">\n                <p>{{ t(`workflow.events.types.${event}.name`) }}</p>\n                <p class=\"text-gray-600 dark:text-gray-300 leading-tight\">\n                  {{ t(`workflow.events.types.${event}.description`) }}\n                </p>\n              </div>\n            </ui-list-item>\n          </ui-list>\n        </ui-popover>\n      </div>\n      <p class=\"mt-4\">{{ t('workflow.events.action') }}</p>\n      <ui-select\n        :model-value=\"actionModal.data.action.type\"\n        class=\"mt-1 w-full\"\n        @change=\"actionModal.data.action = EVENT_ACTIONS[$event]\"\n      >\n        <option\n          v-for=\"action in Object.keys(EVENT_ACTIONS)\"\n          :key=\"action\"\n          :value=\"action\"\n        >\n          {{ t(`workflow.events.actions.${action}.title`) }}\n        </option>\n      </ui-select>\n      <div class=\"mt-2\">\n        <component\n          :is=\"EVENT_ACTIONS_COMP[actionModal.data.action.type]\"\n          :data=\"actionModal.data.action\"\n          @update:data=\"Object.assign(actionModal.data.action, $event)\"\n        />\n      </div>\n      <div class=\"mt-6 flex justify-end space-x-4\">\n        <ui-button @click=\"actionModal.show = false\">\n          {{ t('common.cancel') }}\n        </ui-button>\n        <ui-button\n          :disabled=\"actionModal.data.events.length === 0\"\n          variant=\"accent\"\n          class=\"min-w-[90px]\"\n          @click=\"upsertAction\"\n        >\n          {{\n            actionModal.type === 'edit' ? t('common.update') : t('common.add')\n          }}\n        </ui-button>\n      </div>\n    </ui-modal>\n  </div>\n</template>\n<script setup>\nimport { reactive } from 'vue';\nimport { useI18n } from 'vue-i18n';\nimport { nanoid } from 'nanoid';\nimport cloneDeep from 'lodash.clonedeep';\nimport EventCodeHTTP from './event/EventCodeHTTP.vue';\nimport EventCodeAction from './event/EventCodeAction.vue';\n\nconst props = defineProps({\n  settings: {\n    type: Object,\n    default: () => ({}),\n  },\n});\nconst emit = defineEmits(['update']);\n\nconst { t } = useI18n();\n\nconst EVENT_ACTIONS = {\n  'http-request': {\n    type: 'http-request',\n    url: '',\n    body: '{\\n\\t\"workflowStatus\": {{workflow.status}},\\n\\t\"workflowLogs\": {{workflow.logs}},\\n\\t\"errorMessage\": {{workflow.errorMessage}}\\n}',\n    headers: [],\n    method: 'POST',\n  },\n  'js-code': {\n    code: \"const workflow = automaRefData('workflow');\\nconsole.log(\\n\\tworkflow.status,\\n\\tworkflow.logs,\\n\\tworkflow.errorMessage\\n)\",\n    type: 'js-code',\n  },\n};\nconst EVENT_ACTIONS_COMP = {\n  'js-code': EventCodeAction,\n  'http-request': EventCodeHTTP,\n};\nconst WORKFLOW_EVENTS_CLASSES = {\n  'finish:success': 'bg-green-300 dark:text-black dark:bg-green-200',\n  'finish:failed': 'bg-red-300 dark:text-black dark:bg-red-200',\n};\nconst WORKFLOW_EVENTS = ['finish:success', 'finish:failed'];\n\nconst defaultActionModal = {\n  type: 'add',\n  show: false,\n  data: {\n    name: '',\n    events: [],\n    action: EVENT_ACTIONS['http-request'],\n  },\n};\n\nconst actionModal = reactive({ ...cloneDeep(defaultActionModal) });\n\nfunction updateModalState(detail) {\n  Object.assign(actionModal, { ...cloneDeep(defaultActionModal), ...detail });\n}\nfunction upsertAction() {\n  let copyEvents = [...(props.settings.events ?? [])];\n\n  if (actionModal.type === 'add') {\n    copyEvents.push({\n      id: nanoid(),\n      ...actionModal.data,\n      name: actionModal.data.name || 'Untitled action',\n    });\n  } else {\n    copyEvents = copyEvents.map((event) => {\n      if (event.id !== actionModal.data.id) return event;\n\n      return actionModal.data;\n    });\n  }\n\n  updateModalState({ show: false });\n\n  emit('update', { key: 'events', value: copyEvents });\n}\n</script>\n"
  },
  {
    "path": "src/components/newtab/workflow/settings/SettingsGeneral.vue",
    "content": "<template>\n  <div class=\"flex items-center\">\n    <div class=\"mr-4 flex-1\">\n      <p>\n        {{ t('workflow.settings.onError.title') }}\n      </p>\n      <p class=\"text-sm leading-tight text-gray-600 dark:text-gray-200\">\n        {{ t('workflow.settings.onError.description') }}\n      </p>\n    </div>\n    <ui-select\n      :model-value=\"settings.onError\"\n      @change=\"updateSetting('onError', $event)\"\n    >\n      <option v-for=\"item in onError\" :key=\"item.id\" :value=\"item.id\">\n        {{ t(`workflow.settings.onError.items.${item.name}`) }}\n      </option>\n    </ui-select>\n    <div\n      v-if=\"settings.onError === 'restart-workflow'\"\n      :title=\"t('workflow.settings.restartWorkflow.description')\"\n      class=\"bg-input ml-4 flex items-center rounded-lg transition-colors\"\n    >\n      <input\n        :value=\"settings.restartTimes ?? 3\"\n        type=\"number\"\n        class=\"w-12 appearance-none rounded-lg bg-transparent py-2 pl-2 text-right\"\n        @input=\"updateSetting('restartTimes', +($event.target.value ?? 3))\"\n      />\n      <span class=\"px-2 text-sm\">\n        {{ t('workflow.settings.restartWorkflow.times') }}\n      </span>\n    </div>\n  </div>\n  <div v-if=\"!isFirefox\" class=\"flex items-center pt-4\">\n    <div class=\"mr-4 flex-1\">\n      <p>Workflow Execution</p>\n      <p class=\"text-sm leading-tight text-gray-600 dark:text-gray-200\">\n        Workflow execution environment (Use \"Popup\" if workflow runs more than 5\n        minutes)\n      </p>\n    </div>\n    <a\n      href=\"https://docs.extension.automa.site/workflow/settings.html#workflow-execution\"\n      class=\"mr-2\"\n      target=\"_blank\"\n    >\n      <v-remixicon name=\"riInformationLine\" />\n    </a>\n    <ui-select\n      :model-value=\"settings.execContext || 'popup'\"\n      @change=\"updateSetting('execContext', $event)\"\n    >\n      <option value=\"popup\">Popup</option>\n      <option value=\"background\">Background</option>\n    </ui-select>\n  </div>\n  <div class=\"flex items-center pt-4\">\n    <div class=\"mr-4 flex-1\">\n      <p>\n        {{ t('workflow.settings.notification.title') }}\n      </p>\n      <p class=\"text-sm leading-tight text-gray-600 dark:text-gray-200\">\n        {{\n          t(\n            `workflow.settings.notification.${\n              permissions.has.notifications ? 'description' : 'noPermission'\n            }`\n          )\n        }}\n      </p>\n    </div>\n    <ui-switch\n      v-if=\"permissions.has.notifications\"\n      :model-value=\"settings.notification\"\n      @change=\"updateSetting('notification', $event)\"\n    />\n    <ui-button v-else @click=\"permissions.request(true)\">\n      {{ t('workflow.blocks.clipboard.grantPermission') }}\n    </ui-button>\n  </div>\n  <div\n    v-for=\"item in settingItems\"\n    :key=\"item.id\"\n    class=\"flex items-center pt-4\"\n  >\n    <div class=\"mr-4 flex-1\">\n      <p>\n        {{ item.name }}\n      </p>\n      <p\n        v-if=\"item.notSupport?.includes(browserType)\"\n        class=\"text-sm leading-tight text-red-400 dark:text-red-300\"\n      >\n        {{\n          t('log.messages.browser-not-supported', {\n            browser: browserType,\n          })\n        }}\n      </p>\n      <p v-else class=\"text-sm leading-tight text-gray-600 dark:text-gray-200\">\n        {{ item.description }}\n      </p>\n    </div>\n    <ui-switch\n      v-if=\"!item.notSupport?.includes(browserType)\"\n      :disabled=\"item.disabled\"\n      :model-value=\"settings[item.id]\"\n      @change=\"updateSetting(item.id, $event)\"\n    />\n  </div>\n  <div class=\"flex items-center pt-4\">\n    <div class=\"mr-4 flex-1\">\n      <p>\n        {{ t('workflow.settings.clearCache.title') }}\n      </p>\n      <p class=\"text-sm leading-tight text-gray-600 dark:text-gray-200\">\n        {{ t('workflow.settings.clearCache.description') }}\n      </p>\n    </div>\n    <ui-button @click=\"onClearCacheClick\">\n      {{ t('workflow.settings.clearCache.btn') }}\n    </ui-button>\n  </div>\n  <div class=\"flex items-center pt-4\">\n    <div class=\"mr-4 flex-1\">\n      <p>\n        {{ t('workflow.settings.publicId.title') }}\n      </p>\n      <p class=\"text-sm leading-tight text-gray-600 dark:text-gray-200\">\n        {{ t('workflow.settings.publicId.description') }}\n      </p>\n    </div>\n    <a\n      href=\"https://docs.extension.automa.site/blocks/trigger.html#trigger-using-js-customevent\"\n      target=\"_blank\"\n      rel=\"noopener\"\n      class=\"mr-2 text-gray-600 dark:text-gray-200\"\n    >\n      <v-remixicon name=\"riInformationLine\" />\n    </a>\n    <ui-input\n      :model-value=\"settings.publicId\"\n      placeholder=\"myWorkflowPublicId\"\n      @change=\"updateSetting('publicId', $event)\"\n    />\n  </div>\n</template>\n<script setup>\nimport { useI18n } from 'vue-i18n';\nimport { useToast } from 'vue-toastification';\n// import browser from 'webextension-polyfill';\nimport { useHasPermissions } from '@/composable/hasPermissions';\nimport { clearCache } from '@/utils/helper';\n\nconst props = defineProps({\n  settings: {\n    type: Object,\n    default: () => ({}),\n  },\n});\nconst emit = defineEmits(['update']);\n\nconst { t } = useI18n();\nconst toast = useToast();\nconst permissions = useHasPermissions(['notifications']);\n\nconst isFirefox = BROWSER_TYPE === 'firefox';\n// const isMV2 = browser.runtime.getManifest().manifest_version === 2;\n\nconst browserType = BROWSER_TYPE;\nconst onError = [\n  {\n    id: 'keep-running',\n    name: 'keepRunning',\n  },\n  {\n    id: 'stop-workflow',\n    name: 'stopWorkflow',\n  },\n  {\n    id: 'restart-workflow',\n    name: 'restartWorkflow',\n  },\n];\nconst settingItems = [\n  {\n    id: 'debugMode',\n    notSupport: ['firefox'],\n    name: t('workflow.settings.debugMode.title'),\n    description: t('workflow.settings.debugMode.description'),\n  },\n  {\n    id: 'inputAutocomplete',\n    name: t('workflow.settings.autocomplete.title'),\n    description: t('workflow.settings.autocomplete.description'),\n  },\n  {\n    id: 'reuseLastState',\n    name: t('workflow.settings.reuseLastState.title'),\n    description: t('workflow.settings.reuseLastState.description'),\n  },\n  {\n    id: 'saveLog',\n    name: t('workflow.settings.saveLog'),\n    description: '',\n  },\n  {\n    id: 'executedBlockOnWeb',\n    name: t('workflow.settings.executedBlockOnWeb'),\n    description: '',\n  },\n];\n\nasync function onClearCacheClick() {\n  const cacheCleared = await clearCache(props.workflow);\n  if (cacheCleared) toast(t('workflow.settings.clearCache.info'));\n}\nfunction updateSetting(key, value) {\n  emit('update', { key, value });\n}\n</script>\n"
  },
  {
    "path": "src/components/newtab/workflow/settings/SettingsTable.vue",
    "content": "<template>\n  <div class=\"flex items-center\">\n    <div class=\"grow\">\n      <p>\n        {{ t('workflow.settings.defaultColumn.title') }}\n      </p>\n      <p class=\"text-sm leading-tight text-gray-600 dark:text-gray-200\">\n        {{ t('workflow.settings.defaultColumn.description') }}\n      </p>\n    </div>\n    <ui-switch\n      :model-value=\"settings.insertDefaultColumn\"\n      @change=\"updateSetting('insertDefaultColumn', $event)\"\n    />\n  </div>\n  <transition-expand>\n    <div v-if=\"settings.insertDefaultColumn\" class=\"flex items-center pt-4\">\n      <p class=\"flex-1\">\n        {{ t('workflow.settings.defaultColumn.name') }}\n      </p>\n      <ui-input\n        :model-value=\"settings.defaultColumnName\"\n        :title=\"t('workflow.settings.defaultColumn.name')\"\n        @change=\"updateSetting('defaultColumnName', $event)\"\n      />\n    </div>\n  </transition-expand>\n</template>\n<script setup>\nimport { useI18n } from 'vue-i18n';\n\ndefineProps({\n  settings: {\n    type: Object,\n    default: () => ({}),\n  },\n});\nconst emit = defineEmits(['update']);\n\nconst { t } = useI18n();\n\nfunction updateSetting(key, value) {\n  emit('update', { key, value });\n}\n</script>\n"
  },
  {
    "path": "src/components/newtab/workflow/settings/event/EventCodeAction.vue",
    "content": "<template>\n  <shared-codemirror\n    :model-value=\"data.code\"\n    class=\"h-full w-full\"\n    @change=\"$emit('update:data', { code: $event })\"\n  />\n</template>\n<script setup>\nimport { defineAsyncComponent } from 'vue';\n\nconst SharedCodemirror = defineAsyncComponent(() =>\n  import('@/components/newtab/shared/SharedCodemirror.vue')\n);\n\ndefineProps({\n  data: {\n    type: Object,\n    default: () => ({}),\n  },\n});\ndefineEmits(['update:data']);\n</script>\n"
  },
  {
    "path": "src/components/newtab/workflow/settings/event/EventCodeHTTP.vue",
    "content": "<template>\n  <div class=\"flex items-center gap-2\">\n    <ui-select\n      :model-value=\"data.method\"\n      @change=\"emitData({ method: $event })\"\n    >\n      <option\n        v-for=\"method in ['GET', 'PUT', 'POST', 'PATCH', 'DELETE']\"\n        :key=\"method\"\n        :value=\"method\"\n      >\n        {{ method }}\n      </option>\n    </ui-select>\n    <ui-input\n      :model-value=\"data.url\"\n      placeholder=\"URL\"\n      type=\"url\"\n      class=\"flex-1\"\n      @change=\"emitData({ url: $event })\"\n    />\n  </div>\n  <ui-tabs v-model=\"activeTab\" class=\"mt-1\">\n    <ui-tab value=\"headers\">\n      {{ t('workflow.blocks.webhook.tabs.headers') }}\n    </ui-tab>\n    <ui-tab v-if=\"data.method !== 'GET'\" value=\"body\">\n      {{ t('workflow.blocks.webhook.tabs.body') }}\n    </ui-tab>\n  </ui-tabs>\n  <ui-tab-panels v-model=\"activeTab\">\n    <ui-tab-panel value=\"headers\">\n      <div class=\"mt-4 grid grid-cols-7 justify-items-center gap-2\">\n        <template v-for=\"(header, index) in data.headers\" :key=\"index\">\n          <ui-input\n            v-model=\"header.name\"\n            :title=\"header.name\"\n            :placeholder=\"`Header ${index + 1}`\"\n            type=\"text\"\n            class=\"col-span-3\"\n          />\n          <ui-input\n            v-model=\"header.value\"\n            :title=\"header.value\"\n            placeholder=\"Value\"\n            type=\"text\"\n            class=\"col-span-3\"\n          />\n          <button\n            @click=\"\n              emitData({\n                headers: data.headers.filter((_, idx) => idx !== index),\n              })\n            \"\n          >\n            <v-remixicon name=\"riCloseCircleLine\" size=\"20\" />\n          </button>\n        </template>\n      </div>\n      <ui-button\n        class=\"mt-2\"\n        @click=\"\n          emitData({ headers: [...data.headers, { name: '', value: '' }] })\n        \"\n      >\n        <span> {{ t('workflow.blocks.webhook.buttons.header') }} </span>\n      </ui-button>\n    </ui-tab-panel>\n    <ui-tab-panel value=\"body\" class=\"mt-4\">\n      <shared-codemirror\n        :model-value=\"data.body\"\n        lang=\"json\"\n        class=\"h-full w-full\"\n        @change=\"emitData({ body: $event })\"\n      />\n    </ui-tab-panel>\n  </ui-tab-panels>\n</template>\n<script setup>\nimport { useI18n } from 'vue-i18n';\nimport { defineAsyncComponent, shallowRef } from 'vue';\n\nconst SharedCodemirror = defineAsyncComponent(() =>\n  import('@/components/newtab/shared/SharedCodemirror.vue')\n);\n\ndefineProps({\n  data: {\n    type: Object,\n    default: () => ({}),\n  },\n});\nconst emit = defineEmits(['update:data']);\n\nconst { t } = useI18n();\n\nconst activeTab = shallowRef('headers');\n\nfunction emitData(data) {\n  emit('update:data', data);\n}\n</script>\n"
  },
  {
    "path": "src/components/newtab/workflows/WorkflowsFolder.vue",
    "content": "<template>\n  <div class=\"mt-6 border-t pt-4\">\n    <div class=\"flex items-center text-gray-600 dark:text-gray-300\">\n      <span class=\"flex-1\"> Folders </span>\n      <button\n        class=\"rounded-md transition hover:text-black dark:hover:text-gray-100\"\n        @click=\"newFolder\"\n      >\n        <v-remixicon\n          size=\"20\"\n          name=\"riAddLine\"\n          class=\"inline-block align-sub\"\n        />\n        <span>{{ t('common.new') }}</span>\n      </button>\n    </div>\n    <ui-list class=\"mt-2 space-y-1\">\n      <ui-list-item\n        small\n        class=\"cursor-pointer\"\n        :active=\"modelValue === ''\"\n        @dragover=\"onDragover($event, true)\"\n        @dragleave=\"onDragover($event, false)\"\n        @drop=\"onWorkflowsDrop($event, '')\"\n        @click=\"$emit('update:modelValue', '')\"\n      >\n        <v-remixicon name=\"riFolderLine\" class=\"mr-2\" />\n        <p class=\"text-overflow flex-1\">All</p>\n      </ui-list-item>\n      <ui-list-item\n        v-for=\"folder in folders\"\n        :key=\"folder.id\"\n        :active=\"folder.id === modelValue\"\n        small\n        class=\"group cursor-pointer overflow-hidden\"\n        @dragover=\"onDragover($event, true)\"\n        @dragleave=\"onDragover($event, false)\"\n        @drop=\"onWorkflowsDrop($event, folder.id)\"\n        @click=\"$emit('update:modelValue', folder.id)\"\n      >\n        <v-remixicon name=\"riFolderLine\" class=\"mr-2\" />\n        <p class=\"text-overflow flex-1\">\n          {{ folder.name }}\n        </p>\n        <ui-popover class=\"leading-none\">\n          <template #trigger>\n            <v-remixicon\n              name=\"riMoreLine\"\n              class=\"invisible cursor-pointer group-hover:visible\"\n            />\n          </template>\n          <ui-list class=\"w-36 space-y-1\">\n            <ui-list-item\n              v-close-popover\n              class=\"cursor-pointer\"\n              @click=\"exportFolderWorkflows(folder.id)\"\n            >\n              <v-remixicon name=\"riDownloadLine\" class=\"mr-2 -ml-1\" />\n              <span>\n                {{ t('common.export') }}\n              </span>\n            </ui-list-item>\n            <ui-list-item\n              v-close-popover\n              class=\"cursor-pointer\"\n              @click=\"renameFolder(folder)\"\n            >\n              <v-remixicon name=\"riPencilLine\" class=\"mr-2 -ml-1\" />\n              <span>\n                {{ t('common.rename') }}\n              </span>\n            </ui-list-item>\n            <ui-list-item\n              v-close-popover\n              class=\"cursor-pointer\"\n              @click=\"deleteFolder(folder)\"\n            >\n              <v-remixicon name=\"riDeleteBin7Line\" class=\"mr-2 -ml-1\" />\n              <span>\n                {{ t('common.delete') }}\n              </span>\n            </ui-list-item>\n          </ui-list>\n        </ui-popover>\n      </ui-list-item>\n    </ui-list>\n  </div>\n</template>\n<script setup>\nimport { computed } from 'vue';\nimport { useI18n } from 'vue-i18n';\nimport { useDialog } from '@/composable/dialog';\nimport { parseJSON } from '@/utils/helper';\nimport { useFolderStore } from '@/stores/folder';\nimport { useWorkflowStore } from '@/stores/workflow';\nimport { exportWorkflow } from '@/utils/workflowData';\n\ndefineProps({\n  modelValue: {\n    type: String,\n    default: '',\n  },\n});\nconst emit = defineEmits(['update:modelValue']);\n\nconst { t } = useI18n();\nconst dialog = useDialog();\nconst folderStore = useFolderStore();\nconst workflowStore = useWorkflowStore();\n\nconst folders = computed(() => folderStore.items);\n\nfunction exportFolderWorkflows(folderId) {\n  const workflows = workflowStore.getWorkflows.filter(\n    (item) => item.folderId === folderId\n  );\n  workflows.forEach((workflow) => {\n    exportWorkflow(workflow);\n  });\n}\nfunction onDragover(event, toggle) {\n  const parent = event.target.closest('.ui-list-item');\n  if (!parent) return;\n\n  event.preventDefault();\n  parent.classList.toggle('ring-2', toggle);\n}\nfunction newFolder() {\n  dialog.prompt({\n    title: t('workflows.folder.new'),\n    placeholder: t('workflows.folder.name'),\n    okText: t('common.add'),\n    onConfirm(value) {\n      if (!value || !value.trim()) return false;\n\n      folderStore.addFolder(value);\n\n      return true;\n    },\n  });\n}\nfunction deleteFolder({ name, id }) {\n  dialog.confirm({\n    title: t('workflows.folder.delete'),\n    body: t('message.delete', { name }),\n    okText: t('common.delete'),\n    okVariant: 'danger',\n    onConfirm() {\n      folderStore.deleteFolder(id);\n\n      emit('update:modelValue', '');\n    },\n  });\n}\nfunction renameFolder({ id, name }) {\n  dialog.prompt({\n    inputValue: name,\n    okText: t('common.rename'),\n    title: t('workflows.folder.rename'),\n    placeholder: t('workflows.folder.name'),\n    onConfirm(newName) {\n      if (!newName || !newName.trim()) return false;\n\n      folderStore.updateFolder(id, { name: newName });\n\n      return true;\n    },\n  });\n}\nasync function onWorkflowsDrop({ dataTransfer }, folderId) {\n  const ids = parseJSON(dataTransfer.getData('workflows'), null);\n  if (!ids || !Array.isArray(ids)) return;\n\n  try {\n    for (const id of ids) {\n      await workflowStore.update({\n        id,\n        data: { folderId },\n      });\n    }\n  } catch (error) {\n    console.error(error);\n  }\n}\n</script>\n"
  },
  {
    "path": "src/components/newtab/workflows/WorkflowsHosted.vue",
    "content": "<template>\n  <shared-card\n    v-for=\"workflow in workflows\"\n    :key=\"workflow.hostId\"\n    :data=\"workflow\"\n    :menu=\"menu\"\n    @execute=\"RendererWorkflowService.executeWorkflow(workflow)\"\n    @click=\"$router.push(`/workflows/${$event.hostId}/host`)\"\n    @menuSelected=\"deleteWorkflow(workflow)\"\n  />\n</template>\n<script setup>\nimport SharedCard from '@/components/newtab/shared/SharedCard.vue';\nimport { useDialog } from '@/composable/dialog';\nimport RendererWorkflowService from '@/service/renderer/RendererWorkflowService';\nimport { useHostedWorkflowStore } from '@/stores/hostedWorkflow';\nimport { arraySorter } from '@/utils/helper';\nimport { computed } from 'vue';\nimport { useI18n } from 'vue-i18n';\n\nconst props = defineProps({\n  search: {\n    type: String,\n    default: '',\n  },\n  sort: {\n    type: Object,\n    default: () => ({\n      by: '',\n      order: '',\n    }),\n  },\n});\n\nconst { t } = useI18n();\nconst dialog = useDialog();\nconst hostedWorkflowStore = useHostedWorkflowStore();\n\nconst menu = [\n  { id: 'delete', name: t('common.delete'), icon: 'riDeleteBin7Line' },\n];\n\nconst workflows = computed(() => {\n  const filtered = hostedWorkflowStore.toArray.filter(({ name }) =>\n    name.toLocaleLowerCase().includes(props.search.toLocaleLowerCase())\n  );\n\n  return arraySorter({\n    data: filtered,\n    key: props.sort.by,\n    order: props.sort.order,\n  });\n});\n\nasync function deleteWorkflow(workflow) {\n  dialog.confirm({\n    title: t('workflow.delete'),\n    okVariant: 'danger',\n    body: t('message.delete', { name: workflow.name }),\n    onConfirm: async () => {\n      try {\n        await hostedWorkflowStore.delete(workflow.hostId);\n      } catch (error) {\n        console.error(error);\n      }\n    },\n  });\n}\n</script>\n"
  },
  {
    "path": "src/components/newtab/workflows/WorkflowsLocal.vue",
    "content": "<template>\n  <div\n    v-if=\"workflowStore.getWorkflows.length === 0\"\n    class=\"md:flex items-center md:text-left text-center py-12\"\n  >\n    <img src=\"@/assets/svg/alien.svg\" class=\"w-96\" />\n    <div class=\"ml-4\">\n      <h1 class=\"mb-6 max-w-md text-2xl font-semibold\">\n        {{ t('message.empty') }}\n      </h1>\n    </div>\n  </div>\n  <template v-else>\n    <div v-if=\"pinnedWorkflows.length > 0\" class=\"mb-8 border-b pb-8\">\n      <div class=\"flex items-center\">\n        <v-remixicon name=\"riPushpin2Line\" class=\"mr-2\" size=\"20\" />\n        <span>{{ t('workflow.pinWorkflow.pinned') }}</span>\n      </div>\n      <div class=\"workflows-container mt-4\">\n        <workflows-local-card\n          v-for=\"workflow in pinnedWorkflows\"\n          :key=\"workflow.id\"\n          :workflow=\"workflow\"\n          :is-hosted=\"userStore.hostedWorkflows[workflow.id]\"\n          :is-shared=\"sharedWorkflowStore.getById(workflow.id)\"\n          :is-pinned=\"true\"\n          :menu=\"menu\"\n          @dragstart=\"onDragStart\"\n          @execute=\"RendererWorkflowService.executeWorkflow(workflow)\"\n          @toggle-pin=\"togglePinWorkflow(workflow)\"\n          @toggle-disable=\"toggleDisableWorkflow(workflow)\"\n        />\n      </div>\n    </div>\n    <div class=\"workflows-container\">\n      <workflows-local-card\n        v-for=\"workflow in workflows\"\n        :key=\"workflow.id\"\n        :workflow=\"workflow\"\n        :is-hosted=\"userStore.hostedWorkflows[workflow.id]\"\n        :is-shared=\"sharedWorkflowStore.getById(workflow.id)\"\n        :is-pinned=\"state.pinnedWorkflows.includes(workflow.id)\"\n        :menu=\"menu\"\n        @dragstart=\"onDragStart\"\n        @execute=\"RendererWorkflowService.executeWorkflow(workflow)\"\n        @toggle-pin=\"togglePinWorkflow(workflow)\"\n        @toggle-disable=\"toggleDisableWorkflow(workflow)\"\n      />\n    </div>\n    <div\n      v-if=\"filteredWorkflows.length > 18\"\n      class=\"mt-8 flex items-center justify-between\"\n    >\n      <div>\n        {{ t('components.pagination.text1') }}\n        <select\n          :value=\"pagination.perPage\"\n          class=\"bg-input rounded-md p-1\"\n          @change=\"onPerPageChange\"\n        >\n          <option v-for=\"num in [18, 32, 64, 128]\" :key=\"num\" :value=\"num\">\n            {{ num }}\n          </option>\n        </select>\n        {{\n          t('components.pagination.text2', {\n            count: filteredWorkflows.length,\n          })\n        }}\n      </div>\n      <ui-pagination\n        v-model=\"pagination.currentPage\"\n        :per-page=\"pagination.perPage\"\n        :records=\"filteredWorkflows.length\"\n      />\n    </div>\n  </template>\n  <ui-modal v-model=\"renameState.show\" title=\"Workflow\">\n    <ui-input\n      v-model=\"renameState.name\"\n      :placeholder=\"t('common.name')\"\n      autofocus\n      class=\"mb-4 w-full\"\n      @keyup.enter=\"renameWorkflow\"\n    />\n    <ui-textarea\n      v-model=\"renameState.description\"\n      :placeholder=\"t('common.description')\"\n      height=\"165px\"\n      class=\"w-full dark:text-gray-200\"\n      max=\"300\"\n      style=\"min-height: 140px\"\n    />\n    <p class=\"mb-6 text-right text-gray-600 dark:text-gray-200\">\n      {{ renameState.description.length }}/300\n    </p>\n    <div class=\"flex space-x-2\">\n      <ui-button class=\"w-full\" @click=\"clearRenameModal\">\n        {{ t('common.cancel') }}\n      </ui-button>\n      <ui-button variant=\"accent\" class=\"w-full\" @click=\"renameWorkflow\">\n        {{ t('common.update') }}\n      </ui-button>\n    </div>\n  </ui-modal>\n</template>\n<script setup>\nimport {\n  shallowReactive,\n  computed,\n  onMounted,\n  onBeforeUnmount,\n  watch,\n} from 'vue';\nimport { useI18n } from 'vue-i18n';\nimport SelectionArea from '@viselect/vanilla';\nimport browser from 'webextension-polyfill';\nimport cloneDeep from 'lodash.clonedeep';\nimport { arraySorter } from '@/utils/helper';\nimport { useUserStore } from '@/stores/user';\nimport { useDialog } from '@/composable/dialog';\nimport { useWorkflowStore } from '@/stores/workflow';\nimport { exportWorkflow } from '@/utils/workflowData';\nimport { useSharedWorkflowStore } from '@/stores/sharedWorkflow';\nimport RendererWorkflowService from '@/service/renderer/RendererWorkflowService';\nimport WorkflowsLocalCard from './WorkflowsLocalCard.vue';\n\nconst props = defineProps({\n  search: {\n    type: String,\n    default: '',\n  },\n  folderId: {\n    type: String,\n    default: '',\n  },\n  sort: {\n    type: Object,\n    default: () => ({\n      by: '',\n      order: '',\n    }),\n  },\n  perPage: {\n    type: Number,\n    default: 18,\n  },\n});\nconst emit = defineEmits(['update:perPage']);\n\nconst { t } = useI18n();\nconst dialog = useDialog();\nconst userStore = useUserStore();\nconst workflowStore = useWorkflowStore();\nconst sharedWorkflowStore = useSharedWorkflowStore();\n\nconst state = shallowReactive({\n  pinnedWorkflows: [],\n  selectedWorkflows: [],\n});\nconst renameState = shallowReactive({\n  id: '',\n  name: '',\n  show: false,\n  description: '',\n});\nconst pagination = shallowReactive({\n  currentPage: 1,\n  perPage: +`${props.perPage}` || 18,\n});\n\nconst selection = new SelectionArea({\n  container: '.workflows-list',\n  startareas: ['.workflows-list'],\n  boundaries: ['.workflows-list'],\n  selectables: ['.local-workflow'],\n  behaviour: {\n    overlap: 'invert',\n  },\n});\nselection\n  .on('beforestart', ({ event }) => {\n    return (\n      event.target.tagName !== 'INPUT' &&\n      !event.target.closest('.local-workflow')\n    );\n  })\n  .on('start', () => {\n    /* eslint-disable-next-line */\n    clearSelectedWorkflows();\n    document.body.style.userSelect = 'none';\n  })\n  .on('move', (event) => {\n    event.store.changed.added.forEach((el) => {\n      el.classList.add('ring-2');\n    });\n    event.store.changed.removed.forEach((el) => {\n      el.classList.remove('ring-2');\n    });\n  })\n  .on('stop', (event) => {\n    state.selectedWorkflows = event.store.selected.map(\n      (el) => el.dataset?.workflow\n    );\n    document.body.style.userSelect = '';\n  });\n\nconst filteredWorkflows = computed(() => {\n  const filtered = workflowStore.getWorkflows.filter(\n    ({ name, folderId }) =>\n      name.toLocaleLowerCase().includes(props.search.toLocaleLowerCase()) &&\n      (!props.folderId || props.folderId === folderId)\n  );\n\n  return arraySorter({\n    data: filtered,\n    key: props.sort.by,\n    order: props.sort.order,\n  });\n});\nconst workflows = computed(() =>\n  filteredWorkflows.value.slice(\n    (pagination.currentPage - 1) * pagination.perPage,\n    pagination.currentPage * pagination.perPage\n  )\n);\nconst pinnedWorkflows = computed(() => {\n  const list = [];\n  state.pinnedWorkflows.forEach((workflowId) => {\n    const workflow = workflowStore.getById(workflowId);\n    if (\n      !workflow ||\n      !workflow.name\n        .toLocaleLowerCase()\n        .includes(props.search.toLocaleLowerCase())\n    )\n      return;\n\n    list.push(workflow);\n  });\n\n  return arraySorter({\n    data: list,\n    key: props.sort.by,\n    order: props.sort.order,\n  });\n});\n\nfunction onPerPageChange(event) {\n  const { value } = event.target;\n  pagination.perPage = +value;\n  emit('update:perPage', +value);\n}\nfunction toggleDisableWorkflow({ id, isDisabled }) {\n  workflowStore.update({\n    id,\n    data: {\n      isDisabled: !isDisabled,\n    },\n  });\n}\nfunction clearRenameModal() {\n  Object.assign(renameState, {\n    id: '',\n    name: '',\n    show: false,\n    description: '',\n  });\n}\nfunction initRenameWorkflow({ name, description, id }) {\n  Object.assign(renameState, {\n    id,\n    name,\n    show: true,\n    description,\n  });\n}\nfunction renameWorkflow() {\n  workflowStore.update({\n    id: renameState.id,\n    data: {\n      name: renameState.name,\n      description: renameState.description,\n    },\n  });\n  clearRenameModal();\n}\nfunction deleteWorkflow({ name, id }) {\n  dialog.confirm({\n    title: t('workflow.delete'),\n    okVariant: 'danger',\n    body: t('message.delete', { name }),\n    onConfirm: () => {\n      workflowStore.delete(id);\n    },\n  });\n}\nfunction deleteSelectedWorkflows({ target, key }) {\n  const excludeTags = ['INPUT', 'TEXTAREA', 'SELECT'];\n  if (\n    excludeTags.includes(target.tagName) ||\n    key !== 'Delete' ||\n    state.selectedWorkflows.length === 0\n  )\n    return;\n\n  if (state.selectedWorkflows.length === 1) {\n    const [workflowId] = state.selectedWorkflows;\n    const workflow = workflowStore.getById(workflowId);\n    deleteWorkflow(workflow);\n  } else {\n    dialog.confirm({\n      title: t('workflow.delete'),\n      okVariant: 'danger',\n      body: t('message.delete', {\n        name: `${state.selectedWorkflows.length} workflows`,\n      }),\n      onConfirm: async () => {\n        await workflowStore.delete(state.selectedWorkflows);\n      },\n    });\n  }\n}\nfunction duplicateWorkflow(workflow) {\n  const clonedWorkflow = cloneDeep(workflow);\n  const delKeys = ['$id', 'data', 'id', 'isDisabled'];\n\n  delKeys.forEach((key) => {\n    delete clonedWorkflow[key];\n  });\n\n  clonedWorkflow.createdAt = Date.now();\n  clonedWorkflow.name += ' - copy';\n\n  workflowStore.insert(clonedWorkflow);\n}\nfunction onDragStart({ dataTransfer, target }) {\n  const payload = [...state.selectedWorkflows];\n\n  const targetId = target.dataset?.workflow;\n  if (targetId && !payload.includes(targetId)) payload.push(targetId);\n\n  dataTransfer.setData('workflows', JSON.stringify(payload));\n}\nfunction clearSelectedWorkflows() {\n  state.selectedWorkflows = [];\n\n  selection.getSelection().forEach((el) => {\n    el.classList.remove('ring-2');\n  });\n  selection.clearSelection();\n}\nfunction togglePinWorkflow(workflow) {\n  const index = state.pinnedWorkflows.indexOf(workflow.id);\n  const copyData = [...state.pinnedWorkflows];\n\n  if (index === -1) {\n    copyData.push(workflow.id);\n  } else {\n    copyData.splice(index, 1);\n  }\n\n  state.pinnedWorkflows = copyData;\n  browser.storage.local.set({\n    pinnedWorkflows: copyData,\n  });\n}\n\nconst menu = [\n  {\n    id: 'copy-id',\n    name: 'Copy workflow id',\n    icon: 'riFileCopyLine',\n    action: (workflow) => {\n      navigator.clipboard.writeText(workflow.id).catch((error) => {\n        console.error(error);\n\n        const textarea = document.createElement('textarea');\n        textarea.value = workflow.id;\n        textarea.select();\n        document.execCommand('copy');\n        textarea.blur();\n      });\n    },\n  },\n  {\n    id: 'duplicate',\n    name: t('common.duplicate'),\n    icon: 'riFileCopyLine',\n    action: duplicateWorkflow,\n  },\n  {\n    id: 'export',\n    name: t('common.export'),\n    icon: 'riDownloadLine',\n    action: exportWorkflow,\n  },\n  {\n    id: 'rename',\n    name: t('common.rename'),\n    icon: 'riPencilLine',\n    action: initRenameWorkflow,\n  },\n  {\n    id: 'delete',\n    name: t('common.delete'),\n    icon: 'riDeleteBin7Line',\n    action: deleteWorkflow,\n  },\n];\n\nwatch(\n  () => props.folderId,\n  () => {\n    pagination.currentPage = 1;\n  }\n);\n\nonMounted(() => {\n  window.addEventListener('keydown', deleteSelectedWorkflows);\n\n  browser.storage.local.get('pinnedWorkflows').then((storage) => {\n    state.pinnedWorkflows = storage.pinnedWorkflows || [];\n  });\n});\nonBeforeUnmount(() => {\n  window.removeEventListener('keydown', deleteSelectedWorkflows);\n});\n</script>\n<style>\n.selection-area {\n  background: rgba(46, 115, 252, 0.11);\n  border: 2px solid rgba(98, 155, 255, 0.81);\n  border-radius: 0.1em;\n}\n</style>\n"
  },
  {
    "path": "src/components/newtab/workflows/WorkflowsLocalCard.vue",
    "content": "<template>\n  <shared-card\n    :data=\"workflow\"\n    :data-workflow=\"workflow.id\"\n    draggable=\"true\"\n    class=\"local-workflow cursor-default select-none ring-accent\"\n    @click=\"$router.push(`/workflows/${$event.id}`)\"\n  >\n    <template #header>\n      <div class=\"mb-4 flex items-center\">\n        <template v-if=\"workflow && !workflow.isDisabled\">\n          <ui-img\n            v-if=\"workflow.icon.startsWith('http')\"\n            :src=\"workflow.icon\"\n            class=\"overflow-hidden rounded-lg\"\n            style=\"height: 40px; width: 40px\"\n            alt=\"Can not display\"\n          />\n          <span v-else class=\"bg-box-transparent inline-block rounded-lg p-2\">\n            <v-remixicon :name=\"workflow.icon\" />\n          </span>\n        </template>\n        <p v-else class=\"py-2\">{{ t('common.disabled') }}</p>\n        <div class=\"grow\"></div>\n        <button\n          v-if=\"!workflow.isDisabled\"\n          class=\"invisible group-hover:visible\"\n          @click=\"$emit('execute')\"\n        >\n          <v-remixicon name=\"riPlayLine\" />\n        </button>\n        <ui-popover class=\"ml-2 h-6\">\n          <template #trigger>\n            <button>\n              <v-remixicon name=\"riMoreLine\" />\n            </button>\n          </template>\n          <ui-list class=\"space-y-1\" style=\"min-width: 165px\">\n            <ui-list-item\n              class=\"cursor-pointer\"\n              @click=\"$emit('toggleDisable')\"\n            >\n              <v-remixicon name=\"riToggleLine\" class=\"mr-2 -ml-1\" />\n              <span class=\"capitalize\">\n                {{ t(`common.${workflow.isDisabled ? 'enable' : 'disable'}`) }}\n              </span>\n            </ui-list-item>\n            <ui-list-item class=\"cursor-pointer\" @click=\"$emit('togglePin')\">\n              <v-remixicon name=\"riPushpin2Line\" class=\"mr-2 -ml-1\" />\n              <span>{{\n                t(`workflow.pinWorkflow.${isPinned ? 'unpin' : 'pin'}`)\n              }}</span>\n            </ui-list-item>\n            <ui-list-item\n              v-for=\"item in menu\"\n              :key=\"item.id\"\n              v-close-popover\n              class=\"cursor-pointer\"\n              @click=\"item.action(workflow)\"\n            >\n              <v-remixicon :name=\"item.icon\" class=\"mr-2 -ml-1\" />\n              <span class=\"capitalize\">{{ item.name }}</span>\n            </ui-list-item>\n          </ui-list>\n        </ui-popover>\n      </div>\n    </template>\n    <template #footer-content>\n      <v-remixicon\n        v-if=\"isShared\"\n        v-tooltip:bottom.group=\"\n          t('workflow.share.sharedAs', {\n            name: isShared?.name.slice(0, 64),\n          })\n        \"\n        name=\"riShareLine\"\n        size=\"20\"\n        class=\"ml-2\"\n      />\n      <v-remixicon\n        v-if=\"isHosted\"\n        v-tooltip:bottom.group=\"t('workflow.host.title')\"\n        name=\"riBaseStationLine\"\n        size=\"20\"\n        class=\"ml-2\"\n      />\n    </template>\n  </shared-card>\n</template>\n<script setup>\nimport { useI18n } from 'vue-i18n';\nimport SharedCard from '@/components/newtab/shared/SharedCard.vue';\n\ndefineProps({\n  workflow: {\n    type: Object,\n    default: () => ({}),\n  },\n  menu: {\n    type: Array,\n    default: () => [],\n  },\n  isShared: {\n    type: Object,\n    default: null,\n  },\n  isHosted: {\n    type: Object,\n    default: null,\n  },\n  isPinned: Boolean,\n});\ndefineEmits(['toggleDisable', 'togglePin', 'execute']);\n\nconst { t } = useI18n();\n</script>\n"
  },
  {
    "path": "src/components/newtab/workflows/WorkflowsShared.vue",
    "content": "<template>\n  <div\n    v-if=\"workflows.length === 0\"\n    class=\"md:flex items-center md:text-left text-center py-12\"\n  >\n    <img src=\"@/assets/svg/alien.svg\" class=\"w-96\" />\n    <div class=\"ml-4\">\n      <h1 class=\"mb-6 max-w-md text-2xl font-semibold\">\n        {{ t('message.empty') }}\n      </h1>\n    </div>\n  </div>\n  <div v-else class=\"workflows-container\">\n    <shared-card\n      v-for=\"workflow in workflows\"\n      :key=\"workflow.id\"\n      :data=\"workflow\"\n      :show-details=\"false\"\n      @execute=\"RendererWorkflowService.executeWorkflow(workflow)\"\n      @click=\"$router.push(`/workflows/${$event.id}/shared`)\"\n    />\n  </div>\n</template>\n<script setup>\nimport SharedCard from '@/components/newtab/shared/SharedCard.vue';\nimport RendererWorkflowService from '@/service/renderer/RendererWorkflowService';\nimport { useSharedWorkflowStore } from '@/stores/sharedWorkflow';\nimport { arraySorter } from '@/utils/helper';\nimport { computed } from 'vue';\nimport { useI18n } from 'vue-i18n';\n\nconst { t } = useI18n();\n\nconst props = defineProps({\n  search: {\n    type: String,\n    default: '',\n  },\n  sort: {\n    type: Object,\n    default: () => ({\n      by: '',\n      order: '',\n    }),\n  },\n});\n\nconst sharedWorkflowStore = useSharedWorkflowStore();\n\nconst workflows = computed(() => {\n  const filtered = sharedWorkflowStore.toArray.filter(({ name }) =>\n    name.toLocaleLowerCase().includes(props.search.toLocaleLowerCase())\n  );\n\n  return arraySorter({\n    data: filtered,\n    key: props.sort.by,\n    order: props.sort.order,\n  });\n});\n</script>\n"
  },
  {
    "path": "src/components/newtab/workflows/WorkflowsUserTeam.vue",
    "content": "<template>\n  <p v-if=\"!userStore.user\" class=\"my-4 text-center\">\n    <ui-spinner v-if=\"!userStore.retrieved\" color=\"text-accent\" />\n    <template v-else>\n      You must\n      <a\n        href=\"https://extension.automa.site/auth\"\n        class=\"underline\"\n        target=\"_blank\"\n        >login</a\n      >\n      to use these workflows\n    </template>\n  </p>\n  <div\n    v-else-if=\"!isUnknownTeam && teamWorkflows.length === 0\"\n    class=\"text-center\"\n  >\n    <img src=\"@/assets/svg/files-and-folder.svg\" class=\"mx-auto w-96\" />\n    <p class=\"text-lg font-semibold\">Nothing to see here</p>\n    <p class=\"text-gray-600 dark:text-gray-200\">\n      Browse workflows that been shared by your team\n    </p>\n    <ui-button\n      :href=\"`http://extension.automa.site/workflows?teamId=${teamId}&workflowsBy=team`\"\n      tag=\"a\"\n      target=\"_blank\"\n      variant=\"accent\"\n      class=\"mt-8 inline-block\"\n    >\n      Browse workflows\n    </ui-button>\n  </div>\n  <div v-else class=\"workflows-container\">\n    <shared-card\n      v-for=\"workflow in workflows\"\n      :key=\"workflow.id\"\n      :data=\"workflow\"\n      :menu=\"workflowMenus\"\n      :disabled=\"isUnknownTeam\"\n      @click=\"openWorkflowPage\"\n      @menuSelected=\"onMenuSelected\"\n      @execute=\"RendererWorkflowService.executeWorkflow(workflow)\"\n    >\n      <template #footer-content>\n        <span\n          :class=\"tagColors[workflow.tag]\"\n          class=\"rounded-md p-1 text-sm capitalize text-black\"\n        >\n          {{ workflow.tag }}\n        </span>\n      </template>\n    </shared-card>\n  </div>\n</template>\n<script setup>\nimport SharedCard from '@/components/newtab/shared/SharedCard.vue';\nimport { useDialog } from '@/composable/dialog';\nimport RendererWorkflowService from '@/service/renderer/RendererWorkflowService';\nimport { useTeamWorkflowStore } from '@/stores/teamWorkflow';\nimport { useUserStore } from '@/stores/user';\nimport { fetchApi } from '@/utils/api';\nimport { arraySorter } from '@/utils/helper';\nimport { tagColors } from '@/utils/shared';\nimport { computed } from 'vue';\nimport { useI18n } from 'vue-i18n';\nimport { useRouter } from 'vue-router';\nimport { useToast } from 'vue-toastification';\n\nconst props = defineProps({\n  active: Boolean,\n  search: {\n    type: String,\n    default: '',\n  },\n  teamId: {\n    type: [String, Number],\n    default: '',\n  },\n  sort: {\n    type: Object,\n    default: () => ({\n      by: '',\n      order: '',\n    }),\n  },\n});\n\nconst menu = [\n  {\n    id: 'delete',\n    name: 'Delete',\n    hasAccess: true,\n    icon: 'riDeleteBin7Line',\n    attrs: {\n      class: 'text-red-400 dark:text-red-500',\n    },\n  },\n  {\n    id: 'delete-team',\n    name: 'Delete from team',\n    icon: 'riDeleteBin7Line',\n    permissions: ['owner', 'create'],\n    attrs: {\n      class: 'text-red-400 dark:text-red-500',\n    },\n  },\n];\n\nconst { t } = useI18n();\nconst toast = useToast();\nconst dialog = useDialog();\nconst router = useRouter();\nconst userStore = useUserStore();\nconst teamWorkflowStore = useTeamWorkflowStore();\n\nconst isUnknownTeam = computed(() => props.teamId === '(unknown)');\nconst workflowMenus = computed(() =>\n  menu.filter((item) => {\n    if (!item.permissions) return true;\n\n    return userStore.validateTeamAccess(props.teamId, item.permissions);\n  })\n);\nconst teamWorkflows = computed(() => {\n  if (isUnknownTeam.value) {\n    return Object.keys(teamWorkflowStore.workflows).reduce((acc, teamId) => {\n      const teamExist = userStore.user?.teams?.some(\n        (team) => team.id === teamId || team.id === +teamId\n      );\n      if (!teamExist) {\n        acc.push(...Object.values(teamWorkflowStore.workflows[teamId]));\n      }\n\n      return acc;\n    }, []);\n  }\n\n  return teamWorkflowStore.getByTeam(props.teamId);\n});\nconst workflows = computed(() => {\n  if (!props.active) return [];\n\n  const filtered = teamWorkflows.value.filter(({ name }) =>\n    name.toLocaleLowerCase().includes(props.search.toLocaleLowerCase())\n  );\n\n  return arraySorter({\n    data: filtered,\n    key: props.sort.by,\n    order: props.sort.order,\n  });\n});\n\nfunction onMenuSelected({ id, data }) {\n  if (id === 'delete') {\n    dialog.confirm({\n      title: t('workflow.delete'),\n      okVariant: 'danger',\n      body: t('message.delete', { name: data.name }),\n      onConfirm: () => {\n        teamWorkflowStore.delete(data.teamId, data.id);\n      },\n    });\n  } else if (id === 'delete-team') {\n    dialog.confirm({\n      async: true,\n      title: 'Delete workflow from team',\n      okVariant: 'danger',\n      body: `Are you sure want to delete the \"${data.name}\" workflow from this team?`,\n      onConfirm: async () => {\n        try {\n          const response = await fetchApi(\n            `/teams/${props.teamId}/workflows/${data.id}`,\n            { method: 'DELETE', auth: true }\n          );\n          const result = await response.json();\n\n          if (!response.ok && response.status !== 404)\n            throw new Error(result.message);\n\n          await teamWorkflowStore.delete(props.teamId, data.id);\n\n          return true;\n        } catch (error) {\n          toast.error('Something went wrong');\n          console.error(error);\n          return false;\n        }\n      },\n    });\n  }\n}\nfunction openWorkflowPage({ id }) {\n  if (isUnknownTeam.value) return;\n\n  router.push(`/teams/${props.teamId}/workflows/${id}`);\n}\n</script>\n"
  },
  {
    "path": "src/components/popup/home/HomeSelectBlock.vue",
    "content": "<template>\n  <div class=\"px-4 pb-4\">\n    <div class=\"mt-4 flex items-center\">\n      <button @click=\"$emit('goBack')\">\n        <v-remixicon\n          name=\"riArrowLeftLine\"\n          class=\"-ml-1 mr-1 inline-block align-bottom\"\n        />\n      </button>\n      <p class=\"text-overflow flex-1 font-semibold\">\n        {{ workflow.name }}\n      </p>\n    </div>\n    <p class=\"mt-2\">\n      {{ t('home.record.selectBlock') }}\n    </p>\n    <workflow-editor\n      :minimap=\"false\"\n      :editor-controls=\"false\"\n      :options=\"editorOptions\"\n      class=\"bg-box-transparent h-56 w-full rounded-lg\"\n      @init=\"onEditorInit\"\n    />\n    <ui-button\n      :disabled=\"!state.activeBlock\"\n      variant=\"accent\"\n      class=\"mt-6 w-full\"\n      @click=\"startRecording\"\n    >\n      {{ t('home.record.button') }}\n    </ui-button>\n  </div>\n</template>\n<script setup>\nimport { onMounted, onBeforeUnmount, shallowReactive } from 'vue';\nimport { useI18n } from 'vue-i18n';\nimport convertWorkflowData from '@/utils/convertWorkflowData';\nimport WorkflowEditor from '@/components/newtab/workflow/WorkflowEditor.vue';\n\nconst props = defineProps({\n  workflow: {\n    type: Object,\n    default: () => ({}),\n  },\n});\nconst emit = defineEmits(['goBack', 'record', 'update']);\n\nconst { t } = useI18n();\n\nconst editorOptions = {\n  disabled: true,\n  fitViewOnInit: true,\n  nodesDraggable: false,\n  edgesUpdatable: false,\n  nodesConnectable: false,\n};\n\nconst state = shallowReactive({\n  retrieved: false,\n  activeBlock: null,\n  blockOutput: null,\n});\n\nfunction onEditorInit(editor) {\n  const convertedData = convertWorkflowData(props.workflow);\n  emit('update', { drawflow: convertedData.drawflow });\n\n  editor.setNodes(convertedData.drawflow.nodes);\n  editor.setEdges(convertedData.drawflow.edges);\n}\nfunction clearSelectedHandle() {\n  document.querySelectorAll('.selected-handle').forEach((el) => {\n    el.classList.remove('selected-handle');\n  });\n}\nfunction onClick({ target }) {\n  let selectedHandle = null;\n\n  const handleEl = target.closest('.vue-flow__handle.source');\n  if (handleEl) {\n    clearSelectedHandle();\n    handleEl.classList.add('selected-handle');\n    selectedHandle = handleEl;\n  }\n\n  if (!handleEl) {\n    const nodeEl = target.closest('.vue-flow__node');\n    if (nodeEl) {\n      clearSelectedHandle();\n      const handle = nodeEl.querySelector('.vue-flow__handle.source');\n      handle.classList.add('selected-handle');\n      selectedHandle = handle;\n    }\n  }\n\n  if (!selectedHandle) return;\n\n  const { handleid, nodeid } = selectedHandle.dataset;\n  state.activeBlock = nodeid;\n  state.blockOutput = handleid;\n}\nfunction startRecording() {\n  const options = {\n    name: props.workflow.name,\n    workflowId: props.workflow.id,\n    connectFrom: {\n      id: state.activeBlock,\n      output: state.blockOutput,\n    },\n  };\n\n  emit('record', options);\n}\n\nonMounted(() => {\n  window.addEventListener('click', onClick);\n});\nonBeforeUnmount(() => {\n  window.removeEventListener('click', onClick);\n});\n</script>\n<style>\n.selected-handle {\n  @apply ring-4;\n}\n\n.vue-flow__handle.source {\n  pointer-events: auto !important;\n}\n</style>\n"
  },
  {
    "path": "src/components/popup/home/HomeStartRecording.vue",
    "content": "<template>\n  <ui-tabs\n    v-model=\"state.activeTab\"\n    fill\n    class=\"mx-4\"\n    @change=\"$emit('update', $event)\"\n  >\n    <ui-tab v-for=\"tab in tabs\" :key=\"tab\" :value=\"tab\">\n      {{ t(`home.record.tabs.${tab}`) }}\n    </ui-tab>\n  </ui-tabs>\n  <ui-tab-panels :model-value=\"state.activeTab\">\n    <ui-tab-panel value=\"new\" class=\"mt-3 px-4\">\n      <form @submit.prevent=\"$emit('record', { name: state.workflowName })\">\n        <ui-input\n          v-model=\"state.workflowName\"\n          :label=\"t('home.record.name')\"\n          :placeholder=\"t('common.name')\"\n          autofocus\n          class=\"w-full\"\n        />\n        <ui-button class=\"mt-6 w-full\" variant=\"accent\" type=\"submit\">\n          {{ t('home.record.button') }}\n        </ui-button>\n      </form>\n    </ui-tab-panel>\n    <ui-tab-panel cache value=\"existing\">\n      <home-select-block\n        v-if=\"activeWorkflow\"\n        :workflow=\"activeWorkflow\"\n        @update=\"updateWorkflow\"\n        @record=\"$emit('record', $event)\"\n        @goBack=\"state.activeWorkflow = ''\"\n      />\n      <template v-else>\n        <div class=\"mt-4 px-4\">\n          <ui-input\n            v-model=\"state.query\"\n            class=\"w-full\"\n            prepend-icon=\"riSearch2Line\"\n            :placeholder=\"t('common.search')\"\n          />\n        </div>\n        <ui-list class=\"scroll mt-2 mb-4 h-72 overflow-y-auto px-4\">\n          <ui-list-item\n            v-for=\"workflow in workflows\"\n            :key=\"workflow.id\"\n            small\n            class=\"cursor-pointer\"\n            @click=\"state.activeWorkflow = workflow.id\"\n          >\n            <img\n              v-if=\"workflow.icon?.startsWith('http')\"\n              :src=\"workflow.icon\"\n              class=\"overflow-hidden rounded-lg\"\n              style=\"height: 32px; width: 32px\"\n              alt=\"Can not display\"\n            />\n            <span v-else class=\"bg-box-transparent rounded-lg p-2\">\n              <v-remixicon :name=\"workflow.icon\" size=\"20\" />\n            </span>\n            <div class=\"ml-2 flex-1 overflow-hidden\">\n              <p :title=\"workflow.name\" class=\"text-overflow leading-tight\">\n                {{ workflow.name }}\n              </p>\n              <p\n                :title=\"workflow.description\"\n                class=\"text-overflow text-sm leading-tight text-gray-600\"\n              >\n                {{ workflow.description }}\n              </p>\n            </div>\n          </ui-list-item>\n        </ui-list>\n      </template>\n    </ui-tab-panel>\n  </ui-tab-panels>\n</template>\n<script setup>\nimport { reactive, computed } from 'vue';\nimport { useI18n } from 'vue-i18n';\nimport { useWorkflowStore } from '@/stores/workflow';\nimport HomeSelectBlock from './HomeSelectBlock.vue';\n\nconst emit = defineEmits(['update', 'close', 'record']);\n\nemit('update', 'new');\n\nconst tabs = ['new', 'existing'];\n\nconst { t } = useI18n();\nconst workflowStore = useWorkflowStore();\n\nconst state = reactive({\n  query: '',\n  workflowName: '',\n  activeTab: 'new',\n  activeWorkflow: '',\n});\n\nconst activeWorkflow = computed(() =>\n  workflowStore.getById(state.activeWorkflow)\n);\nconst workflows = computed(() =>\n  workflowStore.getWorkflows\n    .filter(({ name }) =>\n      name.toLocaleLowerCase().includes(state.query.toLocaleLowerCase())\n    )\n    .sort((a, b) => (a.createdAt > b.createdAt ? 1 : -1))\n);\n\nfunction updateWorkflow(data) {\n  workflowStore.update({\n    data,\n    id: state.activeWorkflow,\n  });\n}\n</script>\n"
  },
  {
    "path": "src/components/popup/home/HomeTeamWorkflows.vue",
    "content": "<template>\n  <div class=\"space-y-2 px-5 pb-5\">\n    <ui-card\n      v-for=\"workflow in workflows\"\n      :key=\"workflow.id\"\n      class=\"relative flex w-full items-center space-x-2 hover:ring-2 hover:ring-gray-900\"\n    >\n      <div\n        class=\"text-overflow mr-4 flex-1 cursor-pointer\"\n        @click=\"openWorkflowPage(workflow)\"\n      >\n        <p class=\"text-overflow leading-tight\">{{ workflow.name }}</p>\n        <div class=\"flex items-center text-gray-500\">\n          <span>{{ dayjs(workflow.createdAt).fromNow() }}</span>\n          <div class=\"grow\" />\n          <span\n            :class=\"tagColors[workflow.tag]\"\n            class=\"text-overflow ml-2 rounded-md px-2 py-1 text-sm text-gray-600\"\n            style=\"max-width: 120px\"\n          >\n            {{ workflow.tag }}\n          </span>\n        </div>\n      </div>\n      <p v-if=\"workflow.isDisabled\" class=\"text-sm text-gray-600\">Disabled</p>\n      <button v-else title=\"Execute\" @click=\"executeWorkflow(workflow)\">\n        <v-remixicon name=\"riPlayLine\" />\n      </button>\n    </ui-card>\n  </div>\n</template>\n<script setup>\nimport { computed, onMounted, shallowRef } from 'vue';\nimport { useUserStore } from '@/stores/user';\nimport { sendMessage } from '@/utils/message';\nimport { useTeamWorkflowStore } from '@/stores/teamWorkflow';\nimport { tagColors } from '@/utils/shared';\nimport dayjs from '@/lib/dayjs';\n\nconst props = defineProps({\n  search: {\n    type: String,\n    default: '',\n  },\n});\n\nconst userStore = useUserStore();\nconst teamWorkflowStore = useTeamWorkflowStore();\n\nconst teamWorkflows = shallowRef([]);\n\nconst workflows = computed(() =>\n  teamWorkflows.value.filter((workflow) =>\n    workflow.name.toLocaleLowerCase().includes(props.search.toLocaleLowerCase())\n  )\n);\n\nfunction openWorkflowPage({ teamId, id }) {\n  const url = `/teams/${teamId}/workflows/${id}`;\n  sendMessage('open:dashboard', url, 'background');\n}\nfunction executeWorkflow(workflow) {\n  sendMessage('workflow:execute', workflow, 'background');\n}\n\nonMounted(() => {\n  if (!userStore.user?.teams) return;\n\n  teamWorkflows.value = userStore.user.teams\n    .reduce((acc, team) => {\n      const currentWorkflows = teamWorkflowStore\n        .getByTeam(team.id)\n        .map((workflow) => {\n          workflow.teamId = team.id;\n          workflow.teamName = team.name;\n\n          return workflow;\n        });\n      acc.push(...currentWorkflows);\n\n      return acc;\n    }, [])\n    .sort((a, b) => (a.createdAt > b.createdAt ? 1 : -1));\n});\n</script>\n"
  },
  {
    "path": "src/components/popup/home/HomeWorkflowCard.vue",
    "content": "<template>\n  <ui-card\n    class=\"flex w-full items-center space-x-2 hover:ring-2 hover:ring-gray-900\"\n  >\n    <div\n      class=\"text-overflow flex-1 cursor-pointer\"\n      @click=\"$emit('details', workflow)\"\n    >\n      <p class=\"text-overflow leading-tight\">{{ workflow.name }}</p>\n      <p class=\"leading-tight text-gray-500\">\n        {{ dayjs(workflow.createdAt).fromNow() }}\n      </p>\n    </div>\n    <p v-if=\"workflow.isDisabled\">Disabled</p>\n    <button v-else title=\"Execute\" @click=\"$emit('execute', workflow)\">\n      <v-remixicon name=\"riPlayLine\" />\n    </button>\n    <v-remixicon\n      v-if=\"workflow.isProtected\"\n      name=\"riShieldKeyholeLine\"\n      class=\"text-green-600\"\n    />\n    <ui-popover v-else class=\"h-6\">\n      <template #trigger>\n        <button>\n          <v-remixicon name=\"riMoreLine\" />\n        </button>\n      </template>\n      <ui-list class=\"space-y-1\" style=\"min-width: 160px\">\n        <template v-if=\"tab === 'local'\">\n          <ui-list-item\n            class=\"cursor-pointer capitalize\"\n            @click=\"$emit('update', { isDisabled: !workflow.isDisabled })\"\n          >\n            <v-remixicon name=\"riToggleLine\" class=\"mr-2 -ml-1\" />\n            <span>{{\n              t(`common.${workflow.isDisabled ? 'enable' : 'disable'}`)\n            }}</span>\n          </ui-list-item>\n          <ui-list-item\n            class=\"cursor-pointer capitalize\"\n            @click=\"$emit('togglePin')\"\n          >\n            <v-remixicon name=\"riPushpin2Line\" class=\"mr-2 -ml-1\" />\n            <span>{{ pinned ? 'Unpin workflow' : 'Pin workflow' }}</span>\n          </ui-list-item>\n        </template>\n        <ui-list-item\n          v-for=\"item in filteredMenu\"\n          :key=\"item.name\"\n          v-close-popover\n          class=\"cursor-pointer capitalize\"\n          @click=\"$emit(item.name, workflow)\"\n        >\n          <v-remixicon :name=\"item.icon\" class=\"mr-2 -ml-1\" />\n          <span>{{ item.name }}</span>\n        </ui-list-item>\n      </ui-list>\n    </ui-popover>\n  </ui-card>\n</template>\n<script setup>\nimport { useI18n } from 'vue-i18n';\nimport dayjs from '@/lib/dayjs';\n\nconst props = defineProps({\n  workflow: {\n    type: Object,\n    default: () => ({}),\n  },\n  tab: {\n    type: String,\n    default: 'local',\n  },\n  pinned: Boolean,\n});\ndefineEmits(['execute', 'togglePin', 'rename', 'details', 'delete', 'update']);\n\nconst { t } = useI18n();\n\nconst menu = [\n  { name: 'rename', icon: 'riPencilLine' },\n  { name: 'delete', icon: 'riDeleteBin7Line' },\n];\nconst filteredMenu = menu.filter(({ name }) => {\n  if (name === 'rename' && props.tab !== 'local') return false;\n\n  return true;\n});\n</script>\n"
  },
  {
    "path": "src/components/transitions/TransitionExpand.vue",
    "content": "<script>\nimport { h, Transition, TransitionGroup } from 'vue';\n\n/* eslint-disable */\nexport default {\n  props: {\n    group: Boolean,\n  },\n  setup(props, { slots, attrs }) {\n    function enter (element) {\n      const { width } = getComputedStyle(element);\n\n      element.style.width = width;\n      element.style.position = 'absolute';\n      element.style.visibility = 'hidden';\n      element.style.height = 'auto';\n\n      const { height } = getComputedStyle(element);\n\n      element.style.width = null;\n      element.style.position = null;\n      element.style.visibility = null;\n      element.style.height = 0;\n\n      getComputedStyle(element).height;\n\n      requestAnimationFrame(() => {\n        element.style.height = height;\n      });\n    }\n    function afterEnter (element) {\n      element.style.height = 'auto';\n    }\n    function leave (element) {\n      const { height } = getComputedStyle(element);\n\n      element.style.height = height;\n\n      getComputedStyle(element).height;\n\n      requestAnimationFrame(() => {\n        element.style.height = 0;\n      });\n    }\n\n    return () => h(props.group ? TransitionGroup : Transition, {\n      ...attrs,\n      name: 'expand',\n      onEnter: enter,\n      onAfterEnter: afterEnter,\n      onLeave: leave,\n    }, slots.default)\n  }\n};\n</script>\n<style>\n.expand-enter-active,\n.expand-leave-active {\n  transition: height 0.2s ease-in-out;\n  overflow: hidden;\n}\n\n.expand-enter,\n.expand-leave-to {\n  height: 0;\n}\n</style>\n"
  },
  {
    "path": "src/components/transitions/TransitionSlide.vue",
    "content": "<script>\nimport { h, Transition, TransitionGroup } from 'vue';\n\nexport default {\n  props: {\n    group: Boolean,\n    direction: {\n      type: String,\n      default: 'left',\n      validator: (value) => ['top', 'left', 'right', 'bottom'].includes(value),\n    },\n  },\n  setup(props, { slots, attrs }) {\n    const translateValues = {\n      0: '-100%',\n      1: '100%',\n    };\n    const directionsKey = {\n      top: 0,\n      left: 0,\n      right: 1,\n      bottom: 1,\n    };\n\n    function getTranslateStyle(key = 0) {\n      const isHorizontal = ['left', 'right'].includes(props.direction);\n      const value = translateValues[directionsKey[props.direction] + key];\n\n      if (isHorizontal) return `translateX(${value})`;\n\n      return `translateY(${value})`;\n    }\n    function enter(element) {\n      element.style.transform = getTranslateStyle();\n    }\n    function leave(element) {\n      element.style.transform = getTranslateStyle(1);\n    }\n    function afterEnter(element) {\n      element.style.transform = 'translate(0, 0)';\n    }\n\n    return () =>\n      h(\n        props.group ? TransitionGroup : Transition,\n        {\n          ...attrs,\n          name: 'slide',\n          onEnter: enter,\n          onAfterEnter: afterEnter,\n          onLeave: leave,\n        },\n        slots.default\n      );\n  },\n};\n</script>\n<style>\n.slide-enter-active,\n.slide-leave-active {\n  transition: transform 0.25s ease-out;\n}\n</style>\n"
  },
  {
    "path": "src/components/ui/UiAutocomplete.vue",
    "content": "<template>\n  <ui-popover\n    :id=\"componentId\"\n    v-model=\"state.showPopover\"\n    :class=\"{ block }\"\n    :padding=\"`p-2 max-h-56 overflow-auto scroll ${componentId}`\"\n    trigger-width\n    trigger=\"manual\"\n    class=\"ui-autocomplete\"\n  >\n    <template #trigger>\n      <slot />\n    </template>\n    <p v-if=\"filteredItems.length === 0\" class=\"text-center\">\n      {{ t('message.noData') }}\n    </p>\n    <ui-list v-else class=\"space-y-1\">\n      <ui-list-item\n        v-for=\"(item, index) in filteredItems\"\n        :id=\"`list-item-${index}`\"\n        :key=\"getItem(item, true)\"\n        :class=\"{ 'bg-box-transparent': state.activeIndex === index }\"\n        class=\"cursor-pointer\"\n        @mousedown=\"selectItem(index, true)\"\n        @mouseenter=\"state.activeIndex = index\"\n      >\n        <slot name=\"item\" :item=\"item\">\n          {{ getItem(item) }}\n        </slot>\n      </ui-list-item>\n    </ui-list>\n  </ui-popover>\n</template>\n<script setup>\nimport {\n  computed,\n  onMounted,\n  onBeforeUnmount,\n  shallowReactive,\n  watch,\n} from 'vue';\nimport { useI18n } from 'vue-i18n';\nimport { useComponentId } from '@/composable/componentId';\nimport { debounce } from '@/utils/helper';\n\nconst props = defineProps({\n  modelValue: {\n    type: String,\n    default: '',\n  },\n  items: {\n    type: [Array, Object],\n    default: () => [],\n  },\n  itemKey: {\n    type: String,\n    default: '',\n  },\n  itemLabel: {\n    type: String,\n    default: '',\n  },\n  triggerChar: {\n    type: Array,\n    default: () => [],\n  },\n  customFilter: {\n    type: Function,\n    default: null,\n  },\n  replaceAfter: {\n    type: [String, Array],\n    default: null,\n  },\n  block: Boolean,\n  disabled: Boolean,\n  hideEmpty: Boolean,\n});\nconst emit = defineEmits([\n  'update:modelValue',\n  'change',\n  'search',\n  'select',\n  'cancel',\n  'selected',\n]);\n\nlet input = null;\nconst { t } = useI18n();\nconst componentId = useComponentId('autocomplete');\n\nconst state = shallowReactive({\n  charIndex: -1,\n  searchText: '',\n  activeIndex: -1,\n  showPopover: false,\n  inputChanged: false,\n});\n\nconst getItem = (item, key) =>\n  item[key ? props.itemKey : props.itemLabel] || item;\n\nconst filteredItems = computed(() => {\n  if (!state.showPopover) return [];\n\n  const triggerChar = props.triggerChar.length > 0;\n  const searchText = (\n    triggerChar ? state.searchText : props.modelValue\n  ).toLocaleLowerCase();\n\n  const defaultFilter = ({ item, text }) => {\n    return getItem(item)?.toLocaleLowerCase().includes(text);\n  };\n  const filterFunction = props.customFilter || defaultFilter;\n\n  return props.items.filter(\n    (item, index) =>\n      !state.inputChanged || filterFunction({ item, index, text: searchText })\n  );\n});\n\nfunction getLastKeyBeforeCaret(caretIndex) {\n  const getPosition = (val, index) => ({\n    index,\n    charIndex: input.value.lastIndexOf(val, caretIndex - 1),\n  });\n  const [charData] = props.triggerChar\n    .map(getPosition)\n    .sort((a, b) => b.charIndex - a.charIndex);\n\n  if (charData.index > 0) return -1;\n\n  return charData.charIndex;\n}\nfunction getSearchText(caretIndex, charIndex) {\n  if (charIndex !== -1) {\n    const closeTrigger = (props.triggerChar ?? [])[1];\n    const searchRgxp = new RegExp(\n      `\\\\s${closeTrigger ? `|${closeTrigger}` : ''}`\n    );\n\n    const inputValue = input.value;\n    const afterCaretTxt = inputValue.substring(caretIndex);\n    const lastClosingIdx = afterCaretTxt.search(searchRgxp);\n\n    const charsLength = props.triggerChar.length;\n    const text =\n      input.value.substring(charIndex + charsLength, caretIndex) +\n      afterCaretTxt.substring(0, lastClosingIdx);\n\n    if (!/\\s/.test(text)) {\n      return text;\n    }\n  }\n\n  return null;\n}\nfunction showPopover() {\n  if (props.disabled) return;\n\n  if (props.triggerChar.length < 1) {\n    state.showPopover = true;\n    return;\n  }\n\n  const { selectionStart } = input;\n\n  if (selectionStart >= 0) {\n    const charIndex = getLastKeyBeforeCaret(selectionStart);\n    const text = getSearchText(selectionStart, charIndex);\n\n    emit('search', text);\n\n    if (charIndex >= 0 && text) {\n      state.inputChanged = true;\n      state.showPopover = true;\n      state.searchText = text;\n      state.charIndex = charIndex;\n\n      return;\n    }\n  }\n\n  state.charIndex = -1;\n  state.searchText = '';\n  state.showPopover = false;\n}\nfunction checkInView(container, element, partial = false) {\n  const cTop = container.scrollTop;\n  const cBottom = cTop + container.clientHeight;\n\n  const eTop = element.offsetTop;\n  const eBottom = eTop + element.clientHeight;\n\n  const isTotal = eTop >= cTop && eBottom <= cBottom;\n  const isPartial =\n    partial &&\n    ((eTop < cTop && eBottom > cTop) || (eBottom > cBottom && eTop < cBottom));\n\n  return isTotal || isPartial;\n}\nfunction updateValue(value) {\n  state.inputChanged = true;\n\n  emit('change', value);\n  emit('update:modelValue', value);\n\n  input.value = value;\n  input.dispatchEvent(new Event('input'));\n}\nfunction selectItem(itemIndex, selected) {\n  let selectedItem = filteredItems.value[itemIndex];\n  if (!selectedItem) return;\n\n  selectedItem = getItem(selectedItem);\n\n  let caretPosition;\n  const isTriggerChar = state.charIndex >= 0 && state.searchText;\n\n  if (isTriggerChar) {\n    const val = input.value;\n    const index = state.charIndex;\n    const charLength = props.triggerChar[0].length;\n    const lastSearchIndex = state.searchText.length + index + charLength;\n\n    let charLastIndex = 0;\n\n    if (props.replaceAfter) {\n      const lastChars = Array.isArray(props.replaceAfter)\n        ? props.replaceAfter\n        : [props.replaceAfter];\n      lastChars.forEach((char) => {\n        const searchText = val.slice(0, lastSearchIndex);\n        const lastIndex = searchText.lastIndexOf(char);\n\n        if (lastIndex > charLastIndex && lastIndex > index) {\n          charLastIndex = lastIndex - 1;\n        }\n      });\n    }\n\n    caretPosition = index + charLength + selectedItem.length + charLastIndex;\n    selectedItem =\n      val.slice(0, index + charLength + charLastIndex) +\n      selectedItem +\n      val.slice(lastSearchIndex, val.length);\n  }\n\n  updateValue(selectedItem);\n\n  if (selected) {\n    emit('selected', {\n      index: itemIndex,\n      item: filteredItems.value[itemIndex],\n    });\n  }\n\n  if (isTriggerChar) {\n    input.selectionEnd = caretPosition;\n    const isNotTextarea = input.tagName !== 'TEXTAREA';\n\n    if (isNotTextarea) {\n      input.blur();\n      input.focus();\n    }\n  }\n}\nfunction handleKeydown(event) {\n  if (!state.showPopover) {\n    return;\n  }\n\n  const itemsLength = filteredItems.value.length - 1;\n\n  if (event.key === 'ArrowUp') {\n    if (state.activeIndex <= 0) state.activeIndex = itemsLength;\n    else state.activeIndex -= 1;\n\n    event.preventDefault();\n  } else if (event.key === 'ArrowDown') {\n    if (state.activeIndex >= itemsLength) state.activeIndex = 0;\n    else state.activeIndex += 1;\n\n    event.preventDefault();\n  } else if (event.key === 'Enter' && state.showPopover) {\n    selectItem(state.activeIndex, true);\n\n    event.preventDefault();\n  } else if (event.key === 'Escape') {\n    emit('cancel');\n    state.showPopover = false;\n  }\n}\nfunction handleBlur() {\n  state.showPopover = false;\n}\nfunction handleFocus() {\n  if (props.triggerChar.length < 1) return;\n\n  showPopover();\n}\nfunction handleInput() {\n  state.inputChanged = true;\n}\nfunction attachEvents() {\n  if (!input) return;\n\n  input.addEventListener('blur', handleBlur);\n  input.addEventListener('input', handleInput);\n  input.addEventListener('focus', handleFocus);\n  input.addEventListener('input', showPopover);\n  input.addEventListener('keydown', handleKeydown);\n}\nfunction detachEvents() {\n  if (!input) return;\n\n  input.removeEventListener('blur', handleBlur);\n  input.removeEventListener('input', handleInput);\n  input.removeEventListener('focus', handleFocus);\n  input.removeEventListener('input', showPopover);\n  input.removeEventListener('keydown', handleKeydown);\n}\n\nwatch(\n  () => state.activeIndex,\n  debounce((activeIndex) => {\n    const container = document.querySelector(`.${componentId}`);\n    const element = container.querySelector(`#list-item-${activeIndex}`);\n\n    if (element && !checkInView(container, element)) {\n      element.scrollIntoView({\n        block: 'nearest',\n        behavior: 'smooth',\n      });\n    }\n\n    emit('select', {\n      index: activeIndex,\n      item: filteredItems.value[activeIndex],\n    });\n  }, 100)\n);\nwatch(\n  () => filteredItems,\n  () => {\n    if (filteredItems.value.length === 0 && props.hideEmpty) {\n      state.showPopover = false;\n    }\n  },\n  { deep: true }\n);\nwatch(\n  () => state.showPopover,\n  (value) => {\n    if (!value) state.inputChanged = false;\n  }\n);\n\nonMounted(() => {\n  if (props.modelValue) {\n    const activeIndex = props.items.findIndex(\n      (item) => getItem(item) === props.modelValue\n    );\n\n    if (activeIndex !== -1) state.activeIndex = activeIndex;\n  }\n\n  input = document.querySelector(\n    `#${componentId} input, #${componentId} textarea`\n  );\n\n  attachEvents();\n});\nonBeforeUnmount(() => {\n  detachEvents();\n});\n\ndefineExpose({\n  state,\n});\n</script>\n<style>\n.ui-autocomplete.block,\n.ui-autocomplete.block .ui-popover__trigger {\n  width: 100%;\n  display: block;\n}\n</style>\n"
  },
  {
    "path": "src/components/ui/UiButton.vue",
    "content": "<template>\n  <component\n    :is=\"tag\"\n    role=\"button\"\n    class=\"ui-button relative h-10 transition\"\n    :class=\"[\n      color ? color : variants[btnType][variant],\n      icon ? 'p-2' : 'py-2 px-4',\n      circle ? 'rounded-full' : 'rounded-lg',\n      {\n        'opacity-70': disabled,\n        'pointer-events-none': loading || disabled,\n      },\n    ]\"\n    v-bind=\"{ disabled: loading || disabled, ...$attrs }\"\n  >\n    <span\n      class=\"flex h-full items-center justify-center\"\n      :class=\"{ 'opacity-25': loading }\"\n    >\n      <slot></slot>\n    </span>\n    <div v-if=\"loading\" class=\"button-loading\">\n      <ui-spinner\n        :color=\"\n          variant === 'default'\n            ? 'text-primary'\n            : 'text-white dark:text-gray-900'\n        \"\n      ></ui-spinner>\n    </div>\n  </component>\n</template>\n<script>\nimport UiSpinner from './UiSpinner.vue';\n\nexport default {\n  components: { UiSpinner },\n  props: {\n    icon: Boolean,\n    disabled: Boolean,\n    loading: Boolean,\n    circle: Boolean,\n    color: {\n      type: String,\n      default: '',\n    },\n    tag: {\n      type: String,\n      default: 'button',\n    },\n    btnType: {\n      type: String,\n      default: 'fill',\n    },\n    variant: {\n      type: String,\n      default: 'default',\n    },\n  },\n  setup() {\n    const variants = {\n      transparent: {\n        default: 'hoverable',\n      },\n      fill: {\n        default: 'bg-input',\n        accent:\n          'bg-accent hover:bg-gray-700 dark:bg-gray-100 dark:hover:bg-gray-200 dark:text-black text-white',\n        primary:\n          'bg-primary text-white dark:bg-secondary dark:hover:bg-primary hover:bg-secondary',\n        danger:\n          'bg-red-400 text-white dark:bg-red-500 dark:hover:bg-red-500 hover:bg-red-400',\n      },\n    };\n\n    return {\n      variants,\n    };\n  },\n};\n</script>\n<style>\n.button-loading {\n  position: absolute;\n  top: 50%;\n  left: 50%;\n  transform: translate(-50%, -50%);\n}\n</style>\n"
  },
  {
    "path": "src/components/ui/UiCard.vue",
    "content": "<template>\n  <component\n    :is=\"tag\"\n    v-bind=\"$attrs\"\n    class=\"ui-card rounded-lg bg-white dark:bg-gray-800\"\n    :class=\"[padding, { 'hover:shadow-xl hover:-translate-y-1': hover }]\"\n  >\n    <slot></slot>\n  </component>\n</template>\n<script>\nexport default {\n  props: {\n    hover: {\n      type: Boolean,\n      default: false,\n    },\n    padding: {\n      type: String,\n      default: 'p-4',\n    },\n    tag: {\n      type: String,\n      default: 'div',\n    },\n  },\n};\n</script>\n"
  },
  {
    "path": "src/components/ui/UiCheckbox.vue",
    "content": "<template>\n  <label\n    class=\"checkbox-ui items-center\"\n    :class=\"[block ? 'flex' : 'inline-flex']\"\n  >\n    <div\n      :class=\"{\n        'pointer-events-none opacity-75': disabled,\n      }\"\n      class=\"relative inline-block h-5 w-5 rounded focus-within:ring-2 focus-within:ring-accent\"\n    >\n      <input\n        :class=\"{ indeterminate }\"\n        type=\"checkbox\"\n        class=\"checkbox-ui__input opacity-0\"\n        :value=\"modelValue\"\n        v-bind=\"{ checked: modelValue, disabled }\"\n        @change=\"changeHandler\"\n      />\n      <div\n        class=\"bg-input checkbox-ui__mark absolute top-0 left-0 cursor-pointer rounded border dark:border-gray-700\"\n      >\n        <v-remixicon\n          :name=\"indeterminate ? 'riSubtractLine' : 'riCheckLine'\"\n          size=\"20\"\n          class=\"text-white dark:text-black\"\n        ></v-remixicon>\n      </div>\n    </div>\n    <span v-if=\"$slots.default\" class=\"ml-2 inline-block\">\n      <slot></slot>\n    </span>\n  </label>\n</template>\n<script>\nexport default {\n  props: {\n    modelValue: {\n      type: Boolean,\n      default: false,\n    },\n    indeterminate: {\n      type: Boolean,\n      default: false,\n    },\n    disabled: {\n      type: Boolean,\n      default: null,\n    },\n    block: {\n      type: Boolean,\n      default: null,\n    },\n  },\n  emits: ['update:modelValue', 'change'],\n  setup(props, { emit }) {\n    function changeHandler({ target: { checked } }) {\n      emit('update:modelValue', checked);\n      emit('change', checked);\n    }\n\n    return {\n      changeHandler,\n    };\n  },\n};\n</script>\n<style scoped>\n.checkbox-ui__input:checked ~ .checkbox-ui__mark .v-remixicon,\n.checkbox-ui__input.indeterminate ~ .checkbox-ui__mark .v-remixicon {\n  transform: scale(1) !important;\n}\n.checkbox-ui .v-remixicon {\n  transform: scale(0);\n}\n.checkbox-ui__input:checked ~ .checkbox-ui__mark,\n.checkbox-ui__input.indeterminate ~ .checkbox-ui__mark {\n  @apply bg-accent border-accent bg-opacity-100;\n}\n.checkbox-ui__mark {\n  width: 100%;\n  height: 100%;\n  transition-property: background-color, border-color;\n  transition-timing-function: ease;\n  transition-duration: 200ms;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n}\n.checkbox-ui__mark .v-remixicon {\n  transform: scale(0) !important;\n  transition: transform 200ms ease;\n}\n</style>\n"
  },
  {
    "path": "src/components/ui/UiDialog.vue",
    "content": "<template>\n  <ui-modal\n    :model-value=\"state.show\"\n    content-class=\"max-w-sm\"\n    @close=\"state.show = false\"\n  >\n    <template #header>\n      <h3 class=\"font-semibold\">{{ state.options.title }}</h3>\n    </template>\n    <slot\n      v-if=\"state.options.custom\"\n      v-bind=\"{ options: state.options }\"\n      :name=\"state.type\"\n    />\n    <template v-else>\n      <p class=\"leading-tight text-gray-600 dark:text-gray-200\">\n        {{ state.options.body }}\n      </p>\n      <ui-input\n        v-if=\"state.type === 'prompt'\"\n        v-model=\"state.input\"\n        autofocus\n        :disabled=\"state.loading\"\n        :placeholder=\"state.options.placeholder\"\n        :label=\"state.options.label\"\n        :type=\"\n          state.options.inputType === 'password' && state.showPassword\n            ? 'text'\n            : state.options.inputType\n        \"\n        class=\"w-full\"\n      >\n        <template v-if=\"state.options.inputType === 'password'\" #append>\n          <v-remixicon\n            :name=\"state.showPassword ? 'riEyeOffLine' : 'riEyeLine'\"\n            class=\"absolute right-2\"\n            @click=\"state.showPassword = !state.showPassword\"\n          />\n        </template>\n      </ui-input>\n      <div class=\"mt-8 flex space-x-2\">\n        <ui-button class=\"w-6/12\" @click=\"fireCallback('onCancel')\">\n          {{ state.options.cancelText }}\n        </ui-button>\n        <ui-button\n          class=\"w-6/12\"\n          :loading=\"state.loading\"\n          :variant=\"state.options.okVariant\"\n          @click=\"fireCallback('onConfirm')\"\n        >\n          {{ state.options.okText }}\n        </ui-button>\n      </div>\n    </template>\n  </ui-modal>\n</template>\n<script>\nimport { reactive, watch, onUnmounted } from 'vue';\nimport { useI18n } from 'vue-i18n';\nimport defu from 'defu';\nimport { throttle } from '@/utils/helper';\nimport emitter from '@/lib/mitt';\n\nexport default {\n  setup() {\n    const { t } = useI18n();\n\n    const defaultOptions = {\n      body: '',\n      title: '',\n      label: '',\n      html: false,\n      onCancel: null,\n      onConfirm: null,\n      placeholder: '',\n      inputType: 'text',\n      showLoading: false,\n      okVariant: 'accent',\n      okText: t('common.confirm'),\n      cancelText: t('common.cancel'),\n    };\n    const state = reactive({\n      type: '',\n      input: '',\n      show: false,\n      loading: false,\n      showPassword: false,\n      options: defaultOptions,\n    });\n\n    function handleShowDialog({ type, options }) {\n      state.type = type;\n      state.input = options?.inputValue ?? '';\n      state.options = defu(options, defaultOptions);\n\n      state.show = true;\n    }\n    function destroy() {\n      state.input = '';\n      state.show = false;\n      state.showPassword = false;\n      state.options = defaultOptions;\n    }\n    const fireCallback = throttle((type) => {\n      const callback = state.options[type];\n      const param = state.type === 'prompt' ? state.input : true;\n\n      if (callback) {\n        const isAsync = state.options.async;\n        if (isAsync) state.loading = true;\n\n        const cbReturn = callback(param) ?? true;\n\n        if (typeof cbReturn === 'boolean') {\n          if (cbReturn) destroy();\n          state.loading = false;\n\n          return;\n        }\n        if (isAsync && cbReturn?.then) {\n          cbReturn.then((value) => {\n            if (value) destroy();\n            state.loading = false;\n          });\n\n          return;\n        }\n\n        destroy();\n      } else {\n        destroy();\n      }\n    }, 200);\n    function keyupHandler({ code }) {\n      if (code === 'Enter') {\n        fireCallback('onConfirm');\n      } else if (code === 'Escape') {\n        fireCallback('onCancel');\n      }\n    }\n\n    watch(\n      () => state.show,\n      (value) => {\n        if (value) {\n          window.addEventListener('keyup', keyupHandler);\n        } else {\n          window.removeEventListener('keyup', keyupHandler);\n        }\n      }\n    );\n\n    emitter.on('show-dialog', handleShowDialog);\n\n    onUnmounted(() => {\n      emitter.off('show-dialog', handleShowDialog);\n    });\n\n    return {\n      state,\n      fireCallback,\n    };\n  },\n};\n</script>\n"
  },
  {
    "path": "src/components/ui/UiExpand.vue",
    "content": "<template>\n  <div :aria-expanded=\"show\" :class=\"{ [activeClass]: show }\" class=\"ui-expand\">\n    <button\n      :class=\"[headerClass, { [headerActiveClass]: show }]\"\n      @click=\"toggleExpand\"\n    >\n      <v-remixicon\n        v-if=\"!hideHeaderIcon && !appendIcon\"\n        :rotate=\"show ? 90 : -90\"\n        name=\"riArrowLeftSLine\"\n        class=\"mr-2 -ml-1 transition-transform\"\n      />\n      <slot v-bind=\"{ show }\" name=\"header\" />\n      <v-remixicon\n        v-if=\"appendIcon\"\n        :rotate=\"show ? 90 : -90\"\n        name=\"riArrowLeftSLine\"\n        class=\"-mr-1 ml-2 transition-transform\"\n      />\n    </button>\n    <transition-expand>\n      <div v-if=\"show\" :class=\"panelClass\" class=\"ui-expand__panel\">\n        <slot></slot>\n      </div>\n    </transition-expand>\n  </div>\n</template>\n<script setup>\nimport { watch, ref } from 'vue';\n\nconst props = defineProps({\n  modelValue: {\n    type: Boolean,\n    default: true,\n  },\n  panelClass: {\n    type: String,\n    default: '',\n  },\n  headerClass: {\n    type: String,\n    default: 'px-4 py-2 w-full flex items-center h-full',\n  },\n  headerActiveClass: {\n    type: String,\n    default: '',\n  },\n  activeClass: {\n    type: String,\n    default: '',\n  },\n  hideHeaderIcon: {\n    type: Boolean,\n    default: false,\n  },\n  disabled: Boolean,\n  appendIcon: Boolean,\n});\nconst emit = defineEmits(['update:modelValue']);\n\nconst show = ref(false);\n\nfunction toggleExpand() {\n  if (props.disabled) return;\n\n  show.value = !show.value;\n\n  emit('update:modelValue', show.value);\n}\n\nwatch(\n  () => props.modelValue,\n  (value) => {\n    if (value === show.value) return;\n\n    show.value = value;\n  },\n  { immediate: true }\n);\nwatch(\n  () => props.disabled,\n  () => {\n    show.value = false;\n  }\n);\n</script>\n"
  },
  {
    "path": "src/components/ui/UiFileInput.vue",
    "content": "<template>\n  <div class=\"input-ui inline-block w-full\">\n    <label\n      v-if=\"label\"\n      class=\"ml-1 inline-flex items-center text-sm leading-none text-gray-600 dark:text-gray-200\"\n    >\n      <span>{{ label }}</span>\n      <v-remixicon\n        v-tooltip=\"tooltipContent\"\n        name=\"riInformationLine\"\n        class=\"ml-1\"\n        size=\"16\"\n      />\n    </label>\n    <div class=\"mt-1 w-full\">\n      <input\n        ref=\"fileInput\"\n        type=\"file\"\n        class=\"hidden\"\n        :accept=\"accept\"\n        @change=\"handleFileChange\"\n      />\n      <ui-button\n        :loading=\"uploading\"\n        :disabled=\"uploading\"\n        variant=\"default\"\n        class=\"w-full\"\n        @click=\"fileInput.click()\"\n      >\n        <v-remixicon name=\"riUploadLine\" class=\"mr-2 -ml-1\" />\n        Choose File\n      </ui-button>\n      <p\n        class=\"mt-1 text-sm text-center text-gray-500 dark:text-gray-400\"\n        :class=\"{ 'text-red-500': hasError }\"\n      >\n        {{ statusText }}\n      </p>\n    </div>\n  </div>\n</template>\n<script setup>\nimport {\n  computed,\n  defineEmits,\n  defineProps,\n  onMounted,\n  ref,\n  shallowRef,\n} from 'vue';\nimport { useToast } from 'vue-toastification';\nimport UiButton from './UiButton.vue';\n\nconst props = defineProps({\n  modelValue: {\n    type: [String, Object],\n    default: '',\n  },\n  label: {\n    type: String,\n    default: '',\n  },\n  accept: {\n    type: String,\n    default: '*',\n  },\n  maxSize: {\n    type: Number,\n    default: 30, // in MB\n  },\n  onUpload: {\n    type: Function,\n    default: null,\n  },\n});\n\nconst emit = defineEmits(['update:modelValue', 'change']);\nconst toast = useToast();\n\nconst uploading = ref(false);\nconst fileInput = ref(null);\nconst fileName = shallowRef('');\nconst hasError = ref(false);\n\nconst tooltipContent = computed(() => ({\n  allowHTML: true,\n  content: `Max size: ${\n    props.maxSize\n  }MB<br>Accepted types: ${props.accept.replace(/,/g, ', ')}`,\n  maxWidth: 250,\n}));\n\nconst statusText = computed(() => {\n  if (uploading.value) return 'Uploading...';\n  if (hasError.value) return 'Upload failed. Please try again.';\n  return fileName.value || 'No file selected.';\n});\n\nconst isFileTypeValid = (file) => {\n  if (props.accept === '*') return true;\n\n  const acceptedTypes = props.accept.split(',').map((type) => type.trim());\n  const { name: fName, type: fileType } = file;\n\n  return acceptedTypes.some((acceptedType) => {\n    if (acceptedType.startsWith('.')) {\n      return fName.endsWith(acceptedType);\n    }\n    if (acceptedType.endsWith('/*')) {\n      return fileType.startsWith(acceptedType.slice(0, -1));\n    }\n    return fileType === acceptedType;\n  });\n};\n\nconst resetState = () => {\n  fileInput.value.value = '';\n  fileName.value = '';\n};\n\nconst handleError = (message) => {\n  toast.error(message);\n  hasError.value = true;\n  resetState();\n};\n\nconst handleFileChange = async (event) => {\n  hasError.value = false;\n  const file = event.target.files[0];\n\n  if (!file) {\n    resetState();\n    return;\n  }\n\n  fileName.value = file.name;\n\n  if (!isFileTypeValid(file)) {\n    handleError('Invalid file type.');\n    return;\n  }\n\n  if (file.size > props.maxSize * 1024 * 1024) {\n    handleError(`File size should not exceed ${props.maxSize}MB`);\n    return;\n  }\n\n  if (!props.onUpload) {\n    handleError('onUpload function is not provided');\n    return;\n  }\n\n  uploading.value = true;\n\n  try {\n    const value = await props.onUpload(file);\n\n    emit('update:modelValue', value);\n    emit('change', value);\n    fileName.value = file.name; // Keep filename on success\n  } catch (error) {\n    console.error('Upload error:', error);\n    toast.error(error.message || 'An error occurred during file upload.');\n    handleError(error.message || 'An error occurred during file upload.');\n  } finally {\n    uploading.value = false;\n  }\n};\n\nonMounted(() => {\n  if (typeof props.modelValue === 'object' && props.modelValue?.filename) {\n    fileName.value = props.modelValue.filename;\n  }\n});\n</script>\n"
  },
  {
    "path": "src/components/ui/UiImg.vue",
    "content": "<template>\n  <div ref=\"imageContainer\" class=\"ui-image relative\">\n    <div class=\"flex items-center justify-center\">\n      <slot v-if=\"state.loading\" name=\"loading\">\n        <div\n          class=\"bg-input-dark absolute h-full w-full animate-pulse rounded-lg\"\n        ></div>\n      </slot>\n      <slot v-else-if=\"state.error\" name=\"error\">\n        <p class=\"text-lighter text-center\">Failed to load image</p>\n      </slot>\n      <div\n        v-else\n        :style=\"{\n          backgroundImage: `url(${src})`,\n          backgroundSize: contain ? 'contain' : 'cover',\n        }\"\n        v-bind=\"{ role: alt ? 'img' : null, 'aria-label': alt }\"\n        class=\"absolute top-0 left-0 h-full w-full bg-center bg-no-repeat\"\n      >\n        <slot></slot>\n      </div>\n    </div>\n  </div>\n</template>\n<script>\nimport { ref, shallowReactive, onMounted } from 'vue';\n\nexport default {\n  props: {\n    src: {\n      type: String,\n      default: '',\n    },\n    alt: {\n      type: String,\n      default: '',\n    },\n    lazy: Boolean,\n    contain: Boolean,\n  },\n  emits: ['error', 'load'],\n  setup(props, { emit }) {\n    const imageContainer = ref(null);\n    const state = shallowReactive({\n      loading: true,\n      error: false,\n    });\n\n    function handleImageLoad() {\n      state.loading = false;\n      state.error = false;\n\n      emit('load', true);\n    }\n    function handleImageError() {\n      state.loading = false;\n      state.error = true;\n\n      emit('error', true);\n    }\n    function loadImage() {\n      const image = new Image();\n\n      image.onload = () => handleImageLoad(image);\n      image.onerror = handleImageError;\n      image.src = props.src;\n    }\n\n    const observer = new IntersectionObserver((entries) => {\n      entries.forEach((entry) => {\n        if (entry.isIntersecting) {\n          const { target } = entry;\n          loadImage();\n          observer.unobserve(target);\n        }\n      });\n    });\n\n    onMounted(() => {\n      if (props.lazy) observer.observe(imageContainer.value);\n      else loadImage();\n    });\n\n    return {\n      state,\n      imageContainer,\n    };\n  },\n};\n</script>\n"
  },
  {
    "path": "src/components/ui/UiInput.vue",
    "content": "<template>\n  <div class=\"input-ui inline-block\">\n    <label\n      v-if=\"label || $slots.label\"\n      :for=\"componentId\"\n      class=\"ml-1 inline-block text-sm leading-none text-gray-600 dark:text-gray-200\"\n    >\n      <slot name=\"label\">{{ label }}</slot>\n    </label>\n    <div class=\"relative flex w-full items-center\">\n      <slot name=\"prepend\">\n        <v-remixicon\n          v-if=\"prependIcon\"\n          class=\"absolute left-0 ml-2 text-gray-600 dark:text-gray-200\"\n          :name=\"prependIcon\"\n        ></v-remixicon>\n      </slot>\n      <input\n        v-bind=\"{\n          readonly: disabled || readonly || null,\n          placeholder,\n          type,\n          autocomplete,\n          autofocus,\n          min,\n          max,\n          list,\n          step,\n        }\"\n        :id=\"componentId\"\n        v-autofocus=\"autofocus\"\n        v-imask=\"mask\"\n        :class=\"[\n          statusColors[status],\n          inputClass,\n          {\n            'opacity-75 pointer-events-none': disabled,\n            'pl-10': prependIcon || $slots.prepend,\n            'appearance-none': list,\n          },\n        ]\"\n        :value=\"modelValue\"\n        class=\"bg-input w-full rounded-lg bg-transparent py-2 px-4 transition\"\n        @keydown=\"$emit('keydown', $event)\"\n        @keyup=\"$emit('keyup', $event)\"\n        @blur=\"$emit('blur', $event)\"\n        @focus=\"$emit('focus', $event)\"\n        @input=\"emitValue\"\n      />\n      <slot name=\"append\" />\n    </div>\n  </div>\n</template>\n<script setup>\n/* eslint-disable vue/require-prop-types */\nimport { IMaskDirective as vImask } from 'vue-imask';\nimport { useComponentId } from '@/composable/componentId';\n\nconst props = defineProps({\n  modelModifiers: {\n    default: () => ({}),\n  },\n  disabled: {\n    type: Boolean,\n    default: false,\n  },\n  readonly: {\n    type: Boolean,\n    default: false,\n  },\n  autofocus: {\n    type: Boolean,\n    default: false,\n  },\n  modelValue: {\n    type: [String, Number, Object],\n    default: '',\n  },\n  inputClass: {\n    type: String,\n    default: '',\n  },\n  prependIcon: {\n    type: String,\n    default: '',\n  },\n  label: {\n    type: String,\n    default: '',\n  },\n  list: {\n    type: String,\n    default: null,\n  },\n  type: {\n    type: String,\n    default: 'text',\n  },\n  placeholder: {\n    type: String,\n    default: '',\n  },\n  max: {\n    type: [String, Number],\n    default: null,\n  },\n  min: {\n    type: [String, Number],\n    default: null,\n  },\n  autocomplete: {\n    type: String,\n    default: null,\n  },\n  step: {\n    type: String,\n    default: null,\n  },\n  mask: {\n    type: [Array, Object],\n    default: null,\n  },\n  status: {\n    type: String,\n    default: '',\n  },\n  unmaskValue: Boolean,\n});\nconst emit = defineEmits([\n  'update:modelValue',\n  'change',\n  'keydown',\n  'blur',\n  'keyup',\n  'focus',\n]);\n\nconst componentId = useComponentId('ui-input');\n\nconst statusColors = {\n  error: 'ring-red-400 ring-2 focus:ring-red-400 focus:ring-2',\n};\n\nfunction emitValue(event) {\n  let { value } = event.target;\n\n  if (props.mask && props.unmaskValue) {\n    const { maskRef } = event.target;\n    if (maskRef && maskRef.unmaskedValue) value = maskRef.unmaskedValue;\n  }\n\n  if (props.modelModifiers.lowercase) {\n    value = value.toLocaleLowerCase();\n  } else if (props.modelModifiers.number) {\n    value = +value;\n  }\n\n  emit('update:modelValue', value);\n  emit('change', value);\n}\n</script>\n<style>\n.input-ui input[type='color'] {\n  padding-top: 0 !important;\n  padding-bottom: 0 !important;\n}\n</style>\n"
  },
  {
    "path": "src/components/ui/UiList.vue",
    "content": "<template>\n  <div\n    role=\"listbox\"\n    class=\"ui-list\"\n    :class=\"{ 'pointer-events-none': disabled }\"\n  >\n    <slot></slot>\n  </div>\n</template>\n<script>\nexport default {\n  props: {\n    disabled: Boolean,\n  },\n};\n</script>\n"
  },
  {
    "path": "src/components/ui/UiListItem.vue",
    "content": "<template>\n  <component\n    :is=\"tag\"\n    class=\"ui-list-item flex w-full items-center rounded-lg transition focus:outline-none\"\n    role=\"listitem\"\n    :class=\"[\n      active ? color : 'hoverable',\n      small ? 'p-2' : 'py-2 px-4',\n      { 'pointer-events-none bg-opacity-75': disabled },\n    ]\"\n  >\n    <slot></slot>\n  </component>\n</template>\n<script>\nexport default {\n  props: {\n    active: Boolean,\n    disabled: Boolean,\n    small: Boolean,\n    tag: {\n      type: String,\n      default: 'div',\n    },\n    color: {\n      type: String,\n      default:\n        'bg-primary text-primary dark:bg-secondary dark:text-secondary bg-opacity-10 dark:bg-opacity-10',\n    },\n  },\n};\n</script>\n"
  },
  {
    "path": "src/components/ui/UiModal.vue",
    "content": "<template>\n  <div class=\"modal-ui\">\n    <div v-if=\"$slots.activator\" class=\"modal-ui__activator\">\n      <slot name=\"activator\" v-bind=\"{ open: () => (show = true) }\"></slot>\n    </div>\n    <teleport :to=\"teleportTo\" :disabled=\"disabledTeleport\">\n      <transition name=\"modal\" mode=\"out-in\">\n        <div\n          v-if=\"show\"\n          :class=\"[positions[contentPosition]]\"\n          class=\"modal-ui__content-container z-50 flex justify-center overflow-y-auto\"\n          :style=\"{ 'backdrop-filter': blur && 'blur(2px)' }\"\n        >\n          <div\n            class=\"absolute h-full w-full bg-black bg-opacity-20 dark:bg-opacity-60\"\n            style=\"z-index: -2\"\n            @click=\"closeModal\"\n          />\n          <slot v-if=\"customContent\"></slot>\n          <ui-card\n            v-else\n            class=\"modal-ui__content w-full shadow-lg\"\n            :padding=\"padding\"\n            :class=\"[contentClass]\"\n          >\n            <div class=\"modal-ui__content-header mb-4\">\n              <div class=\"flex items-center justify-between\">\n                <span class=\"content-header\">\n                  <slot name=\"header\">{{ title }}</slot>\n                </span>\n                <slot name=\"header-append\" />\n                <v-remixicon\n                  v-show=\"!persist\"\n                  class=\"cursor-pointer text-gray-600 dark:text-gray-300\"\n                  name=\"riCloseLine\"\n                  size=\"20\"\n                  @click=\"closeModal\"\n                ></v-remixicon>\n              </div>\n            </div>\n            <slot :close=\"closeModal\"></slot>\n          </ui-card>\n        </div>\n      </transition>\n    </teleport>\n  </div>\n</template>\n<script>\nimport { ref, watch } from 'vue';\n\nexport default {\n  props: {\n    modelValue: {\n      type: Boolean,\n      default: false,\n    },\n    teleportTo: {\n      type: String,\n      default: 'body',\n    },\n    contentClass: {\n      type: String,\n      default: 'max-w-lg',\n    },\n    title: {\n      type: String,\n      default: '',\n    },\n    padding: {\n      type: String,\n      default: 'p-4',\n    },\n    contentPosition: {\n      type: String,\n      default: 'center',\n    },\n    customContent: Boolean,\n    persist: Boolean,\n    blur: Boolean,\n    disabledTeleport: Boolean,\n  },\n  emits: ['close', 'update:modelValue'],\n  setup(props, { emit }) {\n    const positions = {\n      center: 'items-center',\n      start: 'items-start',\n    };\n\n    const show = ref(false);\n    const modalContent = ref(null);\n\n    function toggleBodyOverflow(value) {\n      document.body.classList.toggle('overflow-hidden', value);\n    }\n    function closeModal() {\n      if (props.persist) return;\n\n      show.value = false;\n      emit('close', false);\n      emit('update:modelValue', false);\n\n      toggleBodyOverflow(false);\n    }\n    function keyupHandler({ code }) {\n      if (code === 'Escape') closeModal();\n    }\n\n    watch(\n      () => props.modelValue,\n      (value) => {\n        show.value = value;\n      },\n      { immediate: true }\n    );\n\n    watch(show, (value) => {\n      if (value) window.addEventListener('keyup', keyupHandler);\n      else window.removeEventListener('keyup', keyupHandler);\n\n      toggleBodyOverflow(value);\n    });\n\n    return {\n      show,\n      positions,\n      closeModal,\n      modalContent,\n    };\n  },\n};\n</script>\n<style>\n.modal-enter-active,\n.modal-leave-active {\n  transition: opacity 0.3s ease;\n}\n\n.modal-enter-active .modal-ui__content,\n.modal-leave-active .modal-ui__content {\n  transition: transform 0.3s ease;\n  transform: translateY(0px);\n}\n\n.modal-enter-from,\n.modal-leave-to {\n  opacity: 0;\n}\n\n.modal-enter-from .modal-ui__content,\n.modal-leave-to .modal-ui__content {\n  transform: translateY(30px);\n}\n\n.modal-ui__content-container {\n  position: fixed;\n  top: 0;\n  left: 0;\n  width: 100%;\n  height: 100%;\n}\n</style>\n"
  },
  {
    "path": "src/components/ui/UiPaginatedSelect.vue",
    "content": "<template>\n  <div\n    ref=\"root\"\n    class=\"ui-paginated-select relative\"\n    :class=\"{ 'pointer-events-none opacity-75': disabled }\"\n  >\n    <label\n      v-if=\"label\"\n      :for=\"componentId\"\n      class=\"ml-1 text-sm text-gray-600 dark:text-gray-200\"\n      @click=\"toggleDropdown\"\n    >\n      {{ label }}\n    </label>\n    <div class=\"ui-select__content relative flex w-full items-center\">\n      <button\n        :id=\"componentId\"\n        type=\"button\"\n        class=\"bg-input text-left z-10 h-full w-full appearance-none rounded-lg bg-transparent px-4 py-2 pr-10 transition\"\n        :class=\"{ 'text-gray-500': !selectedOptionLabel }\"\n        :disabled=\"disabled\"\n        @click=\"toggleDropdown\"\n      >\n        {{ selectedOptionLabel || placeholder }}\n      </button>\n      <v-remixicon\n        name=\"riArrowDropDownLine\"\n        size=\"28\"\n        class=\"pointer-events-none absolute right-0 mr-2 text-gray-600 transition-transform dark:text-gray-200\"\n        :class=\"{ 'rotate-180': isOpen }\"\n      />\n    </div>\n\n    <div\n      v-if=\"isOpen\"\n      class=\"absolute top-full z-50 mt-1 w-full rounded-lg bg-white shadow-xl dark:bg-gray-800\"\n    >\n      <div class=\"px-2 my-2 w-full\">\n        <ui-input\n          v-model=\"searchKeyword\"\n          :placeholder=\"searchPlaceholder\"\n          prepend-icon=\"riSearch2Line\"\n          autofocus\n          class=\"w-full\"\n        />\n      </div>\n      <ul\n        ref=\"optionsListEl\"\n        class=\"max-h-60 overflow-y-auto p-2 space-y-2\"\n        @scroll=\"handleScroll\"\n      >\n        <li\n          v-for=\"option in options\"\n          :key=\"option[optionValueKey]\"\n          class=\"cursor-pointer rounded-lg px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-700\"\n          :class=\"{\n            'bg-gray-100 font-semibold dark:bg-gray-700':\n              modelValue === option[optionValueKey],\n          }\"\n          @click=\"selectOption(option)\"\n        >\n          {{ option[optionLabelKey] }}\n        </li>\n        <li v-if=\"isLoading\" class=\"px-4 py-2 text-center text-gray-500\">\n          Loading...\n        </li>\n        <li\n          v-if=\"!haveMore && !isLoading && options.length > 0\"\n          class=\"px-4 py-2 text-center text-sm text-gray-500\"\n        >\n          No more results\n        </li>\n        <li\n          v-if=\"!isLoading && options.length === 0\"\n          class=\"px-4 py-2 text-center text-gray-500\"\n        >\n          No results found\n        </li>\n      </ul>\n      <div\n        v-if=\"$slots.footer\"\n        class=\"border-t border-gray-200 p-2 dark:border-gray-700\"\n      >\n        <slot name=\"footer\"></slot>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script setup>\nimport { useComponentId } from '@/composable/componentId';\nimport {\n  computed,\n  defineEmits,\n  defineProps,\n  onBeforeUnmount,\n  onMounted,\n  ref,\n  watch,\n} from 'vue';\n\nconst props = defineProps({\n  modelValue: {\n    type: [String, Number, null],\n    default: null,\n  },\n  label: {\n    type: String,\n    default: '',\n  },\n  placeholder: {\n    type: String,\n    default: 'Select an option',\n  },\n  searchPlaceholder: {\n    type: String,\n    default: 'Search...',\n  },\n  disabled: Boolean,\n  loadOptions: {\n    type: Function,\n    required: true,\n  },\n  optionValueKey: {\n    type: String,\n    required: true,\n  },\n  optionLabelKey: {\n    type: String,\n    required: true,\n  },\n  initialLabel: {\n    type: String,\n    default: '',\n  },\n});\n\nconst emit = defineEmits(['update:modelValue', 'change']);\n\nconst componentId = useComponentId('paginated-select');\n\nconst root = ref(null);\nconst isOpen = ref(false);\nconst options = ref([]);\nconst searchKeyword = ref('');\nconst isLoading = ref(false);\nconst page = ref(1);\nconst haveMore = ref(true);\nconst optionsListEl = ref(null);\nconst localInitialLabel = ref('');\nconst fetchRequestToken = ref(0);\n\nconst selectedOption = computed(() =>\n  options.value.find((opt) => opt[props.optionValueKey] === props.modelValue)\n);\n\nconst selectedOptionLabel = computed(\n  () => selectedOption.value?.[props.optionLabelKey] || localInitialLabel.value\n);\n\nfunction debounce(func, wait) {\n  let timeout;\n  return function executedFunction(...args) {\n    const later = () => {\n      clearTimeout(timeout);\n      func(...args);\n    };\n    clearTimeout(timeout);\n    timeout = setTimeout(later, wait);\n  };\n}\n\nconst fetchOptions = async (isNewSearch) => {\n  // Prevent concurrent scroll requests, but allow new searches to override.\n  if (isLoading.value && !isNewSearch) return;\n\n  isLoading.value = true;\n  if (isNewSearch) {\n    page.value = 1;\n    options.value = [];\n    haveMore.value = true;\n    // Each new search gets a unique token.\n    // This invalidates ongoing requests from previous searches.\n    fetchRequestToken.value += 1;\n  }\n\n  const currentRequestToken = fetchRequestToken.value;\n\n  // Do not fetch if we know there are no more results\n  if (!haveMore.value) {\n    isLoading.value = false;\n    return;\n  }\n\n  try {\n    const { data, hasMore } = await props.loadOptions({\n      query: searchKeyword.value,\n      page: page.value,\n    });\n\n    // If the token has changed, a new search was initiated.\n    // We must ignore the results of this outdated request.\n    if (currentRequestToken !== fetchRequestToken.value) {\n      return;\n    }\n\n    options.value.push(...data);\n    haveMore.value = hasMore;\n    if (haveMore.value) {\n      page.value += 1;\n    }\n  } catch (error) {\n    console.error('Failed to load options:', error);\n    if (currentRequestToken === fetchRequestToken.value) {\n      haveMore.value = false;\n    }\n  } finally {\n    if (currentRequestToken === fetchRequestToken.value) {\n      // Only stop the loading indicator if this is the currently active search.\n      isLoading.value = false;\n    }\n  }\n};\n\nconst debouncedFetch = debounce(() => fetchOptions(true), 300);\n\nwatch(searchKeyword, () => {\n  debouncedFetch();\n});\n\nconst toggleDropdown = () => {\n  if (props.disabled) return;\n  isOpen.value = !isOpen.value;\n};\n\nconst closeDropdown = () => {\n  isOpen.value = false;\n};\n\nconst selectOption = (option) => {\n  const value = option[props.optionValueKey];\n  const label = option[props.optionLabelKey];\n  localInitialLabel.value = label;\n  emit('update:modelValue', value);\n  emit('change', value, label);\n  closeDropdown();\n};\n\nconst handleScroll = (event) => {\n  const el = event.target;\n  const threshold = 50;\n  if (el.scrollTop + el.clientHeight >= el.scrollHeight - threshold) {\n    fetchOptions(false);\n  }\n};\n\nconst handleClickOutside = (event) => {\n  if (root.value && !root.value.contains(event.target)) {\n    closeDropdown();\n  }\n};\n\nonMounted(() => {\n  document.addEventListener('click', handleClickOutside, true);\n  if (props.modelValue && props.initialLabel) {\n    localInitialLabel.value = props.initialLabel;\n  }\n  fetchOptions(true);\n});\n\nonBeforeUnmount(() => {\n  document.removeEventListener('click', handleClickOutside, true);\n});\n</script>\n"
  },
  {
    "path": "src/components/ui/UiPagination.vue",
    "content": "<template>\n  <div class=\"flex items-center\">\n    <ui-button\n      v-tooltip=\"t('components.pagination.prevPage')\"\n      :disabled=\"modelValue <= 1\"\n      icon\n      @click=\"updatePage(modelValue - 1)\"\n    >\n      <v-remixicon name=\"riArrowLeftSLine\" />\n    </ui-button>\n    <div class=\"mx-4\">\n      <input\n        ref=\"inputEl\"\n        v-tooltip=\"t('components.pagination.currentPage')\"\n        :value=\"modelValue\"\n        :max=\"maxPage\"\n        min=\"0\"\n        class=\"bg-input w-10 appearance-none rounded-lg p-2 text-center transition\"\n        type=\"number\"\n        @click=\"$event.target.select()\"\n        @input=\"updatePage(+$event.target.value, $event.target)\"\n      />\n      {{ t('components.pagination.of', { page: maxPage }) }}\n    </div>\n    <ui-button\n      v-tooltip=\"t('components.pagination.nextPage')\"\n      :disabled=\"modelValue >= maxPage\"\n      icon\n      @click=\"updatePage(modelValue + 1)\"\n    >\n      <v-remixicon rotate=\"180\" name=\"riArrowLeftSLine\" />\n    </ui-button>\n  </div>\n</template>\n<script setup>\nimport { computed, ref, watch } from 'vue';\nimport { useI18n } from 'vue-i18n';\n\nconst props = defineProps({\n  modelValue: {\n    type: Number,\n    default: 1,\n  },\n  records: {\n    type: Number,\n    default: 10,\n  },\n  perPage: {\n    type: Number,\n    default: 10,\n  },\n});\nconst emit = defineEmits(['update:modelValue', 'paginate']);\n\nconst { t } = useI18n();\n\nconst inputEl = ref(null);\nconst maxPage = computed(() => Math.ceil(props.records / props.perPage));\n\nfunction emitEvent(page) {\n  emit('update:modelValue', page);\n  emit('paginate', page);\n}\nfunction updatePage(page, element) {\n  let currentPage = page;\n\n  if (currentPage > maxPage.value || currentPage < 1) {\n    if (!element) return;\n\n    currentPage = currentPage > maxPage.value ? maxPage.value : 1;\n  }\n\n  emitEvent(currentPage);\n}\n\nwatch(\n  () => [props.perPage, props.records],\n  () => {\n    emitEvent(1);\n  }\n);\n</script>\n<style scoped>\ninput::-webkit-outer-spin-button,\ninput::-webkit-inner-spin-button {\n  -webkit-appearance: none;\n  margin: 0;\n}\n\ninput[type='number'] {\n  -moz-appearance: textfield;\n}\n</style>\n"
  },
  {
    "path": "src/components/ui/UiPopover.vue",
    "content": "<template>\n  <div class=\"ui-popover inline-block\" :class=\"{ hidden: to }\">\n    <div\n      ref=\"targetEl\"\n      :class=\"triggerClass\"\n      class=\"ui-popover__trigger inline-block h-full\"\n    >\n      <slot name=\"trigger\" v-bind=\"{ isShow }\"></slot>\n    </div>\n    <div\n      ref=\"content\"\n      class=\"ui-popover__content rounded-lg border bg-white shadow-xl dark:border-gray-700 dark:bg-gray-800\"\n      :class=\"[padding]\"\n    >\n      <slot v-bind=\"{ isShow }\"></slot>\n    </div>\n  </div>\n</template>\n<script>\nimport { ref, onMounted, watch, shallowRef, onUnmounted } from 'vue';\nimport createTippy from '@/lib/tippy';\n\nexport default {\n  props: {\n    placement: {\n      type: String,\n      default: 'bottom',\n    },\n    trigger: {\n      type: String,\n      default: 'click',\n    },\n    padding: {\n      type: String,\n      default: 'p-4',\n    },\n    to: {\n      type: [String, Object, HTMLElement],\n      default: '',\n    },\n    options: {\n      type: Object,\n      default: () => ({}),\n    },\n    disabled: {\n      type: Boolean,\n      default: false,\n    },\n    triggerWidth: {\n      type: Boolean,\n      default: false,\n    },\n    modelValue: {\n      type: Boolean,\n      default: false,\n    },\n    triggerClass: {\n      type: String,\n      default: null,\n    },\n  },\n  emits: ['show', 'trigger', 'close', 'update:modelValue'],\n  setup(props, { emit }) {\n    const targetEl = ref(null);\n    const content = ref(null);\n    const isShow = ref(false);\n    const instance = shallowRef(null);\n\n    watch(\n      () => props.options,\n      (value) => {\n        instance.value.setProps(value);\n      },\n      { deep: true }\n    );\n    watch(\n      () => props.disabled,\n      (value) => {\n        if (value) {\n          instance.value.hide();\n          instance.value.disable();\n        } else {\n          instance.value.enable();\n        }\n      }\n    );\n    watch(\n      () => props.modelValue,\n      (value) => {\n        if (value === isShow.value) return;\n\n        isShow.value = value;\n\n        /* eslint-disable-next-line */\n        value ? instance.value.show() : instance.value.hide();\n      }\n    );\n\n    onMounted(() => {\n      /* eslint-disable-next-line */\n      const target = props.to\n        ? typeof to === 'string'\n          ? document.querySelector(props.to)\n          : props.to\n        : targetEl.value;\n\n      instance.value = createTippy(target, {\n        role: 'popover',\n        theme: null,\n        content: content.value,\n        placement: props.placement,\n        trigger: props.trigger,\n        interactive: true,\n        appendTo: () => document.body,\n        onShow: (event) => {\n          if (props.triggerWidth) {\n            event.popper.style.width = `${\n              event.reference.getBoundingClientRect().width\n            }px`;\n          }\n\n          emit('show', event);\n          isShow.value = true;\n        },\n        onHide: () => {\n          emit('close');\n          emit('update:modelValue', false);\n          isShow.value = false;\n        },\n        onTrigger: () => emit('trigger'),\n        ...props.options,\n      });\n\n      if (props.disabled) {\n        instance.value.hide();\n        instance.value.disable();\n      }\n    });\n    onUnmounted(() => {\n      instance.value.destroy();\n    });\n\n    return {\n      isShow,\n      content,\n      targetEl,\n    };\n  },\n};\n</script>\n<style>\n.ui-popover.content-full .ui-popover__trigger {\n  width: 100%;\n}\n</style>\n"
  },
  {
    "path": "src/components/ui/UiRadio.vue",
    "content": "<template>\n  <label class=\"radio-ui inline-flex items-center\">\n    <div\n      class=\"relative inline-block h-5 w-5 rounded-full focus-within:ring-2 focus-within:ring-accent\"\n    >\n      <input\n        type=\"radio\"\n        class=\"radio-ui__input opacity-0\"\n        :value=\"value\"\n        v-bind=\"{ checked: isChecked }\"\n        @change=\"changeHandler\"\n      />\n      <div\n        class=\"bg-input radio-ui__mark absolute top-0 left-0 cursor-pointer rounded-full border\"\n      ></div>\n    </div>\n    <span v-if=\"$slots.default\" class=\"ml-2 inline-block\">\n      <slot></slot>\n    </span>\n  </label>\n</template>\n<script>\nimport { computed } from 'vue';\n\nexport default {\n  props: {\n    modelValue: {\n      type: String,\n      default: '',\n    },\n    value: {\n      type: String,\n      default: undefined,\n    },\n  },\n  emits: ['update:modelValue', 'change'],\n  setup(props, { emit }) {\n    const isChecked = computed(() => props.value === props.modelValue);\n\n    function changeHandler({ target: { value } }) {\n      emit('update:modelValue', value);\n      emit('change', value);\n    }\n\n    return {\n      isChecked,\n      changeHandler,\n    };\n  },\n};\n</script>\n<style scoped>\n.radio-ui__input:checked ~ .radio-ui__mark {\n  border-width: 6px;\n  @apply border-accent;\n}\n.radio-ui__mark {\n  width: 100%;\n  height: 100%;\n  transition-property: background-color, border-color;\n  transition-timing-function: ease;\n  transition-duration: 200ms;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n}\n</style>\n"
  },
  {
    "path": "src/components/ui/UiSelect.vue",
    "content": "<template>\n  <div :class=\"{ 'inline-block': !block }\" class=\"ui-select cursor-pointer\">\n    <label\n      v-if=\"label || $slots.label\"\n      :for=\"selectId\"\n      class=\"ml-1 text-sm text-gray-600 dark:text-gray-200\"\n    >\n      <slot name=\"label\">\n        {{ label }}\n      </slot>\n    </label>\n    <div class=\"ui-select__content relative flex w-full items-center\">\n      <v-remixicon\n        v-if=\"prependIcon\"\n        size=\"20\"\n        :name=\"prependIcon\"\n        class=\"absolute left-0 ml-2 text-gray-600 dark:text-gray-200\"\n      />\n      <select\n        :id=\"selectId\"\n        :disabled=\"disabled\"\n        :class=\"{\n          'pl-8': prependIcon,\n          'opacity-75 pointer-events-none': disabled,\n        }\"\n        :value=\"modelValue\"\n        class=\"bg-input z-10 h-full w-full appearance-none rounded-lg bg-transparent px-4 py-2 pr-10 transition\"\n        @change=\"emitValue\"\n      >\n        <option v-if=\"placeholder\" value=\"\" disabled selected>\n          {{ placeholder }}\n        </option>\n        <slot></slot>\n      </select>\n      <v-remixicon\n        size=\"28\"\n        name=\"riArrowDropDownLine\"\n        class=\"absolute right-0 mr-2 text-gray-600 dark:text-gray-200\"\n      />\n    </div>\n  </div>\n</template>\n<script>\nimport { useComponentId } from '@/composable/componentId';\n\nexport default {\n  props: {\n    modelValue: {\n      type: [String, Number],\n      default: '',\n    },\n    label: {\n      type: String,\n      default: '',\n    },\n    prependIcon: {\n      type: String,\n      default: '',\n    },\n    placeholder: {\n      type: String,\n      default: '',\n    },\n    modelModifiers: {\n      type: Object,\n      default: () => ({}),\n    },\n    block: Boolean,\n    disabled: Boolean,\n    showDetail: Boolean,\n  },\n  emits: ['update:modelValue', 'change'],\n  setup(props, { emit }) {\n    const selectId = useComponentId('select');\n\n    function emitValue(event) {\n      let { value } = event.target;\n\n      if (props.modelModifiers.number) {\n        value = +value;\n      }\n\n      emit('update:modelValue', value);\n      emit('change', value);\n    }\n\n    return {\n      selectId,\n      emitValue,\n    };\n  },\n};\n</script>\n<style>\n.ui-select__arrow {\n  top: 50%;\n  transform: translateY(-50%) rotate(90deg);\n}\n.ui-select option,\n.ui-select optgroup {\n  @apply bg-gray-100 dark:bg-gray-700;\n}\n</style>\n"
  },
  {
    "path": "src/components/ui/UiSpinner.vue",
    "content": "<template>\n  <svg\n    class=\"inline-block animate-spin\"\n    :class=\"[color]\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n    fill=\"none\"\n    :width=\"size\"\n    :height=\"size\"\n    viewBox=\"0 0 24 24\"\n  >\n    <circle\n      class=\"opacity-25\"\n      cx=\"12\"\n      cy=\"12\"\n      r=\"10\"\n      stroke=\"currentColor\"\n      stroke-width=\"4\"\n    ></circle>\n    <path\n      class=\"opacity-75\"\n      fill=\"currentColor\"\n      d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z\"\n    ></path>\n  </svg>\n</template>\n<script>\nexport default {\n  props: {\n    size: {\n      type: [String, Number],\n      default: '24',\n    },\n    color: {\n      type: String,\n      default: 'text-primary',\n    },\n  },\n};\n</script>\n"
  },
  {
    "path": "src/components/ui/UiSwitch.vue",
    "content": "<template>\n  <div\n    class=\"ui-switch bg-input relative inline-flex h-6 w-12 items-center justify-center rounded-full p-1\"\n    :class=\"{ 'pointer-events-none opacity-50': disabled }\"\n  >\n    <input\n      :checked=\"modelValue\"\n      type=\"checkbox\"\n      class=\"absolute left-0 top-0 z-50 h-full w-full cursor-pointer opacity-0\"\n      v-bind=\"{ disabled, readonly: disabled || null }\"\n      @input=\"emitEvent\"\n    />\n    <div\n      class=\"ui-switch__ball absolute z-40 flex h-4 w-4 items-center justify-center rounded-full bg-white shadow-xl\"\n    >\n      <slot v-if=\"$slots.ball\" name=\"ball\"></slot>\n    </div>\n    <div\n      class=\"ui-switch__background absolute left-0 top-0 h-full w-full rounded-md bg-accent\"\n    ></div>\n  </div>\n</template>\n<script>\nexport default {\n  props: {\n    modelValue: {\n      type: Boolean,\n      default: false,\n    },\n    disabled: Boolean,\n  },\n  emits: ['update:modelValue', 'change'],\n  setup(props, { emit }) {\n    return {\n      emitEvent: () => {\n        const newValue = !props.modelValue;\n\n        emit('change', newValue);\n        emit('update:modelValue', newValue);\n      },\n    };\n  },\n};\n</script>\n<style scoped>\n.ui-switch {\n  overflow: hidden;\n  transition: all 250ms ease;\n}\n\n.ui-switch:active {\n  transform: scale(0.93);\n}\n\n.ui-switch__ball {\n  transition: all 250ms ease;\n  left: 6px;\n}\n\n.ui-switch__background {\n  transition: all 250ms ease;\n  margin-left: -100%;\n}\n\n.ui-switch:hover .ui-switch__ball {\n  transform: scale(1.1);\n}\n\n.ui-switch input:focus ~ .ui-switch__ball {\n  transform: scale(1.1);\n}\n\n.ui-switch input:checked ~ .ui-switch__ball {\n  @apply dark:bg-gray-900;\n  background-color: white;\n  left: calc(100% - 21px);\n}\n\n.ui-switch input:checked ~ .ui-switch__background {\n  margin-left: 0;\n}\n</style>\n"
  },
  {
    "path": "src/components/ui/UiTab.vue",
    "content": "<template>\n  <button\n    :aria-selected=\"uiTabs.modelValue.value === value\"\n    :class=\"[\n      uiTabs.type.value,\n      {\n        'pointer-events-none opacity-75': disabled,\n        small: uiTabs.small.value,\n        'flex-1': uiTabs.fill.value,\n        'is-active': uiTabs.modelValue.value === value,\n      },\n    ]\"\n    :tabIndex=\"uiTabs.modelValue.value === value ? 0 : -1\"\n    aria-role=\"tab\"\n    class=\"ui-tab z-[1] transition-colors focus:ring-0\"\n    @mouseenter=\"uiTabs.hoverHandler\"\n    @click=\"uiTabs.updateActive(value)\"\n  >\n    <slot></slot>\n  </button>\n</template>\n<script setup>\nimport { inject } from 'vue';\n\n/* eslint-disable-next-line */\nconst props = defineProps({\n  disabled: {\n    type: Boolean,\n    default: false,\n  },\n  value: {\n    type: [String, Number],\n    default: '',\n  },\n});\n\nconst uiTabs = inject('ui-tabs', {});\n</script>\n<style scoped>\n.ui-tab {\n  z-index: 1;\n  @apply py-3 px-2 border-b-2 border-transparent;\n}\n.ui-tab.small {\n  @apply p-2;\n}\n.ui-tab.fill {\n  @apply rounded-lg border-b-0 px-4 py-2;\n}\n.ui-tab.fill.small {\n  @apply p-2;\n}\n.ui-tab.is-active {\n  @apply border-accent dark:border-gray-100 text-gray-800 dark:text-white;\n}\n.ui-tab.is-active.fill {\n  @apply bg-black bg-opacity-5 dark:bg-gray-200 dark:bg-opacity-5;\n}\n.ui-tab.is-active {\n  @apply border-accent dark:border-gray-100 text-gray-800 dark:text-white;\n}\n</style>\n"
  },
  {
    "path": "src/components/ui/UiTabPanel.vue",
    "content": "<template>\n  <render />\n</template>\n<script setup>\nimport { inject, h, useSlots } from 'vue';\n\n/* eslint-disable-next-line */\nconst props = defineProps({\n  value: {\n    type: [String, Number],\n    default: '',\n  },\n  activeClass: {\n    type: String,\n    default: 'ui-tab-panel--active',\n  },\n  cache: Boolean,\n});\n\nconst slots = useSlots();\nconst uiTabPanels = inject('ui-tab-panels', {});\n\nconst render = () => {\n  const isActive = props.value === uiTabPanels.modelValue.value;\n  const cache = props.cache || uiTabPanels.cache.value;\n  const component = h(\n    'div',\n    {\n      class: [props.activeClass, 'ui-tab-panel'],\n      style: {\n        display: cache && !isActive ? 'none' : null,\n      },\n    },\n    slots\n  );\n\n  if (props.cache || isActive) return component;\n\n  return null;\n};\n</script>\n"
  },
  {
    "path": "src/components/ui/UiTabPanels.vue",
    "content": "<template>\n  <div class=\"ui-tab-panels\">\n    <slot></slot>\n  </div>\n</template>\n<script setup>\nimport { toRefs, provide } from 'vue';\n\n/* eslint-disable-next-line */\nconst props = defineProps({\n  modelValue: {\n    type: [String, Number],\n    default: '',\n  },\n  cache: {\n    type: Boolean,\n    default: false,\n  },\n});\n\nprovide('ui-tab-panels', toRefs(props));\n</script>\n"
  },
  {
    "path": "src/components/ui/UiTable.vue",
    "content": "<template>\n  <div class=\"ui-table\">\n    <table class=\"custom-table h-full w-full\">\n      <thead>\n        <tr>\n          <th\n            v-for=\"header in table.headers\"\n            :key=\"header.value\"\n            :align=\"header.align\"\n            class=\"relative\"\n            v-bind=\"header.attrs\"\n          >\n            <span\n              :class=\"{ 'cursor-pointer': header.sortable }\"\n              class=\"inline-block\"\n              @click=\"updateSort(header)\"\n            >\n              {{ header.text }}\n            </span>\n            <span\n              v-if=\"header.sortable\"\n              class=\"sort-icon ml-1 cursor-pointer\"\n              @click=\"updateSort(header)\"\n            >\n              <v-remixicon\n                v-if=\"sortState.id === header.value\"\n                :rotate=\"sortState.order === 'asc' ? 90 : -90\"\n                class=\"transition-transform\"\n                size=\"20\"\n                name=\"riArrowLeftLine\"\n              />\n              <v-remixicon v-else name=\"riArrowUpDownLine\" size=\"20\" />\n            </span>\n          </th>\n        </tr>\n      </thead>\n      <tbody>\n        <tr v-for=\"item in filteredItems\" :key=\"item[itemKey]\">\n          <slot name=\"item-prepend\" :item=\"item\" />\n          <td\n            v-for=\"header in headers\"\n            v-bind=\"header.rowAttrs\"\n            :key=\"header.value\"\n            :align=\"header.align\"\n            v-on=\"header.rowEvents || {}\"\n          >\n            <slot :name=\"`item-${header.value}`\" :item=\"item\">\n              {{ item[header.value] }}\n            </slot>\n          </td>\n          <slot name=\"item-append\" :item=\"item\" />\n        </tr>\n      </tbody>\n    </table>\n    <div\n      v-if=\"withPagination && filteredItems && filteredItems.length >= 10\"\n      class=\"mt-4 flex items-center justify-between\"\n    >\n      <div>\n        {{ t('components.pagination.text1') }}\n        <select v-model=\"pagination.perPage\" class=\"bg-input rounded-md p-1\">\n          <option\n            v-for=\"num in [10, 15, 25, 50, 100, 150]\"\n            :key=\"num\"\n            :value=\"num\"\n          >\n            {{ num }}\n          </option>\n        </select>\n        {{\n          t('components.pagination.text2', {\n            count: filteredItems.length,\n          })\n        }}\n      </div>\n      <ui-pagination\n        v-model=\"pagination.currentPage\"\n        :per-page=\"pagination.perPage\"\n        :records=\"items.length\"\n      />\n    </div>\n  </div>\n</template>\n<script setup>\nimport { reactive, computed, watch } from 'vue';\nimport { useI18n } from 'vue-i18n';\nimport { isObject, arraySorter } from '@/utils/helper';\n\nconst props = defineProps({\n  headers: {\n    type: Array,\n    default: () => [],\n  },\n  items: {\n    type: Array,\n    default: () => [],\n  },\n  itemKey: {\n    type: String,\n    default: '',\n    required: true,\n  },\n  search: {\n    type: String,\n    default: '',\n  },\n  customFilter: {\n    type: Function,\n    default: null,\n  },\n  withPagination: {\n    type: Boolean,\n    default: false,\n  },\n});\n\nconst { t } = useI18n();\n\nconst table = reactive({\n  headers: [],\n  filterKeys: [],\n});\nconst sortState = reactive({\n  id: '',\n  order: 'asc',\n});\nconst pagination = reactive({\n  perPage: 10,\n  currentPage: 1,\n});\n\nconst sortedItems = computed(() => {\n  const sortedRows = sortState.id\n    ? arraySorter({\n        data: props.items,\n        key: sortState.id,\n        order: sortState.order,\n      })\n    : props.items;\n  if (!props.withPagination) return sortedRows;\n\n  return sortedRows.slice(\n    (pagination.currentPage - 1) * pagination.perPage,\n    pagination.currentPage * pagination.perPage\n  );\n});\nconst filteredItems = computed(() => {\n  if (!props.search) return sortedItems.value;\n\n  const filterFunc =\n    props.customFilter ||\n    ((search, item) => {\n      return table.filterKeys.some((key) => {\n        const value = item[key];\n        if (typeof value === 'string')\n          return value.toLocaleLowerCase().includes(search);\n\n        return value === search;\n      });\n    });\n\n  const search = props.search.toLocaleLowerCase();\n  return sortedItems.value.filter((item, index) =>\n    filterFunc(search, item, index)\n  );\n});\n\nfunction updateSort({ sortable, value }) {\n  if (!sortable) return;\n\n  if (sortState.id !== value) {\n    sortState.id = value;\n    sortState.order = 'asc';\n    return;\n  }\n\n  if (sortState.order === 'asc') {\n    sortState.order = 'desc';\n  } else {\n    sortState.id = '';\n  }\n}\n\nwatch(\n  () => props.headers,\n  (newHeaders) => {\n    const filterKeys = new Set();\n\n    table.headers = newHeaders.map((header) => {\n      const headerObj = {\n        attrs: {},\n        rowAttrs: {},\n        align: 'left',\n        text: header,\n        value: header,\n        sortable: true,\n        filterable: false,\n      };\n\n      if (isObject(header)) Object.assign(headerObj, header);\n      if (headerObj.filterable) filterKeys.add(headerObj.value);\n\n      return headerObj;\n    });\n\n    table.filterKeys = Array.from(filterKeys);\n  },\n  { immediate: true }\n);\n</script>\n<style>\n.sort-icon svg {\n  @apply text-gray-600 dark:text-gray-300 inline-block;\n}\n</style>\n"
  },
  {
    "path": "src/components/ui/UiTabs.vue",
    "content": "<template>\n  <div\n    :class=\"[\n      tabTypes[type] || tabTypes['default'],\n      { [color]: type === 'fill' },\n    ]\"\n    aria-role=\"tablist\"\n    class=\"ui-tabs relative flex items-center space-x-1 text-gray-600 dark:text-gray-200\"\n    @mouseleave=\"showHoverIndicator = false\"\n  >\n    <div\n      v-show=\"showHoverIndicator\"\n      ref=\"hoverIndicator\"\n      class=\"ui-tabs__indicator bg-box-transparent absolute left-0 z-0 rounded-lg\"\n      style=\"top: 50%; transform: translate(0, -50%)\"\n    ></div>\n    <slot></slot>\n  </div>\n</template>\n<script setup>\nimport { provide, toRefs, ref } from 'vue';\n\nconst props = defineProps({\n  modelValue: {\n    type: [String, Number],\n    default: '',\n  },\n  type: {\n    type: String,\n    default: 'default',\n    validator: (value) => ['default', 'fill'].includes(value),\n  },\n  color: {\n    type: String,\n    default: 'bg-box-transparent',\n  },\n  small: Boolean,\n  fill: Boolean,\n});\nconst emit = defineEmits(['update:modelValue', 'change']);\n\nconst tabTypes = {\n  default: 'border-b',\n  fill: 'p-2 rounded-lg',\n};\n\nconst hoverIndicator = ref(null);\nconst showHoverIndicator = ref(false);\n\nfunction updateActive(id) {\n  emit('change', id);\n  emit('update:modelValue', id);\n}\nfunction hoverHandler({ target }) {\n  const isFill = props.type === 'fill';\n\n  if (target.classList.contains('is-active') && isFill) {\n    hoverIndicator.value.style.display = 'none';\n\n    return;\n  }\n\n  const { height, width } = target.getBoundingClientRect();\n  const elHeight = isFill ? height + 3 : height - 11;\n\n  showHoverIndicator.value = true;\n  hoverIndicator.value.style.width = `${width}px`;\n  hoverIndicator.value.style.height = `${elHeight}px`;\n  hoverIndicator.value.style.display = 'inline-block';\n  hoverIndicator.value.style.transform = `translate(${target.offsetLeft}px, -50%)`;\n}\n\nprovide('ui-tabs', {\n  updateActive,\n  hoverHandler,\n  ...toRefs(props),\n});\n</script>\n<style>\n.ui-tabs__indicator {\n  min-height: 24px;\n  min-width: 50px;\n  transition-duration: 200ms;\n  transition-property: transform, width;\n  transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);\n}\n</style>\n"
  },
  {
    "path": "src/components/ui/UiTextarea.vue",
    "content": "<template>\n  <textarea\n    v-bind=\"{ placeholder, maxlength: max }\"\n    :id=\"textareaId\"\n    ref=\"textarea\"\n    :value=\"modelValue\"\n    class=\"ui-textarea ui-input bg-input w-full rounded-lg px-4 py-2 transition\"\n    @input=\"emitValue\"\n    @keyup=\"$emit('keyup', $event)\"\n    @keydown=\"$emit('keydown', $event)\"\n    @focus=\"$emit('focus', $event)\"\n    @blur=\"$emit('blur', $event)\"\n  ></textarea>\n</template>\n<script>\nimport { ref, onMounted } from 'vue';\nimport { useComponentId } from '@/composable/componentId';\n\nexport default {\n  props: {\n    modelValue: {\n      type: String,\n      default: '',\n    },\n    label: {\n      type: String,\n      default: '',\n    },\n    placeholder: {\n      type: String,\n      default: '',\n    },\n    autoresize: {\n      type: Boolean,\n      default: false,\n    },\n    max: {\n      type: [Number, String],\n      default: null,\n    },\n    block: Boolean,\n  },\n  emits: ['update:modelValue', 'change', 'focus', 'blur', 'keyup', 'keydown'],\n  setup(props, { emit }) {\n    const textareaId = useComponentId('textarea');\n    const textarea = ref(null);\n\n    function calcHeight() {\n      if (!props.autoresize) return;\n\n      textarea.value.style.height = 'auto';\n      textarea.value.style.height = `${textarea.value.scrollHeight}px`;\n    }\n    function emitValue(event) {\n      let { value } = event.target;\n      const maxLength = Math.abs(props.max) || Infinity;\n\n      if (value.length > maxLength) {\n        value = value.slice(0, maxLength);\n      }\n\n      emit('update:modelValue', value);\n      emit('change', value);\n      // calcHeight();\n    }\n\n    onMounted(calcHeight);\n\n    return {\n      textarea,\n      emitValue,\n      textareaId,\n    };\n  },\n};\n</script>\n"
  },
  {
    "path": "src/composable/blockValidation.js",
    "content": "import { onMounted, watch, shallowRef } from 'vue';\nimport blocksValidation from '@/newtab/utils/blocksValidation';\n\nexport function useBlockValidation(blockId, data) {\n  const errors = shallowRef('');\n\n  onMounted(() => {\n    const blockValidation = blocksValidation[blockId];\n    if (!blockValidation) return;\n\n    const unwatch = watch(\n      data,\n      (newData) => {\n        blockValidation\n          .func(newData)\n          .then((blockErrors) => {\n            let errorsStr = '';\n            blockErrors.forEach((error) => {\n              errorsStr += `<li>${error}</li>\\n`;\n            });\n\n            errors.value =\n              errorsStr.trim() &&\n              `Issues: <ol class='list-disc list-inside'>${errorsStr}</ol>`;\n          })\n          .catch((error) => {\n            console.error(error);\n          })\n          .finally(() => {\n            if (blockValidation.once) {\n              unwatch();\n            }\n          });\n      },\n      { deep: true, immediate: true }\n    );\n  });\n\n  return { errors };\n}\n"
  },
  {
    "path": "src/composable/commandManager.js",
    "content": "import { shallowRef, computed } from 'vue';\n\nexport function useCommandManager({ maxHistory = 100 } = {}) {\n  const position = shallowRef(0);\n  let history = [null];\n\n  const state = computed(() => ({\n    position: position.value,\n    historyLen: history.length,\n    canUndo: position.value > 0,\n    canRedo: position.value < history.length - 1,\n  }));\n\n  return {\n    state,\n    add(command) {\n      if (position.value < history.length - 1) {\n        history = history.slice(0, position.value + 1);\n      }\n      if (history.length > maxHistory) {\n        history.shift();\n      }\n\n      history.push(command);\n      position.value += 1;\n    },\n    undo() {\n      if (position.value > 0) {\n        history[position.value].undo();\n        position.value -= 1;\n      }\n    },\n    redo() {\n      if (position.value < history.length - 1) {\n        position.value += 1;\n        history[position.value].execute();\n      }\n    },\n  };\n}\n"
  },
  {
    "path": "src/composable/componentId.js",
    "content": "let id = 0;\n\nexport function useComponentId(prefix) {\n  id += 1;\n\n  if (!prefix) return id;\n\n  return `${prefix}--${id}`;\n}\n"
  },
  {
    "path": "src/composable/dialog.js",
    "content": "import emitter from '@/lib/mitt';\n\nexport function useDialog() {\n  const emitDialog = (type, options = {}) => {\n    emitter.emit('show-dialog', { type, options });\n  };\n\n  function confirm(options = {}) {\n    emitDialog('confirm', options);\n  }\n  function prompt(options = {}) {\n    emitDialog('prompt', options);\n  }\n  function custom(type, options = {}) {\n    emitDialog(type, { ...options, custom: true });\n  }\n\n  return {\n    custom,\n    prompt,\n    confirm,\n  };\n}\n"
  },
  {
    "path": "src/composable/editorBlock.js",
    "content": "import { reactive, onMounted } from 'vue';\nimport { getBlocks } from '@/utils/getSharedData';\nimport { categories } from '@/utils/shared';\n\nexport function useEditorBlock(label) {\n  const blocks = getBlocks();\n  const block = reactive({\n    details: {},\n    category: {},\n  });\n\n  onMounted(() => {\n    if (!label) return;\n\n    const details = blocks[label];\n\n    block.details = { id: label, ...details };\n    block.category = categories[details.category];\n  });\n\n  return block;\n}\n"
  },
  {
    "path": "src/composable/groupTooltip.js",
    "content": "import { getCurrentInstance, shallowRef, nextTick, onUnmounted } from 'vue';\nimport { createSingleton } from 'tippy.js';\nimport createTippy, { defaultOptions } from '@/lib/tippy';\n\nexport function useGroupTooltip(elements, options = {}) {\n  const singleton = shallowRef(null);\n  const instance = getCurrentInstance();\n  const context = instance && instance.ctx;\n\n  nextTick(() => {\n    let tippyInstances = [];\n\n    if (Array.isArray(elements)) {\n      tippyInstances = elements.map((el) => el._tippy || createTippy(el));\n    } else {\n      tippyInstances = context._tooltipGroup || [];\n    }\n\n    singleton.value = createSingleton(tippyInstances, {\n      ...defaultOptions,\n      ...options,\n      theme: 'tooltip-theme',\n      placement: 'right',\n      moveTransition: 'transform 0.2s ease-out',\n      overrides: ['placement', 'theme'],\n    });\n\n    if (!elements) {\n      context.__tpSingleton = singleton.value;\n    }\n  });\n  onUnmounted(() => {\n    singleton.value?.destroy();\n  });\n\n  return singleton;\n}\n"
  },
  {
    "path": "src/composable/hasPermissions.js",
    "content": "import { onMounted, shallowReactive } from 'vue';\nimport browser from 'webextension-polyfill';\n\nconst isMV2 = browser.runtime.getManifest().manifest_version === 2;\n\nexport function useHasPermissions(permissions) {\n  const hasPermissions = shallowReactive({});\n\n  function handlePermission(name, status) {\n    hasPermissions[name] = status;\n  }\n  function request(needReload = false) {\n    const reqPermissions = permissions.filter(\n      (permission) => !hasPermissions[permission]\n    );\n\n    browser.permissions\n      .request({ permissions: reqPermissions })\n      .then((status) => {\n        if (!status) return;\n\n        reqPermissions.forEach((permission) => {\n          handlePermission(permission, true);\n        });\n\n        if (typeof needReload === 'boolean' && needReload) {\n          alert('Automa needs to reload to make this feature work');\n\n          if (isMV2) {\n            browser.runtime.getBackgroundPage().then((background) => {\n              background.location.reload();\n            });\n          } else {\n            browser.runtime.reload();\n          }\n        }\n      })\n      .catch((error) => {\n        console.error(error);\n      });\n  }\n\n  onMounted(() => {\n    permissions.forEach((permission) => {\n      browser.permissions\n        .contains({ permissions: [permission] })\n        .then((status) => {\n          handlePermission(permission, status);\n        });\n    });\n  });\n\n  return {\n    request,\n    has: hasPermissions,\n  };\n}\n"
  },
  {
    "path": "src/composable/liveQuery.js",
    "content": "import { liveQuery } from 'dexie';\nimport { useObservable } from '@vueuse/rxjs';\n\nexport function useLiveQuery(querier) {\n  return useObservable(liveQuery(querier));\n}\n"
  },
  {
    "path": "src/composable/shortcut.js",
    "content": "import { onUnmounted, onMounted } from 'vue';\nimport defu from 'defu';\nimport Mousetrap from 'mousetrap';\nimport { isObject, parseJSON } from '@/utils/helper';\n\nconst defaultShortcut = {\n  'page:dashboard': {\n    id: 'page:dashboard',\n    combo: 'option+1',\n  },\n  'page:workflows': {\n    id: 'page:workflows',\n    combo: 'option+w',\n  },\n  'page:schedule': {\n    id: 'page:schedule',\n    combo: 'option+t',\n  },\n  'page:logs': {\n    id: 'page:logs',\n    combo: 'option+l',\n  },\n  'page:storage': {\n    id: 'page:storage',\n    combo: 'option+a',\n  },\n  'page:settings': {\n    id: 'page:settings',\n    combo: 'option+s',\n  },\n  'action:search': {\n    id: 'action:search',\n    combo: 'mod+f',\n  },\n  'action:new': {\n    id: 'action:new',\n    combo: 'mod+option+n',\n  },\n  'editor:duplicate-block': {\n    id: 'editor:duplicate-block',\n    combo: 'mod+option+d',\n  },\n  'editor:search-blocks': {\n    id: 'editor:search-blocks',\n    combo: 'mod+b',\n  },\n  'editor:save': {\n    id: 'editor:save',\n    combo: 'mod+shift+s',\n  },\n  'editor:execute-workflow': {\n    id: 'editor:execute-workflow',\n    combo: 'option+enter',\n  },\n  'editor:toggle-sidebar': {\n    id: 'editor:toggle-sidebar',\n    combo: 'mod+[',\n  },\n};\nconst customShortcut = parseJSON(localStorage.getItem('shortcuts', {})) || {};\n\nexport const mapShortcuts = defu(customShortcut, defaultShortcut);\n\nconst os = navigator.appVersion.indexOf('Mac') !== -1 ? 'mac' : 'win';\nexport function getReadableShortcut(str) {\n  const list = {\n    option: {\n      win: 'alt',\n      mac: 'option',\n    },\n    mod: {\n      win: 'ctrl',\n      mac: '⌘',\n    },\n  };\n  const regex = /option|mod/g;\n  const replacedStr = str.replace(regex, (match) => {\n    return list[match][os];\n  });\n\n  return replacedStr;\n}\n\nexport function getShortcut(id, data) {\n  const shortcut = mapShortcuts[id] || {};\n\n  if (data) shortcut.data = data;\n  if (!shortcut.readable) {\n    shortcut.readable = getReadableShortcut(shortcut.combo);\n  }\n\n  return shortcut;\n}\n\nexport function useShortcut(shortcuts, handler) {\n  Mousetrap.prototype.stopCallback = () => false;\n\n  const extractedShortcuts = {\n    ids: {},\n    keys: [],\n    data: {},\n  };\n  const handleShortcut = (event, combo) => {\n    const shortcutId = extractedShortcuts.ids[combo];\n    const params = {\n      event,\n      ...extractedShortcuts.data[shortcutId],\n    };\n\n    if (shortcutId) event.preventDefault();\n\n    if (typeof params.data === 'function') {\n      params.data(params, event);\n    } else if (handler) {\n      handler(params, event);\n    }\n  };\n  const addShortcutData = ({ combo, id, readable, ...rest }) => {\n    extractedShortcuts.ids[combo] = id;\n    extractedShortcuts.keys.push(combo);\n    extractedShortcuts.data[id] = { combo, id, readable, ...rest };\n  };\n\n  if (isObject(shortcuts)) {\n    addShortcutData(getShortcut(shortcuts.id, shortcuts.data));\n  } else if (typeof shortcuts === 'string') {\n    addShortcutData(getShortcut(shortcuts));\n  } else {\n    shortcuts.forEach((item) => {\n      const currentShortcut =\n        typeof item === 'string' ? getShortcut(item) : item;\n\n      addShortcutData(currentShortcut);\n    });\n  }\n\n  onMounted(() => {\n    Mousetrap.bind(extractedShortcuts.keys, handleShortcut);\n  });\n  onUnmounted(() => {\n    Mousetrap.unbind(extractedShortcuts.keys);\n  });\n\n  return extractedShortcuts.data;\n}\n"
  },
  {
    "path": "src/composable/theme.js",
    "content": "import { ref, onMounted } from 'vue';\nimport browser from 'webextension-polyfill';\n\nconst themes = [\n  { name: 'Light', id: 'light' },\n  { name: 'Dark', id: 'dark' },\n  { name: 'System', id: 'system' },\n];\nconst isPreferDark = () =>\n  window.matchMedia('(prefers-color-scheme: dark)').matches;\n\nexport function useTheme() {\n  const activeTheme = ref('system');\n\n  async function setTheme(theme) {\n    const isValidTheme = themes.some(({ id }) => id === theme);\n\n    if (!isValidTheme) return;\n\n    let isDarkTheme = theme === 'dark';\n\n    if (theme === 'system') isDarkTheme = isPreferDark();\n\n    document.documentElement.classList.toggle('dark', isDarkTheme);\n    activeTheme.value = theme;\n\n    await browser.storage.local.set({ theme });\n  }\n  async function getTheme() {\n    let { theme } = await browser.storage.local.get('theme');\n\n    if (!theme) theme = 'system';\n\n    return theme;\n  }\n  async function init() {\n    const theme = await getTheme();\n\n    await setTheme(theme);\n  }\n\n  onMounted(async () => {\n    activeTheme.value = await getTheme();\n  });\n\n  return {\n    init,\n    themes,\n    activeTheme,\n    set: setTheme,\n    get: getTheme,\n  };\n}\n"
  },
  {
    "path": "src/content/blocksHandler/handlerAttributeValue.js",
    "content": "import handleSelector from '../handleSelector';\n\nfunction handleAttributeValue(block) {\n  return new Promise((resolve, reject) => {\n    let result = [];\n    const { attributeName, multiple, attributeValue, action } = block.data;\n    const isCheckboxOrRadio = (element) => {\n      if (element.tagName !== 'INPUT') return false;\n\n      return ['checkbox', 'radio'].includes(element.getAttribute('type'));\n    };\n\n    handleSelector(block, {\n      onSelected(element) {\n        if (action === 'set') {\n          element.setAttribute(attributeName, attributeValue);\n          return;\n        }\n\n        let value = element.getAttribute(attributeName);\n\n        if (attributeName === 'checked' && isCheckboxOrRadio(element)) {\n          value = element.checked;\n        } else if (attributeName === 'href' && element.tagName === 'A') {\n          value = element.href;\n        }\n\n        if (multiple) result.push(value);\n        else result = value;\n      },\n      onError(error) {\n        reject(error);\n      },\n      onSuccess() {\n        resolve(result);\n      },\n    });\n  });\n}\n\nexport default handleAttributeValue;\n"
  },
  {
    "path": "src/content/blocksHandler/handlerClipboard.js",
    "content": "function clipboard() {\n  return new Promise((resolve) => {\n    const text = window.getSelection().toString();\n    resolve(text);\n  });\n}\n\nexport default clipboard;\n"
  },
  {
    "path": "src/content/blocksHandler/handlerConditions.js",
    "content": "import { customAlphabet } from 'nanoid/non-secure';\nimport { visibleInViewport, isXPath } from '@/utils/helper';\nimport { automaRefDataStr } from '@/workflowEngine/helper';\nimport handleSelector from '../handleSelector';\n\nconst nanoid = customAlphabet('1234567890abcdef', 5);\n\nasync function handleConditionElement({ data, type, id, frameSelector }) {\n  const selectorType = isXPath(data.selector) ? 'xpath' : 'cssSelector';\n\n  const element = await handleSelector({\n    id,\n    data: {\n      ...data,\n      findBy: selectorType,\n    },\n    frameSelector,\n    type: selectorType,\n  });\n  const { 1: actionType } = type.split('#');\n\n  const elementActions = {\n    exists: () => Boolean(element),\n    notExists: () => !element,\n    text: () => element?.innerText ?? null,\n    visibleScreen: () => {\n      if (!element) return false;\n\n      return visibleInViewport(element);\n    },\n    visible: () => {\n      if (!element) return false;\n\n      const { visibility, display } = getComputedStyle(element);\n\n      return visibility !== 'hidden' && display !== 'none';\n    },\n    invisible: () => {\n      if (!element) return false;\n\n      const { visibility, display } = getComputedStyle(element);\n      const styleHidden = visibility === 'hidden' || display === 'none';\n\n      return styleHidden || !visibleInViewport(element);\n    },\n    attribute: ({ attrName }) => {\n      if (!element || !element.hasAttribute(attrName)) return null;\n\n      return element.getAttribute(attrName);\n    },\n  };\n\n  return elementActions[actionType](data);\n}\n\nasync function handleConditionCode({ data, refData }) {\n  return new Promise((resolve, reject) => {\n    const varName = `automa${nanoid()}`;\n\n    const scriptEl = document.createElement('script');\n    scriptEl.textContent = `\n      (async () => {\n        const ${varName} = ${JSON.stringify(refData)};\n        ${automaRefDataStr(varName)}\n        try {\n          ${data.code}\n        } catch (error) {\n          return {\n            $isError: true,\n            message: error.message,\n          }\n        }\n      })()\n        .then((detail) => {\n          window.dispatchEvent(new CustomEvent('__automa-condition-code__', { detail }));\n        });\n    `;\n\n    document.body.appendChild(scriptEl);\n\n    const handleAutomaEvent = ({ detail }) => {\n      scriptEl.remove();\n      window.removeEventListener(\n        '__automa-condition-code__',\n        handleAutomaEvent\n      );\n\n      if (detail.$isError) {\n        reject(new Error(detail.message));\n        return;\n      }\n\n      resolve(detail);\n    };\n\n    window.addEventListener('__automa-condition-code__', handleAutomaEvent);\n  });\n}\n\nexport default async function (data) {\n  let result = null;\n\n  if (data.type.startsWith('element')) {\n    result = await handleConditionElement(data);\n  } else if (data.type.startsWith('code')) {\n    result = await handleConditionCode(data);\n  }\n\n  return result;\n}\n"
  },
  {
    "path": "src/content/blocksHandler/handlerCreateElement.js",
    "content": "import handleSelector from '../handleSelector';\n\nconst positions = {\n  after: 'beforeend',\n  before: 'afterbegin',\n  'next-sibling': 'afterend',\n  'prev-sibling': 'beforebegin',\n};\n\nfunction createNode(tag, attrs = {}, content = '') {\n  const element = document.createElement(tag);\n\n  Object.keys(attrs).forEach((attr) => {\n    element.setAttribute(attr, attrs[attr]);\n  });\n  element.innerHTML = content;\n\n  return element;\n}\n\nasync function createElement(block) {\n  const targetElement = await handleSelector(block);\n  if (!targetElement) throw new Error('element-not-found');\n\n  const { data, id } = block;\n  const baseId = `automa-${id}`;\n\n  if (data.insertAt === 'replace') {\n    const fragments = createNode('template', {}, data.html);\n    targetElement.replaceWith(fragments.content);\n  } else {\n    targetElement.insertAdjacentHTML(positions[data.insertAt], data.html);\n  }\n\n  if (data.css) {\n    const style = createNode('style', { id: `${baseId}-style` }, data.css);\n    document.body.appendChild(style);\n  }\n\n  if (block.preloadCSS) {\n    block.preloadCSS.forEach((style) => {\n      const script = document.createElement('style');\n      script.id = `${baseId}-script`;\n      script.textContent = style.script;\n\n      document.body.appendChild(script);\n    });\n  }\n\n  if (!data?.dontInjectJS) {\n    data.preloadScripts.forEach((item) => {\n      const script = document.createElement(item.type);\n      script.id = `${baseId}-script`;\n      script.textContent = item.script;\n\n      document.body.appendChild(script);\n    });\n\n    const script = document.createElement('script');\n    script.id = `${baseId}-javascript`;\n    script.textContent = `(() => { ${data.automaScript}\\n${data.javascript} })()`;\n\n    document.body.appendChild(script);\n  }\n\n  return true;\n}\n\nexport default createElement;\n"
  },
  {
    "path": "src/content/blocksHandler/handlerElementExists.js",
    "content": "import handleSelector from '../handleSelector';\n\nfunction elementExists(block) {\n  return new Promise((resolve) => {\n    let trying = 0;\n\n    const isExists = async () => {\n      try {\n        const element = await handleSelector(block, { returnElement: true });\n\n        if (!element) throw new Error('element-not-found');\n\n        return true;\n      } catch (error) {\n        return false;\n      }\n    };\n\n    async function checkElement() {\n      if (trying > (block.data.tryCount || 1)) {\n        resolve(false);\n        return;\n      }\n\n      const isElementExist = await isExists();\n\n      if (isElementExist) {\n        resolve(true);\n      } else {\n        trying += 1;\n\n        setTimeout(checkElement, block.data.timeout || 500);\n      }\n    }\n\n    checkElement();\n  });\n}\n\nexport default elementExists;\n"
  },
  {
    "path": "src/content/blocksHandler/handlerElementScroll.js",
    "content": "import handleSelector from '../handleSelector';\n\nfunction isElScrollable(element) {\n  const excludedTags = ['SCRIPT', 'STYLE', 'SVG', 'HEAD'];\n  const isScrollable =\n    element.scrollHeight > element.clientHeight ||\n    element.scrollWidth > element.clientWidth;\n  const isExcluded =\n    element.tagName.includes('-') || excludedTags.includes(element.tagName);\n\n  return isScrollable && !isExcluded;\n}\n\nfunction findScrollableElement(\n  element = document.documentElement,\n  dir = 'down',\n  maxDepth = 5\n) {\n  if (maxDepth === 0) return null;\n\n  const isScrollable = isElScrollable(element);\n  if (isScrollable) return element;\n\n  if (dir === 'up') {\n    const parentEl = element.parentElement;\n    if (!parentEl) return null;\n\n    const scrollableElement = findScrollableElement(\n      parentEl,\n      dir,\n      maxDepth - 1\n    );\n    if (scrollableElement) return scrollableElement;\n  } else {\n    for (let index = 0; index < element.childElementCount; index += 1) {\n      const currentChild = element.children.item(index);\n      const scrollableElement = findScrollableElement(\n        currentChild,\n        dir,\n        maxDepth - 1\n      );\n\n      if (scrollableElement) return scrollableElement;\n    }\n  }\n\n  return null;\n}\n\nfunction elementScroll(block) {\n  function incScrollPos(element, data, vertical = true) {\n    let currentPos = vertical ? element.scrollTop : element.scrollLeft;\n\n    if (data.incY) {\n      currentPos += data.scrollY;\n    } else if (data.incX) {\n      currentPos += data.scrollX;\n    }\n\n    return currentPos;\n  }\n\n  return new Promise((resolve, reject) => {\n    const { data } = block;\n    const behavior = data.smooth ? 'smooth' : 'auto';\n\n    handleSelector(block, {\n      onSelected(element) {\n        if (data.scrollIntoView) {\n          element.scrollIntoView({ behavior, block: 'center' });\n        } else {\n          const scrollableEl =\n            findScrollableElement(element, 'up', 3) ||\n            findScrollableElement(element, 'down', 3) ||\n            element;\n\n          scrollableEl.scroll({\n            behavior,\n            top: data.incY ? incScrollPos(element, data) : data.scrollY,\n            left: data.incX ? incScrollPos(element, data, false) : data.scrollX,\n          });\n        }\n      },\n      onError(error) {\n        reject(error);\n      },\n      onSuccess() {\n        window.dispatchEvent(new Event('scroll'));\n        resolve('');\n      },\n    });\n  });\n}\n\nexport default elementScroll;\n"
  },
  {
    "path": "src/content/blocksHandler/handlerEventClick.js",
    "content": "import { sendMessage } from '@/utils/message';\nimport { sleep } from '@/utils/helper';\nimport { getElementPosition, simulateClickElement } from '../utils';\nimport handleSelector from '../handleSelector';\n\nfunction eventClick(block) {\n  return new Promise((resolve, reject) => {\n    handleSelector(block, {\n      async onSelected(element) {\n        if (block.debugMode) {\n          const { x, y } = await getElementPosition(element);\n          const payload = {\n            tabId: block.activeTabId,\n            method: 'Input.dispatchMouseEvent',\n            params: {\n              x,\n              y,\n              button: 'left',\n            },\n          };\n          const executeCommand = (type) => {\n            payload.params.type = type;\n\n            if (type === 'mousePressed') {\n              payload.params.clickCount = 1;\n            }\n\n            return sendMessage('debugger:send-command', payload, 'background');\n          };\n\n          // bypass the bot detection.\n          await executeCommand('mouseMoved');\n          await sleep(100);\n          await executeCommand('mousePressed');\n          await sleep(100);\n          await executeCommand('mouseReleased');\n\n          return;\n        }\n\n        simulateClickElement(element);\n      },\n      onError(error) {\n        reject(error);\n      },\n      onSuccess() {\n        resolve('');\n      },\n    });\n  });\n}\n\nexport default eventClick;\n"
  },
  {
    "path": "src/content/blocksHandler/handlerForms.js",
    "content": "import handleFormElement from '@/utils/handleFormElement';\nimport { sendMessage } from '@/utils/message';\nimport renderString from '@/workflowEngine/templating/renderString';\nimport handleSelector, { markElement } from '../handleSelector';\nimport synchronizedLock from '../synchronizedLock';\n\nasync function forms(block) {\n  const { data } = block;\n  const elements = await handleSelector(block, { returnElement: true });\n\n  if (!elements) {\n    throw new Error('element-not-found');\n  }\n\n  if (data.getValue) {\n    let result = '';\n\n    if (data.multiple) {\n      result = elements.map((element) => element.value || '');\n    } else {\n      result = elements.value || '';\n    }\n\n    return result;\n  }\n\n  async function typeText(element) {\n    if (block.debugMode && data.type === 'text-field') {\n      // get lock\n      await synchronizedLock.getLock();\n      element.focus?.();\n\n      try {\n        if (data.clearValue) {\n          const backspaceCommands = new Array(element.value?.length ?? 0).fill({\n            type: 'rawKeyDown',\n            unmodifiedText: 'Delete',\n            text: 'Delete',\n            windowsVirtualKeyCode: 46,\n          });\n\n          await sendMessage(\n            'debugger:type',\n            { commands: backspaceCommands, tabId: block.activeTabId, delay: 0 },\n            'background'\n          );\n        }\n\n        const renderedResult = await renderString(data.value, block.refData);\n        const textValue = renderedResult.value || '';\n        const commands = textValue.split('').map((char) => ({\n          type: 'keyDown',\n          text: char === '\\n' ? '\\r' : char,\n        }));\n        const typeDelay = +block.data.delay;\n        await sendMessage(\n          'debugger:type',\n          {\n            commands,\n            tabId: block.activeTabId,\n            delay: Number.isNaN(typeDelay) ? 0 : typeDelay,\n          },\n          'background'\n        );\n      } finally {\n        synchronizedLock.releaseLock();\n      }\n      return;\n    }\n\n    markElement(element, block);\n    await handleFormElement(element, data);\n  }\n\n  if (data.multiple) {\n    const promises = Array.from(elements).map((element) => typeText(element));\n\n    await Promise.allSettled(promises);\n  } else {\n    await typeText(elements);\n  }\n\n  return null;\n}\n\nexport default forms;\n"
  },
  {
    "path": "src/content/blocksHandler/handlerGetText.js",
    "content": "import handleSelector from '../handleSelector';\n\nfunction getText(block) {\n  return new Promise((resolve, reject) => {\n    let regex;\n    let textResult = [];\n    const {\n      regex: regexData,\n      regexExp,\n      prefixText,\n      suffixText,\n      multiple,\n      includeTags,\n      useTextContent,\n    } = block.data;\n\n    if (regexData) {\n      regex = new RegExp(regexData, [...new Set(regexExp)].join(''));\n    }\n\n    handleSelector(block, {\n      onSelected(element) {\n        let text = '';\n\n        if (includeTags) {\n          text = element.outerHTML;\n        } else if (useTextContent) {\n          text = element.textContent;\n        } else {\n          text = element.innerText;\n        }\n\n        if (regex) text = text.match(regex)?.join(' ') ?? text;\n\n        text = (prefixText || '') + text + (suffixText || '');\n\n        if (multiple) {\n          textResult.push(text);\n        } else {\n          textResult = text;\n        }\n      },\n      onError(error) {\n        reject(error);\n      },\n      onSuccess() {\n        resolve(textResult);\n      },\n    });\n  });\n}\n\nexport default getText;\n"
  },
  {
    "path": "src/content/blocksHandler/handlerHoverElement.js",
    "content": "import { sendMessage } from '@/utils/message';\nimport { getElementPosition } from '../utils';\nimport handleSelector from '../handleSelector';\n\nfunction eventClick(block) {\n  return new Promise((resolve, reject) => {\n    handleSelector(block, {\n      async onSelected(element) {\n        const { x, y } = await getElementPosition(element);\n        const payload = {\n          tabId: block.activeTabId,\n          method: 'Input.dispatchMouseEvent',\n          params: {\n            x,\n            y,\n            clickCount: 1,\n            button: 'left',\n            type: 'mousePressed',\n          },\n        };\n\n        await sendMessage('debugger:send-command', payload, 'background');\n      },\n      onError(error) {\n        reject(error);\n      },\n      onSuccess() {\n        resolve('');\n      },\n    });\n  });\n}\n\nexport default eventClick;\n"
  },
  {
    "path": "src/content/blocksHandler/handlerJavascriptCode.js",
    "content": "import { jsContentHandler } from '@/workflowEngine/utils/javascriptBlockUtil';\nimport { getDocumentCtx } from '../handleSelector';\n\nfunction javascriptCode({ data, isPreloadScripts, frameSelector }) {\n  if (!isPreloadScripts && Array.isArray(data))\n    return jsContentHandler(...data);\n  if (!data.scripts) return Promise.resolve({ success: true });\n\n  let $documentCtx = document;\n\n  if (frameSelector) {\n    const iframeCtx = getDocumentCtx(frameSelector);\n    if (!iframeCtx) return Promise.resolve({ success: false });\n\n    $documentCtx = iframeCtx;\n  }\n\n  data.scripts.forEach((script) => {\n    const scriptAttr = `block--${script.id}`;\n\n    const isScriptExists = $documentCtx.querySelector(\n      `.automa-custom-js[${scriptAttr}]`\n    );\n\n    if (isScriptExists) return;\n\n    const scriptEl = $documentCtx.createElement('script');\n    scriptEl.textContent = script.data.code;\n    scriptEl.setAttribute(scriptAttr, '');\n    scriptEl.classList.add('automa-custom-js');\n\n    $documentCtx.documentElement.appendChild(scriptEl);\n  });\n\n  return Promise.resolve({ success: true });\n}\n\nexport default javascriptCode;\n"
  },
  {
    "path": "src/content/blocksHandler/handlerLink.js",
    "content": "import handleSelector, { markElement } from '../handleSelector';\n\nasync function link(block) {\n  const element = await handleSelector(block, { returnElement: true });\n\n  if (!element) {\n    throw new Error('element-not-found');\n  }\n  if (element.tagName !== 'A') {\n    throw new Error('Element is not a link');\n  }\n\n  markElement(element, block);\n\n  const url = element.href;\n  if (url && !block.data.openInNewTab) window.open(url, '_self');\n\n  return url;\n}\n\nexport default link;\n"
  },
  {
    "path": "src/content/blocksHandler/handlerLoopData.js",
    "content": "import { nanoid } from 'nanoid';\nimport handleSelector from '../handleSelector';\nimport { generateLoopSelectors } from '../utils';\n\nexport default async function loopElements(block) {\n  const elements = await handleSelector(block);\n  if (!elements) throw new Error('element-not-found');\n\n  let frameSelector = '';\n  if (block.data.$frameSelector) {\n    frameSelector = `${block.data.$frameSelector} |> `;\n  }\n\n  if (block.onlyGenerate) {\n    generateLoopSelectors(elements, {\n      ...block.data,\n      frameSelector,\n      attrId: block.data.loopId,\n    });\n\n    return {};\n  }\n\n  const attrId = `${block.id}-${nanoid(5)}`;\n  const selectors = generateLoopSelectors(elements, {\n    ...block.data,\n    frameSelector,\n    attrId,\n  });\n  const { origin, pathname } = window.location;\n\n  return {\n    loopId: attrId,\n    elements: selectors,\n    url: origin + pathname,\n  };\n}\n"
  },
  {
    "path": "src/content/blocksHandler/handlerLoopElements.js",
    "content": "import { sleep, isXPath } from '@/utils/helper';\nimport handleSelector from '../handleSelector';\nimport { generateLoopSelectors, simulateClickElement } from '../utils';\n\nfunction getScrollParent(node) {\n  const isElement = node instanceof HTMLElement;\n  const overflowY = isElement && window.getComputedStyle(node).overflowY;\n  const isScrollable = overflowY !== 'visible' && overflowY !== 'hidden';\n\n  if (!node) {\n    return null;\n  }\n  if (isScrollable && node.scrollHeight >= node.clientHeight) {\n    return node;\n  }\n\n  return (\n    getScrollParent(node.parentNode) ||\n    document.scrollingElement ||\n    document.body\n  );\n}\nfunction excludeSelector({ type, selector, loopAttr }) {\n  if (type === 'cssSelector') {\n    return `${selector}:not([automa-loop*=\"${loopAttr}\"])`;\n  }\n\n  return `${selector}[not(contains(@automa-loop, 'gku9rbk-qje-F'))]`;\n}\n\nexport default async function ({ data, id }) {\n  try {\n    let frameSelector = '';\n    if (data.$frameSelector) {\n      frameSelector = `${data.$frameSelector} |> `;\n    }\n\n    const generateItemsSelector = (elements) => {\n      const selectors = generateLoopSelectors(elements, {\n        frameSelector,\n        attrId: data.loopAttrId,\n        startIndex: data.index + 1,\n      });\n\n      return selectors;\n    };\n    const getNewElementsOptions = {\n      id,\n      data: {\n        multiple: true,\n        findBy: data.findBy,\n        waitForSelector: true,\n        waitSelectorTimeout: data.actionElMaxWaitTime * 1000,\n        selector: excludeSelector({\n          type: data.findBy,\n          selector: data.selector,\n          loopAttr: data.loopAttrId,\n        }),\n      },\n    };\n    let elements = null;\n\n    if (data.type.includes('scroll')) {\n      const loopItems = document.querySelectorAll(\n        `[automa-loop*=\"${data.loopAttrId}\"]`\n      );\n      if (loopItems.length === 0) return { continue: true };\n\n      const scrollableParent = getScrollParent(loopItems[0]);\n      if (!scrollableParent) return { continue: true };\n\n      if (data.scrollToBottom) {\n        const { scrollHeight } = scrollableParent;\n        scrollableParent.scrollTo(\n          0,\n          data.type === 'scroll-up' ? 0 : scrollHeight + 30\n        );\n      } else if (data.type === 'scroll-up') {\n        const [firstElement] = loopItems;\n        firstElement.scrollIntoView();\n      } else {\n        const lastElement = loopItems[loopItems.length - 1];\n        lastElement.scrollIntoView();\n      }\n\n      await sleep(500);\n\n      elements = await handleSelector(getNewElementsOptions);\n    } else if (['click-element', 'click-link'].includes(data.type)) {\n      const elementForLoad = await handleSelector({\n        id,\n        data: {\n          waitForSelector: true,\n          waitSelectorTimeout: 2000,\n          selector: data.actionElSelector,\n          findBy: isXPath(data.actionElSelector),\n        },\n      });\n      if (!elementForLoad) return { continue: true };\n\n      if (data.type === 'click-element') {\n        simulateClickElement(elementForLoad);\n        await sleep(500);\n\n        elements = await handleSelector(getNewElementsOptions);\n      } else {\n        if (data.onlyClickLink) {\n          if (elementForLoad.tagName !== 'A' || !elementForLoad.href)\n            return { continue: true };\n\n          window.location.href = elementForLoad.href;\n\n          return {};\n        }\n        elements = await handleSelector(getNewElementsOptions);\n      }\n    }\n\n    if (!elements) return { continue: true };\n\n    return generateItemsSelector(elements);\n  } catch (error) {\n    console.error(error);\n    return { continue: true };\n  }\n}\n"
  },
  {
    "path": "src/content/blocksHandler/handlerPressKey.js",
    "content": "import { isXPath, objectHasKey, sleep } from '@/utils/helper';\nimport { sendMessage } from '@/utils/message';\nimport { keyDefinitions } from '@/utils/USKeyboardLayout';\nimport { queryElements } from '../handleSelector';\n\nconst textFieldTags = ['INPUT', 'TEXTAREA'];\nconst modifierKeys = [\n  { name: 'Alt', id: 1 },\n  { name: 'Meta', id: 4 },\n  { name: 'Shift', id: 8 },\n  { name: 'Control', id: 2 },\n];\n\nasync function pressKeyWithJs({ element, keys, pressTime }) {\n  const details = {\n    key: '',\n    code: '',\n    keyCode: '',\n    bubbles: true,\n    altKey: false,\n    metaKey: false,\n    ctrlKey: false,\n    shiftKey: false,\n    cancelable: true,\n  };\n\n  for (const event of ['keydown', 'keyup']) {\n    for (const key of keys) {\n      const isLetter = /^[a-zA-Z]$/.test(key);\n\n      const isModKey = modifierKeys.some(({ name }) => name === key);\n      const dispatchEvent = () => {\n        const keyDefinition = keyDefinitions[key] || {\n          key,\n          keyCode: 0,\n          code: isLetter ? `Key${key}` : key,\n        };\n        const keyboardEvent = new KeyboardEvent(event, {\n          ...details,\n          ...keyDefinition,\n        });\n\n        element.dispatchEvent(keyboardEvent);\n      };\n\n      if (isModKey) {\n        const modKey = key.charAt(0).toLowerCase() + key.slice(1);\n        details[modKey] = true;\n\n        dispatchEvent();\n\n        return;\n      }\n\n      dispatchEvent();\n\n      if (event !== 'keydown') return;\n\n      const isEditable = element.isContentEditable;\n      const isTextField = textFieldTags.includes(element.tagName);\n\n      if (isEditable || isTextField) {\n        const contentKey = isEditable ? 'textContent' : 'value';\n        if (isLetter || (keyDefinitions[key] && key.length === 1)) {\n          if (isEditable && document.execCommand) {\n            document.execCommand('insertText', false, key);\n          } else {\n            element[contentKey] += key;\n          }\n\n          return;\n        }\n\n        if (key === 'Enter') {\n          const isSubmitForm =\n            element.tagName === 'INPUT' &&\n            element.form &&\n            !details.ctrlKey &&\n            !details.altKey;\n\n          if (isSubmitForm) {\n            element.form.submit();\n            return;\n          }\n\n          element[contentKey] += '\\r\\n';\n        }\n      }\n\n      if (event === 'keyDown' && pressTime > 0) await sleep(pressTime);\n    }\n  }\n}\nasync function pressKeyWithCommand({\n  keys,\n  pressTime,\n  actionType,\n  activeTabId,\n}) {\n  const commands = [];\n  const events =\n    actionType === 'multiple-keys' ? ['keyDown'] : ['keyDown', 'keyUp'];\n\n  for (const event of events) {\n    let modifierKey = 0;\n\n    for (const key of keys) {\n      const command = {\n        tabId: activeTabId,\n        method: 'Input.dispatchKeyEvent',\n        params: {\n          key,\n          code: '',\n          type: event,\n          modifiers: 0,\n          windowsVirtualKeyCode: 0,\n        },\n      };\n      const definition = keyDefinitions[key];\n\n      if (definition) {\n        Object.assign(command.params, definition);\n\n        command.params.windowsVirtualKeyCode = definition.keyCode;\n        command.params.nativeVirtualKeyCode = definition.keyCode;\n\n        const isModKey = modifierKeys.find(({ name }) => name === key);\n        if (isModKey) modifierKey = isModKey.id;\n        else command.params.modifiers = modifierKey;\n      }\n\n      if (!actionType || actionType === 'press-key') {\n        await sendMessage('debugger:send-command', command, 'background');\n      } else {\n        const secondEvent = { ...command.params };\n        if (!objectHasKey(command, 'text')) {\n          secondEvent.text = key;\n        }\n\n        commands.push(command.params, secondEvent);\n      }\n\n      if (event === 'keyDown' && pressTime > 0) await sleep(pressTime);\n    }\n  }\n\n  if (actionType === 'multiple-keys') {\n    await sendMessage(\n      'debugger:type',\n      { commands, tabId: activeTabId },\n      'background'\n    );\n  }\n}\n\nasync function pressKey({ data, debugMode, activeTabId }) {\n  let element = document.activeElement;\n\n  if (data.selector) {\n    const customElement = await queryElements({\n      selector: data.selector,\n      findBy: isXPath(data.selector) ? 'xpath' : 'cssSelector',\n    });\n\n    element = customElement || element;\n  }\n\n  const keys =\n    !data.action || data.action === 'press-key'\n      ? data.keys.split('+')\n      : data.keysToPress.split('');\n  const pressKeyFunction = debugMode ? pressKeyWithCommand : pressKeyWithJs;\n\n  await pressKeyFunction({\n    keys,\n    element,\n    activeTabId,\n    actionType: data.action,\n    pressTime: Number.isNaN(+data.pressTime) ? 0 : Math.abs(+data.pressTime),\n  });\n\n  return '';\n}\n\nexport default pressKey;\n"
  },
  {
    "path": "src/content/blocksHandler/handlerSaveAssets.js",
    "content": "import handleSelector from '../handleSelector';\n\nasync function saveAssets(block) {\n  let elements = await handleSelector(block, { returnElement: true });\n\n  if (!elements) {\n    throw new Error('element-not-found');\n  }\n\n  elements = block.data.multiple ? Array.from(elements) : [elements];\n\n  const srcList = elements.reduce((acc, element) => {\n    const tag = element.tagName;\n\n    if ((tag === 'AUDIO' || tag === 'VIDEO') && !tag.src) {\n      const sourceEl = element.querySelector('source');\n\n      if (sourceEl && sourceEl.src) acc.push(sourceEl.src);\n    } else if (element.src) {\n      acc.push(element.src);\n    }\n\n    return acc;\n  }, []);\n\n  return srcList;\n}\n\nexport default saveAssets;\n"
  },
  {
    "path": "src/content/blocksHandler/handlerSwitchTo.js",
    "content": "import { isXPath } from '@/utils/helper';\nimport handleSelector from '../handleSelector';\n\nconst framesEl = ['IFRAME', 'FRAME'];\n\nfunction switchTo(block) {\n  return new Promise((resolve, reject) => {\n    block.data.findBy = isXPath(block.data.selector) ? 'xpath' : 'cssSelector';\n\n    handleSelector(block, {\n      onSelected(element) {\n        if (!framesEl.includes(element.tagName)) {\n          reject(new Error('not-iframe'));\n          return;\n        }\n\n        const isSameOrigin = element.contentDocument !== null;\n\n        resolve({ url: element.src, isSameOrigin });\n      },\n      onError(error) {\n        reject(error);\n      },\n    });\n  });\n}\n\nexport default switchTo;\n"
  },
  {
    "path": "src/content/blocksHandler/handlerTakeScreenshot.js",
    "content": "import { sleep } from '@/utils/helper';\nimport { sendMessage } from '@/utils/message';\nimport handleSelector from '../handleSelector';\n\nfunction findScrollableElement(\n  element = document.documentElement,\n  maxDepth = 5\n) {\n  if (maxDepth === 0) return null;\n\n  const excludeTags = ['SCRIPT', 'STYLE', 'SVG', 'HEAD'];\n  const isScrollable = element.scrollHeight > window.innerHeight;\n\n  if (isScrollable) return element;\n\n  for (let index = 0; index < element.childElementCount; index += 1) {\n    const currentChild = element.children.item(index);\n    const isExcluded =\n      currentChild.tagName.includes('-') ||\n      excludeTags.includes(currentChild.tagName);\n\n    if (!isExcluded) {\n      const scrollableElement = findScrollableElement(\n        currentChild,\n        maxDepth - 1\n      );\n\n      if (scrollableElement) return scrollableElement;\n    }\n  }\n\n  return null;\n}\nfunction injectStyle() {\n  const style = document.createElement('style');\n  style.innerText =\n    'html::-webkit-scrollbar, body::-webkit-scrollbar, .automa-scrollable-el::-webkit-scrollbar{ width: 0 !important; height: 0 !important } body.is-screenshotting [is-sticky] { position: relative !important; } .hide-fixed [is-fixed] {visibility: hidden !important; opacity: 0 !important;}';\n  style.id = 'automa-css-scroll';\n  document.body.appendChild(style);\n\n  return style;\n}\nfunction canvasToBase64(canvas, { format, quality }) {\n  return canvas.toDataURL(`image/${format}`, quality / 100);\n}\nfunction loadAsyncImg(src) {\n  return new Promise((resolve) => {\n    const image = new Image();\n    image.onload = () => {\n      resolve(image);\n    };\n    image.src = src;\n  });\n}\nasync function takeScreenshot(tabId, options) {\n  await sendMessage('set:active-tab', tabId, 'background');\n  const imageUrl = await sendMessage(\n    'get:tab-screenshot',\n    options,\n    'background'\n  );\n\n  return imageUrl;\n}\nasync function captureElement({ selector, tabId, options, $frameRect }) {\n  const element = await handleSelector(\n    // ? not support frameSelector ?\n    { data: { selector }, tabId },\n    { returnElement: true }\n  );\n\n  if (!element) {\n    const error = new Error('element-not-found');\n\n    throw error;\n  }\n\n  element.scrollIntoView({\n    block: 'center',\n    inline: 'center',\n  });\n\n  await sleep(500);\n  const imageUrl = await takeScreenshot(tabId, options);\n  const image = await loadAsyncImg(imageUrl);\n\n  const canvas = document.createElement('canvas');\n  const context = canvas.getContext('2d');\n  const { height, width, x, y } = element.getBoundingClientRect();\n\n  let windowWidth = window.innerWidth;\n  let windowHeight = window.innerHeight;\n\n  if ($frameRect) {\n    windowWidth = $frameRect.windowWidth;\n    windowHeight = $frameRect.windowHeight;\n  }\n\n  const diffWidth = image.width / windowWidth;\n  const diffHeight = image.height / windowHeight;\n\n  const newWidth = width * diffWidth;\n  const newHeight = height * diffHeight;\n\n  canvas.width = newWidth;\n  canvas.height = newHeight;\n\n  let xPos = x;\n  let yPos = y;\n\n  if ($frameRect) {\n    yPos += $frameRect.y;\n    xPos += $frameRect.x;\n  }\n\n  xPos *= diffWidth;\n  yPos *= diffHeight;\n\n  context.drawImage(\n    image,\n    xPos,\n    yPos,\n    newWidth,\n    newHeight,\n    0,\n    0,\n    newWidth,\n    newHeight\n  );\n\n  return canvasToBase64(canvas, options);\n}\n\nexport default async function ({\n  tabId,\n  options,\n  data: { type, selector, $frameRect },\n}) {\n  if (type === 'element') {\n    const imageUrl = await captureElement({\n      tabId,\n      options,\n      selector,\n      $frameRect,\n    });\n\n    return imageUrl;\n  }\n\n  document.body.classList.add('is-screenshotting');\n\n  const style = injectStyle();\n  const canvas = document.createElement('canvas');\n  const context = canvas.getContext('2d');\n  const maxCanvasSize = BROWSER_TYPE === 'firefox' ? 32767 : 65035;\n\n  const scrollElement = document.querySelector('.automa-scrollable-el');\n  let scrollableElement = scrollElement || findScrollableElement();\n\n  if (!scrollableElement) {\n    const imageUrl = await takeScreenshot(tabId, options);\n\n    return imageUrl;\n  }\n\n  scrollableElement.classList?.add('automa-scrollable-el');\n\n  const originalYPosition = window.scrollY;\n  let originalScrollHeight = scrollableElement.scrollHeight;\n\n  canvas.height =\n    scrollableElement.scrollHeight > maxCanvasSize\n      ? maxCanvasSize\n      : scrollableElement.scrollHeight;\n  canvas.width = window.innerWidth;\n\n  document.body\n    .querySelectorAll('*:not([is-sticky], [is-fixed])')\n    .forEach((el) => {\n      const { position } = getComputedStyle(el);\n\n      if (position === 'sticky') el.setAttribute('is-sticky', '');\n      else if (position === 'fixed') el.setAttribute('is-fixed', '');\n    });\n\n  scrollableElement.scrollTo(0, 0);\n\n  let scaleDiff = 1;\n  let scrollPosition = 0;\n  let canvasAdjusted = false;\n\n  if (scrollableElement.tagName === 'HTML') scrollableElement = window;\n\n  while (scrollPosition <= originalScrollHeight) {\n    const imageUrl = await takeScreenshot(tabId, options);\n\n    if (scrollPosition > 0 && !document.body.classList.contains('hide-fixed')) {\n      document.body.classList.add('hide-fixed');\n    }\n\n    const image = await loadAsyncImg(imageUrl);\n    const newScrollPos = scrollPosition + window.innerHeight;\n\n    if (!canvasAdjusted) {\n      if (canvas.width !== image.width) {\n        scaleDiff = image.width / window.innerWidth;\n\n        canvas.width *= scaleDiff;\n        canvas.height *= scaleDiff;\n\n        originalScrollHeight *= scaleDiff;\n\n        if (canvas.height > maxCanvasSize) canvas.height = maxCanvasSize;\n      }\n\n      canvasAdjusted = true;\n    }\n\n    const newWidth = image.width * scaleDiff;\n    const newHeight = image.height * scaleDiff;\n\n    const sourceYPos =\n      (scrollPosition + window.innerHeight) * scaleDiff - originalScrollHeight;\n\n    context.drawImage(\n      image,\n      0,\n      sourceYPos > 0 ? sourceYPos : 0,\n      newWidth,\n      newHeight,\n      0,\n      scrollPosition * scaleDiff,\n      newWidth,\n      newHeight\n    );\n\n    scrollPosition = newScrollPos;\n    scrollableElement.scrollTo(0, newScrollPos);\n\n    await sleep(1000);\n  }\n\n  style.remove();\n  document.body.classList.remove('hide-fixed');\n  document.body.classList.remove('is-screenshotting');\n\n  scrollableElement.scrollTo(0, originalYPosition);\n\n  return canvasToBase64(canvas, options);\n}\n"
  },
  {
    "path": "src/content/blocksHandler/handlerTriggerEvent.js",
    "content": "import { sendMessage } from '@/utils/message';\nimport simulateEvent from '@/utils/simulateEvent';\nimport simulateMouseEvent from '@/utils/simulateEvent/mouseEvent';\nimport { keyDefinitions } from '@/utils/USKeyboardLayout';\nimport { getElementPosition } from '../utils';\nimport handleSelector from '../handleSelector';\n\nconst modifiers = {\n  altKey: 1,\n  ctrlKey: 2,\n  metKey: 3,\n  shiftKey: 4,\n};\nconst eventHandlers = {\n  'mouse-event': async ({ params, sendCommand, name }) => {\n    const mouseButtons = {\n      0: { id: 1, name: 'left' },\n      1: { id: 4, name: 'middle' },\n      2: { id: 2, name: 'right' },\n    };\n    const commandParams = {\n      button: mouseButtons[params.button]?.name || 'left',\n    };\n\n    if (params.clientX) commandParams.x = +params.clientX;\n    if (params.clientY) commandParams.y = +params.clientY;\n\n    Object.keys(modifiers).forEach((key) => {\n      if (commandParams.modifiers) return;\n      if (params[key]) commandParams.modifiers = modifiers[key];\n    });\n\n    const mouseEvents = simulateMouseEvent({ sendCommand, commandParams });\n    const eventHandler = {\n      mouseover: 'mouseenter',\n      mouseout: 'mouseleave',\n    };\n    const eventName = eventHandler[name] || name;\n\n    await mouseEvents[eventName]();\n  },\n  'keyboard-event': async ({ name, params, sendCommand }) => {\n    const definition = keyDefinitions[params?.key];\n\n    const commandParams = {\n      key: params.key ?? '',\n      code: params.code ?? '',\n      autoRepeat: params.repeat,\n      windowsVirtualKeyCode: params.keyCode ?? 0,\n      type: name === 'keyup' ? 'keyUp' : 'keyDown',\n    };\n\n    if (definition.text || params.key.length === 1) {\n      commandParams.text = definition.text || params.key;\n    }\n\n    Object.keys(modifiers).forEach((key) => {\n      if (commandParams.modifiers) return;\n      if (params[key]) commandParams.modifiers = modifiers[key];\n    });\n\n    await sendCommand('Input.dispatchKeyEvent', commandParams);\n  },\n};\n\nfunction triggerEvent({ data, id, frameSelector, debugMode, activeTabId }) {\n  return new Promise((resolve, reject) => {\n    handleSelector(\n      { data, id, frameSelector },\n      {\n        async onSelected(element) {\n          const eventHandler = eventHandlers[data.eventType];\n\n          if (debugMode && eventHandler) {\n            let elCoordinate = {};\n\n            if (data.eventType === 'mouse-event') {\n              const { x, y } = await getElementPosition(element);\n              elCoordinate = { x, y };\n            }\n\n            const sendCommand = (method, params = {}) => {\n              const payload = {\n                method,\n                params: {\n                  ...elCoordinate,\n                  ...params,\n                },\n                tabId: activeTabId,\n              };\n\n              return sendMessage(\n                'debugger:send-command',\n                payload,\n                'background'\n              );\n            };\n\n            await eventHandler({\n              element,\n              sendCommand,\n              name: data.eventName,\n              params: data.eventParams,\n            });\n\n            return;\n          }\n\n          simulateEvent(element, data.eventName, data.eventParams);\n        },\n        onSuccess() {\n          resolve(data.eventName);\n        },\n        onError(error) {\n          reject(error);\n        },\n      }\n    );\n\n    resolve(data.eventName);\n  });\n}\n\nexport default triggerEvent;\n"
  },
  {
    "path": "src/content/blocksHandler/handlerUploadFile.js",
    "content": "import { sendMessage } from '@/utils/message';\nimport handleSelector from '../handleSelector';\n\nfunction injectFiles(element, files) {\n  const notFileTypeAttr = element.getAttribute('type') !== 'file';\n\n  if (element.tagName !== 'INPUT' || notFileTypeAttr) return;\n\n  element.files = files;\n  element.dispatchEvent(new Event('change', { bubbles: true }));\n}\n\nexport default async function (block) {\n  const elements = await handleSelector(block, { returnElement: true });\n\n  if (!elements) throw new Error('element-not-found');\n\n  const getFile = async (path) => {\n    let fileObject;\n    if (\n      path.includes('|') &&\n      !path.startsWith('file') &&\n      !path.startsWith('http')\n    ) {\n      const [filename, mime, base64] = path.split('|');\n      const response = await fetch(base64);\n      const arrayBuffer = await response.arrayBuffer();\n\n      fileObject = new File([arrayBuffer], filename, { type: mime });\n    } else {\n      const file = await sendMessage('get:file', path, 'background');\n      const name = file?.path?.replace(/^.*[\\\\/]/, '') || '';\n      const blob = await fetch(file.objUrl).then((response) => response.blob());\n\n      if (file.objUrl.startsWith('blob')) URL.revokeObjectURL(file.objUrl);\n\n      fileObject = new File([blob], name, { type: file.type });\n    }\n\n    return fileObject;\n  };\n  const filesPromises = await Promise.all(block.data.filePaths.map(getFile));\n  const dataTransfer = filesPromises.reduce((acc, file) => {\n    acc.items.add(file);\n\n    return acc;\n  }, new DataTransfer());\n\n  if (block.data.multiple) {\n    elements.forEach((element) => {\n      injectFiles(element, dataTransfer.files);\n    });\n  } else {\n    injectFiles(elements, dataTransfer.files);\n  }\n}\n"
  },
  {
    "path": "src/content/blocksHandler/handlerVerifySelector.js",
    "content": "import { sleep } from '@/utils/helper';\nimport handleSelector from '../handleSelector';\n\nconst SLEEP_TIME = 1700;\n\nasync function verifySelector(block) {\n  let elements = await handleSelector(block);\n  if (!elements) {\n    await sleep(SLEEP_TIME);\n    return { notFound: true };\n  }\n\n  if (!block.data.multiple) elements = [elements];\n\n  elements[0].scrollIntoView({\n    block: 'center',\n    inline: 'center',\n    behavior: 'smooth',\n  });\n\n  await sleep(200);\n\n  const divEl = document.createElement('div');\n  divEl.style =\n    'height: 100%; width: 100%; top: 0; left: 0; background-color: rgb(0 0 0 / 0.3); pointer-events: none; position: fixed; z-index: 99999';\n\n  const svgEl = document.createElementNS('http://www.w3.org/2000/svg', 'svg');\n  svgEl.style =\n    'height: 100%; width: 100%; top: 0; left: 0; pointer-events: none; position: relative;';\n\n  divEl.appendChild(svgEl);\n\n  elements.forEach((element) => {\n    const { left, top, width, height } = element.getBoundingClientRect();\n    const rectEl = document.createElementNS(\n      'http://www.w3.org/2000/svg',\n      'rect'\n    );\n\n    rectEl.setAttribute('y', top);\n    rectEl.setAttribute('x', left);\n    rectEl.setAttribute('width', width);\n    rectEl.setAttribute('height', height);\n    rectEl.setAttribute('stroke', '#2563EB');\n    rectEl.setAttribute('stroke-width', '2');\n    rectEl.setAttribute('fill', 'rgba(37, 99, 235, 0.4)');\n\n    svgEl.appendChild(rectEl);\n  });\n\n  document.body.appendChild(divEl);\n\n  await sleep(SLEEP_TIME);\n\n  divEl.remove();\n\n  return { notFound: false };\n}\n\nexport default verifySelector;\n"
  },
  {
    "path": "src/content/blocksHandler.js",
    "content": "import customHandlers from '@business/blocks/contentHandler';\nimport { toCamelCase } from '@/utils/helper';\n\nconst blocksHandler = require.context('./blocksHandler', false, /\\.js$/);\nconst handlers = blocksHandler.keys().reduce((acc, key) => {\n  const name = key.replace(/^\\.\\/handler|\\.js/g, '');\n\n  acc[toCamelCase(name)] = blocksHandler(key).default;\n\n  return acc;\n}, {});\n\nexport default function () {\n  return {\n    ...(customHandlers() || {}),\n    ...handlers,\n  };\n}\n"
  },
  {
    "path": "src/content/commandPalette/App.vue",
    "content": "<template>\n  <div\n    v-if=\"state.active\"\n    class=\"fixed top-0 left-0 h-full w-full bg-black bg-opacity-50 p-4 text-black\"\n    style=\"z-index: 99999999\"\n    @click.self=\"state.active = false\"\n  >\n    <ui-card\n      id=\"workflows-container\"\n      class=\"absolute w-full max-w-2xl\"\n      padding=\"p-0\"\n      style=\"left: 50%; top: 50px; transform: translateX(-50%)\"\n    >\n      <div class=\"p-4\">\n        <label\n          class=\"bg-input flex h-12 items-center rounded-lg px-2 ring-accent transition focus-within:ring-2\"\n        >\n          <img :src=\"logoUrl\" class=\"h-8 w-8\" />\n          <input\n            ref=\"inputRef\"\n            type=\"text\"\n            class=\"h-full flex-1 rounded-lg bg-transparent px-2 focus:ring-0\"\n            :placeholder=\"\n              paramsState.active\n                ? paramsState.workflow.name\n                : 'Search workflows...'\n            \"\n            @input=\"onInput\"\n            @keydown=\"onInputKeydown\"\n          />\n          <template v-for=\"key in state.shortcutKeys\" :key=\"key\">\n            <span\n              class=\"bg-box-transparent ml-1 inline-block rounded-md border-2 border-gray-300 p-1 text-center text-xs font-semibold capitalize text-gray-600\"\n              style=\"min-width: 29px; font-family: inherit\"\n            >\n              {{ getReadableShortcut(key) }}\n            </span>\n          </template>\n        </label>\n      </div>\n      <div\n        class=\"scroll workflows-list overflow-auto px-4 pb-4\"\n        style=\"max-height: calc(100vh - 200px)\"\n      >\n        <div v-if=\"!state.retrieved\" class=\"mb-2 text-center\">\n          <ui-spinner color=\"text-accent\" />\n        </div>\n        <template v-else>\n          <div v-if=\"paramsState.active\">\n            <ul class=\"space-y-4 divide-y\">\n              <li\n                v-for=\"(param, paramIdx) in paramsState.items\"\n                :key=\"paramIdx\"\n              >\n                <component\n                  :is=\"paramsList[param.type].valueComp\"\n                  v-if=\"paramsList[param.type]\"\n                  v-model=\"param.value\"\n                  :label=\"param.name\"\n                  :param-data=\"param\"\n                  class=\"w-full\"\n                />\n                <ui-input\n                  v-else\n                  v-model=\"param.value\"\n                  :type=\"param.inputType || param.type\"\n                  :label=\"param.name\"\n                  :placeholder=\"param.placeholder\"\n                  class=\"w-full\"\n                  @keyup.enter=\"runWorkflow(index, workflow)\"\n                />\n                <p\n                  v-if=\"param.description\"\n                  title=\"Description\"\n                  class=\"ml-1 text-sm\"\n                >\n                  {{ param.description }}\n                </p>\n              </li>\n            </ul>\n          </div>\n          <template v-else>\n            <p\n              v-if=\"state.query && workflows.length === 0\"\n              class=\"text-center text-gray-600\"\n            >\n              Can't find workflows\n            </p>\n            <ui-list v-else class=\"space-y-1\">\n              <ui-list-item\n                v-for=\"(workflow, index) in workflows\"\n                :id=\"`list-item-${index}`\"\n                :key=\"workflow.id\"\n                :active=\"index === state.selectedIndex\"\n                small\n                color=\"bg-box-transparent list-item-active\"\n                class=\"group cursor-pointer\"\n                @mouseenter=\"state.selectedIndex = index\"\n                @click=\"executeWorkflow(workflow)\"\n              >\n                <div class=\"w-8\">\n                  <img\n                    v-if=\"workflow.icon?.startsWith('http')\"\n                    :src=\"workflow.icon\"\n                    class=\"overflow-hidden rounded-lg\"\n                    style=\"height: 26px; width: 26px\"\n                    alt=\"Can not display\"\n                  />\n                  <v-remixicon\n                    v-else\n                    :name=\"workflow.icon || 'riGlobalLine'\"\n                    size=\"26\"\n                  />\n                </div>\n                <div class=\"mx-2 flex-1 overflow-hidden\">\n                  <p class=\"text-overflow\">\n                    {{ workflow.name }}\n                  </p>\n                  <p class=\"text-overflow leading-tight text-gray-500\">\n                    {{ workflow.description }}\n                  </p>\n                </div>\n                <v-remixicon\n                  name=\"riArrowGoForwardLine\"\n                  class=\"invisible text-gray-600 group-hover:visible\"\n                  size=\"20\"\n                  rotate=\"180\"\n                />\n              </ui-list-item>\n            </ui-list>\n          </template>\n        </template>\n      </div>\n      <div class=\"flex items-center px-4 py-2\">\n        <div v-if=\"paramsState.active\" class=\"pl-2 text-gray-500\">\n          <div class=\"flex items-center\">\n            <p class=\"mr-4\">\n              {{ paramsState.workflow.description }}\n            </p>\n            <p>\n              Press\n              <span\n                class=\"bg-box-transparent ml-1 inline-block rounded-md border-2 border-gray-300 p-1 text-center text-xs font-semibold text-gray-600\"\n              >\n                Escape\n              </span>\n              to cancel\n            </p>\n          </div>\n        </div>\n        <p\n          v-else\n          class=\"inline-flex cursor-pointer items-center text-gray-600\"\n          @click=\"openDashboard\"\n        >\n          Open dashboard\n          <v-remixicon\n            name=\"riExternalLinkLine\"\n            class=\"ml-1 inline-block\"\n            size=\"20\"\n          />\n        </p>\n        <div class=\"grow\" />\n        <ui-button\n          v-if=\"paramsState.active\"\n          variant=\"accent\"\n          @click=\"executeWorkflowWithParams\"\n        >\n          Execute\n        </ui-button>\n      </div>\n    </ui-card>\n  </div>\n</template>\n<script setup>\nimport ParameterInputValue from '@/components/newtab/workflow/edit/Parameter/ParameterInputValue.vue';\nimport ParameterJsonValue from '@/components/newtab/workflow/edit/Parameter/ParameterJsonValue.vue';\nimport RendererWorkflowService from '@/service/renderer/RendererWorkflowService';\nimport { debounce, parseJSON } from '@/utils/helper';\nimport { sendMessage } from '@/utils/message';\nimport workflowParameters from '@business/parameters';\nimport cloneDeep from 'lodash.clonedeep';\nimport {\n  computed,\n  inject,\n  onBeforeUnmount,\n  onMounted,\n  reactive,\n  ref,\n  shallowReactive,\n  watch,\n} from 'vue';\nimport browser from 'webextension-polyfill';\n\nconst paramsList = {\n  string: {\n    id: 'string',\n    name: 'Input (string)',\n    valueComp: ParameterInputValue,\n  },\n  json: {\n    id: 'json',\n    name: 'Input (JSON)',\n    valueComp: ParameterJsonValue,\n  },\n};\n\nconst os = navigator.appVersion.indexOf('Mac') !== -1 ? 'mac' : 'win';\n\nconst logoUrl = browser.runtime.getURL(\n  process.env.NODE_ENV === 'development' ? '/icon-dev-128.png' : '/icon-128.png'\n);\n\nconst inputRef = ref(null);\nconst state = shallowReactive({\n  query: '',\n  active: false,\n  workflows: [],\n  shortcutKeys: [],\n  selectedIndex: -1,\n});\nconst paramsState = reactive({\n  items: [],\n  workflow: {},\n  active: false,\n  paramNames: [],\n  activeIndex: 0,\n  inputtedVal: '',\n});\n\nconst rootElement = inject('rootElement');\n\nconst workflows = computed(() =>\n  state.workflows.filter((workflow) =>\n    workflow.name.toLocaleLowerCase().includes(state.query.toLocaleLowerCase())\n  )\n);\n\nfunction getReadableShortcut(str) {\n  const list = {\n    option: {\n      win: 'alt',\n      mac: 'option',\n    },\n    mod: {\n      win: 'ctrl',\n      mac: '⌘',\n    },\n  };\n  const regex = /option|mod/g;\n  const replacedStr = str.replace(regex, (match) => {\n    return list[match][os];\n  });\n\n  return replacedStr;\n}\nfunction clearParamsState() {\n  Object.assign(paramsState, {\n    items: [],\n    workflow: {},\n    active: false,\n    activeIndex: 0,\n    inputtedVal: '',\n  });\n}\nfunction sendExecuteCommand(workflow, options = {}) {\n  const workflowData = {\n    ...workflow,\n    includeTabId: true,\n    options: { ...options, checkParams: false },\n  };\n  RendererWorkflowService.executeWorkflow(workflowData);\n\n  state.active = false;\n}\nfunction executeWorkflow(workflow) {\n  if (!workflow) return;\n\n  let triggerData = workflow.trigger;\n  if (!triggerData) {\n    const triggerNode = workflow.drawflow?.nodes?.find(\n      (node) => node.label === 'trigger'\n    );\n    triggerData = triggerNode?.data;\n  }\n\n  if (triggerData?.parameters?.length > 0) {\n    const keys = new Set();\n    const params = [];\n    triggerData.parameters.forEach((param) => {\n      if (keys.has(param.name)) return;\n\n      params.push(param);\n      keys.add(param.name);\n    });\n\n    const parameters = cloneDeep(triggerData.parameters).map((item) => ({\n      ...item,\n      value: item.defaultValue,\n    }));\n\n    paramsState.workflow = workflow;\n    paramsState.items = parameters;\n\n    paramsState.active = true;\n  } else {\n    sendExecuteCommand(workflow);\n  }\n\n  inputRef.value.value = '';\n  state.query = '';\n  paramsState.inputtedVal = '';\n}\nfunction getParamsValues(params) {\n  const getParamVal = {\n    string: (str) => str,\n    number: (num) => (Number.isNaN(+num) ? 0 : +num),\n    json: (value) => parseJSON(value, null),\n    default: (value) => value,\n  };\n\n  return params.reduce((acc, param) => {\n    const valueFunc =\n      getParamVal[param.type] ||\n      paramsList[param.type]?.getValue ||\n      getParamVal.default;\n    const value = valueFunc(param.value || param.defaultValue);\n    acc[param.name] = value;\n\n    return acc;\n  }, {});\n}\nfunction executeWorkflowWithParams() {\n  const variables = getParamsValues(paramsState.items);\n  sendExecuteCommand(paramsState.workflow, { data: { variables } });\n\n  clearParamsState();\n}\nfunction onKeydown(event) {\n  const { ctrlKey, altKey, metaKey, key, shiftKey } = event;\n\n  if (key === 'Escape') {\n    if (paramsState.active) {\n      clearParamsState();\n    } else {\n      state.active = false;\n    }\n    return;\n  }\n\n  const shortcuts = window._automaShortcuts;\n  if (!shortcuts || shortcuts.length < 1) return;\n\n  const automaShortcut = shortcuts.every((shortcutKey) => {\n    if (shortcutKey === 'mod') return ctrlKey || metaKey;\n    if (shortcutKey === 'shift') return shiftKey;\n    if (shortcutKey === 'option') return altKey;\n\n    return shortcutKey === key.toLowerCase();\n  });\n  if (automaShortcut) {\n    event.preventDefault();\n    state.active = true;\n    state.shortcutKeys = shortcuts;\n  }\n}\nfunction onInputKeydown(event) {\n  const { key } = event;\n\n  if (key !== 'Escape') {\n    event.stopPropagation();\n  }\n\n  if (['ArrowDown', 'ArrowUp'].includes(key)) {\n    let nextIndex = state.selectedIndex;\n    const maxIndex = workflows.value.length - 1;\n\n    if (key === 'ArrowDown') {\n      nextIndex += 1;\n      if (nextIndex > maxIndex) nextIndex = 0;\n    } else if (key === 'ArrowUp') {\n      nextIndex -= 1;\n      if (nextIndex < 0) nextIndex = maxIndex;\n    }\n\n    state.selectedIndex = nextIndex;\n    return;\n  }\n\n  if (key === 'Enter') {\n    if (paramsState.active) return;\n    executeWorkflow(workflows.value[state.selectedIndex]);\n  }\n}\nfunction checkInView(container, element, partial = false) {\n  const cTop = container.scrollTop;\n  const cBottom = cTop + container.clientHeight;\n\n  const eTop = element.offsetTop;\n  const eBottom = eTop + element.clientHeight;\n\n  const isTotal = eTop >= cTop && eBottom <= cBottom;\n  const isPartial =\n    partial &&\n    ((eTop < cTop && eBottom > cTop) || (eBottom > cBottom && eTop < cBottom));\n\n  return isTotal || isPartial;\n}\nfunction onInput(event) {\n  const { value } = event.target;\n\n  if (paramsState.active) {\n    paramsState.inputtedVal = value;\n    paramsState.activeIndex = value.split(';').length - 1;\n  } else {\n    state.query = value;\n  }\n}\nfunction openDashboard() {\n  sendMessage('open:dashboard', '', 'background');\n}\n\nwatch(inputRef, () => {\n  if (!inputRef.value) return;\n\n  inputRef.value.focus();\n});\nwatch(\n  () => state.active,\n  async () => {\n    if (!state.retrieved && state.active) {\n      const {\n        workflows: localWorkflows,\n        workflowHosts,\n        teamWorkflows,\n      } = await browser.storage.local.get([\n        'workflows',\n        'workflowHosts',\n        'teamWorkflows',\n      ]);\n      state.workflows = [\n        ...Object.values(workflowHosts || {}),\n        ...Object.values(localWorkflows || {}),\n        ...Object.values(Object.values(teamWorkflows || {})[0] || {}),\n      ];\n\n      state.retrieved = true;\n    } else if (!state.active) {\n      clearParamsState();\n      state.query = '';\n      state.selectedIndex = -1;\n    }\n  }\n);\nwatch(\n  () => state.selectedIndex,\n  debounce((activeIndex) => {\n    const container = rootElement.shadowRoot.querySelector(\n      '#workflows-container .workflows-list'\n    );\n    const element = rootElement.shadowRoot.querySelector(\n      `#list-item-${activeIndex}`\n    );\n\n    if (element && !checkInView(container, element)) {\n      element.scrollIntoView({\n        block: 'nearest',\n        behavior: 'smooth',\n      });\n    }\n  }, 100)\n);\n\nwindow.initPaletteParams = (data) => {\n  paramsState.items = data.params;\n  paramsState.workflow = data.workflow;\n  paramsState.active = true;\n\n  state.active = true;\n};\n\nonMounted(() => {\n  browser.storage.local.get('automaShortcut').then(({ automaShortcut }) => {\n    if (Array.isArray(automaShortcut) && automaShortcut.length < 1) return;\n\n    let keys = ['mod', 'shift', 'e'];\n    if (automaShortcut) keys = automaShortcut.split('+');\n\n    state.shortcutKeys = keys;\n    window._automaShortcuts = keys;\n  });\n\n  window.addEventListener('keydown', onKeydown);\n  Object.assign(paramsList, workflowParameters());\n});\nonBeforeUnmount(() => {\n  window.removeEventListener('keydown', onKeydown);\n});\n</script>\n"
  },
  {
    "path": "src/content/commandPalette/compsUi.js",
    "content": "import VAutofocus from '@/directives/VAutofocus';\nimport UiCard from '@/components/ui/UiCard.vue';\nimport UiInput from '@/components/ui/UiInput.vue';\nimport UiList from '@/components/ui/UiList.vue';\nimport UiListItem from '@/components/ui/UiListItem.vue';\nimport UiButton from '@/components/ui/UiButton.vue';\nimport UiSelect from '@/components/ui/UiSelect.vue';\nimport UiSpinner from '@/components/ui/UiSpinner.vue';\nimport UiTextarea from '@/components/ui/UiTextarea.vue';\nimport UiPopover from '@/components/ui/UiPopover.vue';\nimport TransitionExpand from '@/components/transitions/TransitionExpand.vue';\n\nexport default function (app) {\n  app.component('UiCard', UiCard);\n  app.component('UiList', UiList);\n  app.component('UiInput', UiInput);\n  app.component('UiButton', UiButton);\n  app.component('UiSelect', UiSelect);\n  app.component('UiPopover', UiPopover);\n  app.component('UiSpinner', UiSpinner);\n  app.component('UiTextarea', UiTextarea);\n  app.component('UiListItem', UiListItem);\n  app.component('TransitionExpand', TransitionExpand);\n\n  app.directive('autofocus', VAutofocus);\n}\n"
  },
  {
    "path": "src/content/commandPalette/icons.js",
    "content": "import {\n  riArrowGoForwardLine,\n  riGlobalLine,\n  riFileTextLine,\n  riEqualizerLine,\n  riTimerLine,\n  riCalendarLine,\n  riFlashlightLine,\n  riLightbulbFlashLine,\n  riDatabase2Line,\n  riWindowLine,\n  riCursorLine,\n  riDownloadLine,\n  riCommandLine,\n  riExternalLinkLine,\n  riArrowDropDownLine,\n} from 'v-remixicon/icons';\n\nexport default {\n  riArrowGoForwardLine,\n  riGlobalLine,\n  riFileTextLine,\n  riEqualizerLine,\n  riTimerLine,\n  riCalendarLine,\n  riFlashlightLine,\n  riLightbulbFlashLine,\n  riDatabase2Line,\n  riWindowLine,\n  riCursorLine,\n  riDownloadLine,\n  riCommandLine,\n  riExternalLinkLine,\n  riArrowDropDownLine,\n};\n"
  },
  {
    "path": "src/content/commandPalette/index.js",
    "content": "import browser from 'webextension-polyfill';\nimport initApp from './main';\nimport injectAppStyles from '../injectAppStyles';\n\nfunction pageLoaded() {\n  return new Promise((resolve) => {\n    const checkDocState = () => {\n      if (document.readyState === 'loading') {\n        setTimeout(checkDocState, 1000);\n        return;\n      }\n\n      resolve();\n    };\n\n    checkDocState();\n  });\n}\n\nexport default async function () {\n  try {\n    const isMainFrame = window.self === window.top;\n    if (!isMainFrame) return;\n\n    const isInvalidURL = /.(json|xml)$/.test(window.location.pathname);\n    if (isInvalidURL) return;\n\n    const { automaShortcut } = await browser.storage.local.get(\n      'automaShortcut'\n    );\n    if (Array.isArray(automaShortcut) && automaShortcut.length === 0) return;\n\n    await pageLoaded();\n\n    const instanceExist = document.querySelector('automa-palette');\n    if (instanceExist) return;\n\n    const element = document.createElement('div');\n    element.attachShadow({ mode: 'open' });\n    element.id = 'automa-palette';\n\n    await injectAppStyles(element.shadowRoot);\n    initApp(element);\n\n    document.body.appendChild(element);\n  } catch (error) {\n    console.error(error);\n  }\n}\n"
  },
  {
    "path": "src/content/commandPalette/main.js",
    "content": "import { createApp } from 'vue';\nimport vRemixicon from 'v-remixicon';\nimport App from './App.vue';\nimport compsUi from './compsUi';\nimport icons from './icons';\n\nconst additionalStyle = `.list-item-active svg { visibility: visible }`;\n\nexport default function (rootElement) {\n  const appRoot = document.createElement('div');\n  appRoot.setAttribute('id', 'app');\n\n  const style = document.createElement('style');\n  style.textContent = additionalStyle;\n\n  rootElement.shadowRoot.appendChild(style);\n  rootElement.shadowRoot.appendChild(appRoot);\n\n  createApp(App)\n    .use(compsUi)\n    .use(vRemixicon, icons)\n    .provide('rootElement', rootElement)\n    .mount(appRoot);\n}\n"
  },
  {
    "path": "src/content/elementObserver.js",
    "content": "import browser from 'webextension-polyfill';\nimport { isXPath, debounce } from '@/utils/helper';\nimport { sendMessage } from '@/utils/message';\nimport FindElement from '@/utils/FindElement';\n\nconst observeElements = {};\n\nconst targetMutationCallback = debounce(([{ target }]) => {\n  let workflowId = target.getAttribute('automa-id');\n\n  if (!workflowId) {\n    const element = target.closest('[automa-id]');\n    if (!element) return;\n    workflowId = element.getAttribute('automa-id');\n  }\n  if (!observeElements[workflowId]) return;\n\n  const { workflow } = observeElements[workflowId];\n  workflow.includeTabId = true;\n\n  sendMessage('workflow:execute', workflow, 'background');\n}, 250);\nconst targetObserver = new MutationObserver(targetMutationCallback);\n\nconst baseMutationCallback = debounce(() => {\n  targetObserver.disconnect();\n  Object.values(observeElements).forEach((detail) => {\n    /* eslint-disable-next-line */\n    tryObserve({ ...detail, observer: targetObserver });\n  });\n}, 250);\nconst baseObserver = new MutationObserver(baseMutationCallback);\n\nexport function matchPatternToRegex(str) {\n  const regexStr = str.replace(/[*?^$]/g, (char) => {\n    if (char === '*') return '[a-zA-Z0-9]*';\n\n    return `\\\\${char}`;\n  });\n  const regex = new RegExp(regexStr);\n\n  return regex;\n}\nfunction tryObserve({ selector, observer, options, id }) {\n  let tryCount = 0;\n\n  const findElement = () => {\n    if (tryCount > 10) return;\n\n    const selectorType = isXPath(selector) ? 'xpath' : 'cssSelector';\n    const element = FindElement[selectorType]({ selector });\n\n    if (!element) {\n      tryCount += 1;\n      setTimeout(findElement, 1000);\n      return;\n    }\n\n    if (id) element.setAttribute('automa-id', id);\n\n    if (!options.attributes || options.attributeFilter.length === 0)\n      delete options.attributeFilter;\n    observer.observe(element, options);\n  };\n\n  findElement();\n}\n\nexport default async function () {\n  const { workflows } = await browser.storage.local.get('workflows');\n  workflows.forEach(({ trigger, id, ...workflowDetail }) => {\n    if (\n      !trigger ||\n      trigger.type !== 'element-change' ||\n      !trigger.observeElement?.selector ||\n      !trigger.observeElement?.matchPattern\n    )\n      return;\n\n    const {\n      baseSelector,\n      baseElOptions,\n      selector,\n      targetOptions,\n      matchPattern,\n    } = trigger.observeElement;\n\n    const regex = matchPatternToRegex(matchPattern);\n    if (!regex.test(window.location.href)) return;\n\n    if (baseSelector)\n      tryObserve({\n        selector: baseSelector,\n        options: baseElOptions,\n        observer: baseObserver,\n      });\n\n    observeElements[id] = {\n      id,\n      selector,\n      options: targetOptions,\n      workflow: { id, trigger, ...workflowDetail },\n    };\n    tryObserve({\n      selector,\n      options: targetOptions,\n      observer: targetObserver,\n      id,\n    });\n  });\n}\n"
  },
  {
    "path": "src/content/elementSelector/App.vue",
    "content": "<template>\n  <div\n    :class=\"{\n      'select-none': state.isDragging,\n      'bg-black bg-opacity-30': !state.hide,\n    }\"\n    class=\"root pointer-events-none fixed top-0 left-0 h-full w-full text-black\"\n    style=\"z-index: 99999999\"\n  >\n    <div\n      ref=\"cardEl\"\n      :style=\"{ transform: `translate(${cardRect.x}px, ${cardRect.y}px)` }\"\n      style=\"width: 320px\"\n      class=\"root-card pointer-events-auto relative z-50 rounded-lg bg-white shadow-xl\"\n    >\n      <div\n        class=\"drag-button absolute z-50 cursor-move rounded-lg bg-white p-2 p-1 shadow-xl\"\n        style=\"top: -15px; left: -15px\"\n      >\n        <v-remixicon\n          name=\"riDragMoveLine\"\n          @mousedown=\"state.isDragging = true\"\n        />\n      </div>\n      <div class=\"flex items-center px-4 pt-4\">\n        <p class=\"text-lg font-semibold\">Automa</p>\n        <div class=\"grow\"></div>\n        <button\n          class=\"hoverable mr-2 rounded-md p-1 transition\"\n          @mousedown.stop.prevent\n          @click.stop.prevent=\"\n            state.hide = !state.hide;\n            clearConnectedPort();\n          \"\n        >\n          <v-remixicon :name=\"state.hide ? 'riEyeOffLine' : 'riEyeLine'\" />\n        </button>\n        <button\n          class=\"hoverable rounded-md p-1 transition\"\n          @mousedown.stop.prevent\n          @click.stop.prevent=\"destroy\"\n        >\n          <v-remixicon name=\"riCloseLine\" />\n        </button>\n      </div>\n      <div class=\"p-4\">\n        <selector-query\n          v-model:selector-type=\"state.selectorType\"\n          v-model:select-list=\"state.selectList\"\n          :selector=\"state.elSelector\"\n          :settings-active=\"state.showSettings\"\n          :selected-count=\"state.selectedElements.length\"\n          @settings=\"state.showSettings = $event\"\n          @selector=\"updateSelector\"\n          @parent=\"selectElementPath('up')\"\n          @child=\"selectElementPath('down')\"\n        />\n        <ui-button\n          v-if=\"state.isSelectBlockElement\"\n          :disabled=\"!state.elSelector\"\n          variant=\"accent\"\n          class=\"mt-4 w-full\"\n          @click=\"saveSelector\"\n        >\n          Select Element\n        </ui-button>\n        <selector-elements-detail\n          v-if=\"\n            !state.showSettings &&\n            !state.hide &&\n            state.selectedElements.length > 0\n          \"\n          v-model:active-tab=\"state.activeTab\"\n          v-bind=\"{\n            elSelector: state.elSelector,\n            selectElements: state.selectElements,\n            hideBlocks: state.isSelectBlockElement,\n            selectedElements: state.selectedElements,\n          }\"\n          @highlight=\"toggleHighlightElement\"\n          @execute=\"state.isExecuting = $event\"\n        />\n        <div\n          v-if=\"\n            state.showSettings && state.selectorType === 'css' && !state.hide\n          \"\n          class=\"mt-4\"\n        >\n          <p class=\"mb-4 font-semibold\">Selector settings</p>\n          <ul class=\"space-y-4\">\n            <li>\n              <label class=\"flex items-center space-x-2\">\n                <ui-switch v-model=\"selectorSettings.idName\" />\n                <p>Include element id</p>\n              </label>\n            </li>\n            <li>\n              <label class=\"flex items-center space-x-2\">\n                <ui-switch v-model=\"selectorSettings.tagName\" />\n                <p>Include tag name</p>\n              </label>\n            </li>\n            <li>\n              <label class=\"flex items-center space-x-2\">\n                <ui-switch v-model=\"selectorSettings.className\" />\n                <p>Include class name</p>\n              </label>\n            </li>\n            <li>\n              <label class=\"flex items-center space-x-2\">\n                <ui-switch v-model=\"selectorSettings.attr\" />\n                <p>Include attributes</p>\n              </label>\n              <template v-if=\"selectorSettings.attr\">\n                <label\n                  class=\"ml-1 mt-2 block text-sm text-gray-600\"\n                  for=\"automa-attribute-names\"\n                >\n                  Attribute names\n                </label>\n                <ui-textarea\n                  id=\"automa-attribute-names\"\n                  v-model=\"selectorSettings.attrNames\"\n                  label=\"Attribute name\"\n                  placeholder=\"data-testid, aria-label, type\"\n                />\n                <span class=\"text-sm\">\n                  Use commas to separate the attribute\n                </span>\n              </template>\n            </li>\n          </ul>\n        </div>\n        <p class=\"mt-1 text-sm text-gray-600\">\n          Click or press\n          <kbd class=\"bg-box-transparent rounded-md p-1\">Space</kbd> to select\n          an element\n        </p>\n      </div>\n    </div>\n  </div>\n  <shared-element-selector\n    :hide=\"state.hide\"\n    :disabled=\"state.hide\"\n    :list=\"state.selectList\"\n    :selector-type=\"state.selectorType\"\n    :selected-els=\"state.selectedElements\"\n    :selector-settings=\"selectorSettings\"\n    with-attributes\n    @selected=\"onElementsSelected\"\n  />\n</template>\n<script setup>\nimport SelectorElementsDetail from '@/components/content/selector/SelectorElementsDetail.vue';\nimport SelectorQuery from '@/components/content/selector/SelectorQuery.vue';\nimport SharedElementSelector from '@/components/content/shared/SharedElementSelector.vue';\nimport findSelector from '@/lib/findSelector';\nimport FindElement from '@/utils/FindElement';\nimport { debounce } from '@/utils/helper';\nimport {\n  inject,\n  onBeforeUnmount,\n  onMounted,\n  reactive,\n  ref,\n  toRaw,\n  watch,\n} from 'vue';\nimport browser from 'webextension-polyfill';\nimport { getElementRect } from '../utils';\nimport getSelectorOptions from './getSelectorOptions';\n\nlet connectedPort = null;\nconst originalFontSize = document.documentElement.style.fontSize;\nconst selectedElement = {\n  path: [],\n  pathIndex: 0,\n  cache: new WeakMap(),\n};\n\nconst rootElement = inject('rootElement');\n\nconst cardEl = ref('cardEl');\nconst state = reactive({\n  hide: false,\n  elSelector: '',\n  destroyed: false,\n  isDragging: false,\n  selectList: false,\n  isExecuting: false,\n  selectElements: [],\n  showSettings: false,\n  selectorType: 'css',\n  selectedElements: [],\n  activeTab: 'attributes',\n  isSelectBlockElement: false,\n});\nconst cardRect = reactive({\n  x: 0,\n  y: 0,\n  height: 0,\n  width: 0,\n});\nconst selectorSettings = reactive({\n  idName: true,\n  tagName: true,\n  attr: true,\n  className: true,\n  attrNames: 'data-testid',\n});\n\nconst cardElementObserver = new ResizeObserver(([entry]) => {\n  const { height, width } = entry.contentRect;\n\n  cardRect.width = width;\n  cardRect.height = height;\n});\n\nconst updateSelector = debounce((selector) => {\n  let frameSelector;\n  let elSelector = selector;\n\n  if (selector.includes('|>')) {\n    [frameSelector, elSelector] = selector.split(/\\|>(.+)/);\n  }\n\n  const selectorType = state.selectorType === 'css' ? 'cssSelector' : 'xpath';\n\n  try {\n    if (frameSelector) {\n      const frame = FindElement[selectorType]({\n        selector: frameSelector,\n        multiple: false,\n      });\n      if (!['IFRAME', 'FRAME'].includes(frame.tagName)) return;\n\n      const { top, left } = frame.getBoundingClientRect();\n      frame.contentWindow.postMessage(\n        {\n          selectorType,\n          selector: elSelector,\n          type: 'automa:find-element',\n          frameRect: { top, left },\n        },\n        '*'\n      );\n      return;\n    }\n\n    const elements = FindElement[selectorType]({\n      selector: elSelector,\n      multiple: true,\n    });\n    state.selectedElements = Array.from(elements || []).map((el) =>\n      getElementRect(el, true)\n    );\n  } catch (error) {\n    console.error(error);\n    state.selectedElements = [];\n  }\n}, 200);\n\nfunction toggleHighlightElement({ index, highlight }) {\n  state.selectedElements[index].highlight = highlight;\n}\nfunction onElementsSelected({ selector, elements, path, selectElements }) {\n  if (path) {\n    selectedElement.path = path;\n    selectedElement.pathIndex = 0;\n  }\n\n  state.elSelector = selector;\n  state.selectedElements = elements || [];\n  state.selectElements = selectElements || [];\n}\nfunction onMousemove({ clientX, clientY }) {\n  if (!state.isDragging) return;\n\n  const height = window.innerHeight;\n  const width = document.documentElement.clientWidth;\n\n  if (clientY < 10) clientY = 10;\n  else if (cardRect.height + clientY > height)\n    clientY = height - cardRect.height;\n\n  if (clientX < 10) clientX = 10;\n  else if (cardRect.width + clientX > width) clientX = width - cardRect.width;\n\n  cardRect.x = clientX;\n  cardRect.y = clientY;\n}\nfunction selectElementPath(type) {\n  let pathIndex =\n    type === 'up'\n      ? selectedElement.pathIndex + 1\n      : selectedElement.pathIndex - 1;\n  let element = selectedElement.path[pathIndex];\n\n  if ((type === 'up' && !element) || element?.tagName === 'BODY') return;\n\n  if (type === 'down' && !element) {\n    const previousElement = selectedElement.path[selectedElement.pathIndex];\n    const childEl = Array.from(previousElement.children).find(\n      (el) => !['STYLE', 'SCRIPT'].includes(el.tagName)\n    );\n\n    if (!childEl) return;\n\n    element = childEl;\n    selectedElement.path.unshift(childEl);\n    pathIndex = 0;\n  }\n\n  selectedElement.pathIndex = pathIndex;\n\n  state.selectedElements = [getElementRect(element, true)];\n  state.elSelector = selectedElement.cache.has(element)\n    ? selectedElement.cache.get(element)\n    : findSelector(element, getSelectorOptions(selectorSettings));\n}\nfunction onMouseup() {\n  if (state.isDragging) state.isDragging = false;\n}\nfunction onMessage({ data }) {\n  if (data.type !== 'automa:selected-elements') return;\n\n  state.selectedElements = data.elements;\n}\nfunction destroy() {\n  rootElement.style.display = 'none';\n\n  Object.assign(state, {\n    hide: true,\n    activeTab: '',\n    elSelector: '',\n    isDragging: false,\n    isExecuting: false,\n    hoveredElements: [],\n    selectedElements: [],\n  });\n\n  const prevSelectedList = document.querySelectorAll('[automa-el-list]');\n  prevSelectedList.forEach((element) => {\n    element.removeAttribute('automa-el-list');\n  });\n\n  document.documentElement.style.fontSize = originalFontSize;\n}\nfunction clearConnectedPort() {\n  if (!connectedPort) return;\n\n  connectedPort = null;\n  state.isSelectBlockElement = false;\n}\nfunction onVisibilityChange() {\n  if (!connectedPort || document.visibilityState !== 'hidden') return;\n\n  clearConnectedPort();\n}\nfunction saveSelector() {\n  if (!connectedPort) return;\n\n  connectedPort.postMessage(state.elSelector);\n  clearConnectedPort();\n  destroy();\n}\nfunction attachListeners() {\n  cardElementObserver.observe(cardEl.value);\n\n  window.addEventListener('message', onMessage);\n  window.addEventListener('mouseup', onMouseup);\n  window.addEventListener('mousemove', onMousemove);\n  document.addEventListener('visibilitychange', onVisibilityChange);\n}\nfunction detachListeners() {\n  cardElementObserver.disconnect();\n\n  window.removeEventListener('message', onMessage);\n  window.removeEventListener('mouseup', onMouseup);\n  window.removeEventListener('mousemove', onMousemove);\n  document.removeEventListener('visibilitychange', onVisibilityChange);\n}\n\nwatch(\n  () => state.isDragging,\n  (value) => {\n    document.body.toggleAttribute('automa-isDragging', value);\n  }\n);\nwatch(\n  selectorSettings,\n  (settings) => {\n    browser.storage.local.set({\n      selectorSettings: toRaw(settings),\n    });\n  },\n  { deep: true }\n);\n\nbrowser.runtime.onConnect.addListener((port) => {\n  clearConnectedPort();\n\n  connectedPort = port;\n  state.isSelectBlockElement = true;\n\n  port.onDisconnect.addListener(clearConnectedPort);\n});\n\nonMounted(() => {\n  browser.storage.local.get('selectorSettings').then((storage) => {\n    const settings = storage.selectorSettings || {};\n    Object.assign(selectorSettings, settings);\n  });\n\n  setTimeout(() => {\n    const { height, width } = cardEl.value.getBoundingClientRect();\n\n    cardRect.x = window.innerWidth - (width + 35);\n    cardRect.y = 20;\n    cardRect.width = width;\n    cardRect.height = height;\n  }, 500);\n\n  attachListeners();\n});\nonBeforeUnmount(() => {\n  detachListeners();\n});\n</script>\n<style>\n.root {\n  font-size: 16px;\n  z-index: 99999;\n  line-height: 1.5 !important;\n  font-family: 'Inter var', sans-serif;\n  font-feature-settings: 'cv02', 'cv03', 'cv04', 'cv11';\n}\n.root-card:hover .drag-button {\n  transform: scale(1);\n}\n.drag-button {\n  transform: scale(0);\n  transition: transform 200ms ease-in-out;\n}\n.main-tab {\n  background-color: transparent !important;\n  padding: 0 !important;\n}\n.main-tab .ui-tab.is-active.fill {\n  @apply bg-accent text-white !important;\n}\n</style>\n"
  },
  {
    "path": "src/content/elementSelector/compsUi.js",
    "content": "import VAutofocus from '@/directives/VAutofocus';\nimport UiTab from '@/components/ui/UiTab.vue';\nimport UiTabs from '@/components/ui/UiTabs.vue';\nimport UiInput from '@/components/ui/UiInput.vue';\nimport UiButton from '@/components/ui/UiButton.vue';\nimport UiSelect from '@/components/ui/UiSelect.vue';\nimport UiExpand from '@/components/ui/UiExpand.vue';\nimport UiSwitch from '@/components/ui/UiSwitch.vue';\nimport UiTextarea from '@/components/ui/UiTextarea.vue';\nimport UiCheckbox from '@/components/ui/UiCheckbox.vue';\nimport UiTabPanel from '@/components/ui/UiTabPanel.vue';\nimport UiTabPanels from '@/components/ui/UiTabPanels.vue';\nimport TransitionExpand from '@/components/transitions/TransitionExpand.vue';\n\nexport default function (app) {\n  app.component('UiTab', UiTab);\n  app.component('UiTabs', UiTabs);\n  app.component('UiInput', UiInput);\n  app.component('UiButton', UiButton);\n  app.component('UiSelect', UiSelect);\n  app.component('UiSwitch', UiSwitch);\n  app.component('UiExpand', UiExpand);\n  app.component('UiTextarea', UiTextarea);\n  app.component('UiCheckbox', UiCheckbox);\n  app.component('UiTabPanel', UiTabPanel);\n  app.component('UiTabPanels', UiTabPanels);\n  app.component('TransitionExpand', TransitionExpand);\n\n  app.directive('autofocus', VAutofocus);\n}\n"
  },
  {
    "path": "src/content/elementSelector/generateElementsSelector.js",
    "content": "import findSelector from '@/lib/findSelector';\nimport { generateXPath } from '../utils';\n\nexport default function ({\n  list,\n  target,\n  selectorType,\n  frameElement,\n  hoveredElements,\n  selectorSettings,\n}) {\n  let selector = '';\n\n  const selectorOptions = selectorSettings || {};\n  const [selectedElement] = hoveredElements;\n  const finderOptions = { ...selectorOptions };\n  let documentCtx = document;\n\n  if (frameElement) {\n    documentCtx = frameElement.contentDocument.body;\n    finderOptions.root = documentCtx;\n  }\n\n  if (list) {\n    const isInList = target.closest('[automa-el-list]');\n\n    if (isInList) {\n      const childSelector = findSelector(target, {\n        root: isInList,\n        ...selectorOptions,\n        idName: () => false,\n      });\n      const listSelector = isInList.getAttribute('automa-el-list');\n\n      selector = `${listSelector} ${childSelector}`;\n    } else {\n      const parentSelector = findSelector(\n        selectedElement.parentElement,\n        finderOptions\n      );\n      selector = `${parentSelector} > ${selectedElement.tagName.toLowerCase()}`;\n\n      const prevSelectedList = documentCtx.querySelectorAll('[automa-el-list]');\n      prevSelectedList.forEach((el) => {\n        el.removeAttribute('automa-el-list');\n      });\n\n      hoveredElements.forEach((el) => {\n        el.setAttribute('automa-el-list', selector);\n      });\n    }\n  } else {\n    selector =\n      selectorType === 'css'\n        ? findSelector(selectedElement, finderOptions)\n        : generateXPath(selectedElement);\n  }\n\n  return selector;\n}\n"
  },
  {
    "path": "src/content/elementSelector/getSelectorOptions.js",
    "content": "export default function ({ idName, tagName, className, attr, attrNames }) {\n  const attrs = attr\n    ? attrNames.split(',').map((item) => item.trim())\n    : ['data-testid'];\n\n  return {\n    idName: () => idName ?? true,\n    tagName: () => tagName ?? true,\n    className: () => className ?? true,\n    attr: (name) => attrs.includes(name),\n  };\n}\n"
  },
  {
    "path": "src/content/elementSelector/icons.js",
    "content": "import {\n  riEyeLine,\n  riCheckLine,\n  riCloseLine,\n  riEyeOffLine,\n  riFileCopyLine,\n  riDragMoveLine,\n  riSettings3Line,\n  riListUnordered,\n  riArrowLeftLine,\n  riArrowLeftSLine,\n  riInformationLine,\n  riArrowDropDownLine,\n} from 'v-remixicon/icons';\n\nexport default {\n  riEyeLine,\n  riCheckLine,\n  riCloseLine,\n  riEyeOffLine,\n  riFileCopyLine,\n  riDragMoveLine,\n  riSettings3Line,\n  riListUnordered,\n  riArrowLeftLine,\n  riArrowLeftSLine,\n  riInformationLine,\n  riArrowDropDownLine,\n};\n"
  },
  {
    "path": "src/content/elementSelector/index.js",
    "content": "import { elementSelectorInstance } from '../utils';\nimport initElementSelector from './main';\nimport injectAppStyles from '../injectAppStyles';\nimport selectorFrameContext from './selectorFrameContext';\n\n(async function () {\n  try {\n    const isMainFrame = window.self === window.top;\n\n    if (isMainFrame) {\n      const isAppExists = elementSelectorInstance();\n      if (isAppExists) return;\n\n      const rootElement = document.createElement('div');\n      rootElement.setAttribute('id', 'app-container');\n      rootElement.classList.add('automa-element-selector');\n      rootElement.attachShadow({ mode: 'open' });\n\n      initElementSelector(rootElement);\n      await injectAppStyles(rootElement.shadowRoot);\n\n      document.documentElement.appendChild(rootElement);\n    } else {\n      const style = document.createElement('style');\n      style.textContent = '[automa-el-list] {outline: 2px dashed #6366f1;}';\n\n      document.body.appendChild(style);\n\n      selectorFrameContext();\n    }\n  } catch (error) {\n    console.error(error);\n  }\n})();\n"
  },
  {
    "path": "src/content/elementSelector/listSelector.js",
    "content": "import findSelector from '@/lib/findSelector';\n\n/* eslint-disable  no-cond-assign */\nexport function getAllSiblings(el, selector) {\n  const siblings = [el];\n\n  const validateElement = (element) => {\n    const isValidSelector = selector ? element.querySelector(selector) : true;\n    const isSameTag = el.tagName === element.tagName;\n\n    return isValidSelector && isSameTag;\n  };\n\n  let nextSibling = el;\n  let prevSibling = el;\n  let elementIndex = 1;\n\n  while ((prevSibling = prevSibling?.previousElementSibling)) {\n    if (validateElement(prevSibling)) {\n      elementIndex += 1;\n\n      siblings.unshift(prevSibling);\n    }\n  }\n  while ((nextSibling = nextSibling?.nextElementSibling)) {\n    if (validateElement(nextSibling)) {\n      siblings.push(nextSibling);\n    }\n  }\n\n  return {\n    elements: siblings,\n    index: elementIndex,\n  };\n}\n\nexport function getCssPath(el, root = document.body) {\n  if (!el) return null;\n\n  const path = [];\n\n  while (el.nodeType === Node.ELEMENT_NODE && !el.isSameNode(root)) {\n    let selector = el.nodeName.toLowerCase();\n\n    if (el.id) {\n      selector += `#${el.id}`;\n\n      path.unshift(selector);\n    } else {\n      let nth = 1;\n      let sib = el;\n\n      while ((sib = sib.previousElementSibling)) {\n        if (sib.nodeName.toLowerCase() === selector) nth += 1;\n      }\n\n      if (nth !== 1) selector += `:nth-of-type(${nth})`;\n\n      path.unshift(selector);\n    }\n\n    el = el.parentNode;\n  }\n\n  return path.join(' > ');\n}\n\nexport function getElementList(el, maxDepth = 50, paths = []) {\n  if (maxDepth === 0 || !el || el.tagName === 'BODY') return null;\n\n  let selector = el.tagName.toLowerCase();\n  const { elements, index } = getAllSiblings(el, paths.join(' > '));\n  let siblings = elements;\n\n  if (index !== 1) selector += `:nth-of-type(${index})`;\n\n  paths.unshift(selector);\n\n  if (siblings.length === 1) {\n    siblings = getElementList(el.parentElement, maxDepth - 1, paths);\n  }\n\n  return siblings;\n}\n\nexport default function (\n  target,\n  { frameElement, onlyInList, selectorSettings } = {}\n) {\n  if (!target) return [];\n\n  const automaListEl = target.closest('[automa-el-list]');\n  let documentCtx = document;\n\n  if (frameElement) {\n    documentCtx = frameElement.contentDocument;\n  }\n\n  if (automaListEl) {\n    if (target.hasAttribute('automa-el-list')) return [];\n\n    const childSelector = findSelector(target, {\n      root: automaListEl,\n      ...(selectorSettings || {}),\n      idName: () => false,\n    });\n    const elements = documentCtx.querySelectorAll(\n      `[automa-el-list] ${childSelector}`\n    );\n\n    return Array.from(elements);\n  }\n\n  if (onlyInList) return [];\n\n  return getElementList(target) || [target];\n}\n"
  },
  {
    "path": "src/content/elementSelector/main.js",
    "content": "import { createApp } from 'vue';\nimport vRemixicon from 'v-remixicon';\nimport App from './App.vue';\nimport compsUi from './compsUi';\nimport icons from './icons';\nimport vueI18n from './vueI18n';\nimport '@/assets/css/tailwind.css';\n\nexport default function (rootElement) {\n  const appRoot = document.createElement('div');\n  appRoot.setAttribute('id', 'app');\n\n  rootElement.shadowRoot.appendChild(appRoot);\n\n  createApp(App)\n    .provide('rootElement', rootElement)\n    .use(vueI18n)\n    .use(vRemixicon, icons)\n    .use(compsUi)\n    .mount(appRoot);\n}\n"
  },
  {
    "path": "src/content/elementSelector/selectorFrameContext.js",
    "content": "import FindElement from '@/utils/FindElement';\nimport { getElementRect } from '../utils';\nimport findElementList from './listSelector';\nimport generateElementsSelector from './generateElementsSelector';\nimport getSelectorOptions from './getSelectorOptions';\n\nlet hoveredElements = [];\nlet prevSelectedElement = null;\n\nfunction getElementRectWithOffset(element, data) {\n  const withAttributes = data.withAttributes && data.click;\n  const elementRect = getElementRect(element, withAttributes);\n\n  elementRect.y += data.top;\n  elementRect.x += data.left;\n\n  return elementRect;\n}\nfunction getElementsRect(data) {\n  const [element] = document.elementsFromPoint(\n    data.clientX - data.left,\n    data.clientY - data.top\n  );\n  if ((!element || element === prevSelectedElement) && !data.click) return;\n\n  const payload = {\n    elements: [],\n    type: 'automa:iframe-element-rect',\n  };\n\n  if (data.click) {\n    if (hoveredElements.length === 0) return;\n\n    payload.click = true;\n\n    const [selectedElement] = hoveredElements;\n    const selector = generateElementsSelector({\n      hoveredElements,\n      list: data.list,\n      target: selectedElement,\n      selectorType: data.selectorType,\n      selectorSettings: getSelectorOptions(data.selectorSettings || {}),\n    });\n\n    payload.selector = selector;\n    payload.elements = hoveredElements.map((el) =>\n      getElementRectWithOffset(el, data)\n    );\n  } else {\n    prevSelectedElement = element;\n    let elementsRect = [];\n\n    if (data.list) {\n      const elements =\n        findElementList(element, {\n          onlyInList: data.onlyInList,\n        }) || [];\n\n      hoveredElements = elements;\n      elementsRect = elements.map((el) => getElementRectWithOffset(el, data));\n    } else {\n      hoveredElements = [element];\n      elementsRect = [getElementRectWithOffset(element, data)];\n    }\n\n    payload.elements = elementsRect;\n  }\n\n  window.top.postMessage(payload, '*');\n}\nfunction resetElementSelector(data) {\n  const prevSelectedList = document.querySelectorAll('[automa-el-list]');\n  prevSelectedList.forEach((el) => {\n    el.removeAttribute('automa-el-list');\n  });\n\n  if (data.clearCache) {\n    hoveredElements = [];\n    prevSelectedElement = null;\n  }\n}\nfunction findElement({ selector, selectorType, frameRect }) {\n  const payload = {\n    elements: [],\n    type: 'automa:selected-elements',\n  };\n\n  try {\n    const elements = FindElement[selectorType]({ multiple: true, selector });\n\n    payload.elements = Array.from(elements || []).map((el) =>\n      getElementRectWithOffset(el, {\n        withAttributes: true,\n        click: true,\n        ...frameRect,\n      })\n    );\n  } catch (error) {\n    console.error(error);\n    payload.elements = [];\n  }\n\n  window.top.postMessage(payload, '*');\n}\nfunction onMessage({ data }) {\n  switch (data.type) {\n    case 'automa:get-element-rect':\n      getElementsRect(data);\n      break;\n    case 'automa:reset-element-selector':\n      resetElementSelector(data);\n      break;\n    case 'automa:find-element':\n      findElement(data);\n      break;\n    default:\n  }\n}\n\nexport default function () {\n  window.addEventListener('message', onMessage);\n}\n"
  },
  {
    "path": "src/content/elementSelector/vueI18n.js",
    "content": "import { createI18n } from 'vue-i18n/dist/vue-i18n.esm-bundler';\nimport enCommon from '@/locales/en/common.json';\nimport enBlocks from '@/locales/en/blocks.json';\n\nconst i18n = createI18n({\n  locale: 'en',\n  legacy: false,\n});\n\ni18n.global.mergeLocaleMessage('en', enCommon);\ni18n.global.mergeLocaleMessage('en', enBlocks);\n\nexport default i18n;\n"
  },
  {
    "path": "src/content/handleSelector.js",
    "content": "import FindElement from '@/utils/FindElement';\nimport { visibleInViewport, isXPath } from '@/utils/helper';\n\n/* eslint-disable consistent-return */\n\nexport function markElement(el, { id, data }) {\n  if (data.markEl) {\n    el.setAttribute(`block--${id}`, '');\n  }\n}\n\nexport function getDocumentCtx(frameSelector) {\n  if (!frameSelector) return document;\n\n  let documentCtx = document;\n\n  const iframeSelectors = frameSelector.split('|>');\n  const type = isXPath(frameSelector) ? 'xpath' : 'cssSelector';\n  iframeSelectors.forEach((selector) => {\n    if (!documentCtx) return;\n\n    const element = FindElement[type]({ selector }, documentCtx);\n    documentCtx = element?.contentDocument;\n  });\n\n  return documentCtx;\n}\n\nexport function queryElements(data, documentCtx = document) {\n  return new Promise((resolve) => {\n    let timeout = null;\n    let isTimeout = false;\n\n    const findSelector = () => {\n      if (isTimeout) return;\n\n      const selectorType = data.findBy || 'cssSelector';\n      const elements = FindElement[selectorType](data, documentCtx);\n      const isElNotFound = !elements || elements.length === 0;\n\n      if (isElNotFound && data.waitForSelector) {\n        setTimeout(findSelector, 200);\n      } else {\n        if (timeout) clearTimeout(timeout);\n        resolve(elements);\n      }\n    };\n\n    findSelector();\n\n    if (data.waitForSelector) {\n      timeout = setTimeout(() => {\n        isTimeout = true;\n        resolve(null);\n      }, data.waitSelectorTimeout);\n    }\n  });\n}\n\nexport default async function (\n  { data, id, frameSelector, debugMode },\n  { onSelected, onError, onSuccess, withDocument } = {}\n) {\n  if (!data || !data.selector) {\n    if (onError) onError(new Error('selector-empty'));\n    return null;\n  }\n\n  const documentCtx = getDocumentCtx(frameSelector);\n\n  if (!documentCtx) {\n    if (onError) onError(new Error('iframe-not-found'));\n\n    return null;\n  }\n\n  try {\n    data.blockIdAttr = `block--${id}`;\n\n    const elements = await queryElements(data, documentCtx);\n\n    if (!elements || elements.length === 0) {\n      if (onError) onError(new Error('element-not-found'));\n\n      return null;\n    }\n\n    const elementsArr = data.multiple ? Array.from(elements) : [elements];\n\n    await Promise.allSettled(\n      elementsArr.map(async (el) => {\n        markElement(el, { id, data });\n\n        if (debugMode) {\n          const isInViewport = visibleInViewport(el);\n          if (!isInViewport) el.scrollIntoView();\n        }\n\n        if (onSelected) await onSelected(el);\n      })\n    );\n\n    if (onSuccess) onSuccess();\n    if (withDocument) {\n      return {\n        elements,\n        document: documentCtx,\n      };\n    }\n\n    return elements;\n  } catch (error) {\n    if (onError) onError(error);\n\n    throw error;\n  }\n}\n"
  },
  {
    "path": "src/content/index.js",
    "content": "import findSelector from '@/lib/findSelector';\nimport { isXPath, toCamelCase } from '@/utils/helper';\nimport { sendMessage } from '@/utils/message';\nimport automa from '@business';\nimport cloneDeep from 'lodash.clonedeep';\nimport { nanoid } from 'nanoid';\nimport browser from 'webextension-polyfill';\nimport blocksHandler from './blocksHandler';\nimport initCommandPalette from './commandPalette';\nimport handleSelector, {\n  getDocumentCtx,\n  queryElements,\n} from './handleSelector';\nimport shortcutListener from './services/shortcutListener';\nimport showExecutedBlock from './showExecutedBlock';\n// import elementObserver from './elementObserver';\nimport { elementSelectorInstance } from './utils';\n\nconst isMainFrame = window.self === window.top;\n\nfunction messageToFrame(frameElement, blockData) {\n  return new Promise((resolve, reject) => {\n    function onMessage({ data }) {\n      if (data.type !== 'automa:block-execute-result') return;\n\n      if (data.result?.$isError) {\n        const error = new Error(data.result.message);\n        error.data = data.result.data;\n\n        reject(error);\n      } else {\n        resolve(data.result);\n      }\n\n      window.removeEventListener('message', onMessage);\n    }\n    window.addEventListener('message', onMessage);\n\n    const messageId = `message:${nanoid(4)}`;\n    browser.storage.local.set({ [messageId]: true }).then(() => {\n      frameElement.contentWindow.postMessage(\n        {\n          messageId,\n          type: 'automa:execute-block',\n          blockData: { ...blockData, frameSelector: '' },\n        },\n        '*'\n      );\n    });\n  });\n}\nasync function executeBlock(data) {\n  const removeExecutedBlock = showExecutedBlock(data, data.executedBlockOnWeb);\n  if (data.data?.selector?.includes('|>')) {\n    const selectorsArr = data.data.selector.split('|>');\n    const selector = selectorsArr.pop();\n    const frameSelector = selectorsArr.join('|>');\n\n    const frameElSelector = selectorsArr.pop();\n\n    let findBy = data?.data?.findBy;\n    if (!findBy) {\n      findBy = isXPath(frameSelector) ? 'xpath' : 'cssSelector';\n    }\n\n    const documentCtx = getDocumentCtx(selectorsArr.join('|>'));\n    const frameElement = await queryElements(\n      {\n        findBy,\n        multiple: false,\n        waitForSelector: 5000,\n        selector: frameElSelector,\n      },\n      documentCtx\n    );\n    const frameError = (message) => {\n      const error = new Error(message);\n      error.data = { selector: frameSelector };\n\n      return error;\n    };\n\n    if (!frameElement) throw frameError('iframe-not-found');\n\n    const isFrameElement = ['IFRAME', 'FRAME'].includes(frameElement.tagName);\n    if (!isFrameElement) throw frameError('not-iframe');\n\n    const { x, y } = frameElement.getBoundingClientRect();\n    const iframeDetails = { x, y };\n\n    if (isMainFrame) {\n      iframeDetails.windowWidth = window.innerWidth;\n      iframeDetails.windowHeight = window.innerHeight;\n    }\n\n    data.data.selector = selector;\n    data.data.$frameRect = iframeDetails;\n    data.data.$frameSelector = frameSelector;\n\n    if (frameElement.contentDocument) {\n      data.frameSelector = frameSelector;\n    } else {\n      const result = await messageToFrame(frameElement, data);\n      return result;\n    }\n  }\n  const handlers = blocksHandler();\n  const handler = handlers[toCamelCase(data.name || data.label)];\n  if (handler) {\n    const result = await handler(data, { handleSelector });\n    removeExecutedBlock();\n\n    return result;\n  }\n\n  const error = new Error(`\"${data.label}\" doesn't have a handler`);\n  console.error(error);\n\n  throw error;\n}\nasync function messageListener({ data, source }) {\n  try {\n    if (data.type === 'automa:get-frame' && isMainFrame) {\n      let frameRect = { x: 0, y: 0 };\n\n      document.querySelectorAll('iframe').forEach((iframe) => {\n        if (iframe.contentWindow !== source) return;\n\n        frameRect = iframe.getBoundingClientRect();\n      });\n\n      source.postMessage(\n        {\n          frameRect,\n          type: 'automa:the-frame-rect',\n        },\n        '*'\n      );\n\n      return;\n    }\n\n    if (data.type === 'automa:execute-block') {\n      const messageToken = await browser.storage.local.get(data.messageId);\n      if (!data.messageId || !messageToken[data.messageId]) {\n        window.top.postMessage(\n          {\n            result: {\n              $isError: true,\n              message: 'Block id is empty',\n              data: {},\n            },\n            type: 'automa:block-execute-result',\n          },\n          '*'\n        );\n        return;\n      }\n\n      await browser.storage.local.remove(data.messageId);\n\n      executeBlock(data.blockData)\n        .then((result) => {\n          window.top.postMessage(\n            {\n              result,\n              type: 'automa:block-execute-result',\n            },\n            '*'\n          );\n        })\n        .catch((error) => {\n          console.error(error);\n          window.top.postMessage(\n            {\n              result: {\n                $isError: true,\n                message: error.message,\n                data: error.data || {},\n              },\n              type: 'automa:block-execute-result',\n            },\n            '*'\n          );\n        });\n    }\n  } catch (error) {\n    console.error(error);\n  }\n}\n\n(() => {\n  if (window.isAutomaInjected) return;\n\n  initCommandPalette();\n\n  let contextElement = null;\n  let $ctxLink = '';\n  let $ctxMediaUrl = '';\n  let $ctxTextSelection = '';\n\n  window.isAutomaInjected = true;\n  window.addEventListener('message', messageListener);\n  window.addEventListener(\n    'contextmenu',\n    ({ target }) => {\n      contextElement = target;\n      $ctxTextSelection = window.getSelection().toString();\n\n      const tag = target.tagName;\n      if (tag === 'A') {\n        $ctxLink = target.href;\n      } else {\n        const closestUrl = target.closest('a');\n        if (closestUrl) $ctxLink = closestUrl.href;\n      }\n\n      const getMediaSrc = (element) => {\n        let mediaSrc = element.src || '';\n\n        if (!mediaSrc.src) {\n          const sourceEl = element.querySelector('source');\n          if (sourceEl) mediaSrc = sourceEl.src;\n        }\n\n        return mediaSrc;\n      };\n\n      const mediaTags = ['AUDIO', 'VIDEO', 'IMG'];\n      if (mediaTags.includes(tag)) {\n        $ctxMediaUrl = getMediaSrc(target);\n      } else {\n        const closestMedia = target.closest('audio,video,img');\n        if (closestMedia) $ctxMediaUrl = getMediaSrc(closestMedia);\n      }\n    },\n    true\n  );\n\n  window.isAutomaInjected = true;\n  window.addEventListener('message', messageListener);\n  window.addEventListener('contextmenu', ({ target }) => {\n    contextElement = target;\n    $ctxTextSelection = window.getSelection().toString();\n  });\n\n  if (isMainFrame) {\n    shortcutListener();\n    // window.addEventListener('load', elementObserver);\n  }\n\n  automa('content');\n\n  browser.runtime.onMessage.addListener(async (data) => {\n    const asyncExecuteBlock = async (block) => {\n      try {\n        const res = await executeBlock(block);\n        return res;\n      } catch (error) {\n        console.error(error);\n        const elNotFound = error.message === 'element-not-found';\n        const isLoopItem = data.data?.selector?.includes('automa-loop');\n\n        if (!elNotFound || !isLoopItem) return Promise.reject(error);\n\n        const findLoopEl = data.loopEls.find(({ url }) =>\n          window.location.href.includes(url)\n        );\n\n        const blockData = { ...data.data, ...findLoopEl, multiple: true };\n        const loopBlock = {\n          ...data,\n          onlyGenerate: true,\n          data: blockData,\n        };\n\n        await blocksHandler().loopData(loopBlock);\n        return executeBlock(block);\n      }\n    };\n\n    if (data.isBlock) {\n      const res = await asyncExecuteBlock(data);\n      return res;\n    }\n\n    switch (data.type) {\n      case 'input-workflow-params':\n        window.initPaletteParams?.(data.data);\n        return Boolean(window.initPaletteParams);\n      case 'content-script-exists':\n        return true;\n      case 'automa-element-selector': {\n        return elementSelectorInstance();\n      }\n      case 'context-element': {\n        let $ctxElSelector = '';\n\n        if (contextElement) {\n          $ctxElSelector = findSelector(contextElement);\n          contextElement = null;\n        }\n        if (!$ctxTextSelection) {\n          $ctxTextSelection = window.getSelection().toString();\n        }\n\n        const cloneContextData = cloneDeep({\n          $ctxLink,\n          $ctxMediaUrl,\n          $ctxElSelector,\n          $ctxTextSelection,\n        });\n\n        $ctxLink = '';\n        $ctxMediaUrl = '';\n        $ctxElSelector = '';\n        $ctxTextSelection = '';\n\n        return cloneContextData;\n      }\n      default:\n        return null;\n    }\n  });\n})();\n\nwindow.addEventListener('__automa-fetch__', (event) => {\n  const { id, resource, type } = event.detail;\n  const sendResponse = (payload) => {\n    window.dispatchEvent(\n      new CustomEvent(`__automa-fetch-response-${id}__`, {\n        detail: { id, ...payload },\n      })\n    );\n  };\n\n  sendMessage('fetch', { type, resource }, 'background')\n    .then((result) => {\n      sendResponse({ isError: false, result });\n    })\n    .catch((error) => {\n      sendResponse({ isError: true, result: error.message });\n    });\n});\n\nwindow.addEventListener('DOMContentLoaded', async () => {\n  const link = window.location.pathname;\n  const isAutomaWorkflow = /.+\\.automa\\.json$/.test(link);\n  if (!isAutomaWorkflow) return;\n\n  const accept = window.confirm(\n    'Do you want to add this workflow into Automa?'\n  );\n  if (!accept) return;\n  const workflow = JSON.parse(document.documentElement.innerText);\n\n  const { workflows: workflowsStorage } = await browser.storage.local.get(\n    'workflows'\n  );\n\n  const workflowId = nanoid();\n  const workflowData = {\n    ...workflow,\n    id: workflowId,\n    dataColumns: [],\n    createdAt: Date.now(),\n    table: workflow.table || workflow.dataColumns,\n  };\n\n  if (Array.isArray(workflowsStorage)) {\n    workflowsStorage.push(workflowData);\n  } else {\n    workflowsStorage[workflowId] = workflowData;\n  }\n\n  await browser.storage.local.set({ workflows: workflowsStorage });\n\n  alert('Workflow installed');\n});\n"
  },
  {
    "path": "src/content/injectAppStyles.js",
    "content": "import browser from 'webextension-polyfill';\n\nexport function generateStyleEl(css, classes = true) {\n  const style = document.createElement('style');\n  style.textContent = css;\n\n  if (classes) {\n    style.classList.add('automa-element-selector');\n  }\n\n  return style;\n}\n\nexport default async function (appRoot, customCss = '') {\n  try {\n    const response = await fetch(\n      browser.runtime.getURL('/elementSelector.css')\n    );\n    const mainCSS = await response.text();\n    const appStyleEl = generateStyleEl(mainCSS + customCss, false);\n    appRoot.appendChild(appStyleEl);\n\n    const fontStyleExists = document.head.querySelector(\n      '.automa-element-selector'\n    );\n\n    if (!fontStyleExists) {\n      const commonCSS =\n        '\\n.automa-element-selector { direction: ltr } \\n [automa-isDragging] { user-select: none } \\n [automa-el-list] {outline: 2px dashed #6366f1;}';\n\n      const fontURL = browser.runtime.getURL('/Inter-roman-latin.var.woff2');\n      const fontCSS = `@font-face { font-family: \"Inter var\"; font-weight: 100 900; font-display: swap; font-style: normal; font-named-instance: \"Regular\"; src: url(\"${fontURL}\") format(\"woff2\") }`;\n      const fontStyleEl = generateStyleEl(fontCSS + commonCSS);\n\n      document.head.appendChild(fontStyleEl);\n    }\n  } catch (error) {\n    console.error(error);\n  }\n}\n"
  },
  {
    "path": "src/content/services/recordWorkflow/App.vue",
    "content": "<template>\n  <div\n    ref=\"rootEl\"\n    class=\"content fixed top-0 left-0 overflow-hidden rounded-lg bg-white text-black shadow-xl\"\n    style=\"z-index: 99999999; font-size: 16px\"\n    :style=\"{\n      transform: `translate(${draggingState.xPos}px, ${draggingState.yPos}px)`,\n    }\"\n  >\n    <div\n      class=\"hoverable flex select-none items-center px-4 py-2 transition\"\n      :class=\"[draggingState.dragging ? 'cursor-grabbing' : 'cursor-grab']\"\n      @mouseup=\"toggleDragging(false, $event)\"\n      @mousedown=\"toggleDragging(true, $event)\"\n    >\n      <span\n        class=\"relative flex cursor-pointer items-center justify-center rounded-full bg-red-400\"\n        style=\"height: 24px; width: 24px\"\n        title=\"Stop recording\"\n        @click=\"stopRecording\"\n      >\n        <v-remixicon\n          name=\"riRecordCircleLine\"\n          class=\"relative z-10\"\n          size=\"20\"\n        />\n        <span\n          class=\"absolute animate-ping rounded-full bg-red-400\"\n          style=\"height: 80%; width: 80%; animation-duration: 1.3s\"\n        ></span>\n      </span>\n      <p class=\"ml-2 font-semibold\">Automa</p>\n      <div class=\"grow\"></div>\n      <v-remixicon name=\"mdiDragHorizontal\" />\n    </div>\n    <div class=\"p-4\">\n      <template v-if=\"selectState.status === 'idle'\">\n        <button\n          class=\"bg-input w-full rounded-lg px-4 py-2 transition\"\n          @click=\"startSelecting()\"\n        >\n          Select element\n        </button>\n        <button\n          class=\"bg-input mt-2 w-full rounded-lg px-4 py-2 transition\"\n          @click=\"startSelecting(true)\"\n        >\n          Select list element\n        </button>\n      </template>\n      <div v-else-if=\"selectState.status === 'selecting'\" class=\"leading-tight\">\n        <p v-if=\"selectState.selectedElements.length === 0\">\n          Select an element by clicking on it\n        </p>\n        <template v-else>\n          <template v-if=\"selectState.list && !selectState.listId\">\n            <label for=\"list-id\" class=\"ml-1\" style=\"font-size: 14px\">\n              Element list id\n            </label>\n            <input\n              id=\"list-id\"\n              v-model=\"tempListId\"\n              placeholder=\"listId\"\n              class=\"bg-input w-full rounded-lg px-4 py-2\"\n              @keyup.enter=\"saveElementListId\"\n            />\n            <button\n              :class=\"{ 'opacity-75 pointer-events-none': !tempListId }\"\n              class=\"mt-2 w-full rounded-lg bg-accent px-4 py-2 text-white\"\n              @click=\"saveElementListId\"\n            >\n              Save\n            </button>\n          </template>\n          <template v-else>\n            <div class=\"flex w-full items-center space-x-2\">\n              <input\n                :value=\"selectState.childSelector || selectState.parentSelector\"\n                class=\"bg-input w-full rounded-lg px-4 py-2\"\n                readonly\n              />\n              <template\n                v-if=\"\n                  !selectState.list && !selectState.childSelector.includes('|>')\n                \"\n              >\n                <button @click=\"selectElementPath('up')\">\n                  <v-remixicon name=\"riArrowLeftLine\" rotate=\"90\" />\n                </button>\n                <button @click=\"selectElementPath('down')\">\n                  <v-remixicon name=\"riArrowLeftLine\" rotate=\"-90\" />\n                </button>\n              </template>\n            </div>\n            <select\n              v-model=\"addBlockState.activeBlock\"\n              class=\"bg-input mt-2 w-full rounded-lg px-4 py-2\"\n            >\n              <option value=\"\" disabled selected>Select what to do</option>\n              <option\n                v-for=\"block in addBlockState.blocks\"\n                :key=\"block\"\n                :value=\"block\"\n              >\n                {{ tasks[block].name }}\n              </option>\n            </select>\n            <template\n              v-if=\"\n                ['get-text', 'attribute-value'].includes(\n                  addBlockState.activeBlock\n                )\n              \"\n            >\n              <select\n                v-if=\"addBlockState.activeBlock === 'attribute-value'\"\n                v-model=\"addBlockState.activeAttr\"\n                class=\"bg-input mt-2 block w-full rounded-lg px-4 py-2\"\n              >\n                <option value=\"\" selected disabled>Select attribute</option>\n                <option\n                  v-for=\"(value, name) in addBlockState.attributes\"\n                  :key=\"name\"\n                  :value=\"name\"\n                >\n                  {{ name }}({{ value.slice(0, 64) }})\n                </option>\n              </select>\n              <label\n                for=\"variable-name\"\n                class=\"ml-2 mt-2 text-sm text-gray-600\"\n              >\n                Assign to variable\n              </label>\n              <input\n                id=\"variable-name\"\n                v-model=\"addBlockState.varName\"\n                placeholder=\"Variable name\"\n                class=\"bg-input w-full rounded-lg px-4 py-2\"\n              />\n              <label\n                for=\"select-column\"\n                class=\"ml-2 mt-2 text-sm text-gray-600\"\n              >\n                Insert to table\n              </label>\n              <select\n                id=\"select-column\"\n                v-model=\"addBlockState.column\"\n                class=\"bg-input block w-full rounded-lg px-4 py-2\"\n              >\n                <option value=\"\" selected>Select column [none]</option>\n                <option\n                  v-for=\"column in addBlockState.workflowColumns\"\n                  :key=\"column.id\"\n                  :value=\"column.id\"\n                >\n                  {{ column.name }}\n                </option>\n              </select>\n            </template>\n            <button\n              v-if=\"addBlockState.activeBlock\"\n              :class=\"{\n                'pointer-events-none opacity-75':\n                  addBlockState.activeBlock === 'attribute-value' &&\n                  !addBlockState.activeAttr,\n              }\"\n              class=\"mt-4 block w-full rounded-lg bg-accent px-4 py-2 text-white\"\n              @click=\"addFlowItem\"\n            >\n              Save\n            </button>\n          </template>\n        </template>\n        <p class=\"mt-4\" style=\"font-size: 14px\">\n          Press <kbd class=\"bg-box-transparent rounded-md p-1\">Esc</kbd> to\n          cancel\n        </p>\n      </div>\n    </div>\n  </div>\n  <shared-element-selector\n    v-if=\"selectState.isSelecting\"\n    :selected-els=\"selectState.selectedElements\"\n    with-attributes\n    only-in-list\n    :list=\"selectState.list\"\n    :pause=\"\n      selectState.selectedElements.length > 0 &&\n      selectState.list &&\n      !selectState.listId\n    \"\n    @selected=\"onElementsSelected\"\n  />\n</template>\n<script setup>\nimport { ref, reactive, watch, onMounted, onBeforeUnmount } from 'vue';\nimport browser from 'webextension-polyfill';\nimport { toCamelCase } from '@/utils/helper';\nimport { tasks } from '@/utils/shared';\nimport findSelector from '@/lib/findSelector';\nimport SharedElementSelector from '@/components/content/shared/SharedElementSelector.vue';\nimport { getElementRect } from '../../utils';\nimport addBlock from './addBlock';\n\nconst mouseRelativePos = { x: 0, y: 0 };\nconst elementsPath = {\n  path: [],\n  cache: new WeakMap(),\n};\n\nconst rootEl = ref(null);\nconst tempListId = ref('');\n\nconst selectState = reactive({\n  listId: '',\n  list: false,\n  pathIndex: 0,\n  status: 'idle',\n  isInList: false,\n  listSelector: '',\n  childSelector: '',\n  isSelecting: false,\n  selectedElements: [],\n});\nconst draggingState = reactive({\n  yPos: 20,\n  dragging: false,\n  xPos: window.innerWidth - 300,\n});\nconst addBlockState = reactive({\n  blocks: [],\n  column: '',\n  varName: '',\n  attributes: [],\n  activeAttr: '',\n  activeBlock: '',\n  workflowColumns: [],\n});\n\nconst blocksList = {\n  IMG: ['save-assets', 'attribute-value'],\n  VIDEO: ['save-assets', 'attribute-value'],\n  AUDIO: ['save-assets', 'attribute-value'],\n  default: ['get-text', 'attribute-value'],\n};\n\nfunction stopRecording() {\n  browser.runtime.sendMessage({\n    type: 'background--recording:stop',\n  });\n}\nfunction getElementBlocks(element) {\n  if (!element) return;\n\n  const elTag = element.tagName;\n  const blocks = [...(blocksList[elTag] || blocksList.default)];\n  const attrBlockIndex = blocks.indexOf('attribute-value');\n\n  if (attrBlockIndex !== -1) {\n    addBlockState.attributes = element.attributes;\n  }\n\n  addBlockState.blocks = blocks;\n}\nfunction onElementsSelected({ selector, elements, path }) {\n  if (path) {\n    elementsPath.path = path;\n    selectState.pathIndex = 0;\n  }\n\n  getElementBlocks(elements[0]);\n  selectState.selectedElements = elements;\n\n  if (selectState.list) {\n    if (!selectState.listSelector) {\n      selectState.isInList = false;\n      selectState.listSelector = selector;\n      selectState.childSelector = selector;\n      return;\n    }\n\n    selectState.isInList = true;\n    selector = selector.replace(selectState.listSelector, '');\n  }\n\n  selectState.childSelector = selector;\n}\nfunction addFlowItem() {\n  const saveData = Boolean(addBlockState.column);\n  const assignVariable = Boolean(addBlockState.varName);\n  const block = {\n    id: addBlockState.activeBlock,\n    data: {\n      saveData,\n      assignVariable,\n      waitForSelector: true,\n      dataColumn: addBlockState.column,\n      variableName: addBlockState.varName,\n      selector: selectState.list\n        ? selectState.listSelector\n        : selectState.childSelector,\n    },\n  };\n\n  if (selectState.list) {\n    if (selectState.isInList || selectState.listId) {\n      const childSelector = selectState.isInList\n        ? selectState.childSelector\n        : '';\n      block.data.selector = `{{loopData@${selectState.listId}}} ${childSelector}`;\n    } else {\n      block.data.multiple = true;\n    }\n  }\n\n  if (addBlockState.activeBlock === 'attribute-value') {\n    block.data.attributeName = addBlockState.activeAttr;\n  }\n\n  addBlock(block).then(() => {\n    addBlockState.column = '';\n    addBlockState.varName = '';\n    addBlockState.activeAttr = '';\n  });\n}\nfunction selectElementPath(type) {\n  let pathIndex =\n    type === 'up' ? selectState.pathIndex + 1 : selectState.pathIndex - 1;\n  let element = elementsPath.path[pathIndex];\n\n  if ((type === 'up' && !element) || element?.tagName === 'BODY') return;\n\n  if (type === 'down' && !element) {\n    const previousElement = elementsPath.path[selectState.pathIndex];\n    const childEl = Array.from(previousElement.children).find(\n      (el) => !['STYLE', 'SCRIPT'].includes(el.tagName)\n    );\n\n    if (!childEl) return;\n\n    element = childEl;\n    elementsPath.path.unshift(childEl);\n    pathIndex = 0;\n  }\n\n  selectState.pathIndex = pathIndex;\n  selectState.selectedElements = [getElementRect(element)];\n  selectState.childSelector = elementsPath.cache.has(element)\n    ? elementsPath.cache.get(element)\n    : findSelector(element);\n}\nfunction clearSelectState() {\n  if (selectState.list && selectState.listId) {\n    addBlock({\n      id: 'loop-breakpoint',\n      description: selectState.listId,\n      data: {\n        loopId: selectState.listId,\n      },\n    });\n  }\n\n  selectState.listId = '';\n  selectState.list = false;\n  selectState.status = 'idle';\n  selectState.listSelector = '';\n  selectState.childSelector = '';\n  selectState.parentSelector = '';\n  selectState.isSelecting = false;\n  selectState.selectedElements = [];\n\n  const selectedList = document.querySelectorAll('[automa-el-list]');\n  selectedList.forEach((element) => {\n    element.removeAttribute('automa-el-list');\n  });\n\n  const frameElements = document.querySelectorAll('iframe, frame');\n  frameElements.forEach((element) => {\n    element.contentWindow.postMessage(\n      {\n        type: 'automa:reset-element-selector',\n      },\n      '*'\n    );\n  });\n\n  document.body.removeAttribute('automa-selecting');\n}\nfunction saveElementListId() {\n  if (!tempListId.value) return;\n\n  selectState.listId = toCamelCase(tempListId.value);\n  tempListId.value = '';\n\n  addBlock({\n    id: 'loop-data',\n    description: selectState.listId,\n    data: {\n      loopThrough: 'elements',\n      loopId: selectState.listId,\n      elementSelector: selectState.listSelector,\n    },\n  });\n}\nfunction toggleDragging(value, event) {\n  if (value) {\n    const bounds = rootEl.value.getBoundingClientRect();\n    const y = event.clientY - bounds.top;\n    const x = event.clientX - bounds.left;\n\n    mouseRelativePos.x = x;\n    mouseRelativePos.y = y;\n  } else {\n    mouseRelativePos.x = 0;\n    mouseRelativePos.y = 0;\n  }\n\n  draggingState.dragging = value;\n}\nfunction onKeyup({ key }) {\n  if (key !== 'Escape') return;\n\n  clearSelectState();\n\n  window.removeEventListener('keyup', onKeyup);\n}\nfunction startSelecting(list = false) {\n  selectState.list = list;\n  selectState.isSelecting = true;\n  selectState.status = 'selecting';\n\n  document.body.setAttribute('automa-selecting', '');\n\n  window.addEventListener('keyup', onKeyup);\n}\nfunction onMousemove({ clientX, clientY }) {\n  if (!draggingState.dragging) return;\n\n  draggingState.xPos = clientX - mouseRelativePos.x;\n  draggingState.yPos = clientY - mouseRelativePos.y;\n}\nfunction attachListeners() {\n  window.addEventListener('mousemove', onMousemove);\n}\nfunction detachListeners() {\n  window.removeEventListener('keyup', onKeyup);\n  window.removeEventListener('mousemove', onMousemove);\n}\n\nwatch(\n  () => selectState.selectedElements,\n  () => {\n    addBlockState.column = '';\n    addBlockState.varName = '';\n    addBlockState.activeBlock = '';\n  }\n);\n\nonMounted(() => {\n  attachListeners();\n\n  browser.storage.local\n    .get(['recording', 'workflows'])\n    .then(({ recording, workflows }) => {\n      const workflow = Object.values(workflows).find(\n        ({ id }) => recording.workflowId === id\n      );\n\n      addBlockState.workflowColumns = workflow?.table || [];\n    });\n});\nonBeforeUnmount(detachListeners);\n</script>\n"
  },
  {
    "path": "src/content/services/recordWorkflow/addBlock.js",
    "content": "import browser from 'webextension-polyfill';\n\nexport default async function (detail, save = true) {\n  const { isRecording, recording } = await browser.storage.local.get([\n    'isRecording',\n    'recording',\n  ]);\n\n  if (!isRecording || !recording) return null;\n\n  let addedBlock = detail;\n\n  if (typeof detail === 'function') addedBlock = detail(recording);\n  else recording.flows.push(detail);\n\n  if (save) await browser.storage.local.set({ recording });\n\n  return { recording, addedBlock };\n}\n"
  },
  {
    "path": "src/content/services/recordWorkflow/icons.js",
    "content": "import { riRecordCircleLine, riArrowLeftLine } from 'v-remixicon/icons';\n\nexport default {\n  riRecordCircleLine,\n  riArrowLeftLine,\n  mdiDragHorizontal:\n    'M3,15V13H5V15H3M3,11V9H5V11H3M7,15V13H9V15H7M7,11V9H9V11H7M11,15V13H13V15H11M11,11V9H13V11H11M15,15V13H17V15H15M15,11V9H17V11H15M19,15V13H21V15H19M19,11V9H21V11H19Z',\n};\n"
  },
  {
    "path": "src/content/services/recordWorkflow/index.js",
    "content": "import browser from 'webextension-polyfill';\nimport initElementSelector from './main';\nimport initRecordEvents from './recordEvents';\nimport selectorFrameContext from '../../elementSelector/selectorFrameContext';\n\n(async () => {\n  try {\n    let elementSelectorInstance = null;\n    const isMainFrame = window.self === window.top;\n    const destroyRecordEvents = await initRecordEvents(isMainFrame);\n\n    if (isMainFrame) {\n      const element = document.querySelector('#automa-recording');\n      if (element) return;\n\n      elementSelectorInstance = await initElementSelector();\n    } else {\n      const style = document.createElement('style');\n      style.textContent = '[automa-el-list] {outline: 2px dashed #6366f1;}';\n\n      document.body.appendChild(style);\n\n      selectorFrameContext();\n    }\n\n    browser.runtime.onMessage.addListener(function messageListener({ type }) {\n      if (type === 'recording:stop') {\n        if (elementSelectorInstance) {\n          elementSelectorInstance.unmount();\n        }\n\n        destroyRecordEvents();\n        browser.runtime.onMessage.removeListener(messageListener);\n      }\n    });\n  } catch (error) {\n    console.error(error);\n  }\n})();\n"
  },
  {
    "path": "src/content/services/recordWorkflow/main.js",
    "content": "import { createApp } from 'vue';\nimport vRemixicon from 'v-remixicon';\nimport App from './App.vue';\nimport icons from './icons';\nimport injectAppStyles from '../../injectAppStyles';\n\nconst customCSS = `\n  #app {\n    font-family: 'Inter var';\n    line-height: 1.5;\n  }\n  .content {\n    width: 250px;\n  }\n`;\n\nexport default function () {\n  const rootElement = document.createElement('div');\n  rootElement.attachShadow({ mode: 'open' });\n  rootElement.setAttribute('id', 'automa-recording');\n  rootElement.classList.add('automa-element-selector');\n  document.body.appendChild(rootElement);\n\n  return injectAppStyles(rootElement.shadowRoot, customCSS).then(() => {\n    const appRoot = document.createElement('div');\n    appRoot.setAttribute('id', 'app');\n    rootElement.shadowRoot.appendChild(appRoot);\n\n    const app = createApp(App).use(vRemixicon, icons);\n    app.mount(appRoot);\n\n    return app;\n  });\n}\n"
  },
  {
    "path": "src/content/services/recordWorkflow/recordEvents.js",
    "content": "import { nanoid } from 'nanoid';\nimport browser from 'webextension-polyfill';\nimport { debounce } from '@/utils/helper';\nimport { recordPressedKey } from '@/utils/recordKeys';\nimport findSelector, { finder } from '@/lib/findSelector';\nimport addBlockToFlow from './addBlock';\n\nlet isMainFrame = true;\n\nconst isAutomaInstance = (target) =>\n  target.id === 'automa-recording' ||\n  document.body.hasAttribute('automa-selecting');\nconst isTextFieldEl = (el) => ['INPUT', 'TEXTAREA'].includes(el.tagName);\n\nasync function addBlock(detail) {\n  try {\n    const data = await addBlockToFlow(detail, isMainFrame);\n\n    if (!isMainFrame || !data || !data.addedBlock) {\n      let frameSelector = null;\n\n      if (window.frameElement) {\n        frameSelector = finder(window.frameElement, {\n          root: window.frameElement.ownerDocument,\n        });\n      }\n\n      window.top.postMessage(\n        {\n          frameSelector,\n          recording: data.recording,\n          type: 'automa:record-events',\n        },\n        '*'\n      );\n    }\n  } catch (error) {\n    console.error(error);\n  }\n}\n\nfunction onChange({ target }) {\n  if (isAutomaInstance(target)) return;\n\n  const isInputEl = target.tagName === 'INPUT';\n  const inputType = target.getAttribute('type');\n  const execludeInput = isInputEl && ['checkbox', 'radio'].includes(inputType);\n\n  if (execludeInput) return;\n\n  let block = null;\n  const selector = findSelector(target);\n  const isSelectEl = target.tagName === 'SELECT';\n  const elementName = target.ariaLabel || target.name;\n\n  if (isInputEl && inputType === 'file') {\n    block = {\n      id: 'upload-file',\n      description: elementName,\n      data: {\n        selector,\n        waitForSelector: true,\n        description: elementName,\n        filePaths: [target.value],\n      },\n    };\n  } else if (isSelectEl) {\n    block = {\n      id: 'forms',\n      data: {\n        selector,\n        delay: 100,\n        type: 'select',\n        clearValue: true,\n        value: target.value,\n        waitForSelector: true,\n        description: `Element Name (${elementName})`,\n      },\n    };\n  } else {\n    block = {\n      id: 'trigger-event',\n      data: {\n        selector,\n        eventName: 'change',\n        eventType: 'event',\n        waitForSelector: true,\n        eventParams: { bubbles: true },\n      },\n    };\n  }\n\n  addBlock((recording) => {\n    const lastFlow = recording.flows.at(-1);\n    if (\n      block.id === 'upload-file' &&\n      lastFlow &&\n      lastFlow.id === 'event-click'\n    ) {\n      recording.flows.pop();\n    }\n\n    if (\n      block.data.type === 'text-field' &&\n      block.data.selector === lastFlow?.data?.selector\n    )\n      return null;\n\n    recording.flows.push(block);\n\n    return block;\n  });\n}\nasync function onKeydown(event) {\n  if (isAutomaInstance(event.target) || event.repeat) return;\n\n  const isTextField = isTextFieldEl(event.target);\n  const enterKey = event.key === 'Enter';\n  let isSubmitting = false;\n\n  if (isTextField) {\n    const inputInForm = event.target.form && event.target.tagName === 'INPUT';\n    if (enterKey && inputInForm) {\n      event.preventDefault();\n\n      await addBlock({\n        id: 'forms',\n        data: {\n          delay: 100,\n          clearValue: true,\n          type: 'text-field',\n          waitForSelector: true,\n          value: event.target.value,\n          selector: findSelector(event.target),\n        },\n      });\n\n      isSubmitting = true;\n    } else {\n      return;\n    }\n  }\n\n  recordPressedKey(event, (keysArr) => {\n    const selector = isTextField && enterKey ? findSelector(event.target) : '';\n    const keys = keysArr.join('+');\n\n    addBlock((recording) => {\n      const block = {\n        id: 'press-key',\n        description: `Press: ${keys}`,\n        data: {\n          keys,\n          selector,\n        },\n      };\n\n      const lastFlow = recording.flows.at(-1);\n      if (lastFlow && lastFlow.id === 'press-key') {\n        if (!lastFlow.groupId) lastFlow.groupId = nanoid();\n        block.groupId = lastFlow.groupId;\n      }\n\n      recording.flows.push(block);\n\n      if (isSubmitting) {\n        setTimeout(() => {\n          event.target.form.submit();\n        }, 500);\n      }\n\n      return block;\n    });\n  });\n}\nfunction onClick(event) {\n  const { target } = event;\n  if (isAutomaInstance(target)) return;\n\n  const isTextField =\n    (target.tagName === 'INPUT' && target.getAttribute('type') === 'text') ||\n    ['SELECT', 'TEXTAREA'].includes(target.tagName);\n\n  if (isTextField) return;\n\n  let isClickLink = false;\n  const selector = findSelector(target);\n\n  if (target.tagName === 'A') {\n    if (event.ctrlKey || event.metaKey) return;\n\n    const openInNewTab = target.getAttribute('target') === '_blank';\n    isClickLink = true;\n\n    if (openInNewTab) {\n      event.preventDefault();\n\n      const description = (target.innerText || target.href)?.slice(0, 24) || '';\n\n      addBlock({\n        id: 'link',\n        description,\n        data: {\n          selector,\n          description,\n        },\n      });\n\n      window.open(event.target.href, '_blank');\n\n      return;\n    }\n  }\n\n  const elText =\n    (target.innerText || target.ariaLabel || target.title)?.slice(0, 24) || '';\n\n  addBlock({\n    isClickLink,\n    id: 'event-click',\n    description: elText,\n    data: {\n      selector,\n      description: elText,\n      waitForSelector: true,\n    },\n  });\n}\n\nconst onMessage = debounce(({ data, source }) => {\n  if (data.type !== 'automa:record-events') return;\n\n  let { frameSelector } = data;\n\n  if (!frameSelector) {\n    const frames = document.querySelectorAll('iframe, frame');\n\n    frames.forEach((frame) => {\n      if (frame.contentWindow !== source) return;\n\n      frameSelector = finder(frame);\n    });\n  }\n\n  if (!frameSelector) return;\n\n  const lastFlow = data.recording.flows.at(-1);\n  if (!lastFlow) return;\n\n  const lastIndex = data.recording.flows.length - 1;\n  data.recording.flows[\n    lastIndex\n  ].data.selector = `${frameSelector} |> ${lastFlow.data.selector}`;\n\n  browser.storage.local.set({ recording: data.recording });\n}, 100);\nconst onScroll = debounce(({ target }) => {\n  if (isAutomaInstance(target)) return;\n\n  const isDocument = target === document;\n  const element = isDocument ? document.documentElement : target;\n  const selector = isDocument ? 'html' : findSelector(target);\n\n  addBlock((recording) => {\n    const lastFlow = recording.flows[recording.flows.length - 1];\n    const verticalScroll = element.scrollTop || element.scrollY || 0;\n    const horizontalScroll = element.scrollLeft || element.scrollX || 0;\n\n    if (lastFlow && lastFlow.id === 'element-scroll') {\n      lastFlow.data.scrollY = verticalScroll;\n      lastFlow.data.scrollX = horizontalScroll;\n\n      return;\n    }\n\n    recording.flows.push({\n      id: 'element-scroll',\n      data: {\n        selector,\n        smooth: true,\n        scrollY: verticalScroll,\n        scrollX: horizontalScroll,\n      },\n    });\n  });\n}, 500);\n\nconst onInputTextField = debounce(({ target }) => {\n  const selector = target.dataset.automaElSelector;\n  if (!selector) return;\n\n  addBlock((recording) => {\n    const lastFlow = recording.flows[recording.flows.length - 1];\n    if (\n      lastFlow &&\n      lastFlow.id === 'forms' &&\n      lastFlow.data.selector === selector\n    ) {\n      lastFlow.data.value = target.value;\n      return;\n    }\n\n    const elementName = (target.ariaLabel || target.name || '').slice(0, 12);\n    recording.flows.push({\n      id: 'forms',\n      data: {\n        selector,\n        delay: 100,\n        clearValue: true,\n        type: 'text-field',\n        value: target.value,\n        waitForSelector: true,\n        description: `Text field (${elementName})`,\n      },\n    });\n  });\n}, 300);\n\nfunction onFocusIn({ target }) {\n  if (!isTextFieldEl(target)) return;\n\n  target.setAttribute('data-automa-el-selector', findSelector(target));\n  target.addEventListener('input', onInputTextField);\n}\nfunction onFocusOut({ target }) {\n  if (!isTextFieldEl(target)) return;\n\n  target.removeEventListener('input', onInputTextField);\n}\n\nexport function cleanUp() {\n  if (isMainFrame) {\n    window.removeEventListener('message', onMessage);\n    document.removeEventListener('scroll', onScroll, true);\n  }\n\n  document.removeEventListener('click', onClick, true);\n  document.removeEventListener('change', onChange, true);\n  document.removeEventListener('focusin', onFocusIn, true);\n  document.removeEventListener('keydown', onKeydown, true);\n  document.removeEventListener('focusout', onFocusOut, true);\n}\n\nexport default async function (mainFrame) {\n  const { isRecording } = await browser.storage.local.get('isRecording');\n\n  isMainFrame = mainFrame;\n\n  if (isRecording) {\n    if (isMainFrame) {\n      window.addEventListener('message', onMessage);\n      document.addEventListener('scroll', onScroll, true);\n    }\n\n    if (isTextFieldEl(document.activeElement)) {\n      onFocusIn({ target: document.activeElement });\n    }\n\n    document.addEventListener('click', onClick, true);\n    document.addEventListener('change', onChange, true);\n    document.addEventListener('focusin', onFocusIn, true);\n    document.addEventListener('keydown', onKeydown, true);\n    document.addEventListener('focusout', onFocusOut, true);\n  }\n\n  return cleanUp;\n}\n"
  },
  {
    "path": "src/content/services/shortcutListener.js",
    "content": "import Mousetrap from 'mousetrap';\nimport browser from 'webextension-polyfill';\nimport { sendMessage } from '@/utils/message';\n\nMousetrap.prototype.stopCallback = function () {\n  return false;\n};\n\nfunction automaCustomEventListener(findWorkflow) {\n  function customEventListener({ detail }) {\n    if (!detail || (!detail.id && !detail.publicId)) return;\n\n    const workflowId = detail.id || detail.publicId;\n    const workflow = findWorkflow(workflowId, Boolean(detail.publicId));\n\n    if (!workflow) return;\n\n    workflow.options = {\n      data: detail.data || {},\n    };\n    sendMessage('workflow:execute', workflow, 'background');\n  }\n\n  window.addEventListener('__automaExecuteWorkflow', customEventListener);\n  window.addEventListener('automa:execute-workflow', customEventListener);\n}\nfunction workflowShortcutsListener(findWorkflow, shortcutsObj) {\n  const shortcuts = Object.entries(shortcutsObj);\n\n  if (shortcuts.length === 0) return;\n\n  const keyboardShortcuts = shortcuts.reduce((acc, [id, value]) => {\n    let workflowId = id;\n    if (id.startsWith('trigger')) {\n      const { 1: triggerWorkflowId } = id.split(':');\n      workflowId = triggerWorkflowId;\n    }\n\n    const workflow = findWorkflow(workflowId);\n    if (!workflow) return acc;\n\n    (acc[value] = acc[value] || []).push({\n      id,\n      workflow,\n      activeInInput: workflow.trigger?.activeInInput || false,\n    });\n\n    return acc;\n  }, {});\n\n  Mousetrap.bind(Object.keys(keyboardShortcuts), ({ target }, command) => {\n    const isInputElement =\n      ['INPUT', 'SELECT', 'TEXTAREA'].includes(target.tagName) ||\n      target?.contentEditable === 'true';\n\n    keyboardShortcuts[command].forEach((item) => {\n      if (!item.activeInInput && isInputElement) return;\n\n      sendMessage('workflow:execute', item.workflow, 'background');\n    });\n\n    return true;\n  });\n}\nasync function getWorkflows() {\n  const {\n    workflows: localWorkflows,\n    workflowHosts,\n    teamWorkflows,\n  } = await browser.storage.local.get([\n    'workflows',\n    'workflowHosts',\n    'teamWorkflows',\n  ]);\n\n  return [\n    ...Object.values(workflowHosts || {}),\n    ...Object.values(localWorkflows || {}),\n    ...Object.values(Object.values(teamWorkflows || {})[0] || {}),\n  ];\n}\n\nexport default async function () {\n  try {\n    const storage = await browser.storage.local.get('shortcuts');\n    let workflows = await getWorkflows();\n\n    const findWorkflow = (id, publicId = false) => {\n      const workflow = workflows.find((item) => {\n        if (publicId) {\n          return item.settings.publicId === id;\n        }\n\n        return item.id === id;\n      });\n\n      return workflow;\n    };\n\n    browser.storage.onChanged.addListener(({ automaShortcut, shortcuts }) => {\n      if (automaShortcut) {\n        if (\n          Array.isArray(automaShortcut.newValue) &&\n          automaShortcut.newValue.length < 1\n        ) {\n          window._automaShortcuts = [];\n        } else {\n          const automaShortcutArr = automaShortcut.newValue.split('+');\n\n          window._automaShortcuts = automaShortcutArr;\n        }\n      }\n      if (shortcuts) {\n        Mousetrap.reset();\n        getWorkflows().then((updatedWorkflows) => {\n          workflows = updatedWorkflows;\n          workflowShortcutsListener(findWorkflow, shortcuts.newValue || {});\n        });\n      }\n    });\n\n    automaCustomEventListener(findWorkflow);\n    workflowShortcutsListener(findWorkflow, storage.shortcuts || {});\n  } catch (error) {\n    console.error(error);\n  }\n}\n"
  },
  {
    "path": "src/content/services/webService.js",
    "content": "import { objectHasKey, parseJSON } from '@/utils/helper';\nimport { sendMessage } from '@/utils/message';\nimport { openDB } from 'idb';\nimport deepmerge from 'lodash.merge';\nimport { nanoid } from 'nanoid';\nimport browser from 'webextension-polyfill';\n\nfunction initWebListener() {\n  const listeners = {};\n\n  function on(name, callback) {\n    (listeners[name] = listeners[name] || []).push(callback);\n  }\n\n  window.addEventListener('__automa-ext__', ({ detail }) => {\n    if (!detail || !objectHasKey(listeners, detail.type)) return;\n\n    listeners[detail.type].forEach((listener) => {\n      listener(detail.data);\n    });\n  });\n\n  return { on };\n}\nfunction sendMessageBack(type, payload = {}) {\n  const event = new CustomEvent(`__automa-ext__${type}`, {\n    detail: payload,\n  });\n\n  window.dispatchEvent(event);\n}\n\nwindow.addEventListener('DOMContentLoaded', async () => {\n  try {\n    document.body.setAttribute(\n      'data-atm-ext-installed',\n      browser.runtime.getManifest().version\n    );\n\n    const { workflows } = await browser.storage.local.get('workflows');\n    const db = await openDB('automa', 1, {\n      upgrade(event) {\n        event.createObjectStore('store');\n      },\n    });\n\n    await db.put('store', workflows, 'workflows');\n\n    const webListener = initWebListener();\n    webListener.on('open-dashboard', ({ path }) => {\n      if (!path) return;\n\n      sendMessage('open:dashboard', path, 'background');\n    });\n    webListener.on('open-workflow', ({ workflowId }) => {\n      if (!workflowId) return;\n\n      sendMessage('open:dashboard', `/workflows/${workflowId}`, 'background');\n    });\n    webListener.on('add-workflow', async ({ workflow }) => {\n      try {\n        const { workflows: workflowsStorage } = await browser.storage.local.get(\n          'workflows'\n        );\n\n        const workflowId = nanoid();\n        const workflowData = {\n          ...workflow,\n          id: workflowId,\n          dataColumns: [],\n          createdAt: Date.now(),\n          table: workflow.table || workflow.dataColumns,\n        };\n\n        workflowData.drawflow =\n          typeof workflowData.drawflow === 'string'\n            ? parseJSON(workflowData.drawflow, workflowData.drawflow)\n            : workflowData.drawflow;\n\n        if (Array.isArray(workflowsStorage)) {\n          workflowsStorage.push(workflowData);\n        } else {\n          workflowsStorage[workflowId] = workflowData;\n        }\n\n        await browser.storage.local.set({ workflows: workflowsStorage });\n        sendMessage(\n          'workflow:added',\n          { workflowId, workflowData },\n          'background'\n        );\n      } catch (error) {\n        console.error(error);\n      }\n    });\n    webListener.on('add-team-workflow', async ({ workflow }) => {\n      let { teamWorkflows } = await browser.storage.local.get('teamWorkflows');\n\n      let workflowData = {\n        ...workflow,\n        createdAt: Date.now(),\n        table: workflow.table ?? [],\n      };\n      workflowData.drawflow =\n        typeof workflowData.drawflow === 'string'\n          ? parseJSON(workflowData.drawflow, workflowData.drawflow)\n          : workflowData.drawflow;\n\n      if (!teamWorkflows) teamWorkflows = {};\n      if (!teamWorkflows[workflowData.teamId])\n        teamWorkflows[workflowData.teamId] = {};\n\n      const workflowToMerge =\n        teamWorkflows[workflowData.teamId][workflow.id] || null;\n      if (workflowToMerge) {\n        workflowData = deepmerge(workflowToMerge, workflowData);\n      }\n\n      teamWorkflows[workflowData.teamId][workflow.id] = workflowData;\n      await browser.storage.local.set({ teamWorkflows });\n\n      const triggerBlock = workflowData.drawflow.nodes?.find(\n        (node) => node.label === 'trigger'\n      );\n      if (triggerBlock) {\n        await sendMessage(\n          'workflow:register',\n          { triggerBlock, workflowId: workflowData.id },\n          'background'\n        );\n      }\n\n      sendMessage(\n        'workflow:added',\n        {\n          workflowId: workflowData.id,\n          teamId: workflowData.teamId,\n          source: 'team',\n        },\n        'background'\n      );\n    });\n    webListener.on('check-team-workflow', async ({ teamId, workflowId }) => {\n      const { teamWorkflows } = await browser.storage.local.get(\n        'teamWorkflows'\n      );\n      const workflowExist = Boolean(teamWorkflows?.[teamId]?.[workflowId]);\n\n      window.dispatchEvent(\n        new CustomEvent('__automa-team-workflow__', {\n          detail: { exists: workflowExist },\n        })\n      );\n    });\n    webListener.on('add-package', async (data) => {\n      try {\n        const { savedBlocks } = await browser.storage.local.get('savedBlocks');\n        const packages = savedBlocks || [];\n\n        packages.push({ ...data.package, createdAt: Date.now() });\n\n        await browser.storage.local.set({ savedBlocks: packages });\n\n        sendMessage('dashboard:refresh-packages', '', 'background');\n      } catch (error) {\n        console.error(error);\n      }\n    });\n    webListener.on('update-package', async (data) => {\n      const { savedBlocks } = await browser.storage.local.get('savedBlocks');\n      const packages = savedBlocks || [];\n\n      const index = packages.findIndex((pkg) => pkg.id === data.id);\n      if (index === -1) return;\n\n      Object.assign(packages[index], data.package);\n\n      await browser.storage.local.set({ savedBlocks: packages });\n\n      sendMessage('dashboard:refresh-packages', '', 'background');\n    });\n    webListener.on('send-message', async ({ type, data }) => {\n      if (type === 'package-installed') {\n        const { savedBlocks } = await browser.storage.local.get('savedBlocks');\n        const packages = savedBlocks || [];\n        const isInstalled = packages.some((pkg) => pkg.id === data);\n\n        sendMessageBack(type, isInstalled);\n      } else if (type === 'get-workflows') {\n        const storage = await browser.storage.local.get('workflows');\n        sendMessageBack(type, storage.workflows);\n      }\n    });\n  } catch (error) {\n    console.error(error);\n  }\n});\n\nwindow.addEventListener('user-logout', () => {\n  browser.storage.local.remove(['session', 'sessionToken']);\n});\n\nwindow.addEventListener('app-mounted', async () => {\n  try {\n    const STORAGE_KEY = 'supabase.auth.token';\n    const webStorageAuthData = parseJSON(\n      localStorage.getItem(STORAGE_KEY),\n      null\n    );\n    const extensionStorage = await browser.storage.local.get([\n      'session',\n      'sessionToken',\n    ]);\n\n    const setUserSession = async () => {\n      const saveToStorage = { session: webStorageAuthData };\n\n      const isGoogleProvider =\n        webStorageAuthData?.user?.user_metadata?.iss.includes('google.com');\n      const { session: currSession, sessionToken: currSessionToken } =\n        await browser.storage.local.get(['session', 'sessionToken']);\n      if (\n        isGoogleProvider &&\n        ((webStorageAuthData &&\n          webStorageAuthData.user.id === currSession?.user.id) ||\n          !currSessionToken)\n      ) {\n        saveToStorage.sessionToken = {\n          access: webStorageAuthData.provider_token,\n          refresh: webStorageAuthData.provider_refresh_token,\n        };\n      }\n      if (!isGoogleProvider) {\n        browser.storage.local.remove('sessionToken');\n      }\n\n      await browser.storage.local.set(saveToStorage);\n    };\n\n    if (webStorageAuthData && !extensionStorage.session) {\n      await setUserSession();\n    } else if (webStorageAuthData && extensionStorage.session) {\n      if (webStorageAuthData.user.id !== extensionStorage.session.id) {\n        await setUserSession();\n      } else {\n        const currentSession = { ...extensionStorage.session };\n        if (extensionStorage.sessionToken) {\n          currentSession.provider_token = extensionStorage.sessionToken.access;\n          currentSession.provider_refresh_token =\n            extensionStorage.sessionToken.refresh;\n        }\n\n        localStorage.setItem(STORAGE_KEY, JSON.stringify(currentSession));\n      }\n    }\n  } catch (error) {\n    console.error(error);\n  }\n});\n"
  },
  {
    "path": "src/content/showExecutedBlock.js",
    "content": "import { tasks } from '@/utils/shared';\n\nfunction generateElement(block) {\n  return `\n    <div style=\"display: flex; align-items: center\">\n      <svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        id=\"spinner\"\n        fill=\"transparent\"\n        width=\"24\"\n        height=\"24\"\n        viewBox=\"0 0 24 24\"\n      >\n        <circle\n          class=\"opacity-25\"\n          cx=\"12\"\n          cy=\"12\"\n          r=\"10\"\n          stroke=\"currentColor\"\n          stroke-width=\"4\"\n        ></circle>\n        <path\n          class=\"opacity-75\"\n          fill=\"currentColor\"\n          d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z\"\n        ></path>\n      </svg>\n      <p id=\"block-name\">${block.name}</p>\n    </div>\n  `;\n}\n\nexport default function (data, enable) {\n  if (!enable) {\n    return () => {};\n  }\n\n  const block = tasks[data.label];\n  if (!block) return () => {};\n  let container = document.querySelector('.automa-executed-block');\n\n  if (!container) {\n    container = document.createElement('div');\n    container.classList.add('automa-executed-block');\n    document.body.appendChild(container);\n\n    const style = document.createElement('style');\n    style.classList.add('automa-executed-block');\n    style.innerHTML = `\n      @keyframes spin {\n        from {\n          transform: rotate(0deg);\n        }\n        to {\n          transform: rotate(360deg);\n        }\n      }\n\n      .automa-executed-block .opacity-25 {\n        opacity: 0.25;\n      }\n      .automa-executed-block .opacity-75 {\n        opacity: 0.75;\n      }\n      .automa-executed-block {\n        color: #18181b;\n        width: 250px;\n        position: fixed;\n        border-radius: 12px;\n        bottom: 12px;\n        right: 12px;\n        padding: 14px;\n        background-color: white;\n        font-size: 16px;\n        font-family: sans-serif;\n        box-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);\n        z-index: 99999;\n      }\n      .automa-executed-block #spinner {\n        color: currentColor;\n        display: inline-block;\n        animation: spin 1s linear infinite;\n      }\n      .automa-executed-block p {\n        margin: 0;\n        padding: 0;\n        margin-left: 8px;\n      }\n    `;\n    document.body.appendChild(style);\n  }\n  container.innerHTML = generateElement(block);\n\n  return () => {\n    const elements = document.querySelectorAll('.automa-executed-block');\n    elements.forEach((el) => {\n      el.remove();\n    });\n  };\n}\n"
  },
  {
    "path": "src/content/synchronizedLock.js",
    "content": "class SynchronizedLock {\n  constructor() {\n    this.lock = false;\n    this.queue = [];\n  }\n\n  async getLock(timeout = 10000) {\n    while (this.lock) {\n      await new Promise((resolve) => {\n        this.queue.push(resolve);\n        setTimeout(() => {\n          const index = this.queue.indexOf(resolve);\n          if (index !== -1) {\n            this.queue.splice(index, 1);\n            console.warn('SynchronizedLock timeout');\n            resolve();\n          }\n        }, timeout);\n      });\n    }\n\n    this.lock = true;\n  }\n\n  releaseLock() {\n    this.lock = false;\n    const resolve = this.queue.shift();\n    if (resolve) resolve();\n  }\n}\n\nconst synchronizedLock = new SynchronizedLock();\n\nexport default synchronizedLock;\n"
  },
  {
    "path": "src/content/utils.js",
    "content": "export function simulateClickElement(element) {\n  const eventOpts = { bubbles: true, view: window };\n\n  element.dispatchEvent(new MouseEvent('mousedown', eventOpts));\n  element.dispatchEvent(new MouseEvent('mouseup', eventOpts));\n\n  if (element.click) {\n    element.click();\n  } else {\n    element.dispatchEvent(new PointerEvent('click', { bubbles: true }));\n  }\n\n  element.focus?.();\n}\n\nexport function generateLoopSelectors(\n  elements,\n  { max, attrId, frameSelector, reverseLoop, startIndex = 0 }\n) {\n  const selectors = [];\n  let elementsList = elements;\n\n  if (reverseLoop) {\n    elementsList = Array.from(elements).reverse();\n  }\n\n  elementsList.forEach((el, index) => {\n    if (max > 0 && selectors.length - 1 > max) return;\n\n    const attrName = 'automa-loop';\n    const attrValue = `${attrId}--${(startIndex || 0) + index}`;\n\n    el.setAttribute(attrName, attrValue);\n    selectors.push(`${frameSelector}[${attrName}=\"${attrValue}\"]`);\n  });\n\n  return selectors;\n}\n\nexport function elementSelectorInstance() {\n  const rootElementExist = document.querySelector(\n    '#app-container.automa-element-selector'\n  );\n\n  if (rootElementExist) {\n    rootElementExist.style.display = 'block';\n\n    return true;\n  }\n\n  return false;\n}\n\nexport function getElementRect(target, withAttributes) {\n  if (!target) return {};\n\n  const { x, y, height, width } = target.getBoundingClientRect();\n  const result = {\n    width: width + 4,\n    height: height + 4,\n    x: x - 2,\n    y: y - 2,\n  };\n\n  if (withAttributes) {\n    const attributes = {};\n\n    Array.from(target.attributes).forEach(({ name, value }) => {\n      if (name === 'automa-el-list') return;\n\n      attributes[name] = value;\n    });\n\n    result.attributes = attributes;\n    result.tagName = target.tagName;\n  }\n\n  return result;\n}\n\nexport function getElementPath(el, root = document.documentElement) {\n  const path = [el];\n\n  /* eslint-disable-next-line */\n  while ((el = el.parentNode) && !el.isEqualNode(root)) {\n    path.push(el);\n  }\n\n  return path;\n}\n\nexport function generateXPath(element, root = document.body) {\n  if (!element) return null;\n  if (element.id !== '') return `id(\"${element.id}\")`;\n  if (element === root) return `//${element.tagName}`;\n\n  let ix = 0;\n  const siblings = element.parentNode.childNodes;\n\n  for (let index = 0; index < siblings.length; index += 1) {\n    const sibling = siblings[index];\n\n    if (sibling === element) {\n      return `${generateXPath(element.parentNode)}/${element.tagName}[${\n        ix + 1\n      }]`;\n    }\n\n    if (sibling.nodeType === 1 && sibling.tagName === element.tagName) {\n      ix += 1;\n    }\n  }\n\n  return null;\n}\n\n// no used\nexport function automaRefDataStr(varName) {\n  return `\nfunction findData(obj, path) {\n  const paths = path.split('.');\n  const isWhitespace = paths.length === 1 && !/\\\\\\\\S/.test(paths[0]);\n\n  if (path.startsWith('$last') && Array.isArray(obj)) {\n    paths[0] = obj.length - 1;\n  }\n\n  if (paths.length === 0 || isWhitespace) return obj;\n  else if (paths.length === 1) return obj[paths[0]];\n\n  let result = obj;\n\n  for (let i = 0; i < paths.length; i++) {\n    if (result[paths[i]] == undefined) {\n      return undefined;\n    } else {\n      result = result[paths[i]];\n    }\n  }\n\n  return result;\n}\nfunction automaRefData(keyword, path = '') {\n  const data = ${varName}[keyword];\n\n  if (!data) return;\n\n  return findData(data, path);\n}\n  `;\n}\n\nfunction messageTopFrame(windowCtx) {\n  return new Promise((resolve) => {\n    let timeout = null;\n\n    const messageListener = ({ data }) => {\n      if (data.type !== 'automa:the-frame-rect') return;\n\n      clearTimeout(timeout);\n      windowCtx.removeEventListener('message', messageListener);\n      resolve(data.frameRect);\n    };\n\n    timeout = setTimeout(() => {\n      windowCtx.removeEventListener('message', messageListener);\n      resolve(null);\n    }, 5000);\n\n    windowCtx.addEventListener('message', messageListener);\n    windowCtx.top.postMessage({ type: 'automa:get-frame' }, '*');\n  });\n}\nexport async function getElementPosition(element) {\n  const elWindow = element.ownerDocument.defaultView;\n  const isInFrame = elWindow !== window.top;\n  const { width, height, x, y } = element.getBoundingClientRect();\n\n  const position = {\n    x: x + width / 2,\n    y: y + height / 2,\n  };\n\n  if (!isInFrame) return position;\n\n  try {\n    const frameEl = elWindow.frameElement;\n    let frameRect = null;\n\n    if (frameEl) {\n      frameRect = frameEl.getBoundingClientRect();\n    } else {\n      frameRect = await messageTopFrame(elWindow);\n\n      if (!frameRect) throw new Error('Iframe not found');\n    }\n\n    position.x += frameRect.x;\n    position.y += frameRect.y;\n\n    return position;\n  } catch (error) {\n    console.error(error);\n    return position;\n  }\n}\n"
  },
  {
    "path": "src/db/logs.js",
    "content": "import Dexie from 'dexie';\n\nconst dbLogs = new Dexie('logs');\ndbLogs.version(1).stores({\n  ctxData: '++id, logId',\n  logsData: '++id, logId',\n  histories: '++id, logId',\n  items: '++id, name, endedAt, workflowId, status, collectionId',\n});\n\nexport const defaultLogItem = {\n  name: '',\n  endedAt: 0,\n  message: '',\n  startedAt: 0,\n  parentLog: null,\n  workflowId: null,\n  status: 'success',\n  collectionId: null,\n};\n\nexport default dbLogs;\n"
  },
  {
    "path": "src/db/storage.js",
    "content": "import Dexie from 'dexie';\n\nconst dbStorage = new Dexie('storage');\ndbStorage.version(2).stores({\n  tablesData: '++id, tableId',\n  tablesItems: '++id, name, createdAt, modifiedAt',\n  variables: '++id, &name',\n  credentials: '++id, &name',\n});\n\nexport default dbStorage;\n"
  },
  {
    "path": "src/directives/VAutofocus.js",
    "content": "export default {\n  mounted(el, { value = true }) {\n    if (value) el.focus();\n  },\n};\n"
  },
  {
    "path": "src/directives/VClosePopover.js",
    "content": "import { hideAll } from 'tippy.js';\n\nexport default {\n  mounted(el) {\n    el.addEventListener('click', hideAll);\n  },\n  beforeUnmount(el) {\n    el.removeEventListener('click', hideAll);\n  },\n};\n"
  },
  {
    "path": "src/directives/VTooltip.js",
    "content": "import createTippy from '@/lib/tippy';\n\nfunction getContent(content) {\n  if (typeof content === 'string') {\n    return { content };\n  }\n\n  if (typeof content === 'object' && content !== null) {\n    return content;\n  }\n\n  return {};\n}\n\nexport default {\n  mounted(el, { value, arg = 'top', instance, modifiers }) {\n    const content = getContent(value);\n\n    const tooltip = createTippy(el, {\n      ...content,\n      theme: 'tooltip-theme',\n      placement: arg,\n    });\n\n    if (modifiers.group) {\n      if (!Array.isArray(instance._tooltipGroup)) instance._tooltipGroup = [];\n\n      instance._tooltipGroup.push(tooltip);\n    }\n  },\n  updated(el, { value, arg = 'top' }) {\n    const content = getContent(value);\n\n    el._tippy.setProps({\n      placement: arg,\n      ...content,\n    });\n  },\n};\n"
  },
  {
    "path": "src/execute/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n\t<meta charset=\"UTF-8\">\n\t<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n\t<title>Execute</title>\n</head>\n<body>\n</body>\n</html>\n"
  },
  {
    "path": "src/execute/index.js",
    "content": "import { parseJSON } from '@/utils/helper';\nimport { sendMessage } from '@/utils/message';\nimport Browser from 'webextension-polyfill';\n\nfunction getWorkflowDetail() {\n  let hash = window.location.hash.slice(1);\n  if (!hash.startsWith('/')) hash = `/${hash}`;\n\n  const { pathname, searchParams } = new URL(window.location.origin + hash);\n\n  const variables = {};\n  const { 1: workflowId } = pathname.split('/');\n\n  searchParams.forEach((key, value) => {\n    const varValue = parseJSON(decodeURIComponent(value), '##_empty');\n    if (varValue === '##_empty') return;\n\n    variables[key] = varValue;\n  });\n\n  return { workflowId: workflowId ?? '', variables };\n}\n\nfunction writeResult(text) {\n  document.body.innerText = text;\n}\n\n(async () => {\n  try {\n    const { workflowId, variables } = getWorkflowDetail();\n    if (!workflowId) {\n      writeResult('Invalid path');\n      return;\n    }\n\n    const { workflows } = await Browser.storage.local.get('workflows');\n\n    let workflow = workflows[workflowId];\n    if (!workflow && Array.isArray(workflows)) {\n      workflow = workflows.find((item) => item.id === workflowId);\n    }\n\n    if (!workflow) {\n      writeResult('Workflow not found');\n      return;\n    }\n\n    const hasVariables = Object.keys(variables).length > 0;\n\n    writeResult('Executing workflow');\n\n    sendMessage(\n      'workflow:execute',\n      {\n        ...workflow,\n        options: { checkParam: !hasVariables, data: { variables } },\n      },\n      'background'\n    ).then(() => {\n      setTimeout(window.close, 1000);\n    });\n  } catch (error) {\n    console.error(error);\n  }\n})();\n"
  },
  {
    "path": "src/lib/compsUi.js",
    "content": "import VTooltip from '../directives/VTooltip';\nimport VAutofocus from '../directives/VAutofocus';\nimport VClosePopover from '../directives/VClosePopover';\n\nconst uiComponents = require.context('../components/ui', false, /\\.vue$/);\nconst transitionComponents = require.context(\n  '../components/transitions',\n  false,\n  /\\.vue$/\n);\n\nfunction componentsExtractor(app, components) {\n  components.keys().forEach((key) => {\n    const componentName = key.replace(/(.\\/)|\\.vue$/g, '');\n    const component = components(key)?.default ?? {};\n\n    app.component(componentName, component);\n  });\n}\n\nexport default function (app) {\n  app.directive('tooltip', VTooltip);\n  app.directive('autofocus', VAutofocus);\n  app.directive('close-popover', VClosePopover);\n\n  componentsExtractor(app, uiComponents);\n  componentsExtractor(app, transitionComponents);\n}\n"
  },
  {
    "path": "src/lib/cronstrue.js",
    "content": "import cronstrue from 'cronstrue';\nimport 'cronstrue/locales/fr';\nimport 'cronstrue/locales/zh_TW';\nimport 'cronstrue/locales/zh_CN';\n\nconst supportedLocales = ['en', 'zh', 'zh-tw', 'fr'];\nconst altLocaleId = {\n  zh: 'zh_CN',\n  'zh-TW': 'zh_TW',\n};\n\nexport function readableCron(expression) {\n  const currentLang = document.documentElement.lang;\n  const locale = supportedLocales.includes(currentLang)\n    ? altLocaleId[currentLang] || currentLang\n    : 'en';\n\n  return cronstrue.toString(expression, { locale });\n}\n\nexport default cronstrue;\n"
  },
  {
    "path": "src/lib/dayjs.js",
    "content": "import dayjs from 'dayjs';\nimport relativeTime from 'dayjs/plugin/relativeTime';\nimport 'dayjs/locale/zh';\nimport 'dayjs/locale/zh-tw';\nimport 'dayjs/locale/vi';\nimport 'dayjs/locale/fr';\nimport 'dayjs/locale/it';\nimport 'dayjs/locale/uk';\nimport 'dayjs/locale/tr';\nimport 'dayjs/locale/es';\nimport 'dayjs/locale/pt'\n\ndayjs.extend(relativeTime);\n\nexport default dayjs;\n"
  },
  {
    "path": "src/lib/findSelector.js",
    "content": "import { finder as finderLib } from '@medv/finder';\n\nconst ariaAttrs = ['data-testid'];\n\nexport const finder = finderLib;\n\nexport default function (element, options = {}) {\n  let selector = finder(element, {\n    tagName: () => true,\n    attr: (name, value) => name === 'id' || (ariaAttrs.includes(name) && value),\n    ...options,\n  });\n\n  const tag = element.tagName.toLowerCase();\n  if (!selector.startsWith(tag) && !selector.includes(' ')) {\n    selector = `${tag}${selector}`;\n  }\n\n  return selector;\n}\n"
  },
  {
    "path": "src/lib/mitt.js",
    "content": "import mitt from 'mitt';\n\nconst emitter = mitt();\n\nexport default emitter;\n"
  },
  {
    "path": "src/lib/pinia.js",
    "content": "import { createPinia } from 'pinia';\nimport browser from 'webextension-polyfill';\n\nfunction saveToStoragePlugin({ store, options }) {\n  store.saveToStorage = (key) => {\n    const storageKey = options.storageMap[key];\n    if (!storageKey || !store.retrieved) return null;\n\n    const value = JSON.parse(JSON.stringify(store[key]));\n\n    return browser.storage.local.set({ [storageKey]: value });\n  };\n}\n\nconst pinia = createPinia();\npinia.use(saveToStoragePlugin);\n\nexport default pinia;\n"
  },
  {
    "path": "src/lib/query-selector-shadow-dom/index.js",
    "content": "/* eslint-disable */\n/* \nForked from: https://github.com/webdriverio/query-selector-shadow-dom/\n*/\n\nimport { normalizeSelector } from './normalize';\n\n/**\n* Finds first matching elements on the page that may be in a shadow root using a complex selector of n-depth\n*\n* Don't have to specify all shadow roots to button, tree is travered to find the correct element\n*\n* Example querySelectorAllDeep('downloads-item:nth-child(4) #remove');\n*\n* Example should work on chrome://downloads outputting the remove button inside of a download card component\n*\n* Example find first active download link element querySelectorDeep('#downloads-list .is-active a[href^=\"https://\"]');\n*\n* Another example querySelectorAllDeep('#downloads-list div#title-area + a');\ne.g.\n*/\nexport function querySelectorAllDeep(\n  selector,\n  root = document,\n  allElements = null\n) {\n  return _querySelectorDeep(selector, true, root, allElements);\n}\n\nexport function querySelectorDeep(\n  selector,\n  root = document,\n  allElements = null\n) {\n  return _querySelectorDeep(selector, false, root, allElements);\n}\n\nfunction _querySelectorDeep(selector, findMany, root, allElements = null) {\n  selector = normalizeSelector(selector);\n  const lightElement = root.querySelector(selector);\n\n  if (document.head.createShadowRoot || document.head.attachShadow) {\n    // no need to do any special if selector matches something specific in light-dom\n    if (!findMany && lightElement) {\n      return lightElement;\n    }\n\n    // split on commas because those are a logical divide in the operation\n    const selectionsToMake = splitByCharacterUnlessQuoted(selector, ',');\n\n    return selectionsToMake.reduce(\n      (acc, minimalSelector) => {\n        // if not finding many just reduce the first match\n        if (!findMany && acc) {\n          return acc;\n        }\n        // do best to support complex selectors and split the query\n        const splitSelector = splitByCharacterUnlessQuoted(\n          minimalSelector\n            // remove white space at start of selector\n            .replace(/^\\s+/g, '')\n            .replace(/\\s*([>+~]+)\\s*/g, '$1'),\n          ' '\n        )\n          // filter out entry white selectors\n          .filter((entry) => !!entry)\n          // convert \"a > b\" to [\"a\", \"b\"]\n          .map((entry) => splitByCharacterUnlessQuoted(entry, '>'));\n\n        const possibleElementsIndex = splitSelector.length - 1;\n        const lastSplitPart =\n          splitSelector[possibleElementsIndex][\n            splitSelector[possibleElementsIndex].length - 1\n          ];\n        const possibleElements = collectAllElementsDeep(\n          lastSplitPart,\n          root,\n          allElements\n        );\n        const findElements = findMatchingElement(\n          splitSelector,\n          possibleElementsIndex,\n          root\n        );\n        if (findMany) {\n          acc = acc.concat(possibleElements.filter(findElements));\n          return acc;\n        }\n        acc = possibleElements.find(findElements);\n        return acc || null;\n      },\n      findMany ? [] : null\n    );\n  }\n  if (!findMany) {\n    return lightElement;\n  }\n  return root.querySelectorAll(selector);\n}\n\nfunction findMatchingElement(splitSelector, possibleElementsIndex, root) {\n  return (element) => {\n    let position = possibleElementsIndex;\n    let parent = element;\n    let foundElement = false;\n    while (parent && !isDocumentNode(parent)) {\n      let foundMatch = true;\n      if (splitSelector[position].length === 1) {\n        foundMatch = parent.matches(splitSelector[position]);\n      } else {\n        // selector is in the format \"a > b\"\n        // make sure a few parents match in order\n        const reversedParts = [].concat(splitSelector[position]).reverse();\n        let newParent = parent;\n        for (const part of reversedParts) {\n          if (!newParent || !newParent.matches(part)) {\n            foundMatch = false;\n            break;\n          }\n          newParent = findParentOrHost(newParent, root);\n        }\n      }\n\n      if (foundMatch && position === 0) {\n        foundElement = true;\n        break;\n      }\n      if (foundMatch) {\n        position--;\n      }\n      parent = findParentOrHost(parent, root);\n    }\n    return foundElement;\n  };\n}\n\nfunction splitByCharacterUnlessQuoted(selector, character) {\n  return selector.match(/\\\\?.|^$/g).reduce(\n    (p, c) => {\n      if (c === '\"' && !p.sQuote) {\n        p.quote ^= 1;\n        p.a[p.a.length - 1] += c;\n      } else if (c === \"'\" && !p.quote) {\n        p.sQuote ^= 1;\n        p.a[p.a.length - 1] += c;\n      } else if (!p.quote && !p.sQuote && c === character) {\n        p.a.push('');\n      } else {\n        p.a[p.a.length - 1] += c;\n      }\n      return p;\n    },\n    { a: [''] }\n  ).a;\n}\n\n/**\n * Checks if the node is a document node or not.\n * @param {Node} node\n * @returns {node is Document | DocumentFragment}\n */\nfunction isDocumentNode(node) {\n  return (\n    node.nodeType === Node.DOCUMENT_FRAGMENT_NODE ||\n    node.nodeType === Node.DOCUMENT_NODE\n  );\n}\n\nfunction findParentOrHost(element, root) {\n  const { parentNode } = element;\n  return parentNode && parentNode.host && parentNode.nodeType === 11\n    ? parentNode.host\n    : parentNode === root\n    ? null\n    : parentNode;\n}\n\nconst getShadowRoot = (element) => {\n  if (element === document) return element;\n\n  return BROWSER_TYPE === 'firefox'\n    ? element.openOrClosedShadowRoot\n    : chrome.dom.openOrClosedShadowRoot(element);\n}\n\n/**\n * Finds all elements on the page, inclusive of those within shadow roots.\n * @param {string=} selector Simple selector to filter the elements by. e.g. 'a', 'div.main'\n * @return {!Array<string>} List of anchor hrefs.\n * @author ebidel@ (Eric Bidelman)\n * License Apache-2.0\n */\nexport function collectAllElementsDeep(\n  selector = null,\n  root,\n  cachedElements = null\n) {\n  let allElements = [];\n\n  if (cachedElements) {\n    allElements = cachedElements;\n  } else {\n    const findAllElements = function (nodes) {\n      for (let i = 0; i < nodes.length; i++) {\n        const el = nodes[i];\n        allElements.push(el);\n\n        const shadowRoot = getShadowRoot(el);\n        // If the element has a shadow root, dig deeper.\n        if (shadowRoot) {\n          findAllElements(shadowRoot.querySelectorAll('*'));\n        }\n      }\n    };\n\n    const rootShadowRoot = getShadowRoot(root);\n    if (rootShadowRoot) {\n      findAllElements(rootShadowRoot.querySelectorAll('*'));\n    }\n    findAllElements(root.querySelectorAll('*'));\n  }\n\n  return selector\n    ? allElements.filter((el) => el.matches(selector))\n    : allElements;\n}\n"
  },
  {
    "path": "src/lib/query-selector-shadow-dom/normalize.js",
    "content": "/* istanbul ignore file */\n/* eslint-disable */\n\n// normalize-selector-rev-02.js\n/*\n  author: kyle simpson (@getify)\n  original source: https://gist.github.com/getify/9679380\n\n  modified for tests by david kaye (@dfkaye)\n  21 march 2014\n\n  rev-02 incorporate kyle's changes 3/2/42014\n*/\n\nexport function normalizeSelector(sel) {\n  // save unmatched text, if any\n  function saveUnmatched() {\n    if (unmatched) {\n      // whitespace needed after combinator?\n      if (tokens.length > 0 && /^[~+>]$/.test(tokens[tokens.length - 1])) {\n        tokens.push(' ');\n      }\n\n      // save unmatched text\n      tokens.push(unmatched);\n    }\n  }\n\n  var tokens = [];\n  let match;\n  let unmatched;\n  let regex;\n  const state = [0];\n  let next_match_idx = 0;\n  let prev_match_idx;\n  const not_escaped_pattern = /(?:[^\\\\]|(?:^|[^\\\\])(?:\\\\\\\\)+)$/;\n  const whitespace_pattern = /^\\s+$/;\n  const state_patterns = [\n    /\\s+|\\/\\*|[\"'>~+[(]/g, // general\n    /\\s+|\\/\\*|[\"'[\\]()]/g, // [..] set\n    /\\s+|\\/\\*|[\"'[\\]()]/g, // (..) set\n    null, // string literal (placeholder)\n    /\\*\\//g, // comment\n  ];\n  sel = sel.trim();\n\n  // eslint-disable-next-line no-constant-condition\n  while (true) {\n    unmatched = '';\n\n    regex = state_patterns[state[state.length - 1]];\n\n    regex.lastIndex = next_match_idx;\n    match = regex.exec(sel);\n\n    // matched text to process?\n    if (match) {\n      prev_match_idx = next_match_idx;\n      next_match_idx = regex.lastIndex;\n\n      // collect the previous string chunk not matched before this token\n      if (prev_match_idx < next_match_idx - match[0].length) {\n        unmatched = sel.substring(\n          prev_match_idx,\n          next_match_idx - match[0].length\n        );\n      }\n\n      // general, [ ] pair, ( ) pair?\n      if (state[state.length - 1] < 3) {\n        saveUnmatched();\n\n        // starting a [ ] pair?\n        if (match[0] === '[') {\n          state.push(1);\n        }\n        // starting a ( ) pair?\n        else if (match[0] === '(') {\n          state.push(2);\n        }\n        // starting a string literal?\n        else if (/^[\"']$/.test(match[0])) {\n          state.push(3);\n          state_patterns[3] = new RegExp(match[0], 'g');\n        }\n        // starting a comment?\n        else if (match[0] === '/*') {\n          state.push(4);\n        }\n        // ending a [ ] or ( ) pair?\n        else if (/^[\\])]$/.test(match[0]) && state.length > 0) {\n          state.pop();\n        }\n        // handling whitespace or a combinator?\n        else if (/^(?:\\s+|[~+>])$/.test(match[0])) {\n          // need to insert whitespace before?\n          if (\n            tokens.length > 0 &&\n            !whitespace_pattern.test(tokens[tokens.length - 1]) &&\n            state[state.length - 1] === 0\n          ) {\n            // add normalized whitespace\n            tokens.push(' ');\n          }\n\n          // case-insensitive attribute selector CSS L4\n          if (\n            state[state.length - 1] === 1 &&\n            tokens.length === 5 &&\n            tokens[2].charAt(tokens[2].length - 1) === '='\n          ) {\n            tokens[4] = ` ${tokens[4]}`;\n          }\n\n          // whitespace token we can skip?\n          if (whitespace_pattern.test(match[0])) {\n            continue;\n          }\n        }\n\n        // save matched text\n        tokens.push(match[0]);\n      }\n      // otherwise, string literal or comment\n      else {\n        // save unmatched text\n        tokens[tokens.length - 1] += unmatched;\n\n        // unescaped terminator to string literal or comment?\n        if (not_escaped_pattern.test(tokens[tokens.length - 1])) {\n          // comment terminator?\n          if (state[state.length - 1] === 4) {\n            // ok to drop comment?\n            if (\n              tokens.length < 2 ||\n              whitespace_pattern.test(tokens[tokens.length - 2])\n            ) {\n              tokens.pop();\n            }\n            // otherwise, turn comment into whitespace\n            else {\n              tokens[tokens.length - 1] = ' ';\n            }\n\n            // handled already\n            match[0] = '';\n          }\n\n          state.pop();\n        }\n\n        // append matched text to existing token\n        tokens[tokens.length - 1] += match[0];\n      }\n    }\n    // otherwise, end of processing (no more matches)\n    else {\n      unmatched = sel.substr(next_match_idx);\n      saveUnmatched();\n\n      break;\n    }\n  }\n\n  return tokens.join('').trim();\n}\n"
  },
  {
    "path": "src/lib/tippy.js",
    "content": "import tippy from 'tippy.js';\nimport 'tippy.js/animations/shift-toward-subtle.css';\n\nexport const defaultOptions = {\n  animation: 'shift-toward-subtle',\n  theme: 'my-theme',\n};\n\nexport default function (el, options = {}) {\n  el?.setAttribute('vtooltip', '');\n\n  const instance = tippy(el, {\n    ...defaultOptions,\n    ...options,\n  });\n\n  return instance;\n}\n"
  },
  {
    "path": "src/lib/tmpl.js",
    "content": "import * as tmpl from '@n8n_io/riot-tmpl';\n\ntmpl.brackets.set('{{ }}');\n\nexport default tmpl;\n"
  },
  {
    "path": "src/lib/vRemixicon.js",
    "content": "import {\n  riAB,\n  riAddLine,\n  riAlertLine,\n  riArrowDropDownLine,\n  riArrowGoBackLine,\n  riArrowGoForwardLine,\n  riArrowLeftLine,\n  riArrowLeftRightLine,\n  riArrowLeftSLine,\n  riArrowRightLine,\n  riArrowRightUpLine,\n  riArrowUpDownLine,\n  riArticleLine,\n  riBaseStationLine,\n  riBold,\n  riBook3Line,\n  riBracketsLine,\n  riBrush2Line,\n  riBug2Line,\n  riCalendarLine,\n  riChat3Line,\n  riCheckboxCircleLine,\n  riCheckDoubleLine,\n  riCheckLine,\n  riClipboardLine,\n  riCloseCircleLine,\n  riCloseLine,\n  riCodeSSlashLine,\n  riCommandLine,\n  riCompass3Line,\n  riComputerLine,\n  riCursorLine,\n  riDatabase2Line,\n  riDeleteBin7Line,\n  riDiscordLine,\n  riDoubleQuotesL,\n  riDownloadCloud2Line,\n  riDownloadLine,\n  riDragDropLine,\n  riDriveFill,\n  riDriveLine,\n  riEarthLine,\n  riEditBoxLine,\n  riEqualizerLine,\n  riErrorWarningLine,\n  riExternalLinkLine,\n  riEyeLine,\n  riEyeOffLine,\n  riFileCopyLine,\n  riFileDownloadLine,\n  riFileEditLine,\n  riFileHistoryLine,\n  riFileLine,\n  riFileListLine,\n  riFileShredLine,\n  riFileTextLine,\n  riFileUploadLine,\n  riFilter2Line,\n  riFlagLine,\n  riFlashlightLine,\n  riFlowChart,\n  riFocus3Line,\n  riFocusLine,\n  riFolderLine,\n  riFolderOpenLine,\n  riFolderZipLine,\n  riFontSize2,\n  riFullscreenLine,\n  riGithubFill,\n  riGlobalLine,\n  riGroupLine,\n  riGuideLine,\n  riH1,\n  riH2,\n  riHandHeartLine,\n  riHardDrive2Line,\n  riHistoryLine,\n  riHome5Line,\n  riHtml5Line,\n  riImageLine,\n  riIncreaseDecreaseLine,\n  riInformationLine,\n  riInputCursorMove,\n  riItalic,\n  riKey2Line,\n  riKeyboardLine,\n  riLightbulbFlashLine,\n  riLightbulbLine,\n  riLink,\n  riLinkM,\n  riLinksLine,\n  riLinkUnlinkM,\n  riLoader2Line,\n  riLock2Line,\n  riLoginCircleLine,\n  riLogoutCircleRLine,\n  riMagicLine,\n  riMindMap,\n  riMore2Line,\n  riMoreLine,\n  riMouseLine,\n  riNotification3Line,\n  riParagraph,\n  riPauseLine,\n  riPencilLine,\n  riPlayLine,\n  riPushpin2Fill,\n  riPushpin2Line,\n  riRecordCircleFill,\n  riRecordCircleLine,\n  riRefreshFill,\n  riRefreshLine,\n  riRepeat2Line,\n  riRestartLine,\n  riSaveLine,\n  riSearch2Line,\n  riSettings3Line,\n  riShareLine,\n  riShieldKeyholeLine,\n  riShieldLine,\n  riSideBarFill,\n  riSideBarLine,\n  riSliceLine,\n  riSortAsc,\n  riSortDesc,\n  riStopLine,\n  riStrikethrough2,\n  riSubtractLine,\n  riTable2,\n  riTBoxLine,\n  riTeamLine,\n  riTimeLine,\n  riTimerFlashLine,\n  riTimerLine,\n  riToggleFill,\n  riToggleLine,\n  riTwitterLine,\n  riUploadCloud2Line,\n  riUploadLine,\n  riUser3Line,\n  riUserLine,\n  riWindow2Line,\n  riWindowLine,\n  riYoutubeLine,\n} from 'v-remixicon/icons';\nimport { computed, h, inject } from 'vue';\n\nexport const icons = {\n  riH1,\n  riH2,\n  riAB,\n  riBold,\n  riLink,\n  riLinkM,\n  riItalic,\n  riTable2,\n  riEyeLine,\n  riAddLine,\n  riSortAsc,\n  riMindMap,\n  riKey2Line,\n  riTBoxLine,\n  riSaveLine,\n  riPlayLine,\n  riMoreLine,\n  riStopLine,\n  riSortDesc,\n  riTimeLine,\n  riFlagLine,\n  riFileLine,\n  riBug2Line,\n  riTeamLine,\n  riLinksLine,\n  riGroupLine,\n  riGuideLine,\n  riChat3Line,\n  riEarthLine,\n  riLock2Line,\n  riSliceLine,\n  riHome5Line,\n  riShareLine,\n  riBook3Line,\n  riPauseLine,\n  riFlowChart,\n  riMore2Line,\n  riMouseLine,\n  riFocusLine,\n  riFontSize2,\n  riParagraph,\n  riImageLine,\n  riCloseLine,\n  riCheckLine,\n  riTimerLine,\n  riMagicLine,\n  riHtml5Line,\n  riToggleFill,\n  riToggleLine,\n  riFolderLine,\n  riAlertLine,\n  riGithubFill,\n  riEyeOffLine,\n  riWindowLine,\n  riPencilLine,\n  riBrush2Line,\n  riGlobalLine,\n  riShieldLine,\n  riCursorLine,\n  riUploadLine,\n  riFocus3Line,\n  riTwitterLine,\n  riDiscordLine,\n  riLinkUnlinkM,\n  riYoutubeLine,\n  riSideBarLine,\n  riSideBarFill,\n  riWindow2Line,\n  riRefreshLine,\n  riRefreshFill,\n  riFilter2Line,\n  riRestartLine,\n  riSearch2Line,\n  riEditBoxLine,\n  riHistoryLine,\n  riRepeat2Line,\n  riCommandLine,\n  riArticleLine,\n  riKeyboardLine,\n  riFileEditLine,\n  riCompass3Line,\n  riFolderOpenLine,\n  riComputerLine,\n  riFileCopyLine,\n  riCalendarLine,\n  riFileTextLine,\n  riSubtractLine,\n  riBracketsLine,\n  riPushpin2Line,\n  riPushpin2Fill,\n  riDownloadCloud2Line,\n  riDownloadLine,\n  riFileListLine,\n  riDragDropLine,\n  riDriveLine,\n  riDriveFill,\n  riClipboardLine,\n  riCheckDoubleLine,\n  riDoubleQuotesL,\n  riLightbulbLine,\n  riFolderZipLine,\n  riFileShredLine,\n  riHandHeartLine,\n  riDatabase2Line,\n  riSettings3Line,\n  riArrowLeftLine,\n  riEqualizerLine,\n  riStrikethrough2,\n  riFileUploadLine,\n  riCodeSSlashLine,\n  riHardDrive2Line,\n  riDeleteBin7Line,\n  riArrowLeftSLine,\n  riFullscreenLine,\n  riFlashlightLine,\n  riTimerFlashLine,\n  riBaseStationLine,\n  riFileHistoryLine,\n  riInformationLine,\n  riArrowUpDownLine,\n  riArrowGoBackLine,\n  riInputCursorMove,\n  riCloseCircleLine,\n  riRecordCircleFill,\n  riRecordCircleLine,\n  riErrorWarningLine,\n  riExternalLinkLine,\n  riUploadCloud2Line,\n  riFileDownloadLine,\n  riShieldKeyholeLine,\n  riArrowDropDownLine,\n  riNotification3Line,\n  riArrowLeftRightLine,\n  riArrowGoForwardLine,\n  riCheckboxCircleLine,\n  riLightbulbFlashLine,\n  riIncreaseDecreaseLine,\n  riArrowRightLine,\n  riArrowRightUpLine,\n  riLoginCircleLine,\n  riLogoutCircleRLine,\n  riUser3Line,\n  riUserLine,\n  riLoader2Line,\n  riKey:\n    'M14.52 2C15.549 2 16.535 2.409 17.262 3.136L20.864 6.738C21.224 7.09801 21.5096 7.52542 21.7045 7.99581C21.8993 8.46619 21.9996 8.97035 21.9996 9.4795C21.9996 9.98865 21.8993 10.4928 21.7045 10.9632C21.5096 11.4336 21.224 11.861 20.864 12.221L18.221 14.864C17.5799 15.5045 16.7347 15.9004 15.8322 15.983C14.9297 16.0656 14.0267 15.8296 13.28 15.316L13.175 15.238L7.293 21.121C6.83776 21.5747 6.24884 21.8702 5.613 21.964L5.393 21.991L5.172 22H4C2.986 22 2.133 21.241 2.009 20.177L2 20V18.828C2 18.124 2.248 17.442 2.73 16.868L2.879 16.707L3.293 16.293C3.48049 16.1054 3.73481 16.0001 4 16H5V15C5.00003 14.7551 5.08996 14.5187 5.25272 14.3356C5.41547 14.1526 5.63975 14.0357 5.883 14.007L6 14H7V13C6.99998 12.7802 7.07238 12.5665 7.206 12.392L7.293 12.292L8.761 10.823L8.685 10.72C8.28573 10.139 8.05142 9.46059 8.007 8.757L8 8.521C8 7.492 8.409 6.506 9.136 5.779L11.779 3.136C12.5061 2.40915 13.4919 2.00057 14.52 2ZM15.015 7H14.995C14.7324 7 14.4723 7.05173 14.2296 7.15224C13.987 7.25275 13.7665 7.40007 13.5808 7.58579C13.3951 7.7715 13.2478 7.99198 13.1472 8.23463C13.0467 8.47728 12.995 8.73736 12.995 9C12.995 9.26264 13.0467 9.52272 13.1472 9.76537C13.2478 10.008 13.3951 10.2285 13.5808 10.4142C13.7665 10.5999 13.987 10.7472 14.2296 10.8478C14.4723 10.9483 14.7324 11 14.995 11H15.015C15.5454 11 16.0541 10.7893 16.4292 10.4142C16.8043 10.0391 17.015 9.53043 17.015 9C17.015 8.46957 16.8043 7.96086 16.4292 7.58579C16.0541 7.21071 15.5454 7 15.015 7Z',\n  mdiEqual: 'M19,10H5V8H19V10M19,16H5V14H19V16Z',\n  mdiPackageVariantClosed:\n    'M21,16.5C21,16.88 20.79,17.21 20.47,17.38L12.57,21.82C12.41,21.94 12.21,22 12,22C11.79,22 11.59,21.94 11.43,21.82L3.53,17.38C3.21,17.21 3,16.88 3,16.5V7.5C3,7.12 3.21,6.79 3.53,6.62L11.43,2.18C11.59,2.06 11.79,2 12,2C12.21,2 12.41,2.06 12.57,2.18L20.47,6.62C20.79,6.79 21,7.12 21,7.5V16.5M12,4.15L10.11,5.22L16,8.61L17.96,7.5L12,4.15M6.04,7.5L12,10.85L13.96,9.75L8.08,6.35L6.04,7.5M5,15.91L11,19.29V12.58L5,9.21V15.91M19,15.91V9.21L13,12.58V19.29L19,15.91Z',\n  mdiVariable:\n    'M20.41,3C21.8,5.71 22.35,8.84 22,12C21.8,15.16 20.7,18.29 18.83,21L17.3,20C18.91,17.57 19.85,14.8 20,12C20.34,9.2 19.89,6.43 18.7,4L20.41,3M5.17,3L6.7,4C5.09,6.43 4.15,9.2 4,12C3.66,14.8 4.12,17.57 5.3,20L3.61,21C2.21,18.29 1.65,15.17 2,12C2.2,8.84 3.3,5.71 5.17,3M12.08,10.68L14.4,7.45H16.93L13.15,12.45L15.35,17.37H13.09L11.71,14L9.28,17.33H6.76L10.66,12.21L8.53,7.45H10.8L12.08,10.68Z',\n  mdiRegex:\n    'M16,16.92C15.67,16.97 15.34,17 15,17C14.66,17 14.33,16.97 14,16.92V13.41L11.5,15.89C11,15.5 10.5,15 10.11,14.5L12.59,12H9.08C9.03,11.67 9,11.34 9,11C9,10.66 9.03,10.33 9.08,10H12.59L10.11,7.5C10.3,7.25 10.5,7 10.76,6.76V6.76C11,6.5 11.25,6.3 11.5,6.11L14,8.59V5.08C14.33,5.03 14.66,5 15,5C15.34,5 15.67,5.03 16,5.08V8.59L18.5,6.11C19,6.5 19.5,7 19.89,7.5L17.41,10H20.92C20.97,10.33 21,10.66 21,11C21,11.34 20.97,11.67 20.92,12H17.41L19.89,14.5C19.7,14.75 19.5,15 19.24,15.24V15.24C19,15.5 18.75,15.7 18.5,15.89L16,13.41V16.92H16V16.92M5,19A2,2 0 0,1 7,17A2,2 0 0,1 9,19A2,2 0 0,1 7,21A2,2 0 0,1 5,19H5Z',\n  mdiCookieOutline:\n    'M20.87 10.5C20.6 10 20 10 20 10H18V9C18 8 17 8 17 8H15V7C15 6 14 6 14 6H13V4C13 3 12 3 12 3C7.03 3 3 7.03 3 12C3 16.97 7.03 21 12 21C16.97 21 21 16.97 21 12C21 11.5 20.96 11 20.87 10.5M11.32 18.96C12 18.82 12.5 18.22 12.5 17.5C12.5 16.67 11.83 16 11 16S9.5 16.67 9.5 17.5C9.5 18 9.76 18.47 10.16 18.74C7.54 18.04 5.5 15.81 5.09 13.12C5 12.61 5 12.11 5 11.62C5.07 12.39 5.71 13 6.5 13C7.33 13 8 12.33 8 11.5S7.33 10 6.5 10C5.82 10 5.25 10.46 5.07 11.08C5.47 8 7.91 5.5 11 5.07V6.5C11 7.33 11.67 8 12.5 8H13V8.5C13 9.33 13.67 10 14.5 10H16V10.5C16 11.33 16.67 12 17.5 12H19C19 16.08 15.5 19.36 11.32 18.96M9.5 9C8.67 9 8 8.33 8 7.5S8.67 6 9.5 6 11 6.67 11 7.5 10.33 9 9.5 9M13 12.5C13 13.33 12.33 14 11.5 14S10 13.33 10 12.5 10.67 11 11.5 11 13 11.67 13 12.5M18 14.5C18 15.33 17.33 16 16.5 16S15 15.33 15 14.5 15.67 13 16.5 13 18 13.67 18 14.5Z',\n  mdiDrag:\n    'M7,19V17H9V19H7M11,19V17H13V19H11M15,19V17H17V19H15M7,15V13H9V15H7M11,15V13H13V15H11M15,15V13H17V15H15M7,11V9H9V11H7M11,11V9H13V11H11M15,11V9H17V11H15M7,7V5H9V7H7M11,7V5H13V7H11M15,7V5H17V7H15Z',\n  webhookIcon:\n    'M11.1339746,6.5 C11.410117,6.02170738 12.0217074,5.85783222 12.5,6.1339746 C12.6618283,6.22740623 12.7876628,6.35923925 12.8726385,6.51131768 L12.8728316,6.51197847 L15.5280985,11.258439 C17.7104719,10.5794609 20.1487626,11.4726335 21.3395353,13.5351122 C22.7202472,15.9265753 21.9008714,18.9845274 19.5094083,20.3652392 C18.0316717,21.2184108 16.2512131,21.2502691 14.7625925,20.5024023 L14.5614635,20.3955914 L15.5391998,18.650876 C16.4572736,19.1653634 17.5817121,19.1687941 18.5094083,18.6331884 C19.9442862,17.8047613 20.4359116,15.9699901 19.6074845,14.5351122 C18.8086441,13.15148 17.0740576,12.6449281 15.6646114,13.35331 L15.5094083,13.437036 L15.4923149,13.408 L14.6887376,13.8579251 L11.1271684,7.48802153 C10.9611644,7.19043272 10.9514715,6.81610467 11.1339746,6.5 Z M3.69009425,12.2357175 L5.00912623,13.7390986 C4.15631502,14.4873355 3.78941522,15.6581523 4.08821537,16.7732896 C4.5170408,18.373688 6.16205167,19.3234354 7.76244998,18.89461 C9.00692272,18.5611545 9.88400974,17.477653 9.97851857,16.2240731 L9.98665134,16.0438959 L10.0023149,16.043 L10.0029449,15 L17,15 C17.1696897,14.9996082 17.342108,15.0428156 17.5,15.1339746 C17.9782926,15.410117 18.1421678,16.0217074 17.8660254,16.5 C17.6809741,16.8205182 17.3452819,16.9998396 17.000012,17.0001665 L17,17 L11.8855308,17.0005501 C11.512912,18.8188035 10.1440497,20.3270146 8.28008807,20.8264616 C5.61275756,21.5411707 2.87107277,19.9582583 2.15636371,17.2909277 C1.67853507,15.507647 2.22314594,13.6374116 3.52361777,12.3885391 L3.69009425,12.2357175 Z M12,2 C14.6887547,2 16.8818181,4.12230671 16.9953805,6.78311038 L17,7 L15,7 C15,5.34314575 13.6568542,4 12,4 C10.3431458,4 9,5.34314575 9,7 C9,8.03663591 9.52964394,8.97937227 10.3797433,9.52549875 L10.4663149,9.577 L11.4242264,10.1109289 L7.87365874,16.4865392 L7.8723149,16.486 L7.8660254,16.5 C7.60960748,16.9441289 7.06395121,17.1171529 6.60436072,16.9185096 L6.5,16.8660254 C6.02170738,16.589883 5.85783222,15.9782926 6.1339746,15.5 L6.12634126,15.5134608 L8.7500113,10.80028 C7.65887804,9.86710067 7,8.49115157 7,7 C7,4.23857625 9.23857625,2 12,2 Z',\n  mdiGoogleSheet:\n    'M19,11V9H11V5H9V9H5V11H9V19H11V11H19M19,3C19.5,3 20,3.2 20.39,3.61C20.8,4 21,4.5 21,5V19C21,19.5 20.8,20 20.39,20.39C20,20.8 19.5,21 19,21H5C4.5,21 4,20.8 3.61,20.39C3.2,20 3,19.5 3,19V5C3,4.5 3.2,4 3.61,3.61C4,3.2 4.5,3 5,3H19Z',\n  mdiCursorDefaultClickOutline:\n    'M11.5,11L17.88,16.37L17,16.55L16.36,16.67C15.73,16.8 15.37,17.5 15.65,18.07L15.92,18.65L17.28,21.59L15.86,22.25L14.5,19.32L14.24,18.74C13.97,18.15 13.22,17.97 12.72,18.38L12.21,18.78L11.5,19.35V11M10.76,8.69A0.76,0.76 0 0,0 10,9.45V20.9C10,21.32 10.34,21.66 10.76,21.66C10.95,21.66 11.11,21.6 11.24,21.5L13.15,19.95L14.81,23.57C14.94,23.84 15.21,24 15.5,24C15.61,24 15.72,24 15.83,23.92L18.59,22.64C18.97,22.46 19.15,22 18.95,21.63L17.28,18L19.69,17.55C19.85,17.5 20,17.43 20.12,17.29C20.39,16.97 20.35,16.5 20,16.21L11.26,8.86L11.25,8.87C11.12,8.76 10.95,8.69 10.76,8.69M15,10V8H20V10H15M13.83,4.76L16.66,1.93L18.07,3.34L15.24,6.17L13.83,4.76M10,0H12V5H10V0M3.93,14.66L6.76,11.83L8.17,13.24L5.34,16.07L3.93,14.66M3.93,3.34L5.34,1.93L8.17,4.76L6.76,6.17L3.93,3.34M7,10H2V8H7V10',\n};\n\nconst component = {\n  name: 'v-remixicon',\n  props: {\n    name: String,\n    title: String,\n    viewBox: {\n      type: String,\n      default: '0 0 24 24',\n    },\n    size: {\n      type: [String, Number],\n      default: 24,\n    },\n    fill: {\n      type: String,\n      default: 'currentColor',\n    },\n    rotate: {\n      type: [Number, String],\n      default: 0,\n    },\n    path: {\n      type: String,\n      default: '',\n    },\n  },\n  setup(props) {\n    const injectIcons = inject('remixicons');\n    const icon = computed(() => {\n      if (props.path) return props.path;\n\n      const iconStr = injectIcons[props.name];\n\n      if (typeof iconStr === 'undefined') {\n        console.error(\n          `[v-remixicon] ${props.name} name of the icon is incorrect`\n        );\n        return null;\n      }\n\n      return iconStr;\n    });\n\n    return () =>\n      h(\n        'svg',\n        {\n          viewBox: props.viewBox,\n          fill: props.fill,\n          height: props.size,\n          width: props.size,\n          class: 'v-remixicon',\n          xmlns: 'http://www.w3.org/2000/svg',\n          style: {\n            transform: props.rotate ? `rotate(${props.rotate}deg)` : null,\n          },\n        },\n        [\n          props.title ? h('title', {}, props.title) : null,\n          h('g', {}, [\n            h('path', { fill: 'none', d: 'M0 0h24v24H0z' }),\n            h('path', {\n              'fill-rule': 'nonzero',\n              d: icon.value,\n            }),\n          ]),\n        ]\n      );\n  },\n};\n\nexport default {\n  install(app) {\n    app.provide('remixicons', icons);\n    app.component('VRemixicon', component);\n  },\n};\n"
  },
  {
    "path": "src/lib/vue-toastification.js",
    "content": "import Toast from 'vue-toastification';\nimport 'vue-toastification/dist/index.css';\n\nexport default Toast;\n"
  },
  {
    "path": "src/lib/vueI18n.js",
    "content": "import { nextTick } from 'vue';\nimport { createI18n } from 'vue-i18n/dist/vue-i18n.esm-bundler';\nimport { supportLocales } from '@/utils/shared';\nimport dayjs from './dayjs';\n\nconst i18n = createI18n({\n  legacy: false,\n  fallbackLocale: 'en',\n});\n\nexport function setI18nLanguage(locale) {\n  i18n.global.locale.value = locale;\n\n  document.querySelector('html').setAttribute('lang', locale);\n}\n\nexport async function loadLocaleMessages(locale, location) {\n  const isLocaleSupported = supportLocales.some(({ id }) => id === locale);\n\n  if (!isLocaleSupported) {\n    console.error(`${locale} locale is not supported`);\n\n    return null;\n  }\n\n  const importLocale = async (path, merge = false) => {\n    try {\n      const messages = await import(\n        /* webpackChunkName: \"locales/locale-[request]\" */ `../locales/${locale}/${path}`\n      );\n\n      if (merge) {\n        i18n.global.mergeLocaleMessage(locale, messages.default);\n      } else {\n        i18n.global.setLocaleMessage(locale, messages.default);\n      }\n    } catch (error) {\n      console.error(error);\n    }\n  };\n\n  if (locale !== 'en' && !i18n.global.availableLocales.includes('en')) {\n    await loadLocaleMessages('en', location);\n  }\n\n  dayjs.locale(locale);\n\n  await importLocale('common.json');\n  await importLocale('popup.json', true);\n  await importLocale(`${location}.json`, true);\n  await importLocale('blocks.json', true);\n\n  return nextTick();\n}\n\nexport default i18n;\n"
  },
  {
    "path": "src/locales/en/blocks.json",
    "content": "{\n  \"collection\": {\n    \"blocks\": {\n      \"export-result\": {\n        \"name\": \"Export result\",\n        \"description\": \"Export the collection result as JSON\"\n      }\n    }\n  },\n  \"workflow\": {\n    \"blocks\": {\n      \"base\": {\n        \"title\": \"Blocks\",\n        \"moveToGroup\": \"Move block to blocks group\",\n        \"selector\": \"Element selector\",\n        \"selectorOptions\": \"Selector options\",\n        \"timeout\": \"Timeout (milliseconds)\",\n        \"noPermission\": \"Automa doesn't have enough permissions to perform this action\",\n        \"grantPermission\": \"Grant permission\",\n        \"action\": \"Action\",\n        \"element\": {\n          \"select\": \"Select an element\",\n          \"verify\": \"Verify selector\"\n        },\n        \"settings\": {\n          \"title\": \"Block settings\",\n          \"blockTimeout\": {\n            \"title\": \"Block execution timeout (millisecond)\",\n            \"description\": \"The maximum execution time of the block (0 to disable)\"\n          },\n          \"line\": {\n            \"title\": \"Lines\",\n            \"label\": \"Label\",\n            \"animated\": \"Animated\",\n            \"select\": \"Select line\",\n            \"to\": \"Line to {name} block\",\n            \"lineColor\": \"Color\"\n          }\n        },\n        \"toggle\": {\n          \"enable\": \"Enable block\",\n          \"disable\": \"Disable block\"\n        },\n        \"onError\": {\n          \"info\": \"These rules will apply when an error occurs on the block\",\n          \"button\": \"On error\",\n          \"title\": \"On error occurs\",\n          \"retry\": \"Retry action\",\n          \"fallbackTitle\": \"Will execute when an error occurs in the block\",\n          \"times\": {\n            \"name\": \"Times\",\n            \"description\": \"The number of times to retry the action\"\n          },\n          \"interval\": {\n            \"name\": \"Interval\",\n            \"description\": \"The time interval to wait between each try\",\n            \"second\": \"second\"\n          },\n          \"toDo\": {\n            \"error\": \"Throw error\",\n            \"continue\": \"Continue flow\",\n            \"fallback\": \"Execute fallback\",\n            \"restart\": \"Restart flow\"\n          },\n          \"insertData\": {\n            \"name\": \"Insert data\"\n          }\n        },\n        \"table\": {\n          \"checkbox\": \"Insert to table\",\n          \"select\": \"Select column\",\n          \"extraRow\": {\n            \"checkbox\": \"Add extra row\",\n            \"placeholder\": \"Value\",\n            \"title\": \"Value of the extra row\"\n          }\n        },\n        \"findElement\": {\n          \"placeholder\": \"Find element by\",\n          \"options\": {\n            \"cssSelector\": \"CSS Selector\",\n            \"xpath\": \"XPath\"\n          }\n        },\n        \"markElement\": {\n          \"title\": \"An element will not be selected if have been selected before\",\n          \"text\": \"Mark element\"\n        },\n        \"multiple\": {\n          \"title\": \"Select multiple element\",\n          \"text\": \"Multiple\"\n        },\n        \"waitSelector\": {\n          \"title\": \"Wait for selector\",\n          \"timeout\": \"Selector timeout (ms)\"\n        },\n        \"downloads\": {\n          \"onConflict\": {\n            \"uniquify\": \"Uniquify\",\n            \"overwrite\": \"Overwrite\",\n            \"prompt\": \"Prompt\"\n          }\n        }\n      },\n      \"ai-workflow\": {\n        \"name\": \"AI Workflow\",\n        \"description\": \"A workflow that is created by AI-Power\"\n      },\n      \"wait-connections\": {\n        \"name\": \"Wait connections\",\n        \"description\": \"Wait for all connections before continuing to the next block\",\n        \"specificFlow\": \"Only continue a specific flow\",\n        \"selectFlow\": \"Select flow\"\n      },\n      \"cookie\": {\n        \"name\": \"Cookie\",\n        \"description\": \"Get, set, or remove cookies\",\n        \"types\": {\n          \"get\": \"Get cookies\",\n          \"set\": \"Set cookie\",\n          \"remove\": \"Remove cookies\",\n          \"getAll\": \"Get all cookies\"\n        },\n        \"useJson\": \"Use JSON format\"\n      },\n      \"note\": {\n        \"name\": \"Note\"\n      },\n      \"slice-variable\": {\n        \"name\": \"Slice variable\",\n        \"description\": \"Extracts a section of a variable value\",\n        \"start\": \"Start index\",\n        \"end\": \"End index\"\n      },\n      \"workflow-state\": {\n        \"name\": \"Workflow state\",\n        \"description\": \"Manage workflows states\",\n        \"actions\": {\n          \"stop\": \"Stop workflows\"\n        },\n        \"error\": {\n          \"throwError\": \"Throw error\",\n          \"message\": \"Error message\"\n        }\n      },\n      \"regex-variable\": {\n        \"name\": \"RegEx variable\",\n        \"description\": \"Matching a variable value against a regular expression\"\n      },\n      \"data-mapping\": {\n        \"source\": \"Source\",\n        \"destination\": \"Destination\",\n        \"name\": \"Data mapping\",\n        \"edit\": \"Edit data map\",\n        \"dataSource\": \"Data source\",\n        \"description\": \"Map data of a variable or table\",\n        \"addSource\": \"Add source\",\n        \"addDestination\": \"Add destination\"\n      },\n      \"sort-data\": {\n        \"name\": \"Sort data\",\n        \"description\": \"Sort the items of data\",\n        \"property\": \"Sort by the item's property\",\n        \"addProperty\": \"Add property\"\n      },\n      \"increase-variable\": {\n        \"name\": \"Increase variable\",\n        \"description\": \"Increase the value of a variable by a specific amount\",\n        \"increase\": \"Increase by\"\n      },\n      \"notification\": {\n        \"name\": \"notification\",\n        \"description\": \"Display a notification\",\n        \"title\": \"Title\",\n        \"message\": \"Message\",\n        \"imageUrl\": \"Image URL (optional)\",\n        \"iconUrl\": \"Icon URL (optional)\"\n      },\n      \"delete-data\": {\n        \"name\": \"Delete data\",\n        \"description\": \"Delete table or variable data\",\n        \"from\": \"Data from\",\n        \"allColumns\": \"[All columns]\"\n      },\n      \"log-data\": {\n        \"name\": \"Get log data\",\n        \"description\": \"Get the latest log data of a workflow\",\n        \"data\": \"Log data\"\n      },\n      \"tab-url\": {\n        \"name\": \"Get tab URL\",\n        \"description\": \"Get the tab URL\",\n        \"select\": \"Select tab\",\n        \"types\": {\n          \"active-tab\": \"Active tab\",\n          \"all\": \"All tabs\"\n        },\n        \"query\": {\n          \"title\": \"Query\",\n          \"matchPatterns\": \"@:workflow.blocks.switch-tab.matchPattern (optional)\",\n          \"tabTitle\": \"Tab title (optional)\"\n        }\n      },\n      \"reload-tab\": {\n        \"name\": \"Reload tab\",\n        \"description\": \"Reload the active tab\"\n      },\n      \"press-key\": {\n        \"name\": \"Press key\",\n        \"description\": \"Press a key or a combination\",\n        \"target\": \"Target element (optional)\",\n        \"key\": \"Key\",\n        \"detect\": \"Detect key\",\n        \"actions\": {\n          \"press-key\": \"Press a key\",\n          \"multiple-keys\": \"Press multiple keys\"\n        },\n        \"press-time\": \"Press time (milliseconds)\"\n      },\n      \"save-assets\": {\n        \"name\": \"Save assets\",\n        \"description\": \"Save assets (image, video, audio, or file) from an element or URL\",\n        \"filename\": \"Filename (optional)\",\n        \"saveDownloadIds\": \"Save items' download IDs\",\n        \"contentTypes\": {\n          \"title\": \"Type\",\n          \"element\": \"Media element (image, audio, or video)\",\n          \"url\": \"URL\"\n        }\n      },\n      \"handle-dialog\": {\n        \"name\": \"Handle dialog\",\n        \"description\": \"Accepts or dismisses a JavaScript initiated dialog (alert, confirm, prompt, or onbeforeunload)\",\n        \"accept\": \"Accept dialog\",\n        \"promptText\": {\n          \"label\": \"Prompt text (optional)\",\n          \"description\": \"The text to enter into the prompt dialog before accepting\"\n        }\n      },\n      \"handle-download\": {\n        \"name\": \"Handle download\",\n        \"description\": \"Handle downloaded file\",\n        \"timeout\": \"Timeout (milliseconds)\",\n        \"noPermission\": \"Don't have permission to access the downloads\",\n        \"onConflict\": \"On conflict\",\n        \"waitFile\": \"Wait for the file to be downloaded\",\n        \"downloadId\": \"File download ID (optional)\",\n        \"filePath\": \"File path\"\n      },\n      \"insert-data\": {\n        \"name\": \"Insert data\",\n        \"description\": \"Insert data into table or variable\"\n      },\n      \"clipboard\": {\n        \"name\": \"Clipboard\",\n        \"description\": \"Get the copied text from the clipboard\",\n        \"data\": \"Clipboard data\",\n        \"noPermission\": \"Don't have permission to access the clipboard\",\n        \"grantPermission\": \"Grant permission\",\n        \"copySelection\": \"Copy the selected text on page\",\n        \"types\": {\n          \"get\": \"Get clipboard data\",\n          \"insert\": \"Insert text to clipboard\"\n        }\n      },\n      \"hover-element\": {\n        \"name\": \"Hover element\",\n        \"description\": \"Hover over an element\"\n      },\n      \"create-element\": {\n        \"name\": \"Create element\",\n        \"description\": \"Create an element and insert it into the page\",\n        \"edit\": \"Edit element\",\n        \"wrap\": \"Wrap the element inside\",\n        \"insertEl\": {\n          \"title\": \"Insert element\",\n          \"items\": {\n            \"before\": \"As first child\",\n            \"after\": \"As last child\",\n            \"next-sibling\": \"As next sibling\",\n            \"prev-sibling\": \"As previous sibling\",\n            \"replace\": \"Replace target element\"\n          }\n        }\n      },\n      \"upload-file\": {\n        \"name\": \"Upload file\",\n        \"description\": \"Upload file into <input type=\\\"file\\\"> element\",\n        \"filePath\": \"URL or File path\",\n        \"addFile\": \"Add file\",\n        \"onlyURL\": \"Only uploading files from a URL is supported in the Firefox browser\",\n        \"requirement\": \"Read the requirements before using this block\",\n        \"noFileAccess\": \"Automa doesn't have access to files\"\n      },\n      \"browser-event\": {\n        \"name\": \"Browser event\",\n        \"description\": \"Executes the next block when the specified event is triggered\",\n        \"events\": \"Events\",\n        \"timeout\": \"Timeout (milliseconds)\",\n        \"activeTabLoaded\": \"Active tab\",\n        \"setAsActiveTab\": \"Set as active tab\"\n      },\n      \"blocks-group-2\": {\n        \"name\": \"@:workflow.blocks.blocks-group.name 2\",\n        \"description\": \"@:workflow.blocks.blocks-group.description\"\n      },\n      \"blocks-group\": {\n        \"name\": \"Blocks group\",\n        \"groupName\": \"Group name\",\n        \"description\": \"Grouping blocks\",\n        \"dropText\": \"Drag & drop a block here\",\n        \"cantAdd\": \"Can't add \\\"{blockName}\\\" block to the group\"\n      },\n      \"trigger\": {\n        \"name\": \"Trigger\",\n        \"description\": \"Block where the workflow will start executing\",\n        \"addTime\": \"Add time\",\n        \"selectDay\": \"Select day\",\n        \"timeExist\": \"You already added a trigger at {time} on {day}\",\n        \"fixedDelay\": \"Fixed delay\",\n        \"contextMenus\": {\n          \"noPermission\": \"This trigger requires \\\"contextMenus\\\" permission to be working\",\n          \"grantPermission\": \"Grant permission\",\n          \"appearIn\": \"Will appear in\",\n          \"contextName\": \"Workflow name in the context menu\"\n        },\n        \"days\": [\n          \"Sunday\",\n          \"Monday\",\n          \"Tuesday\",\n          \"Wednesday\",\n          \"Thursday\",\n          \"Friday\",\n          \"Saturday\"\n        ],\n        \"useRegex\": \"Use regex\",\n        \"shortcut\": {\n          \"tooltip\": \"Record shortcut\",\n          \"stopRecord\": \"Stop recording\",\n          \"checkboxTitle\": \"Execute shortcut even when you're in an input element\",\n          \"checkbox\": \"Active while in input\",\n          \"note\": \"Note: keyboard shortcut only works when you're on a webpage\"\n        },\n        \"forms\": {\n          \"triggerWorkflow\": \"Trigger workflow\",\n          \"interval\": \"Interval (minutes)\",\n          \"delay\": \"Delay (minutes)\",\n          \"date\": \"Date\",\n          \"time\": \"Time\",\n          \"url\": \"URL or Regex\",\n          \"shortcut\": \"Shortcut\",\n          \"cron-expression\": \"Cron expression\"\n        },\n        \"element-change\": {\n          \"target\": \"Target element to observe\",\n          \"optionsInfo\": \"Which element mutation will trigger the workflow\",\n          \"targetWebsite\": \"The Match Pattern of the website where the target element is (click to see more Match Pattern examples)\",\n          \"baseEl\": {\n            \"title\": \"Base element (optional)\",\n            \"description\": \"Automa will restart observing the target element when this element changes\"\n          },\n          \"subtree\": {\n            \"title\": \"Include subtree\",\n            \"description\": \"Extend monitoring to the entire subtree of the target element\"\n          },\n          \"childList\": {\n            \"title\": \"Child list\",\n            \"description\": \"Monitor the addition of new child elements or the removal of existing ones\"\n          },\n          \"attributes\": {\n            \"title\": \"Attributes\",\n            \"description\": \"Watch for changes to the attribute values of the target element\"\n          },\n          \"attributeFilter\": {\n            \"title\": \"Attribute filter\",\n            \"separate\": \"Use commas (,) to separate attribute names\",\n            \"description\": \"Only monitor specific attributes (leave blank to monitor all)\"\n          },\n          \"characterData\": {\n            \"title\": \"Character data\",\n            \"description\": \"Monitor changes to the character data/text within the target element\"\n          }\n        },\n        \"items\": {\n          \"manual\": \"Manually\",\n          \"interval\": \"Interval\",\n          \"cron-job\": \"Cron job\",\n          \"date\": \"On a specific date\",\n          \"context-menu\": \"Context menu\",\n          \"element-change\": \"On element change\",\n          \"specific-day\": \"On a specific day\",\n          \"visit-web\": \"When visiting a website\",\n          \"on-startup\": \"On browser startup\",\n          \"keyboard-shortcut\": \"Keyboard shortcut\"\n        }\n      },\n      \"execute-workflow\": {\n        \"name\": \"Execute workflow\",\n        \"overwriteNote\": \"This will overwrite the global data of the selected workflow\",\n        \"select\": \"Select workflow\",\n        \"executeId\": \"Execute Id (optional)\",\n        \"description\": \"\",\n        \"insertAllVars\": \"Use all current workflow variables\",\n        \"insertVars\": \"Insert current workflow variables\",\n        \"useCommas\": \"Use commas to separate the variable name\",\n        \"insertAllGlobalData\": \"Use all current workflow globalData\"\n      },\n      \"google-sheets-drive\": {\n        \"name\": \"@:workflow.blocks.google-sheets.name (GDrive)\",\n        \"description\": \"@:workflow.blocks.google-sheets.description\",\n        \"connected\": \"Connected sheets\",\n        \"select\": \"Select sheet\",\n        \"connect\": \"Connect sheet\"\n      },\n      \"google-drive\": {\n        \"name\": \"Google Drive\",\n        \"description\": \"Upload files to Google Drive\",\n        \"actions\": {\n          \"upload\": \"Upload files\"\n        }\n      },\n      \"google-sheets\": {\n        \"name\": \"Google Sheets\",\n        \"description\": \"Read or update Google Sheets data\",\n        \"previewData\": \"Preview data\",\n        \"firstRow\": \"Use the first row as keys\",\n        \"keysAsFirstRow\": \"Use keys as the first row\",\n        \"insertData\": \"Insert data\",\n        \"valueInputOption\": \"Value input option\",\n        \"insertDataOption\": \"Insert data option\",\n        \"rangeToSearch\": \"Range to start the search\",\n        \"dataFrom\": {\n          \"label\": \"Data from\",\n          \"options\": {\n            \"data-columns\": \"Table\",\n            \"custom\": \"Custom\"\n          }\n        },\n        \"refKey\": {\n          \"label\": \"Reference key (optional)\",\n          \"placeholder\": \"Key name\"\n        },\n        \"spreadsheetId\": {\n          \"label\": \"Spreadsheet Id\",\n          \"link\": \"See how to get spreadsheet Id\",\n          \"required\": \"Spreadsheet ID is required\"\n        },\n        \"range\": {\n          \"label\": \"Range\",\n          \"link\": \"Click to see more examples\",\n          \"required\": \"Range is required\"\n        },\n        \"select\": {\n          \"get\": \"Get spreadsheet cell values\",\n          \"getRange\": \"Get spreadsheet range\",\n          \"update\": \"Update spreadsheet cell values\",\n          \"append\": \"Append spreadsheet cell values\",\n          \"clear\": \"Clear spreadsheet cell values\",\n          \"create\": \"Create a spreadsheet\",\n          \"add-sheet\": \"Add sheet\"\n        }\n      },\n      \"active-tab\": {\n        \"name\": \"Active tab\",\n        \"description\": \"Set the tab you're in as the active tab\"\n      },\n      \"proxy\": {\n        \"name\": \"Proxy\",\n        \"description\": \"Set the proxy of the browser\",\n        \"clear\": \"Clear all proxies\",\n        \"bypass\": {\n          \"label\": \"Bypass list\",\n          \"note\": \"Use commas (,) to separate URL\"\n        }\n      },\n      \"new-window\": {\n        \"name\": \"New window\",\n        \"description\": \"Create a new window\",\n        \"top\": \"Top\",\n        \"left\": \"Left\",\n        \"height\": \"Height\",\n        \"width\": \"Width\",\n        \"note\": \"Note: use 0 to disable\",\n        \"position\": \"Window position\",\n        \"size\": \"Window size\",\n        \"windowState\": {\n          \"placeholder\": \"Window state\",\n          \"options\": {\n            \"normal\": \"Normal\",\n            \"minimized\": \"Minimized\",\n            \"maximized\": \"Maximized\",\n            \"fullscreen\": \"Fullscreen\"\n          }\n        },\n        \"incognito\": {\n          \"text\": \"Set as an incognito window\",\n          \"note\": \"You must enable 'Allow in incognito' for this extension first\"\n        }\n      },\n      \"go-back\": {\n        \"name\": \"Go back\",\n        \"description\": \"Go back to the previous page\"\n      },\n      \"forward-page\": {\n        \"name\": \"Go forward\",\n        \"description\": \"Go forward to the next page\"\n      },\n      \"close-tab\": {\n        \"name\": \"Close tab/window\",\n        \"description\": \"\",\n        \"url\": \"Match Patterns\",\n        \"activeTab\": \"Close active tab\",\n        \"allWindows\": \"Close all windows\"\n      },\n      \"event-click\": {\n        \"name\": \"Click element\",\n        \"description\": \"\"\n      },\n      \"delay\": {\n        \"name\": \"Delay\",\n        \"description\": \"Add a delay before executing the next block\",\n        \"input\": {\n          \"title\": \"Delay in milliseconds\",\n          \"placeholder\": \"(milliseconds)\"\n        }\n      },\n      \"parameter-prompt\": {\n        \"name\": \"Parameter Prompt\"\n      },\n      \"get-text\": {\n        \"name\": \"Get text\",\n        \"description\": \"Get text from an element\",\n        \"checkbox\": \"Insert to table\",\n        \"includeTags\": \"Include HTML tags\",\n        \"prefixText\": {\n          \"placeholder\": \"Text prefix\",\n          \"title\": \"Add prefix to the text\"\n        },\n        \"suffixText\": {\n          \"placeholder\": \"Text suffix\",\n          \"title\": \"Add suffix to the text\"\n        }\n      },\n      \"export-data\": {\n        \"name\": \"Export data\",\n        \"description\": \"Export workflow data\",\n        \"exportAs\": \"Export as\",\n        \"refKey\": \"Reference key\",\n        \"bomHeader\": \"Add UTF-8 BOM\",\n        \"dataToExport\": {\n          \"placeholder\": \"Data to export\",\n          \"options\": {\n            \"data-columns\": \"Table\",\n            \"google-sheets\": \"Google Sheets\",\n            \"variable\": \"Variable\"\n          }\n        }\n      },\n      \"element-scroll\": {\n        \"name\": \"Scroll element\",\n        \"description\": \"\",\n        \"scrollY\": \"Scroll vertical\",\n        \"scrollX\": \"Scroll horizontal\",\n        \"intoView\": \"Scroll into view\",\n        \"smooth\": \"Smooth scroll\",\n        \"incScrollX\": \"Increment horizontal scroll\",\n        \"incScrollY\": \"Increment vertical scroll\"\n      },\n      \"switch-tab\": {\n        \"name\": \"Switch tab\",\n        \"description\": \"Switch between tab\",\n        \"matchPattern\": \"Match Patterns\",\n        \"url\": \"New tab URL\",\n        \"createIfNoMatch\": \"Create if there's no match\"\n      },\n      \"new-tab\": {\n        \"name\": \"New tab\",\n        \"description\": \"\",\n        \"url\": \"New tab URL\",\n        \"tab-zoom\": \"Tab zoom\",\n        \"customUserAgent\": \"Use custom User-Agent\",\n        \"activeTab\": \"Set as active tab\",\n        \"tabToGroup\": \"Add tab to a group\",\n        \"waitTabLoaded\": \"Wait until the tab is loaded\",\n        \"updatePrevTab\": {\n          \"title\": \"Use the previously opened new tab instead of creating a new one\",\n          \"text\": \"Update previously opened tab\"\n        }\n      },\n      \"link\": {\n        \"name\": \"Link\",\n        \"description\": \"Open link element\",\n        \"openInNewTab\": \"Open in new tab\"\n      },\n      \"attribute-value\": {\n        \"name\": \"Attribute value\",\n        \"description\": \"Get the value of an element attribute\",\n        \"forms\": {\n          \"name\": \"Attribute name\",\n          \"checkbox\": \"Insert to table\",\n          \"column\": \"Select column\",\n          \"value\": \"Attribute value\",\n          \"action\": {\n            \"get\": \"Get attribute value\",\n            \"set\": \"Set attribute value\"\n          },\n          \"extraRow\": {\n            \"checkbox\": \"Add extra row\",\n            \"placeholder\": \"Value\",\n            \"title\": \"Value of the extra row\"\n          }\n        }\n      },\n      \"forms\": {\n        \"name\": \"Forms\",\n        \"description\": \"\",\n        \"selected\": \"Selected\",\n        \"type\": \"Form type\",\n        \"getValue\": \"Get form value\",\n        \"text-field\": {\n          \"name\": \"Text field\",\n          \"value\": \"Value\",\n          \"clearValue\": \"Clear form value\",\n          \"delay\": {\n            \"placeholder\": \"Delay\",\n            \"label\": \"Typing delay (millisecond)(0 to disable)\"\n          }\n        },\n        \"select\": {\n          \"name\": \"Select\"\n        },\n        \"radio\": {\n          \"name\": \"Radio\"\n        },\n        \"checkbox\": {\n          \"name\": \"Checkbox\"\n        }\n      },\n      \"repeat-task\": {\n        \"name\": \"Repeat task\",\n        \"description\": \"\",\n        \"times\": \"times\",\n        \"repeatFrom\": \"Repeat from\"\n      },\n      \"javascript-code\": {\n        \"name\": \"JavaScript code\",\n        \"description\": \"Execute your JavaScript code in the web page\",\n        \"availabeFuncs\": \"Available functions:\",\n        \"removeAfterExec\": \"Remove after block execution\",\n        \"everyNewTab\": \"Execute in every new tab\",\n        \"context\": {\n          \"name\": \"Execution context\",\n          \"items\": {\n            \"website\": \"Active tab\",\n            \"background\": \"Background\"\n          }\n        },\n        \"modal\": {\n          \"tabs\": {\n            \"code\": \"JavaScript code\",\n            \"preloadScript\": \"Preload script\"\n          }\n        },\n        \"timeout\": {\n          \"placeholder\": \"Timeout (milliseconds)\",\n          \"title\": \"JavaScript code execution timeout\"\n        }\n      },\n      \"trigger-event\": {\n        \"name\": \"Trigger event\",\n        \"description\": \"\",\n        \"selectEvent\": \"Select event\"\n      },\n      \"conditions\": {\n        \"name\": \"Conditions\",\n        \"add\": \"Add path\",\n        \"retryConditions\": \"Retry if no conditions are met\",\n        \"description\": \"Conditional block\",\n        \"refresh\": \"Refresh condition connections\",\n        \"fallbackTitle\": \"Executes when all comparisons don't meet the requirement\",\n        \"equals\": \"Equals\",\n        \"gt\": \"Greater than\",\n        \"gte\": \"Greater than or equal\",\n        \"lt\": \"Less than\",\n        \"lte\": \"Less than or equal\",\n        \"ne\": \"Not equals\",\n        \"contains\": \"Contains\"\n      },\n      \"element-exists\": {\n        \"name\": \"Element exists\",\n        \"description\": \"Check if an element exists\",\n        \"selector\": \"Element selector\",\n        \"fallbackTitle\": \"Executes when the element doesn't exist\",\n        \"throwError\": \"Throw an error if doesn't exist\",\n        \"tryFor\": {\n          \"title\": \"How many times to try to check if the element exists\",\n          \"label\": \"Try for\"\n        },\n        \"timeout\": {\n          \"label\": \"Timeout (milliseconds)\",\n          \"title\": \"Timeout for each try\"\n        }\n      },\n      \"webhook\": {\n        \"name\": \"HTTP request\",\n        \"description\": \"Make an HTTP Request\",\n        \"contentType\": \"Content type\",\n        \"method\": \"Request method\",\n        \"url\": \"Request URL\",\n        \"fallback\": \"Executes when the HTTP request fails\",\n        \"buttons\": {\n          \"header\": \"Add header\"\n        },\n        \"timeout\": {\n          \"placeholder\": \"Timeout\",\n          \"title\": \"HTTP request execution timeout (ms)\"\n        },\n        \"tabs\": {\n          \"headers\": \"Headers\",\n          \"body\": \"Body\",\n          \"response\": \"Response\"\n        }\n      },\n      \"while-loop\": {\n        \"name\": \"While loop\",\n        \"description\": \"Executes blocks while the condition is met\",\n        \"editCondition\": \"Edit condition\",\n        \"fallback\": \"Executes when the condition is false\"\n      },\n      \"loop-elements\": {\n        \"name\": \"Loop elements\",\n        \"description\": \"Iterate through elements\",\n        \"loadMore\": \"Load more elements\",\n        \"scrollToBottom\": \"Scroll to bottom\",\n        \"scrollToTop\": \"Scroll to top\",\n        \"actions\": {\n          \"none\": \"None\",\n          \"click-element\": \"Click an element\",\n          \"scroll\": \"Scroll down\",\n          \"click-link\": \"Click a link\",\n          \"scroll-up\": \"Scroll up\"\n        }\n      },\n      \"loop-data\": {\n        \"name\": \"Loop data\",\n        \"description\": \"Iterate through a table or your custom data\",\n        \"loopId\": \"Loop ID\",\n        \"refKey\": \"Reference key\",\n        \"startIndex\": \"Start from index\",\n        \"resumeLastWorkflow\": \"Resume last workflow\",\n        \"reverse\": \"Reverse loop order\",\n        \"modal\": {\n          \"fileTooLarge\": \"File too large to edit\",\n          \"maxFile\": \"Max file size is 1MB\",\n          \"options\": {\n            \"firstRow\": \"Use the first row as keys\"\n          }\n        },\n        \"buttons\": {\n          \"clear\": \"Clear data\",\n          \"insert\": \"Insert data\",\n          \"import\": \"Import file\"\n        },\n        \"maxLoop\": {\n          \"title\": \"Max number of data to loop\",\n          \"label\": \"Max data to loop (0 to disable)\"\n        },\n        \"loopThrough\": {\n          \"placeholder\": \"Loop through\",\n          \"fromNumber\": \"From number\",\n          \"toNumber\": \"To number\",\n          \"options\": {\n            \"numbers\": \"Numbers\",\n            \"variable\": \"Variable\",\n            \"data-columns\": \"Table\",\n            \"table\": \"Table\",\n            \"custom-data\": \"Custom data\",\n            \"google-sheets\": \"Google Sheets\",\n            \"elements\": \"Elements\"\n          }\n        }\n      },\n      \"loop-breakpoint\": {\n        \"name\": \"Loop breakpoint\",\n        \"description\": \"To indicate where the Loop Data block must stop\"\n      },\n      \"take-screenshot\": {\n        \"name\": \"Take screenshot\",\n        \"fullPage\": \"Take full page screenshot\",\n        \"description\": \"Take a screenshot of current active tab\",\n        \"imageQuality\": \"Image quality\",\n        \"saveToColumn\": \"Insert screenshot to table\",\n        \"saveToComputer\": \"Save screenshot to computer\",\n        \"types\": {\n          \"title\": \"Take a screenshot of\",\n          \"page\": \"A page\",\n          \"fullpage\": \"A full page\",\n          \"element\": \"An element\"\n        }\n      },\n      \"switch-to\": {\n        \"name\": \"Switch frame\",\n        \"description\": \"Switch between the main window and an iframe\",\n        \"iframeSelector\": \"Element selector\",\n        \"windowTypes\": {\n          \"main\": \"Main window\",\n          \"iframe\": \"Iframe\"\n        }\n      },\n      \"debugMode\": {\n        \"title\": \"Debug mode\",\n        \"description\": \"Execute block using the Chrome DevTools Protocol\"\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "src/locales/en/common.json",
    "content": "{\n  \"common\": {\n    \"dashboard\": \"Dashboard\",\n    \"workflow\": \"Workflow | Workflows\",\n    \"collection\": \"Collection | Collections\",\n    \"log\": \"Log | Logs\",\n    \"block\": \"Block | Blocks\",\n    \"schedule\": \"Schedule\",\n    \"folder\": \"Folder | Folders\",\n    \"new\": \"New\",\n    \"docs\": \"Documentation\",\n    \"search\": \"Search\",\n    \"example\": \"Example | Examples\",\n    \"import\": \"Import\",\n    \"export\": \"Export\",\n    \"rename\": \"Rename\",\n    \"execute\": \"Execute\",\n    \"delete\": \"Delete\",\n    \"cancel\": \"Cancel\",\n    \"settings\": \"Settings\",\n    \"options\": \"Options\",\n    \"confirm\": \"Confirm\",\n    \"name\": \"Name\",\n    \"all\": \"All\",\n    \"add\": \"Add\",\n    \"save\": \"Save\",\n    \"data\": \"data\",\n    \"stop\": \"Stop\",\n    \"sheet\": \"Sheet\",\n    \"pause\": \"Pause\",\n    \"resume\": \"Resume\",\n    \"action\": \"Action | Actions\",\n    \"packages\": \"Packages\",\n    \"storage\": \"Storage\",\n    \"editor\": \"Editor\",\n    \"running\": \"Running\",\n    \"globalData\": \"Global data\",\n    \"fileName\": \"File name\",\n    \"description\": \"Description\",\n    \"disable\": \"Disable\",\n    \"disabled\": \"Disabled\",\n    \"enable\": \"Enable\",\n    \"fallback\": \"Fallback\",\n    \"update\": \"Update\",\n    \"feature\": \"Feature\",\n    \"duplicate\": \"Duplicate\",\n    \"password\": \"Password\",\n    \"category\": \"Category\",\n    \"optional\": \"Optional\",\n    \"0disable\": \"0 to disable\",\n    \"millisecond\": \"millisecond | milliseconds\"\n  },\n  \"message\": {\n    \"noBlock\": \"No block\",\n    \"noData\": \"No data to show\",\n    \"noTriggerBlock\": \"Can't find a trigger block\",\n    \"useDynamicData\": \"Learn how to add dynamic data\",\n    \"delete\": \"Are you sure you want to delete \\\"{name}\\\"?\",\n    \"empty\": \"Oops... It looks like you don't have any items\",\n    \"maxSizeExceeded\": \"The file size has exceeded the maximum allowed\",\n    \"notSaved\": \"Do you really want to leave? You have unsaved changes!\",\n    \"somethingWrong\": \"Something went wrong\",\n    \"limitExceeded\": \"You have exceeded the limit\"\n  },\n  \"sort\": {\n    \"sortBy\": \"Sort by\",\n    \"name\": \"Name\",\n    \"createdAt\": \"Created date\",\n    \"updatedAt\": \"Last update\",\n    \"mostUsed\": \"Most used\"\n  },\n  \"logStatus\": {\n    \"stopped\": \"stopped\",\n    \"error\": \"error\",\n    \"success\": \"success\"\n  }\n}\n"
  },
  {
    "path": "src/locales/en/newtab.json",
    "content": "{\n  \"home\": {\n    \"viewAll\": \"View all\",\n    \"communities\": \"Communities\"\n  },\n  \"welcome\": {\n    \"title\": \"Welcome to Automa! 🎉\",\n    \"text\": \"Get started by reading the documentation or browsing workflows in the Automa Marketplace.\",\n    \"marketplace\": \"Marketplace\"\n  },\n  \"packages\": {\n    \"name\": \"Package | Packages\",\n    \"add\": \"Add package\",\n    \"icon\": \"Package icon\",\n    \"open\": \"Open packages\",\n    \"new\": \"New package\",\n    \"import\": \"Import package\",\n    \"set\": \"Set as a package\",\n    \"settings\": {\n      \"asBlock\": \"Set package as block\"\n    },\n    \"categories\": {\n      \"my\": \"My Packages\",\n      \"installed\": \"Installed Packages\"\n    }\n  },\n  \"scheduledWorkflow\": {\n    \"title\": \"Scheduled workflows\",\n    \"nextRun\": \"Next run\",\n    \"active\": \"Active\",\n    \"refresh\": \"Refresh\",\n    \"schedule\": {\n      \"title\": \"Schedule\",\n      \"types\": {\n        \"everyDay\": \"Every day\",\n        \"general\": \"Every {time}\",\n        \"interval\": \"Every {time} minutes\"\n      }\n    }\n  },\n  \"storage\": {\n    \"title\": \"Storage\",\n    \"table\": {\n      \"add\": \"Add table\",\n      \"edit\": \"Edit table\",\n      \"createdAt\": \"Created at\",\n      \"modifiedAt\": \"Modified at\",\n      \"rowsCount\": \"Rows count\",\n      \"delete\": \"Delete table\"\n    }\n  },\n  \"credential\": {\n    \"title\": \"Credential | Credentials\",\n    \"add\": \"Add credential\",\n    \"use\": {\n      \"title\": \"Used credentials\",\n      \"description\": \"This workflow uses these credentials\"\n    }\n  },\n  \"workflowPermissions\": {\n    \"title\": \"Workflow permissions\",\n    \"description\": \"This workflow requires these permissions to run properly\",\n    \"contextMenus\": {\n      \"title\": \"Context menu\",\n      \"description\": \"To execute the workflow via the context menu\"\n    },\n    \"clipboardRead\": {\n      \"title\": \"Clipboard\",\n      \"description\": \"For accessing the clipboard data\"\n    },\n    \"notifications\": {\n      \"title\": \"Notification\",\n      \"description\": \"For displaying a notification\"\n    },\n    \"downloads\": {\n      \"title\": \"Download\",\n      \"description\": \"Saving the page assets and renaming the downloaded file\"\n    },\n    \"cookies\": {\n      \"title\": \"Cookies\",\n      \"description\": \"Read, set, or remove cookies\"\n    }\n  },\n  \"updateMessage\": {\n    \"text1\": \"Automa has been updated to v{version},\",\n    \"text2\": \"see what's new.\"\n  },\n  \"workflows\": {\n    \"folder\": {\n      \"new\": \"New folder\",\n      \"name\": \"Folder name\",\n      \"delete\": \"Delete folder\",\n      \"rename\": \"Rename folder\"\n    }\n  },\n  \"auth\": {\n    \"title\": \"Auth\",\n    \"signIn\": \"Sign in\",\n    \"username\": \"You need to set your username first\",\n    \"clickHere\": \"Click here\",\n    \"text\": \"You need to be signed in before you can do that\"\n  },\n  \"running\": {\n    \"start\": \"Started on {date}\",\n    \"message\": \"This only displays the last 5 logs\"\n  },\n  \"settings\": {\n    \"theme\": \"Theme\",\n    \"shortcuts\": {\n      \"duplicate\": \"Shortcut already use by \\\"{name}\\\"\"\n    },\n    \"editor\": {\n      \"title\": \"Title\",\n      \"curvature\": {\n        \"title\": \"Line Curvature\",\n        \"line\": \"Line\",\n        \"reroute\": \"Reroute\",\n        \"rerouteFirstLast\": \"Reroute first & last point\"\n      },\n      \"arrow\": {\n        \"title\": \"Line arrow\",\n        \"description\": \"Add an arrow at the end of the line\"\n      },\n      \"snapGrid\": {\n        \"title\": \"Snap to the grid\",\n        \"description\": \"Snap to the grid when moving a block\"\n      },\n      \"saveWhenExecute\": {\n        \"title\": \"Auto-save when execute workflow\",\n        \"description\": \"Workflow changes will be saved when executing the workflow\"\n      }\n    },\n    \"deleteLog\": {\n      \"title\": \"Auto-delete workflow logs\",\n      \"after\": \"Delete after\",\n      \"deleteAfter\": {\n        \"never\": \"Never\",\n        \"days\": \"{day} days\"\n      }\n    },\n    \"language\": {\n      \"label\": \"Language\",\n      \"helpTranslate\": \"Can't find your language? Help translate.\",\n      \"reloadPage\": \"Reload the page for the change to take effect\"\n    },\n    \"menu\": {\n      \"general\": \"General\",\n      \"profile\": \"Profile\",\n      \"backup\": \"Backup Workflows\",\n      \"editor\": \"Editor\",\n      \"shortcuts\": \"Shortcuts\",\n      \"about\": \"About\"\n    },\n    \"backupWorkflows\": {\n      \"title\": \"Local Backup\",\n      \"invalidPassword\": \"Invalid password\",\n      \"workflowsAdded\": \"{count} workflows have been added\",\n      \"name\": \"Backup workflows\",\n      \"needSignin\": \"You need to sign in first\",\n      \"backup\": {\n        \"button\": \"Backup\",\n        \"settings\": \"Backup settings\",\n        \"encrypt\": \"Encrypt with password\",\n        \"schedule\": \"Schedule local backup\"\n      },\n      \"restore\": {\n        \"title\": \"Restore workflows\",\n        \"button\": \"Restore\",\n        \"update\": \"Update if workflow exists\"\n      },\n      \"cloud\": {\n        \"buttons\": {\n          \"local\": \"Local\",\n          \"cloud\": \"Cloud\"\n        },\n        \"location\": \"Location\",\n        \"delete\": \"Delete backup\",\n        \"title\": \"Cloud Backup\",\n        \"sync\": \"Sync\",\n        \"lastSync\": \"Last sync\",\n        \"lastBackup\": \"Last backup\",\n        \"select\": \"Select workflows\",\n        \"storedWorkflows\": \"Workflows that are stored in the cloud\",\n        \"selected\": \"Selected\",\n        \"selectText\": \"Select workflows that you want to backup\",\n        \"selectAll\": \"Select all\",\n        \"deselectAll\": \"Deselect all\",\n        \"needSelectWorkflow\": \"You need to select workflows that you want to backup\"\n      }\n    },\n    \"profile\": {\n      \"title\": \"Profile\",\n      \"loading\": \"Loading profile...\",\n      \"signedInAs\": \"Signed in as\",\n      \"signIn\": \"Sign in\",\n      \"signInDesc\": \"Sign in to backup your workflows and access cloud features\",\n      \"signOut\": \"Sign out\",\n      \"signingOut\": \"Signing out...\",\n      \"signedOut\": \"You have been signed out\",\n      \"signOutConfirmTitle\": \"Are you sure?\",\n      \"signOutConfirmMessage\": \"Signing out will remove all your authentication data and local session. Your workflows will remain on your device.\",\n      \"warningMessage\": \"This action will sign you out from your account on this device only.\",\n      \"signOutNote\": \"You can sign back in anytime to restore access to cloud features.\",\n      \"notSignedIn\": \"You are not signed in\",\n      \"teamsCount\": \"{count} team(s)\",\n      \"avatar\": \"Profile picture\",\n      \"username\": \"Username\",\n      \"email\": \"Email\"\n    }\n  },\n  \"workflow\": {\n    \"events\": {\n      \"title\": \"Workflow Events\",\n      \"add-action\": \"Add action\",\n      \"description\": \"Perform actions when the event occurs.\",\n      \"event\": \"Event | Events\",\n      \"action\": \"Action\",\n      \"actions\": {\n        \"js-code\": {\n          \"title\": \"Execute JS Code\"\n        },\n        \"http-request\": {\n          \"title\": \"HTTP Request\"\n        }\n      },\n      \"types\": {\n        \"finish:success\": {\n          \"name\": \"Finish (success)\",\n          \"description\": \"Workflow execution finished with success\"\n        },\n        \"finish:failed\": {\n          \"name\": \"Finish (failed)\",\n          \"description\": \"Workflow execution finished with error\"\n        }\n      }\n    },\n    \"previewMode\": {\n      \"title\": \"Preview mode\",\n      \"description\": \"You're in preview mode, changes you've made won't be saved\"\n    },\n    \"pinWorkflow\": {\n      \"pin\": \"Pin workflow\",\n      \"unpin\": \"Unpin workflow\",\n      \"pinned\": \"Pinned workflows\"\n    },\n    \"parameters\": {\n      \"add\": \"Add parameter\",\n      \"preferInTab\": \"Prefer input parameters in the tab\"\n    },\n    \"my\": \"My workflows\",\n    \"testing\": {\n      \"title\": \"Testing mode\",\n      \"nextBlock\": \"Next block\",\n      \"startRun\": \"Start run at\",\n      \"disabled\": \"Save changes first\"\n    },\n    \"import\": \"Import workflow\",\n    \"new\": \"New workflow\",\n    \"delete\": \"Delete workflow\",\n    \"browse\": \"Browse workflows\",\n    \"name\": \"Workflow name\",\n    \"rename\": \"Rename workflow\",\n    \"backupCloud\": \"Backup workflow to cloud\",\n    \"add\": \"Add workflow\",\n    \"clickToEnable\": \"Click to enable\",\n    \"toggleSidebar\": \"Toggle sidebar\",\n    \"cantEdit\": \"Can't edit shared workflow\",\n    \"undo\": \"Undo\",\n    \"redo\": \"Redo\",\n    \"autoAlign\": {\n      \"title\": \"Auto-align\"\n    },\n    \"blocksFolder\": {\n      \"title\": \"Blocks folder\",\n      \"add\": \"Add blocks to folder\",\n      \"save\": \"Save to folder\"\n    },\n    \"searchBlocks\": {\n      \"title\": \"Search blocks in the editor\"\n    },\n    \"conditionBuilder\": {\n      \"title\": \"Condition builder\",\n      \"add\": \"Add condition\",\n      \"and\": \"AND\",\n      \"or\": \"OR\",\n      \"topAwait\": \"Support top-level await and \\\"automaRefData\\\" function\"\n    },\n    \"host\": {\n      \"title\": \"Host workflow\",\n      \"set\": \"Set as host workflow\",\n      \"id\": \"Host ID\",\n      \"add\": \"Add hosted workflow\",\n      \"sync\": {\n        \"title\": \"Sync\",\n        \"description\": \"Sync with host workflow\"\n      },\n      \"messages\": {\n        \"hostExist\": \"You have already added this host\",\n        \"notFound\": \"Can't find a hosted workflow with the ID \\\"{id}\\\"\",\n        \"successAdded\": \"Successfully added hosted workflow with the ID \\\"{id}\\\"\"\n      }\n    },\n    \"type\": {\n      \"local\": \"Local\",\n      \"shared\": \"Shared\",\n      \"host\": \"Host\"\n    },\n    \"unpublish\": {\n      \"title\": \"Unpublish workflow\",\n      \"button\": \"Unpublish\",\n      \"body\": \"Are you sure you want to unpublish the workflow \\\"{name}\\\"?\"\n    },\n    \"share\": {\n      \"url\": \"Share URL\",\n      \"publish\": \"Publish\",\n      \"sharedAs\": \"Shared as \\\"{name}\\\"\",\n      \"title\": \"Share workflow\",\n      \"download\": \"Save workflow to local\",\n      \"edit\": \"Edit description\",\n      \"fetchLocal\": \"Fetch local workflow\",\n      \"update\": \"Update\",\n      \"unpublish\": \"Unpublish\",\n      \"linkCopied\": \"Link copied to clipboard\"\n    },\n    \"variables\": {\n      \"title\": \"Variable | Variables\",\n      \"name\": \"Variable name\",\n      \"assign\": \"Assign to variable\"\n    },\n    \"protect\": {\n      \"title\": \"Protect workflow\",\n      \"remove\": \"Remove protection\",\n      \"button\": \"Protect\",\n      \"note\": \"Note: this password will be required later on to edit or delete the workflow.\"\n    },\n    \"locked\": {\n      \"title\": \"This Workflow is Protected\",\n      \"body\": \"Input the password to unlock it\",\n      \"unlock\": \"Unlock\",\n      \"messages\": {\n        \"incorrect-password\": \"Incorrect password\"\n      }\n    },\n    \"state\": {\n      \"executeBy\": \"Executed by: \\\"{name}\\\"\"\n    },\n    \"table\": {\n      \"title\": \"Table | Tables\",\n      \"placeholder\": \"Search or add a column\",\n      \"select\": \"Select column\",\n      \"column\": {\n        \"name\": \"Column name\",\n        \"type\": \"Data type\"\n      }\n    },\n    \"sidebar\": {\n      \"workflowIcon\": \"Workflow icon\"\n    },\n    \"editor\": {\n      \"zoomIn\": \"Zoom in\",\n      \"zoomOut\": \"Zoom out\",\n      \"resetZoom\": \"Reset zoom\",\n      \"duplicate\": \"Duplicate\",\n      \"copy\": \"Copy\",\n      \"paste\": \"Paste\",\n      \"group\": \"Group blocks\",\n      \"ungroup\": \"Ungroup blocks\"\n    },\n    \"settings\": {\n      \"saveLog\": \"Save workflow log\",\n      \"executedBlockOnWeb\": \"Show executed block on the web page\",\n      \"notification\": {\n        \"title\": \"Workflow notification\",\n        \"description\": \"Show workflow status (success or failed) after it executed\",\n        \"noPermission\": \"This option requires \\\"notifications\\\" permission to work\"\n      },\n      \"publicId\": {\n        \"title\": \"Workflow public ID\",\n        \"description\": \"Set a public ID to execute the workflow via a JavaScript custom event\"\n      },\n      \"aipower\": {\n        \"title\": \"AI Power token\",\n        \"description\": \"Set a AI Power token to execute the workflow via a AI Power\"\n      },\n      \"defaultColumn\": {\n        \"title\": \"Insert into the default column\",\n        \"description\": \"Insert data to the default column if there's no column selected in the block\",\n        \"name\": \"Default column name\"\n      },\n      \"autocomplete\": {\n        \"title\": \"Autocomplete\",\n        \"description\": \"Enable autocomplete in the input block (disable if it makes Automa unstable)\"\n      },\n      \"clearCache\": {\n        \"title\": \"Clear cache\",\n        \"description\": \"Clear cache (state and loop index) of the workflow\",\n        \"info\": \"Successfully cleared workflow cache\",\n        \"btn\": \"Clear\"\n      },\n      \"reuseLastState\": {\n        \"title\": \"Reuse last workflow's state\",\n        \"description\": \"Use the state data (table, variables, and global data) from the last executed workflow\"\n      },\n      \"debugMode\": {\n        \"title\": \"Debug mode\",\n        \"description\": \"Execute the workflow using the Chrome DevTools Protocol\"\n      },\n      \"restartWorkflow\": {\n        \"for\": \"Restart for\",\n        \"times\": \"Times\",\n        \"description\": \"Max number of times the workflow will restart\"\n      },\n      \"onError\": {\n        \"title\": \"On workflow error\",\n        \"description\": \"Set the action to take if an error occurs in the workflow\",\n        \"items\": {\n          \"keepRunning\": \"Keep running\",\n          \"stopWorkflow\": \"Stop workflow\",\n          \"restartWorkflow\": \"Restart workflow\"\n        }\n      },\n      \"timeout\": {\n        \"title\": \"Workflow timeout (milliseconds)\"\n      },\n      \"blockDelay\": {\n        \"title\": \"Block delay (milliseconds)\",\n        \"description\": \"Add delay before executing each of the blocks\"\n      },\n      \"tabLoadTimeout\": {\n        \"title\": \"Tab load timeout\",\n        \"description\": \"Maximum time to load a tab in milliseconds, input 0 to disable the timeout\"\n      }\n    }\n  },\n  \"collection\": {\n    \"description\": \"Execute your workflows in sequence\",\n    \"new\": \"New collection\",\n    \"delete\": \"Delete collection\",\n    \"add\": \"Add collection\",\n    \"rename\": \"Rename collection\",\n    \"flow\": \"Flow\",\n    \"dragDropText\": \"Drop a workflow or block in here\",\n    \"options\": {\n      \"atOnce\": {\n        \"title\": \"Execute all workflows in the collection at once\",\n        \"description\": \"Blocks will not execute when this option is used\"\n      }\n    },\n    \"globalData\": {\n      \"note\": \"This will overwrite the global data of the workflow\"\n    }\n  },\n  \"log\": {\n    \"flowId\": \"Flow ID\",\n    \"goBack\": \"Go back to \\\"{name}\\\"'s logs\",\n    \"goWorkflow\": \"Go to workflow\",\n    \"startedDate\": \"Started date\",\n    \"duration\": \"Duration\",\n    \"selectAll\": \"Select all\",\n    \"deselectAll\": \"Deselect all\",\n    \"deleteSelected\": \"Delete selected logs\",\n    \"clearLogs\": {\n      \"title\": \"Clear logs\",\n      \"description\": \"Are you sure you want to clear all logs?\"\n    },\n    \"types\": {\n      \"stop\": \"Workflow is stopped\",\n      \"finish\": \"Finish\"\n    },\n    \"messages\": {\n      \"url-empty\": \"URL is empty\",\n      \"invalid-url\": \"URL is not valid\",\n      \"conditions-empty\": \"Conditions are empty\",\n      \"invalid-proxy-host\": \"Invalid proxy host\",\n      \"workflow-disabled\": \"Workflow is disabled\",\n      \"selector-empty\": \"Element selector is empty\",\n      \"invalid-body\": \"Content body is not a valid JSON\",\n      \"invalid-active-tab\": \"\\\"{url}\\\" is an invalid URL\",\n      \"empty-spreadsheet-id\": \"Spreadsheet ID is empty\",\n      \"invalid-loop-data\": \"Invalid data to loop through\",\n      \"empty-workflow\": \"You must select a workflow first\",\n      \"active-tab-removed\": \"Workflow active tab was removed\",\n      \"empty-spreadsheet-range\": \"Spreadsheet range is empty\",\n      \"stop-timeout\": \"Workflow was stopped due to timeout\",\n      \"no-file-access\": \"Automa doesn't have access to the file\",\n      \"no-workflow\": \"Can't find a workflow with the ID \\\"{workflowId}\\\"\",\n      \"no-match-tab\": \"Can't find a tab matching the pattern \\\"{pattern}\\\"\",\n      \"no-clipboard-acces\": \"Don't have permission to access the clipboard\",\n      \"browser-not-supported\": \"This feature not supported in {browser} browser\",\n      \"element-not-found\": \"Can't find an element with the selector \\\"{selector}\\\"\",\n      \"no-permission\": \"Don't have \\\"{permission}\\\" permission to perform this action\",\n      \"not-iframe\": \"Element with \\\"{selector}\\\" selector is not an iframe element\",\n      \"iframe-not-found\": \"Can't find an iframe element with the selector \\\"{selector}\\\"\",\n      \"workflow-infinite-loop\": \"Can't execute the workflow to prevent an infinite loop\",\n      \"not-debug-mode\": \"The workflow must run in debug mode for this block to work properly\",\n      \"no-iframe-id\": \"Can't find the Frame ID for the iframe element with the selector \\\"{selector}\\\"\",\n      \"no-tab\": \"Can't connect to a tab, use \\\"New tab\\\" or \\\"Active tab\\\" block before using the \\\"{name}\\\" block\"\n    },\n    \"description\": {\n      \"text\": \"{status} on {date} in {duration}\",\n      \"status\": {\n        \"success\": \"Succeeded\",\n        \"error\": \"Failed\",\n        \"stopped\": \"Stopped\"\n      }\n    },\n    \"delete\": {\n      \"title\": \"Delete log\",\n      \"description\": \"Are you sure you want to delete all the selected logs?\"\n    },\n    \"exportData\": {\n      \"title\": \"Export data\",\n      \"types\": {\n        \"json\": \"JSON\",\n        \"csv\": \"CSV\",\n        \"plain-text\": \"Plain text\"\n      }\n    },\n    \"filter\": {\n      \"title\": \"Filter\",\n      \"byStatus\": \"By status\",\n      \"byDate\": {\n        \"title\": \"By date\",\n        \"items\": {\n          \"lastDay\": \"Last day\",\n          \"last7Days\": \"Last seven days\",\n          \"last30Days\": \"Last thirty days\"\n        }\n      }\n    }\n  },\n  \"components\": {\n    \"pagination\": {\n      \"text1\": \"Showing\",\n      \"text2\": \"items out of {count}\",\n      \"nextPage\": \"Next page\",\n      \"currentPage\": \"Current page\",\n      \"prevPage\": \"Previous page\",\n      \"of\": \"of {page}\"\n    }\n  }\n}\n"
  },
  {
    "path": "src/locales/en/popup.json",
    "content": "{\n  \"recording\": {\n    \"stop\": \"Stop recording\",\n    \"title\": \"Recording\"\n  },\n  \"home\": {\n    \"record\": {\n      \"title\": \"Record workflow\",\n      \"button\": \"Record\",\n      \"name\": \"Workflow name\",\n      \"selectBlock\": \"Select a block to start from\",\n      \"anotherBlock\": \"Can't start from this block\",\n      \"tabs\": {\n        \"new\": \"New workflow\",\n        \"existing\": \"Existing workflow\"\n      }\n    },\n    \"elementSelector\": {\n      \"name\": \"Element selector\",\n      \"noAccess\": \"Don't have access to this site\"\n    },\n    \"workflow\": {\n      \"new\": \"New workflow\",\n      \"rename\": \"Rename workflow\",\n      \"delete\": \"Delete workflow\",\n      \"type\": {\n        \"host\": \"Host\",\n        \"local\": \"Local\"\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "src/locales/es/blocks.json",
    "content": "{\n  \"collection\": {\n    \"blocks\": {\n      \"export-result\": {\n        \"name\": \"Exportar los resultados\",\n        \"description\": \"Exportar el resultado de la recogida como JSON\"\n      }\n    }\n  },\n  \"workflow\": {\n    \"blocks\": {\n      \"base\": {\n        \"title\": \"Bloques\",\n        \"moveToGroup\": \"Mover bloque a grupo de bloques\",\n        \"selector\": \"Selector de elementos\",\n        \"selectorOptions\": \"Opciones de selección\",\n        \"timeout\": \"Tiempo de espera (milisegundos)\",\n        \"noPermission\": \"Automa no tiene permisos suficientes para realizar esta acción\",\n        \"grantPermission\": \"Conceder permiso\",\n        \"action\": \"Acción\",\n        \"element\": {\n          \"select\": \"Seleccione un elemento\",\n          \"verify\": \"Verificar selector\"\n        },\n        \"settings\": {\n          \"title\": \"Ajustes del bloque\",\n          \"blockTimeout\": {\n            \"title\": \"Tiempo de espera de ejecución del bloque (milisegundo)\",\n            \"description\": \"El tiempo máximo de ejecución del bloque (de 0 a desactivar)\"\n          },\n          \"line\": {\n            \"title\": \"Líneas\",\n            \"label\": \"Etiqueta\",\n            \"animated\": \"Animated\",\n            \"select\": \"Seleccionar línea\",\n            \"to\": \"Línea a {name} bloque\",\n            \"lineColor\": \"Color\"\n          }\n        },\n        \"toggle\": {\n          \"enable\": \"Habilitar bloque\",\n          \"disable\": \"Deshabilitar bloque\"\n        },\n        \"onError\": {\n          \"info\": \"Estas normas se aplicarán cuando se produzca un error en el bloque\",\n          \"button\": \"error\",\n          \"title\": \"En caso de error\",\n          \"retry\": \"Reintentar acción\",\n          \"fallbackTitle\": \"Se ejecutará cuando se produzca un error en el bloque\",\n          \"times\": {\n            \"name\": \"Tiempo\",\n            \"description\": \"El número de veces que se debe reintentar la acción\"\n          },\n          \"interval\": {\n            \"name\": \"Intervalo\",\n            \"description\": \"El intervalo de tiempo a esperar entre cada intento\",\n            \"second\": \"Segundos\"\n          },\n          \"toDo\": {\n            \"error\": \"Lanzar error\",\n            \"continue\": \"Seguir fluyendo\",\n            \"fallback\": \"Ejecutar fallback\",\n            \"restart\": \"Reiniciar el flujo\"\n          },\n          \"insertData\": {\n            \"name\": \"Insertar datos\"\n          }\n        },\n        \"table\": {\n          \"checkbox\": \"Insertar en tabla\",\n          \"select\": \"Seleccionar columna\",\n          \"extraRow\": {\n            \"checkbox\": \"Agregar una fila extra\",\n            \"placeholder\": \"Valor\",\n            \"title\": \"Valor de la fila extra\"\n          }\n        },\n        \"findElement\": {\n          \"placeholder\": \"Encontrar elemento por\",\n          \"options\": {\n            \"cssSelector\": \"CSS Selector\",\n            \"xpath\": \"XPath\"\n          }\n        },\n        \"markElement\": {\n          \"title\": \"Un elemento no se seleccionará si ya se ha seleccionado antes\",\n          \"text\": \"Elemento de marca\"\n        },\n        \"multiple\": {\n          \"title\": \"Seleccionar varios elementos\",\n          \"text\": \"Multiples\"\n        },\n        \"waitSelector\": {\n          \"title\": \"Esperar al selector\",\n          \"timeout\": \"Tiempo de espera del selector (ms)\"\n        },\n        \"downloads\": {\n          \"onConflict\": {\n            \"uniquify\": \"Uniquify\",\n            \"overwrite\": \"Sobrescribir\",\n            \"prompt\": \"Prompt\"\n          }\n        }\n      },\n      \"wait-connections\": {\n        \"name\": \"Conexiones de espera\",\n        \"description\": \"Esperar todas las conexiones antes de continuar con el siguiente bloque\",\n        \"specificFlow\": \"Continuar sólo un flujo específico\",\n        \"selectFlow\": \"Seleccionar flujo\"\n      },\n      \"cookie\": {\n        \"name\": \"Cookie\",\n        \"description\": \"Obtener, establecer o eliminar cookies\",\n        \"types\": {\n          \"get\": \"Obtener cookie\",\n          \"set\": \"Establecer cookie\",\n          \"remove\": \"Eliminar cookies\",\n          \"getAll\": \"Obtener todas las cookies\"\n        },\n        \"useJson\": \"Utilizar el formato JSON\"\n      },\n      \"note\": {\n        \"name\": \"Nota\"\n      },\n      \"slice-variable\": {\n        \"name\": \"Variable de corte\",\n        \"description\": \"Extrae una sección del valor de una variable\",\n        \"start\": \"Iniciar index\",\n        \"end\": \"Finalizar index\"\n      },\n      \"workflow-state\": {\n        \"name\": \"Estado del Flujo de Trabajo\",\n        \"description\": \"Gestionar los estados de los flujos de trabajo\",\n        \"actions\": {\n          \"stop\": \"Detener flujos de trabajo\"\n        }\n      },\n      \"regex-variable\": {\n        \"name\": \"RegEx variable\",\n        \"description\": \"Comparación de un valor variable con una expresión regular\"\n      },\n      \"data-mapping\": {\n        \"source\": \"Fuente\",\n        \"destination\": \"Destino\",\n        \"name\": \"mappeando Datos\",\n        \"edit\": \"Editar los datos del mapa\",\n        \"dataSource\": \"Fuente de Datos\",\n        \"description\": \"Asignar datos de una variable o tabla\",\n        \"addSource\": \"Añadir fuente\",\n        \"addDestination\": \"Añadir destino\"\n      },\n      \"sort-data\": {\n        \"name\": \"Ordenar datos\",\n        \"description\": \"Ordenar los elementos de datos\",\n        \"property\": \"Ordenar por la propiedad del artículo\",\n        \"addProperty\": \"Añadir propiedad\"\n      },\n      \"increase-variable\": {\n        \"name\": \"Aumento variable\",\n        \"description\": \"Aumentar el valor de una variable en una cantidad específica\",\n        \"increase\": \"Aumentar en\"\n      },\n      \"notification\": {\n        \"name\": \"notificación\",\n        \"description\": \"Mostrar una notificación\",\n        \"title\": \"Titulo\",\n        \"message\": \"Mensaje\",\n        \"imageUrl\": \"Imagen URL (opcional)\",\n        \"iconUrl\": \"Icono URL (opcional)\"\n      },\n      \"delete-data\": {\n        \"name\": \"Borrar datos\",\n        \"description\": \"Borrar datos de tablas o variables\",\n        \"from\": \"Datos de\",\n        \"allColumns\": \"[All columns]\"\n      },\n      \"log-data\": {\n        \"name\": \"Obtener datos de registro\",\n        \"description\": \"Obtener los últimos datos de registro de un flujo de trabajo\",\n        \"data\": \"Log data\"\n      },\n      \"tab-url\": {\n        \"name\": \"Get tab URL\",\n        \"description\": \"Obtener la URL de la pestaña\",\n        \"select\": \"Seleccionar pestaña\",\n        \"types\": {\n          \"active-tab\": \"Pestaña Activa\",\n          \"all\": \"All tabs\"\n        },\n        \"query\": {\n          \"title\": \"Query\",\n          \"matchPatterns\": \"@:workflow.blocks.switch-tab.matchPattern (opcional)\",\n          \"tabTitle\": \"Titulo de la pestaña (opcional)\"\n        }\n      },\n      \"reload-tab\": {\n        \"name\": \"Recargar pestaña\",\n        \"description\": \"Recargar la pestaña activa\"\n      },\n      \"press-key\": {\n        \"name\": \"Presionar tecla\",\n        \"description\": \"Presionar una tecla o una combinación\",\n        \"target\": \"Elemento objetivo (opcional)\",\n        \"key\": \"Key\",\n        \"detect\": \"Detect key\",\n        \"actions\": {\n          \"press-key\": \"Presione una tecla\",\n          \"multiple-keys\": \"Presione muchas teclas\"\n        },\n        \"press-time\": \"Tiempo de pulsación (milisegundos)\"\n      },\n      \"save-assets\": {\n        \"name\": \"Guardar activos\",\n        \"description\": \"Guardar activos (image, video, audio, or file) de un elemento o URL\",\n        \"filename\": \"Nombre del archivo (opcional)\",\n        \"saveDownloadIds\": \"Guardar los ID de descarga de los artículos\",\n        \"contentTypes\": {\n          \"title\": \"Type\",\n          \"element\": \"Elemento multimedia (imagen, audio o vídeo)\",\n          \"url\": \"URL\"\n        }\n      },\n      \"handle-dialog\": {\n        \"name\": \"Handle dialog\",\n        \"description\": \"Acepta o rechaza un cuadro de diálogo iniciado por JavaScript (alerta, confirmación, aviso).)\",\n        \"accept\": \"Aceptar diálogo\",\n        \"promptText\": {\n          \"label\": \"Prompt text (opcional)\",\n          \"description\": \"El texto a introducir en el diálogo antes de aceptar\"\n        }\n      },\n      \"handle-download\": {\n        \"name\": \"Handle download\",\n        \"description\": \"Handle downloaded file\",\n        \"timeout\": \"Tiempo de espera (milisegundos)\",\n        \"noPermission\": \"No tiene permiso para acceder a las descargas\",\n        \"onConflict\": \"En conflicto\",\n        \"waitFile\": \"Espere a que se descargue el archivo\",\n        \"downloadId\": \"ID de descarga de archivos (opcional)\",\n        \"filePath\": \"Ruta del archivo\"\n      },\n      \"insert-data\": {\n        \"name\": \"Insertar datos\",\n        \"description\": \"Insertar datos en una tabla o variable\"\n      },\n      \"clipboard\": {\n        \"name\": \"Portapapeles\",\n        \"description\": \"Obtener el texto copiado del portapapeles\",\n        \"data\": \"Datos del Portapapeles\",\n        \"noPermission\": \"No tiene permiso para acceder al portapapeles\",\n        \"grantPermission\": \"Conceder permiso\",\n        \"copySelection\": \"Copiar el texto seleccionado en la página\",\n        \"types\": {\n          \"get\": \"Obtener datos del portapapeles\",\n          \"insert\": \"Insertar texto en el portapapeles\"\n        }\n      },\n      \"hover-element\": {\n        \"name\": \"Hover element\",\n        \"description\": \"Hover over an element\"\n      },\n      \"create-element\": {\n        \"name\": \"Crear elemento\",\n        \"description\": \"Crear un elemento e insertarlo en la página\",\n        \"edit\": \"Editar elemento\",\n        \"wrap\": \"Envolver el elemento\",\n        \"insertEl\": {\n          \"title\": \"Insertar elemento\",\n          \"items\": {\n            \"before\": \"As first child\",\n            \"after\": \"As last child\",\n            \"next-sibling\": \"As next sibling\",\n            \"prev-sibling\": \"As previous sibling\",\n            \"replace\": \"Sustituir elemento de destino\"\n          }\n        }\n      },\n      \"upload-file\": {\n        \"name\": \"Cargar archivo\",\n        \"description\": \"Upload file into <input type=\\\"file\\\"> element\",\n        \"filePath\": \"URL o ruta de archivo\",\n        \"addFile\": \"Añadir archivo\",\n        \"onlyURL\": \"El navegador Firefox sólo admite la carga de archivos desde una URL.\",\n        \"requirement\": \"Lea los requisitos antes de utilizar este bloque\",\n        \"noFileAccess\": \"Automa no tiene acceso a los archivos\"\n      },\n      \"browser-event\": {\n        \"name\": \"Evento del navegador\",\n        \"description\": \"Ejecuta el siguiente bloque cuando se activa el evento especificado\",\n        \"events\": \"Eventos\",\n        \"timeout\": \"Tiempo de espera (milisegundos)\",\n        \"activeTabLoaded\": \"Pestaña Activa\",\n        \"setAsActiveTab\": \"Establecer como Pestaña Activa\"\n      },\n      \"blocks-group-2\": {\n        \"name\": \"@:workflow.blocks.blocks-group.name 2\",\n        \"description\": \"@:workflow.blocks.blocks-group.description\"\n      },\n      \"blocks-group\": {\n        \"name\": \"Grupo de Bloques\",\n        \"groupName\": \"Nombre del grupo\",\n        \"description\": \"Agrupando Bloques\",\n        \"dropText\": \"Arrastra y suelta el bloque aquí\",\n        \"cantAdd\": \"No se puede añadir \\\"{blockName}\\\" bloque al grupo\"\n      },\n      \"trigger\": {\n        \"name\": \"Trigger\",\n        \"description\": \"Bloque donde comenzará a ejecutarse el flujo de trabajo\",\n        \"addTime\": \"Añadir tiempo\",\n        \"selectDay\": \"Seleccionar día\",\n        \"timeExist\": \"Ya ha añadido un activador en {time} en {day}\",\n        \"fixedDelay\": \"Retraso fijo\",\n        \"contextMenus\": {\n          \"noPermission\": \"Este activador requiere \\\"contextMenus\\\" permiso para trabajar\",\n          \"grantPermission\": \"Conceder permiso\",\n          \"appearIn\": \"Aparecerá en\",\n          \"contextName\": \"Nombre del flujo de trabajo en el menú contextual\"\n        },\n        \"days\": [\n          \"Domingo\",\n          \"Lunes\",\n          \"Martes\",\n          \"Miércoles\",\n          \"Jueves\",\n          \"Viernes\",\n          \"Sábado\"\n        ],\n        \"useRegex\": \"Use regex\",\n        \"shortcut\": {\n          \"tooltip\": \"Atajo de registro\",\n          \"stopRecord\": \"Detener la grabación\",\n          \"checkboxTitle\": \"Ejecutar acceso directo incluso cuando estás en un elemento de entrada\",\n          \"checkbox\": \"Activo en entrada\",\n          \"note\": \"Nota: el atajo de teclado sólo funciona cuando estás en una página web\"\n        },\n        \"forms\": {\n          \"triggerWorkflow\": \"Trigger workflow\",\n          \"interval\": \"Intervalo (minutos)\",\n          \"delay\": \"Retraso (minutos)\",\n          \"date\": \"Fecha\",\n          \"time\": \"Tiempo\",\n          \"url\": \"URL or Regex\",\n          \"shortcut\": \"Atajo\",\n          \"cron-expression\": \"Expresión Cron\"\n        },\n        \"element-change\": {\n          \"target\": \"Elemento objetivo a observar\",\n          \"optionsInfo\": \"Qué mutación de elemento activará el flujo de trabajo\",\n          \"targetWebsite\": \"El patrón de coincidencia del sitio web en el que se encuentra el elemento de destino (haga clic para ver más ejemplos de patrones de coincidencia)\",\n          \"baseEl\": {\n            \"title\": \"Base element (opcional)\",\n            \"description\": \"Automa reiniciará la observación del elemento de destino cuando este elemento cambie\"\n          },\n          \"subtree\": {\n            \"title\": \"Incluir subárbol\",\n            \"description\": \"Ampliar la supervisión a todo el subárbol del elemento de destino\"\n          },\n          \"childList\": {\n            \"title\": \"Child list\",\n            \"description\": \"Supervisar la adición de nuevos elementos hijo o la eliminación de los existentes.\"\n          },\n          \"attributes\": {\n            \"title\": \"Atributos\",\n            \"description\": \"Vigilar los cambios en los valores de los atributos del elemento de destino\"\n          },\n          \"attributeFilter\": {\n            \"title\": \"Filtro de atributos\",\n            \"separate\": \"Usa comas (,) Para separar los nombre de los atributos\",\n            \"description\": \"Supervisar sólo atributos específicos (dejar en blanco para supervisar todos)\"\n          },\n          \"characterData\": {\n            \"title\": \"Character data\",\n            \"description\": \"Supervisar los cambios en los datos de caracteres/texto dentro del elemento de destino.\"\n          }\n        },\n        \"items\": {\n          \"manual\": \"Manualmente\",\n          \"interval\": \"Intervalo\",\n          \"cron-job\": \"Cron job\",\n          \"date\": \"En una fecha determinada\",\n          \"context-menu\": \"Menú contextual\",\n          \"element-change\": \"Al cambiar de elemento\",\n          \"specific-day\": \"En un día concreto\",\n          \"visit-web\": \"Al visitar un sitio web\",\n          \"on-startup\": \"Al iniciar el navegador\",\n          \"keyboard-shortcut\": \"Atajo de teclado\"\n        }\n      },\n      \"execute-workflow\": {\n        \"name\": \"Ejecutar flujo de trabajo\",\n        \"overwriteNote\": \"Esto sobrescribirá los datos globales del flujo de trabajo seleccionado\",\n        \"select\": \"Seleccionar flujo de trabajo\",\n        \"executeId\": \"Id de ejecución (opcional)\",\n        \"description\": \"\",\n        \"insertAllVars\": \"Utilizar todas las variables de flujo de trabajo actuales\",\n        \"insertVars\": \"Insertar variables de flujo de trabajo actuales\",\n        \"useCommas\": \"Utilice comas para separar el nombre de la variable\",\n        \"insertAllGlobalData\": \"Utilizar todo el flujo de trabajo actual globalData\"\n      },\n      \"google-sheets-drive\": {\n        \"name\": \"@:workflow.blocks.google-sheets.name (GDrive)\",\n        \"description\": \"@:workflow.blocks.google-sheets.description\",\n        \"connected\": \"Connected sheets\",\n        \"select\": \"Seleccionar hoja\",\n        \"connect\": \"Conectar hoja\"\n      },\n      \"google-drive\": {\n        \"name\": \"Google Drive\",\n        \"description\": \"Cargar archivos en Google Drive\",\n        \"actions\": {\n          \"upload\": \"Cargar archivos\"\n        }\n      },\n      \"google-sheets\": {\n        \"name\": \"Hojas de cálculo de Google\",\n        \"description\": \"Leer o actualizar datos de Google Sheets\",\n        \"previewData\": \"Previsualizar datos\",\n        \"firstRow\": \"Utilizar la primera fila como clave\",\n        \"keysAsFirstRow\": \"Utilizar llaves como primera fila\",\n        \"insertData\": \"Insertar datos\",\n        \"valueInputOption\": \"Opción de entrada de valor\",\n        \"insertDataOption\": \"Opción de insertar datos\",\n        \"rangeToSearch\": \"Rango para iniciar la búsqueda\",\n        \"dataFrom\": {\n          \"label\": \"Data from\",\n          \"options\": {\n            \"data-columns\": \"Table\",\n            \"custom\": \"Custom\"\n          }\n        },\n        \"refKey\": {\n          \"label\": \"Clave de referencia (opcional)\",\n          \"placeholder\": \"Key name\"\n        },\n        \"spreadsheetId\": {\n          \"label\": \"Id de hoja de cálculo\",\n          \"link\": \"Ver cómo obtener el Id de hoja de cálculo\"\n        },\n        \"range\": {\n          \"label\": \"Rango\",\n          \"link\": \"Haga clic para ver más ejemplos\"\n        },\n        \"select\": {\n          \"get\": \"Obtener los valores de las celdas de una hoja de cálculo\",\n          \"getRange\": \"Obtener rango de hoja de cálculo\",\n          \"update\": \"Actualizar los valores de las celdas de una hoja de cálculo\",\n          \"append\": \"Añadir valores de celdas de una hoja de cálculo\",\n          \"clear\": \"Borrar los valores de las celdas de una hoja de cálculo\",\n          \"create\": \"Crear una hoja de cálculo\",\n          \"add-sheet\": \"Añadir hoja\"\n        }\n      },\n      \"active-tab\": {\n        \"name\": \"Pestaña Activa\",\n        \"description\": \"Establece la pestaña en la que estás como Pestaña Activa\"\n      },\n      \"proxy\": {\n        \"name\": \"Proxy\",\n        \"description\": \"Establecer el proxy del navegador\",\n        \"clear\": \"Borrar todos los proxies\",\n        \"bypass\": {\n          \"label\": \"Bypass list\",\n          \"note\": \"Utilice comas (,) para separar las URL\"\n        }\n      },\n      \"new-window\": {\n        \"name\": \"Nueva ventana\",\n        \"description\": \"Crear una nueva ventana\",\n        \"top\": \"arriba\",\n        \"left\": \"izquierda\",\n        \"height\": \"alto\",\n        \"width\": \"ancho\",\n        \"note\": \"Nota: utilice 0 para desactivar\",\n        \"position\": \"Posición de la ventana\",\n        \"size\": \"Tamaño de la ventana\",\n        \"windowState\": {\n          \"placeholder\": \"Estado de la ventana\",\n          \"options\": {\n            \"normal\": \"Normal\",\n            \"minimized\": \"Minimizado\",\n            \"maximized\": \"Maximizado\",\n            \"fullscreen\": \"Pantalla completa\"\n          }\n        },\n        \"incognito\": {\n          \"text\": \"Establecer como ventana de incógnito\",\n          \"note\": \"Primero debe activar la opción 'Permitir en incógnito' para esta extensión.\"\n        }\n      },\n      \"go-back\": {\n        \"name\": \"Volver atrás\",\n        \"description\": \"Volver a la página anterior\"\n      },\n      \"forward-page\": {\n        \"name\": \"Seguir adelante\",\n        \"description\": \"Pasar a la página siguiente\"\n      },\n      \"close-tab\": {\n        \"name\": \"Cerrar pestaña/ventana\",\n        \"description\": \"\",\n        \"url\": \"Patrones de coincidencia\",\n        \"activeTab\": \"Cerrar Pestaña Activa\",\n        \"allWindows\": \"Cerrar todas las ventanas\"\n      },\n      \"event-click\": {\n        \"name\": \"Elemento de clic\",\n        \"description\": \"\"\n      },\n      \"delay\": {\n        \"name\": \"Retraso\",\n        \"description\": \"Añade un retardo antes de ejecutar el siguiente bloque\",\n        \"input\": {\n          \"title\": \"Retraso en los milisegundos\",\n          \"placeholder\": \"(milisegundos)\"\n        }\n      },\n      \"parameter-prompt\": {\n        \"name\": \"Parameter Prompt\"\n      },\n      \"get-text\": {\n        \"name\": \"Obtener texto\",\n        \"description\": \"Obtener texto de un elemento\",\n        \"checkbox\": \"Insertar en tabla\",\n        \"includeTags\": \"Incluir etiquetas HTML\",\n        \"prefixText\": {\n          \"placeholder\": \"Text prefix\",\n          \"title\": \"Add prefix to the text\"\n        },\n        \"suffixText\": {\n          \"placeholder\": \"Text suffix\",\n          \"title\": \"Add suffix to the text\"\n        }\n      },\n      \"export-data\": {\n        \"name\": \"Exportar datos\",\n        \"description\": \"Exportar datos del flujo de trabajo\",\n        \"exportAs\": \"Exportar como\",\n        \"refKey\": \"Clave de referencia\",\n        \"bomHeader\": \"Add UTF-8 BOM\",\n        \"dataToExport\": {\n          \"placeholder\": \"Datos a exportar\",\n          \"options\": {\n            \"data-columns\": \"Table\",\n            \"google-sheets\": \"Hojas de cálculo de Google\",\n            \"variable\": \"Variable\"\n          }\n        }\n      },\n      \"element-scroll\": {\n        \"name\": \"Elemento de desplazamiento\",\n        \"description\": \"\",\n        \"scrollY\": \"Desplazamiento vertical\",\n        \"scrollX\": \"Desplazamiento horizontal\",\n        \"intoView\": \"Desplazarse a la vista\",\n        \"smooth\": \"Desplazamiento suave\",\n        \"incScrollX\": \"Incremento del desplazamiento horizontal\",\n        \"incScrollY\": \"Incrementar el desplazamiento vertical\"\n      },\n      \"switch-tab\": {\n        \"name\": \"Cambiar Pestaña\",\n        \"description\": \"Cambiar entre pestañas\",\n        \"matchPattern\": \"Match Patterns\",\n        \"url\": \"URL de la nueva pestaña\",\n        \"createIfNoMatch\": \"Crear si no hay coincidencias\"\n      },\n      \"new-tab\": {\n        \"name\": \"Nueva pestaña\",\n        \"description\": \"\",\n        \"url\": \"Nueva pestaña URL\",\n        \"tab-zoom\": \"Tab zoom\",\n        \"customUserAgent\": \"Use custom User-Agent\",\n        \"activeTab\": \"Establecer como Pestaña Activa\",\n        \"tabToGroup\": \"Agregar pestaña a un grupo\",\n        \"waitTabLoaded\": \"Espere a que se cargue la pestaña\",\n        \"updatePrevTab\": {\n          \"title\": \"Utilizar la pestaña abierta anteriormente en lugar de crear una nueva.\",\n          \"text\": \"Actualizar pestaña abierta anteriormente\"\n        }\n      },\n      \"link\": {\n        \"name\": \"Link\",\n        \"description\": \"Abrir elemento de enlace\",\n        \"openInNewTab\": \"Abrir en una nueva pestaña\"\n      },\n      \"attribute-value\": {\n        \"name\": \"Valor del atributo\",\n        \"description\": \"Obtener el valor de un atributo de un elemento\",\n        \"forms\": {\n          \"name\": \"Nombre del atributo\",\n          \"checkbox\": \"Insertar en tabla\",\n          \"column\": \"Seleccionar columna\",\n          \"value\": \"Valor del atributo\",\n          \"action\": {\n            \"get\": \"Obtener valor de atributo\",\n            \"set\": \"Establecer valor de atributo\"\n          },\n          \"extraRow\": {\n            \"checkbox\": \"Agregar fila extra\",\n            \"placeholder\": \"Valor\",\n            \"title\": \"Valor de la fila extra\"\n          }\n        }\n      },\n      \"forms\": {\n        \"name\": \"Formuario\",\n        \"description\": \"\",\n        \"selected\": \"Seleccionado\",\n        \"type\": \"Tipo de formulario\",\n        \"getValue\": \"Obtener valor del formulario\",\n        \"text-field\": {\n          \"name\": \"Campo de texto\",\n          \"value\": \"Valor\",\n          \"clearValue\": \"Borrar valor del formulario\",\n          \"delay\": {\n            \"placeholder\": \"Retraso\",\n            \"label\": \"Retraso al teclear (milisegundos)(0 para desactivar)\"\n          }\n        },\n        \"select\": {\n          \"name\": \"Select\"\n        },\n        \"radio\": {\n          \"name\": \"Radio\"\n        },\n        \"checkbox\": {\n          \"name\": \"Checkbox\"\n        }\n      },\n      \"repeat-task\": {\n        \"name\": \"Repetir la tarea\",\n        \"description\": \"\",\n        \"times\": \"tiempo\",\n        \"repeatFrom\": \"Repetir desde\"\n      },\n      \"javascript-code\": {\n        \"name\": \"codigo JavaScript\",\n        \"description\": \"Ejecute su código JavaScript en la página web\",\n        \"availabeFuncs\": \"Funciones disponibles:\",\n        \"removeAfterExec\": \"Eliminar tras la ejecución del bloque\",\n        \"everyNewTab\": \"Ejecutar en cada nueva pestaña\",\n        \"context\": {\n          \"name\": \"Contexto de ejecución\",\n          \"items\": {\n            \"website\": \"Pestaña Activa\",\n            \"background\": \"Background\"\n          }\n        },\n        \"modal\": {\n          \"tabs\": {\n            \"code\": \"Codigo JavaScript\",\n            \"preloadScript\": \"Script de precarga\"\n          }\n        },\n        \"timeout\": {\n          \"placeholder\": \"Tiempo de espera (milisegundos)\",\n          \"title\": \"Tiempo de espera de ejecución de código JavaScript\"\n        }\n      },\n      \"trigger-event\": {\n        \"name\": \"Evento desencadenante\",\n        \"description\": \"\",\n        \"selectEvent\": \"Seleccionar evento\"\n      },\n      \"conditions\": {\n        \"name\": \"Condiciones\",\n        \"add\": \"Añadir ruta\",\n        \"retryConditions\": \"Reintentar si no se cumplen las condiciones\",\n        \"description\": \"Bloque condicional\",\n        \"refresh\": \"Actualizar conexiones de condiciones\",\n        \"fallbackTitle\": \"Se ejecuta cuando todas las comparaciones no cumplen el requisito\",\n        \"equals\": \"Es igual a\",\n        \"gt\": \"Mayor que\",\n        \"gte\": \"Mayor o igual que\",\n        \"lt\": \"Menos de\",\n        \"lte\": \"Menor o igual que\",\n        \"ne\": \"No es igual\",\n        \"contains\": \"Contiene\"\n      },\n      \"element-exists\": {\n        \"name\": \"El elemento existe\",\n        \"description\": \"Comprobar si existe un elemento\",\n        \"selector\": \"Selector de elementos\",\n        \"fallbackTitle\": \"Se ejecuta cuando el elemento no existe\",\n        \"throwError\": \"Lanza un error si no existe\",\n        \"tryFor\": {\n          \"title\": \"Cuántas veces hay que intentar comprobar si el elemento existe\",\n          \"label\": \"Inténtalo\"\n        },\n        \"timeout\": {\n          \"label\": \"Tiempo de espera (milisegundos)\",\n          \"title\": \"Tiempo de espera para cada intento\"\n        }\n      },\n      \"webhook\": {\n        \"name\": \"Solicitud HTTP\",\n        \"description\": \"Realizar una solicitud HTTP\",\n        \"contentType\": \"Tipo de contenido\",\n        \"method\": \"Método de solicitud\",\n        \"url\": \"Solicitar URL\",\n        \"fallback\": \"Se ejecuta cuando falla la petición HTTP\",\n        \"buttons\": {\n          \"header\": \"Add header\"\n        },\n        \"timeout\": {\n          \"placeholder\": \"Tiempo de espera\",\n          \"title\": \"Tiempo de espera de ejecución de la solicitud HTTP (ms)\"\n        },\n        \"tabs\": {\n          \"headers\": \"Headers\",\n          \"body\": \"Body\",\n          \"response\": \"Respuesta\"\n        }\n      },\n      \"while-loop\": {\n        \"name\": \"Bucle while\",\n        \"description\": \"Ejecuta bloques mientras se cumple la condición\",\n        \"editCondition\": \"Editar condición\",\n        \"fallback\": \"Se ejecuta cuando la condición es falsa\"\n      },\n      \"loop-elements\": {\n        \"name\": \"Elementos de bucle\",\n        \"description\": \"Iterar por los elementos\",\n        \"loadMore\": \"Cargar más elementos\",\n        \"scrollToBottom\": \"Desplácese hasta abajo\",\n        \"scrollToTop\": \"Ir arriba\",\n        \"actions\": {\n          \"none\": \"Ninguno\",\n          \"click-element\": \"Haga clic en un elemento\",\n          \"scroll\": \"Desplácese\",\n          \"click-link\": \"Haga clic en un enlace\",\n          \"scroll-up\": \"Desplácese hacia arriba\"\n        }\n      },\n      \"loop-data\": {\n        \"name\": \"Datos del bucle\",\n        \"description\": \"Iterar a través de una tabla o de sus datos personalizados\",\n        \"loopId\": \"ID de bucle\",\n        \"refKey\": \"Clave de referencia\",\n        \"startIndex\": \"Empezar desde el índice\",\n        \"resumeLastWorkflow\": \"Reanudar el último flujo de trabajo\",\n        \"reverse\": \"Invertir el orden de los bucles\",\n        \"modal\": {\n          \"fileTooLarge\": \"Archivo demasiado grande para editar\",\n          \"maxFile\": \"El tamaño máximo del archivo es de 1 MB\",\n          \"options\": {\n            \"firstRow\": \"Utilizar la primera fila como clave\"\n          }\n        },\n        \"buttons\": {\n          \"clear\": \"Limpiar datos\",\n          \"insert\": \"Insertar datos\",\n          \"import\": \"Importar fichero\"\n        },\n        \"maxLoop\": {\n          \"title\": \"Número máximo de datos en bucle\",\n          \"label\": \"Datos máximos en bucle (0 para desactivar)\"\n        },\n        \"loopThrough\": {\n          \"placeholder\": \"En bucle\",\n          \"fromNumber\": \"From number\",\n          \"toNumber\": \"Al número\",\n          \"options\": {\n            \"numbers\": \"Números\",\n            \"variable\": \"Variable\",\n            \"data-columns\": \"Datos en columna\",\n            \"table\": \"Cuadro\",\n            \"custom-data\": \"Datos personalizados\",\n            \"google-sheets\": \"Hojas de cálculo de Google\",\n            \"elements\": \"Elementos\"\n          }\n        }\n      },\n      \"loop-breakpoint\": {\n        \"name\": \"Punto de interrupción del bucle\",\n        \"description\": \"Para indicar dónde debe detenerse el bloque Loop Data\"\n      },\n      \"take-screenshot\": {\n        \"name\": \"Hacer una captura de pantalla\",\n        \"fullPage\": \"Captura de pantalla de página completa\",\n        \"description\": \"Haz una captura de pantalla de la Pestaña Activa actual\",\n        \"imageQuality\": \"Calidad de la imagen\",\n        \"saveToColumn\": \"Insertar una captura de pantalla en una tabla\",\n        \"saveToComputer\": \"Guardar captura de pantalla en el ordenador\",\n        \"types\": {\n          \"title\": \"Haga una captura de pantalla de\",\n          \"page\": \"Una página\",\n          \"fullpage\": \"Una página completa\",\n          \"element\": \"Un elemento\"\n        }\n      },\n      \"switch-to\": {\n        \"name\": \"Switch frame\",\n        \"description\": \"Cambiar entre la ventana principal y un iframe\",\n        \"iframeSelector\": \"Selector de elementos\",\n        \"windowTypes\": {\n          \"main\": \"Ventana principal\",\n          \"iframe\": \"Iframe\"\n        }\n      },\n      \"debugMode\": {\n        \"title\": \"Modo depuración\",\n        \"description\": \"Ejecutar bloque utilizando el protocolo DevTools de Chrome\"\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "src/locales/es/common.json",
    "content": "{\n  \"common\": {\n    \"dashboard\": \"Dashboard\",\n    \"workflow\": \"Flujo de Trabajo | Flujos de Trabajo\",\n    \"collection\": \"Coleccion | Colecciones\",\n    \"log\": \"Log | Logs\",\n    \"block\": \"Bloque | Bloques\",\n    \"schedule\": \"Horario\",\n    \"folder\": \"Carpeta | Carpetas\",\n    \"new\": \"Nuevo\",\n    \"docs\": \"Documentacion\",\n    \"search\": \"Buscar\",\n    \"example\": \"Ejemplo | Ejemplos\",\n    \"import\": \"Importar\",\n    \"export\": \"Exportar\",\n    \"rename\": \"Renombrar\",\n    \"execute\": \"Ejecutar\",\n    \"delete\": \"Eliminar\",\n    \"cancel\": \"Cancelar\",\n    \"settings\": \"Ajustes\",\n    \"options\": \"Opciones\",\n    \"confirm\": \"Confirmar\",\n    \"name\": \"Nombre\",\n    \"all\": \"Todo\",\n    \"add\": \"Agregar\",\n    \"save\": \"Guardar\",\n    \"data\": \"Dato\",\n    \"stop\": \"Detener\",\n    \"sheet\": \"Hoja\",\n    \"pause\": \"Pausar\",\n    \"resume\": \"Resumen\",\n    \"action\": \"Accion | Acciones\",\n    \"packages\": \"Paquete\",\n    \"storage\": \"Almacenamiento\",\n    \"editor\": \"editor\",\n    \"running\": \"Ejecutando\",\n    \"globalData\": \"Datos Globales\",\n    \"fileName\": \"Nombre del fichero\",\n    \"description\": \"Descripcion\",\n    \"disable\": \"Desactivar\",\n    \"disabled\": \"Desactivado\",\n    \"enable\": \"Habilitar\",\n    \"fallback\": \"Respuesta\",\n    \"update\": \"Actualizacion\",\n    \"feature\": \"Característica\",\n    \"duplicate\": \"Duplicado\",\n    \"password\": \"Contraseña\",\n    \"category\": \"Categoria\",\n    \"optional\": \"Opcional\",\n    \"0disable\": \"0 para desactivar\",\n    \"millisecond\": \"milisegundo | milisegundos\"\n  },\n  \"message\": {\n    \"noBlock\": \"Ningún bloque\",\n    \"noData\": \"No hay datos que lo demuestren\",\n    \"noTriggerBlock\": \"No puedo encontrar un bloque desencadenador\",\n    \"useDynamicData\": \"Aprende a añadir datos dinámicos\",\n    \"delete\": \"¿Estás seguro de que quieres borrar \\\"{name}\\\"?\",\n    \"empty\": \"Ups... Parece que usted no tiene ningún artículo\",\n    \"maxSizeExceeded\": \"El tamaño del archivo ha superado el máximo permitido\",\n    \"notSaved\": \"¿De verdad quieres irte? ¡Tienes cambios sin guardar!\",\n    \"somethingWrong\": \"Algo salió mal\",\n    \"limitExceeded\": \"Ha superado el límite\"\n  },\n  \"sort\": {\n    \"sortBy\": \"Ordenar por\",\n    \"name\": \"Nombre\",\n    \"createdAt\": \"Fecha de creación\",\n    \"updatedAt\": \"Última actualización\",\n    \"mostUsed\": \"Los más utilizados\"\n  },\n  \"logStatus\": {\n    \"stopped\": \"parado\",\n    \"error\": \"error\",\n    \"success\": \"éxito\"\n  }\n}\n"
  },
  {
    "path": "src/locales/es/newtab.json",
    "content": "{\n  \"home\": {\n    \"viewAll\": \"Ver todo\",\n    \"communities\": \"Comunidades\"\n  },\n  \"welcome\": {\n    \"title\": \"Bienvenido a Automa! 🎉\",\n    \"text\": \"Para empezar, lea la documentación o consulte los flujos de trabajo en Automa Marketplace.\",\n    \"marketplace\": \"Marketplace\"\n  },\n  \"packages\": {\n    \"name\": \"Package | Packages\",\n    \"add\": \"Añadir paquete\",\n    \"icon\": \"Icono del paquete\",\n    \"open\": \"Abrir paquetes\",\n    \"new\": \"Nuevo paquete\",\n    \"import\": \"Importar paquete\",\n    \"set\": \"Establecer como un paquete\",\n    \"settings\": {\n      \"asBlock\": \"Establecer paquete como bloque\"\n    },\n    \"categories\": {\n      \"my\": \"Mis Paquetes\",\n      \"installed\": \"Instalar Paquetes\"\n    }\n  },\n  \"scheduledWorkflow\": {\n    \"title\": \"Flujos de trabajo programados\",\n    \"nextRun\": \"Próxima ejecución\",\n    \"active\": \"Activar\",\n    \"refresh\": \"Refrescar\",\n    \"schedule\":{\n      \"title\": \"Horario\",\n      \"types\": {\n        \"everyDay\": \"Todos los días\",\n        \"general\": \"Cada {time}\",\n        \"interval\": \"Cada {time} minutos\"\n      }\n    }\n  },\n  \"storage\": {\n    \"title\": \"Almacenamiento\",\n    \"table\": {\n      \"add\": \"Añadir tabla\",\n      \"edit\": \"Editar Tabla\",\n      \"createdAt\": \"Creado en\",\n      \"modifiedAt\": \"Modificado en\",\n      \"rowsCount\": \"Recuento de filas\",\n      \"delete\": \"Borrar tabla\"\n    }\n  },\n  \"credential\": {\n    \"title\": \"Credencial | Credenciales\",\n    \"add\": \"Añadir credencial\",\n    \"use\": {\n      \"title\": \"Credenciales usadas\",\n      \"description\": \"Este flujo de trabajo utiliza estas credenciales\"\n    }\n  },\n  \"workflowPermissions\": {\n    \"title\": \"Permisos de flujo de trabajo\",\n    \"description\": \"Este flujo de trabajo requiere estos permisos para ejecutarse correctamente\",\n    \"contextMenus\": {\n      \"title\": \"Menú contextual\",\n      \"description\": \"Para ejecutar el flujo de trabajo a través del menú contextual\"\n    },\n    \"clipboardRead\": {\n      \"title\": \"Portapapeles\",\n      \"description\": \"Para acceder a los datos del portapapeles\"\n    },\n    \"notifications\": {\n      \"title\": \"Notificación\",\n      \"description\": \"Para mostrar una notificación\"\n    },\n    \"downloads\": {\n      \"title\": \"Descargar\",\n      \"description\": \"Guardar los activos de la página y cambiar el nombre del archivo descargado\"\n    },\n    \"cookies\": {\n      \"title\": \"Cookies\",\n      \"description\": \"Leer, establecer, o quitar cookies\"\n    }\n  },\n  \"updateMessage\": {\n    \"text1\": \"Automa ha sido actualizado a v{version},\",\n    \"text2\": \"ver las novedades.\"\n  },\n  \"workflows\": {\n    \"folder\": {\n      \"new\": \"Nueva carpeta\",\n      \"name\": \"Nombre de la carpeta\",\n      \"delete\": \"Eliminar carpeta\",\n      \"rename\": \"Cambiar nombre de carpeta\"\n    }\n  },\n  \"auth\": {\n    \"title\": \"Auth\",\n    \"signIn\": \"Iniciar sesión\",\n    \"username\": \"Primero tienes que configurar tu nombre de usuario\",\n    \"clickHere\": \"Pulse aquí\",\n    \"text\": \"Tienes que iniciar sesión antes de poder hacerlo.\"\n  },\n  \"running\": {\n    \"start\": \"Comenzó el {date}\",\n    \"message\": \"Esto sólo muestra los últimos 5 registros\"\n  },\n  \"settings\": {\n    \"theme\": \"Tema\",\n    \"shortcuts\": {\n      \"duplicate\": \"Acceso directo ya utilizado por \\\"{name}\\\"\"\n    },\n    \"editor\": {\n      \"title\": \"Titulo\",\n      \"curvature\": {\n        \"title\": \"Curvatura de la línea\",\n        \"line\": \"Línea\",\n        \"reroute\": \"Redirigir\",\n        \"rerouteFirstLast\": \"Redirigir primer y último punto\"\n      },\n      \"arrow\": {\n        \"title\": \"Fecha\",\n        \"description\": \"Añadir una flecha al final de la línea\"\n      },\n      \"snapGrid\": {\n        \"title\": \"Ajustar a la cuadrícula\",\n        \"description\": \"Ajustarse a la cuadrícula al mover un bloque\"\n      },\n      \"saveWhenExecute\": {\n        \"title\": \"Guardado automático al ejecutar el flujo de trabajo\",\n        \"description\": \"Los cambios en el flujo de trabajo se guardarán al ejecutarlo\"\n      }\n    },\n    \"deleteLog\": {\n      \"title\": \"Borrado automático de registros de flujo de trabajo\",\n      \"after\": \"Borrar después de\",\n      \"deleteAfter\": {\n        \"never\": \"Nunca\",\n        \"days\": \"{day} dias\"\n      }\n    },\n    \"language\": {\n      \"label\": \"Idioma\",\n      \"helpTranslate\": \"¿No encuentra su idioma? Puedes ayuda a traducir.\",\n      \"reloadPage\": \"Recargar la página para que el cambio surta efecto\"\n    },\n    \"menu\": {\n      \"backup\": \"Flujos de trabajo de copia de seguridad\",\n      \"editor\": \"Editor\",\n      \"general\": \"General\",\n      \"shortcuts\": \"Atajos\",\n      \"about\": \"Acerca de\"\n    },\n    \"backupWorkflows\": {\n      \"title\": \"Copia de seguridad local\",\n      \"invalidPassword\": \"Contraseña no válida\",\n      \"workflowsAdded\": \"{count} se han añadido flujos de trabajo\",\n      \"name\": \"Flujos de trabajo de copia de seguridad\",\n      \"needSignin\": \"Primero tienes que registrarte\",\n      \"backup\": {\n        \"button\": \"Copia de seguridad\",\n        \"settings\": \"Configuración de la copia de seguridad\",\n        \"encrypt\": \"Cifrar con contraseña\",\n        \"schedule\": \"Programar copia de seguridad local\"\n      },\n      \"restore\": {\n        \"title\": \"Restaurar flujos de trabajo\",\n        \"button\": \"Restaurar\",\n        \"update\": \"Actualizar si el flujo de trabajo existe\"\n      },\n      \"cloud\": {\n        \"buttons\": {\n          \"local\": \"Local\",\n          \"cloud\": \"Nube\"\n        },\n        \"location\": \"Ubicación\",\n        \"delete\": \"Eliminar copia de seguridad\",\n        \"title\": \"Respaldo en la Nube\",\n        \"sync\": \"Sincronizar\",\n        \"lastSync\": \"Ultima Sincronización\",\n        \"lastBackup\": \"Ultimo Respaldo\",\n        \"select\": \"Seleccionar flujos de trabajo\",\n        \"storedWorkflows\": \"Flujos de trabajo almacenados en la nube\",\n        \"selected\": \"Selección\",\n        \"selectText\": \"Seleccione los flujos de trabajo de los que desea hacer una copia de seguridad\",\n        \"selectAll\": \"Seleccionar todo\",\n        \"deselectAll\": \"Deseleccionar todo\",\n        \"needSelectWorkflow\": \"Debe seleccionar los flujos de trabajo de los que desea hacer una copia de seguridad\"\n      }\n    }\n  },\n  \"workflow\": {\n    \"events\": {\n      \"title\": \"Eventos de flujo de trabajo\",\n      \"add-action\": \"Añadir acción\",\n      \"description\": \"Realizar acciones cuando se produzca el evento.\",\n      \"event\": \"Evento | Eventos\",\n      \"action\": \"Acción\",\n      \"actions\": {\n        \"js-code\": {\n          \"title\": \"Ejecutar código JS\"\n        },\n        \"http-request\": {\n          \"title\": \"Solicitud HTTP\"\n        }\n      },\n      \"types\": {\n        \"finish:success\": {\n          \"name\": \"Finalizar (éxito)\",\n          \"description\": \"La ejecución del flujo de trabajo finaliza con éxito\"\n        },\n        \"finish:failed\": {\n          \"name\": \"Finalizar (fallido)\",\n          \"description\": \"La ejecución del flujo de trabajo ha finalizado con error\"\n        }\n      }\n    },\n    \"previewMode\": {\n      \"title\": \"Modo de vista previa\",\n      \"description\": \"Estás en el modo de vista previa, los cambios realizados no se guardarán.\"\n    },\n    \"pinWorkflow\": {\n      \"pin\": \"Fijar flujo de trabajo\",\n      \"unpin\": \"Quitar flujo de trabajo\",\n      \"pinned\": \"Flujos de trabajo anclados\"\n    },\n    \"parameters\": {\n      \"add\": \"Añadir parámetro\",\n      \"preferInTab\": \"Preferir parámetros de entrada en la pestaña\"\n    },\n    \"my\": \"Mis flujos de trabajo\",\n    \"testing\": {\n      \"title\": \"Modo de Prueba\",\n      \"nextBlock\": \"Siguiente bloque\",\n      \"startRun\": \"Comience a ejecutar en\",\n      \"disabled\": \"Guarde primero los cambios\"\n    },\n    \"import\": \"Importar flujo de trabajo\",\n    \"new\": \"Nuevo Flujo\",\n    \"delete\": \"Eliminar Flujo\",\n    \"browse\": \"Explorar flujos\",\n    \"name\": \"Nombre del flujo\",\n    \"rename\": \"Renombrar flujo\",\n    \"backupCloud\": \"Copia de seguridad en la nube del flujo\",\n    \"add\": \"Añadir flujo de trabajo\",\n    \"clickToEnable\": \"Haga clic para activar\",\n    \"toggleSidebar\": \"Alternar barra lateral\",\n    \"cantEdit\": \"No se puede editar el flujo de trabajo compartido\",\n    \"undo\": \"Deshacer\",\n    \"redo\": \"Rehacer\",\n    \"autoAlign\": {\n      \"title\": \"Auto-alear\"\n    },\n    \"blocksFolder\": {\n      \"title\": \"Bloque de carpetas\",\n      \"add\": \"Añadir bloques a la carpeta\",\n      \"save\": \"Guardar en carpeta\"\n    },\n    \"searchBlocks\": {\n      \"title\": \"Buscar bloques en el editor\"\n    },\n    \"conditionBuilder\": {\n      \"title\": \"Condición de constructor\",\n      \"add\": \"Añadir condición\",\n      \"and\": \"AND\",\n      \"or\": \"OR\",\n      \"topAwait\": \"Apoyar la espera de alto nivel y \\\"automaRefData\\\" función\"\n    },\n    \"host\": {\n      \"title\": \"Flujo de trabajo del host\",\n      \"set\": \"Establecer como flujo de trabajo anfitrión\",\n      \"id\": \"ID de host\",\n      \"add\": \"Añadir flujo de trabajo alojado\",\n      \"sync\": {\n        \"title\": \"Sincronización\",\n        \"description\": \"Sincronización con el flujo de trabajo del host\"\n      },\n      \"messages\": {\n        \"hostExist\": \"Ya ha añadido este host\",\n        \"notFound\": \"No se puede encontrar un flujo de trabajo alojado con el ID \\\"{id}\\\"\",\n        \"successAdded\": \"Flujo de trabajo anfitrión añadido con éxito con el ID \\\"{id}\\\"\"\n      }\n    },\n    \"type\": {\n      \"local\": \"Local\",\n      \"shared\": \"Compartido\",\n      \"host\": \"Host\"\n    },\n    \"unpublish\": {\n      \"title\": \"Flujo de trabajo de despublicación\",\n      \"button\": \"Anular la publicación\",\n      \"body\": \"¿Está seguro de que desea anular la publicación del flujo de trabajo \\\"{name}\\\"?\"\n    },\n    \"share\": {\n      \"url\": \"Compartir URL\",\n      \"publish\": \"Publique\",\n      \"sharedAs\": \"Compartido como \\\"{name}\\\"\",\n      \"title\": \"Compartir flujo de trabajo\",\n      \"download\": \"Guardar flujo de trabajo en local\",\n      \"edit\": \"Editar descripción\",\n      \"fetchLocal\": \"Obtener flujo de trabajo local\",\n      \"update\": \"Actualización\",\n      \"unpublish\": \"Anule la publicación\",\n      \"linkCopied\": \"Enlace copiado al portapapeles\"\n    },\n    \"variables\": {\n      \"title\": \"Variable | Variables\",\n      \"name\": \"Nombre de la variable\",\n      \"assign\": \"Asignar a variable\"\n    },\n    \"protect\": {\n      \"title\": \"Proteger el flujo de trabajo\",\n      \"remove\": \"Quitar la protección\",\n      \"button\": \"Proteja\",\n      \"note\": \"Nota: esta contraseña será necesaria más adelante para editar o eliminar el flujo de trabajo.\"\n    },\n    \"locked\": {\n      \"title\": \"Este flujo de trabajo está protegido\",\n      \"body\": \"Introduce la contraseña para desbloquearlo\",\n      \"unlock\": \"Desbloquear\",\n      \"messages\": {\n        \"incorrect-password\": \"Contraseña incorrecta\"\n      }\n    },\n    \"state\": {\n      \"executeBy\": \"Ejecutado por: \\\"{name}\\\"\"\n    },\n    \"table\": {\n      \"title\": \"Tabla | Tablas\",\n      \"placeholder\": \"Buscar o añadir una columna\",\n      \"select\": \"Seleccionar columna\",\n      \"column\": {\n        \"name\": \"Nombre de columna\",\n        \"type\": \"Tipo de datos\"\n      }\n    },\n    \"sidebar\": {\n      \"workflowIcon\": \"Icono de flujo de trabajo\"\n    },\n    \"editor\": {\n      \"zoomIn\": \"Ampliar\",\n      \"zoomOut\": \"Alejar\",\n      \"resetZoom\": \"Restablecer zoom\",\n      \"duplicate\": \"Duplicar\",\n      \"copy\": \"Copiar\",\n      \"paste\": \"Pegar\",\n      \"group\": \"Agrupar bloques\",\n      \"ungroup\": \"Desagrupar bloques\"\n    },\n    \"settings\": {\n      \"saveLog\": \"Guardar registro de flujo de trabajo\",\n      \"executedBlockOnWeb\": \"Mostrar el bloque ejecutado en la página web\",\n      \"notification\": {\n        \"title\": \"Notificación de flujo de trabajo\",\n        \"description\": \"Mostrar el estado del flujo de trabajo (success or failed) después de ejecutar\",\n        \"noPermission\": \"Esta opción requiere \\\"notifications\\\" permiso para trabajar\"\n      },\n      \"publicId\": {\n        \"title\": \"ID público del flujo de trabajo\",\n        \"description\": \"Establezca un ID público para ejecutar el flujo de trabajo a través de un evento personalizado de JavaScript\"\n      },\n      \"defaultColumn\": {\n        \"title\": \"Insertar en la columna por defecto\",\n        \"description\": \"Insertar datos en la columna por defecto si no hay ninguna columna seleccionada en el bloque\",\n        \"name\": \"Nombre de columna por defecto\"\n      },\n      \"autocomplete\": {\n        \"title\": \"Autocompletar\",\n        \"description\": \"Activar autocompletar en el bloque de entrada (desactivar si hace inestable Automa)\"\n      },\n      \"clearCache\": {\n        \"title\": \"Limpiar cache\",\n        \"description\": \"Limpiar cache (state and loop index) del flujo de trabajo\",\n        \"info\": \"Se ha borrado correctamente la caché del flujo de trabajo\",\n        \"btn\": \"Limpiar\"\n      },\n      \"reuseLastState\": {\n        \"title\": \"Reutilizar el estado del último flujo de trabajo\",\n        \"description\": \"Utilizar los datos estatales (tabla, variables y datos globales) del último flujo de trabajo ejecutado\"\n      },\n      \"debugMode\": {\n        \"title\": \"Modo depuración\",\n        \"description\": \"Ejecutar el flujo de trabajo mediante el protocolo DevTools de Chrome\"\n      },\n      \"restartWorkflow\": {\n        \"for\": \"Reiniciar\",\n        \"times\": \"Veces\",\n        \"description\": \"Número máximo de veces que se reiniciará el flujo de trabajo\"\n      },\n      \"onError\": {\n        \"title\": \"Error en el flujo de trabajo\",\n        \"description\": \"Establecer la acción a tomar si se produce un error en el flujo de trabajo\",\n        \"items\": {\n          \"keepRunning\": \"Seguir Ejecutando\",\n          \"stopWorkflow\": \"Detener el flujo de trabajo\",\n          \"restartWorkflow\": \"Reiniciar el flujo de trabajo\"\n        }\n      },\n      \"timeout\": {\n        \"title\": \"Tiempo de espera del flujo de trabajo (milisegundos)\"\n      },\n      \"blockDelay\": {\n        \"title\": \"Retraso en bloque (milisegundos)\",\n        \"description\": \"Añadir retardo antes de ejecutar cada uno de los bloques\"\n      },\n      \"tabLoadTimeout\": {\n        \"title\": \"Tiempo de espera de carga de la pestaña\",\n        \"description\": \"Tiempo máximo para cargar una pestaña en milisegundos, introduzca 0 para desactivar el tiempo de espera\"\n      }\n    }\n  },\n  \"collection\": {\n    \"description\": \"Ejecute sus flujos de trabajo en secuencia\",\n    \"new\": \"Nueva colección\",\n    \"delete\": \"Borrar colección\",\n    \"add\": \"Añadir colección\",\n    \"rename\": \"Renombrar colección\",\n    \"flow\": \"Flujo\",\n    \"dragDropText\": \"Introduzca aquí un flujo de trabajo o un bloque\",\n    \"options\": {\n      \"atOnce\": {\n        \"title\": \"Ejecutar todos los flujos de trabajo de la colección a la vez\",\n        \"description\": \"Los bloques no se ejecutarán cuando se utilice esta opción\"\n      }\n    },\n    \"globalData\": {\n      \"note\": \"Esto sobrescribirá los datos globales del flujo de trabajo\"\n    }\n  },\n  \"log\": {\n    \"flowId\": \"ID de flujo\",\n    \"goBack\": \"Volver a \\\"{name}\\\" Registros\",\n    \"goWorkflow\": \"Ir al flujo de trabajo\",\n    \"startedDate\": \"Fecha de inicio\",\n    \"duration\": \"Duración\",\n    \"selectAll\": \"Seleccionar todo\",\n    \"deselectAll\": \"Deseleccionar todo\",\n    \"deleteSelected\": \"Borrar los registros seleccionados\",\n    \"clearLogs\": {\n      \"title\": \"Borrar registros\",\n      \"description\": \"¿Estás seguro de que quieres borrar todos los registros?\"\n    },\n    \"types\": {\n      \"stop\": \"El flujo de trabajo se detiene\",\n      \"finish\": \"Acabado\"\n    },\n    \"messages\": {\n      \"url-empty\": \"La URL está vacía\",\n      \"invalid-url\": \"La URL no es válida\",\n      \"conditions-empty\": \"Las condiciones están vacías\",\n      \"invalid-proxy-host\": \"Proxy host no válido\",\n      \"workflow-disabled\": \"Flujo de trabajo desactivado\",\n      \"selector-empty\": \"El selector de elementos está vacío\",\n      \"invalid-body\": \"El cuerpo del contenido no es un JSON válido\",\n      \"invalid-active-tab\": \"\\\"{url}\\\" es una URL no válida\",\n      \"empty-spreadsheet-id\": \"El ID de la hoja de cálculo está vacío\",\n      \"invalid-loop-data\": \"Datos no válidos para el bucle\",\n      \"empty-workflow\": \"Primero debe seleccionar un flujo de trabajo\",\n      \"active-tab-removed\": \"Se ha eliminado la pestaña de flujo de trabajo activo\",\n      \"empty-spreadsheet-range\": \"El rango de la hoja de cálculo está vacío\",\n      \"stop-timeout\": \"El flujo de trabajo se detuvo debido a un tiempo de espera\",\n      \"no-file-access\": \"Automa no tiene acceso al fichero\",\n      \"no-workflow\": \"No se puede encontrar un flujo de trabajo con el ID \\\"{workflowId}\\\"\",\n      \"no-match-tab\": \"No se puede encontrar una pestaña que coincida con el patrón \\\"{pattern}\\\"\",\n      \"no-clipboard-acces\": \"No tiene permiso para acceder al portapapeles\",\n      \"browser-not-supported\": \"Esta función no está disponible en {browser} navegador\",\n      \"element-not-found\": \"No se puede encontrar un elemento con el selector \\\"{selector}\\\"\",\n      \"no-permission\": \"No tiene \\\"{permission}\\\" permiso para realizar esta acción\",\n      \"not-iframe\": \"Elemento con \\\"{selector}\\\" el selector no es un elemento iframe\",\n      \"iframe-not-found\": \"No se puede encontrar un elemento iframe con el selector \\\"{selector}\\\"\",\n      \"workflow-infinite-loop\": \"No se puede ejecutar el flujo de trabajo para evitar un bucle infinito\",\n      \"not-debug-mode\": \"El flujo de trabajo debe ejecutarse en modo de depuración para que este bloque funcione correctamente\",\n      \"no-iframe-id\": \"No se puede encontrar el Frame ID para el elemento iframe con el selector \\\"{selector}\\\"\",\n      \"no-tab\": \"No se puede conectar a una ficha, utilice \\\"New tab\\\" o \\\"Active tab\\\" antes de utilizar el bloque \\\"{name}\\\" bloque\"\n    },\n    \"description\": {\n      \"text\": \"{status} on {date} in {duration}\",\n      \"status\": {\n        \"success\": \"Con éxito\",\n        \"error\": \"Fallido\",\n        \"stopped\": \"Detenido\"\n      }\n    },\n    \"delete\": {\n      \"title\": \"Eliminar Registro\",\n      \"description\": \"¿Estás seguro de que quieres borrar todos los registros seleccionados?\"\n    },\n    \"exportData\": {\n      \"title\": \"Exportar datos\",\n      \"types\": {\n        \"json\": \"JSON\",\n        \"csv\": \"CSV\",\n        \"plain-text\": \"Texto sin formato\"\n      }\n    },\n    \"filter\": {\n      \"title\": \"Filtros\",\n      \"byStatus\": \"Por estado\",\n      \"byDate\": {\n        \"title\": \"Por fecha\",\n        \"items\": {\n          \"lastDay\": \"Último día\",\n          \"last7Days\": \"Últimos siete días\",\n          \"last30Days\": \"Últimos treinta días\"\n        }\n      }\n    }\n  },\n  \"components\": {\n    \"pagination\": {\n      \"text1\": \"Mostrar\",\n      \"text2\": \"Elementos de {count}\",\n      \"nextPage\": \"Siguiente página\",\n      \"currentPage\": \"Página actual\",\n      \"prevPage\": \"Página anterior\",\n      \"of\": \"de {page}\"\n    }\n  }\n}"
  },
  {
    "path": "src/locales/es/popup.json",
    "content": "{\n  \"recording\": {\n    \"stop\": \"Detener la grabación\",\n    \"title\": \"Grabación\"\n  },\n  \"home\": {\n    \"record\": {\n      \"title\": \"Registro del flujo de trabajo\",\n      \"button\": \"Registro\",\n      \"name\": \"Nombre del flujo de trabajo\",\n      \"selectBlock\": \"Seleccione un bloque para empezar\",\n      \"anotherBlock\": \"No se puede empezar desde este bloque\",\n      \"tabs\": {\n        \"new\": \"Nuevo flujo de trabajo\",\n        \"existing\": \"Flujo de trabajo existente\"\n      }\n    },\n    \"elementSelector\": {\n      \"name\": \"Selector de elementos\",\n      \"noAccess\": \"No tiene acceso a este sitio\"\n    },\n    \"workflow\": {\n      \"new\": \"Nuevo flujo de trabajo\",\n      \"rename\": \"Renombrar flujo de trabajo\",\n      \"delete\": \"Eliminar flujo de trabajo\",\n      \"type\": {\n        \"host\": \"Host\",\n        \"local\": \"Local\"\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "src/locales/fr/blocks.json",
    "content": "{\n  \"collection\": {\n    \"blocks\": {\n      \"export-result\": {\n        \"name\": \"Exporter le résultat\",\n        \"description\": \"Exporte le résultat de la collection au format JSON\"\n      }\n    }\n  },\n  \"workflow\": {\n    \"blocks\": {\n      \"base\": {\n        \"title\": \"Blocs\",\n        \"moveToGroup\": \"Déplacer le bloc vers le groupe de blocs\",\n        \"selector\": \"Sélecteur d'éléments\",\n        \"selectorOptions\": \"Options du Sélecteur\",\n        \"timeout\": \"Délai d'attente (millisecondes)\",\n        \"noPermission\": \"Automa n'a pas les droits nécessaires pour effectuer cette action\",\n        \"grantPermission\": \"Accorder les droits\",\n        \"action\": \"Action\",\n        \"element\": {\n          \"select\": \"Choisir un element\",\n          \"verify\": \"Vérifier le sélecteur\"\n        },\n        \"settings\": {\n          \"title\": \"Paramètres du Bloc\",\n          \"blockTimeout\": {\n            \"title\": \"Délai d'éxécution du bloc (millisecond)\",\n            \"description\": \"Durée maximale d'attente pour l'éxécution du Bloc (0 pour désactiver)\"\n          },\n          \"line\": {\n            \"title\": \"Lignes\",\n            \"label\": \"Label\",\n            \"animated\": \"Animé\",\n            \"select\": \"Choisir une ligne\",\n            \"to\": \"N° ligne en {name} block\",\n            \"lineColor\": \"Couleur\"\n          }\n        },\n        \"toggle\": {\n          \"enable\": \"Activer le bloc\",\n          \"disable\": \"Désactiver le bloc\"\n        },\n        \"onError\": {\n          \"info\": \"Ces règles seront appliquées en cas d'erreur dans le bloc\",\n          \"button\": \"Si Erreur\",\n          \"title\": \"Quand une errur survient\",\n          \"retry\": \"Retenter l'action\",\n          \"fallbackTitle\": \"Exécuté en cas d'erreur\",\n          \"times\": {\n            \"name\": \"Essais\",\n            \"description\": \"Nombre d'essais pour éxécuter l'action\"\n          },\n          \"interval\": {\n            \"name\": \"Intervalle\",\n            \"description\": \"Durée de l'intervalle entre chaque essai\",\n            \"second\": \"secondes\"\n          },\n          \"toDo\": {\n            \"error\": \"en cas d'erreur\",\n            \"continue\": \"Continuer le processus\",\n            \"fallback\": \"Fonction de substitution\",\n            \"restart\": \"Redémarrer le processus\"\n          },\n          \"insertData\": {\n            \"name\": \"Entrer des données\"\n          }\n        },\n        \"table\": {\n          \"checkbox\": \"Insérer dans le tableau\",\n          \"select\": \"Selectionner une colonne\",\n          \"extraRow\": {\n            \"checkbox\": \"Ajouter une ligne supplémentaire\",\n            \"placeholder\": \"Valeur\",\n            \"title\": \"Valeur de la ligne supplémentaire\"\n          }\n        },\n        \"findElement\": {\n          \"placeholder\": \"Rechercher un élément par\",\n          \"options\": {\n            \"cssSelector\": \"Sélecteur CSS\",\n            \"xpath\": \"XPath\"\n          }\n        },\n        \"markElement\": {\n          \"title\": \"Un élément ne sera pas sélectionné s'il a déjà été sélectionné avant\",\n          \"text\": \"Marquer l'élément\"\n        },\n        \"multiple\": {\n          \"title\": \"Sélectionner plusieurs éléments\",\n          \"text\": \"Multiple\"\n        },\n        \"waitSelector\": {\n          \"title\": \"Attendre le sélecteur\",\n          \"timeout\": \"Délai d'attente du sélecteur (ms)\"\n        },\n        \"downloads\": {\n          \"onConflict\": {\n            \"uniquify\": \"Rendre unique\",\n            \"overwrite\": \"Ecraser\",\n            \"prompt\": \"Demander\"\n          }\n        }\n      },\n      \"wait-connections\": {\n        \"name\": \"Attendre les connexions\",\n        \"description\": \"Attendre que les connexions soient établies avant de continuer\",\n        \"specificFlow\": \"Continuer un processus spécifique\",\n        \"selectFlow\": \"Choisir un processus\"\n      },\n      \"cookie\": {\n        \"name\": \"Cookie\",\n        \"description\": \"Lire, définir, ou supprimer des cookies\",\n        \"types\": {\n          \"get\": \"Lire un cookie\",\n          \"set\": \"Définir un cookie\",\n          \"remove\": \"Supprimer des cookies\",\n          \"getAll\": \"Lire tous les cookies\"\n        },\n        \"useJson\": \"Utiliser le format JSON\"\n      },\n      \"note\": {\n        \"name\": \"Note\"\n      },\n      \"slice-variable\": {\n        \"name\": \"Extraction (Slice)\",\n        \"description\": \"Extraire une section de la chaine d'une variable\",\n        \"start\": \"Indice de début\",\n        \"end\": \"Indice de fin\"\n      },\n      \"workflow-state\": {\n        \"name\": \"Etats du Workflow\",\n        \"description\": \"Contrôler les états du workflow\",\n        \"actions\": {\n          \"stop\": \"Arrêter le workflow\"\n        }\n      },\n      \"regex-variable\": {\n        \"name\": \"formule RegEx\",\n        \"description\": \"Utiliser une formule RegEx sur la valeur d'une variable\"\n      },\n      \"data-mapping\": {\n        \"source\": \"Source\",\n        \"destination\": \"Destination\",\n        \"name\": \"Affectation des données\",\n        \"edit\": \"Editer l'affectation des données\",\n        \"dataSource\": \"Source des données\",\n        \"description\": \"Affecter les données d'une variable ou d'un tableau\",\n        \"addSource\": \"Ajouter une source\",\n        \"addDestination\": \"Ajouter une destination\"\n      },\n      \"sort-data\": {\n        \"name\": \"Classer les données\",\n        \"description\": \"Ordonner les éléments des données\",\n        \"property\": \"Classer en fonction des propriétés de l'élément\",\n        \"addProperty\": \"Ajouter une propriété\"\n      },\n      \"increase-variable\": {\n        \"name\": \"Augmenter une variable\",\n        \"description\": \"Augmenter la valeur d'une variable d'un montant donné\",\n        \"increase\": \"Augmenter de\"\n      },\n      \"notification\": {\n        \"name\": \"notification\",\n        \"description\": \"Afficher une notification\",\n        \"title\": \"Titre\",\n        \"message\": \"Message\",\n        \"imageUrl\": \"Adresse de l'image (optionnel)\",\n        \"iconUrl\": \"Adresse de l'icone (optionnel)\"\n      },\n      \"delete-data\": {\n        \"name\": \"Supprimer les données\",\n        \"description\": \"Supprimer le contenu d'un tableau ou d'une variable\",\n        \"from\": \"Données provenant de\",\n        \"allColumns\": \"[Toutes les colonnes]\"\n      },\n      \"log-data\": {\n        \"name\": \"Voir le log\",\n        \"description\": \"Visualiser les données de log récentes relatives au workflow\",\n        \"data\": \"Données du log\"\n      },\n      \"tab-url\": {\n        \"name\": \"Obtenir l'URL de l'onglet\",\n        \"description\": \"Obtenir l'URL de l'onglet\",\n        \"select\": \"Sélectionner l'onglet\",\n        \"types\": {\n          \"active-tab\": \"Activer l'onglet\",\n          \"all\": \"Tous les onglets\"\n        },\n        \"query\": {\n          \"title\": \"Requête\",\n          \"matchPatterns\": \"@:workflow.blocks.switch-tab.matchPattern (optionnel)\",\n          \"tabTitle\": \"Intitulé de l'onglet (optionnel)\"\n        }\n      },\n      \"reload-tab\": {\n        \"name\": \"Recharger l'onglet\",\n        \"description\": \"Recharger l'onglet actif\"\n      },\n      \"press-key\": {\n        \"name\": \"Appuyer sur une touche\",\n        \"description\": \"Appuyer sur une touche ou une combinaison\",\n        \"target\": \"Element cible (optionnel)\",\n        \"key\": \"Touche\",\n        \"detect\": \"Detecter les touches\",\n        \"actions\": {\n          \"press-key\": \"Appuyer sur une touche\",\n          \"multiple-keys\": \"Appuyer sur plusieurs touches\"\n        }\n      },\n      \"save-assets\": {\n        \"name\": \"Sauvegarder les assets\",\n        \"description\": \"Sauvegarder les assets (image, video, audio, ou fichier) d'un élément ou d'une URL\",\n        \"filename\": \"Fichier (optionnel)\",\n        \"saveDownloadIds\": \"Sauvegarder les éléments' récupérer les IDs\",\n        \"contentTypes\": {\n          \"title\": \"Type\",\n          \"element\": \"Media (image, audio, ou video)\",\n          \"url\": \"URL\"\n        }\n      },\n      \"handle-dialog\": {\n        \"name\": \"Boites de dialogue\",\n        \"description\": \"Valider ou ignorer les boites de dialogue Javascript (alerte, confirmation, demande, ou onbeforeunload\",\n        \"accept\": \"Valider\",\n        \"promptText\": {\n          \"label\": \"Texte à insérer (optionnel)\",\n          \"description\": \"Le texte à insérer dans la boite de dialogue avant de valider\"\n        }\n      },\n      \"handle-download\": {\n        \"name\": \"Téléchargement\",\n        \"description\": \"Options de téléchargement des fichiers\",\n        \"timeout\": \"Délai d'attente (millisecondes)\",\n        \"noPermission\": \"Vous n'êtes pas autorisé à accéder au répertoire de téléchargement\",\n        \"onConflict\": \"Si collision\",\n        \"waitFile\": \"Attendre la fin du téLéchargement de fichier\",\n        \"downloadId\": \"ID du fichier téléchargé (optionnel)\",\n        \"filePath\": \"Chemin du fichier\"\n      },\n      \"insert-data\": {\n        \"name\": \"Insérer des données\",\n        \"description\": \"Insérer des données dans la table ou une variable\"\n      },\n      \"clipboard\": {\n        \"name\": \"Presse-papiers\",\n        \"description\": \"Récupérer le texte copié à partir du presse-papiers\",\n        \"data\": \"Données du presse-papiers\",\n        \"noPermission\": \"Vous n'êtes pas autorisé à accéder au presse-papiers\",\n        \"grantPermission\": \"Donner la permission\"\n      },\n      \"hover-element\": {\n        \"name\": \"Survoler un élément\",\n        \"description\": \"Survolez un élément\"\n      },\n      \"create-element\": {\n        \"name\": \"Créer un élément\",\n        \"description\": \"Créer un élément et l'injecter dans la page\",\n        \"edit\": \"Modifier un élément\",\n        \"wrap\": \"Insérer un élément entre\",\n        \"insertEl\": {\n          \"title\": \"Insérer un élément\",\n          \"items\": {\n            \"before\": \"comme 'first child' (premier enfant\",\n            \"after\": \"comme 'last child' (dernier enfant)\",\n            \"next-sibling\": \"en tant que prochain voisin\",\n            \"prev-sibling\": \"en tant que précédent voisin\",\n            \"replace\": \"Remplace l'élément cible\"\n          }\n        }\n      },\n      \"upload-file\": {\n        \"name\": \"Envoyer un fichier\",\n        \"description\": \"Envoyer le fichier dans l'élément <input type=\\\"file\\\">\",\n        \"filePath\": \"URL ou chemin d'accès au fichier\",\n        \"addFile\": \"Ajouter un fichier\",\n        \"requirement\": \"Voir les conditions requises avant d'utiliser ce bloc\",\n        \"noFileAccess\": \"Automa n'a pas accès aux fichiers\"\n      },\n      \"browser-event\": {\n        \"name\": \"Événement du navigateur\",\n        \"description\": \"Exécuter le bloc suivant lorsque l'événement est déclenché\",\n        \"events\": \"Événements\",\n        \"timeout\": \"Délai d'attente (millisecondes)\",\n        \"activeTabLoaded\": \"Onglet actif\",\n        \"setAsActiveTab\": \"Définir comme onglet actif\"\n      },\n      \"blocks-group\": {\n        \"name\": \"Groupe de blocs\",\n        \"groupName\": \"Nom du groupe\",\n        \"description\": \"Regroupement de blocs\",\n        \"dropText\": \"Faites glisser et déposez un bloc ici\",\n        \"cantAdd\": \"Impossible d'ajouter le bloc \\\"{blockName}\\\" au groupe.\"\n      },\n      \"trigger\": {\n        \"name\": \"Déclencheur\",\n        \"description\": \"Bloc où le workflow commencera à s'exécuter\",\n        \"addTime\": \"Ajouter une heure\",\n        \"selectDay\": \"Sélectionnez un jour\",\n        \"timeExist\": \"Vous avez déjà ajouté {time} le {day}\",\n        \"fixedDelay\": \"Délai fixe\",\n        \"contextMenus\": {\n          \"noPermission\": \"Ce déclencheur requiert les autorisations \\\"contextMenus\\\" pour fonctionner\",\n          \"grantPermission\": \"Autoriser\",\n          \"appearIn\": \"figurera dans\",\n          \"contextName\": \"Nom du workflow dans le menu contextuel\"\n        },\n        \"days\": [\n          \"Dimanche\",\n          \"Lundi\",\n          \"Mardi\",\n          \"Mercredi\",\n          \"Jeudi\",\n          \"Vendredi\",\n          \"Samedi\"\n        ],\n        \"useRegex\": \"Utiliser une Regex\",\n        \"shortcut\": {\n          \"tooltip\": \"Enregistrer un raccourci\",\n          \"stopRecord\": \"Arrête d'enregistrer\",\n          \"checkboxTitle\": \"Exécuter le raccourci même lorsque vous êtes dans un élément de saisie\",\n          \"checkbox\": \"Actif dans un élément de saisie\",\n          \"note\": \"Note: le raccourci clavier ne fonctionne que lorsque vous êtes sur une page web\"\n        },\n        \"forms\": {\n          \"triggerWorkflow\": \"Déclencher le workflow\",\n          \"interval\": \"Intervalle (minutes)\",\n          \"delay\": \"Délai (minutes)\",\n          \"date\": \"Date\",\n          \"time\": \"Heure\",\n          \"url\": \"URL ou Regex\",\n          \"shortcut\": \"Rccourci\"\n        },\n        \"element-change\": {\n          \"target\": \"Choisir l'élément à observer\",\n          \"optionsInfo\": \"Quelle mutation déclenchera le workflow\",\n          \"targetWebsite\": \"La modèle correspondant du site où se trouve l'élément ciblé (cliquer pour voir plus d'exemples de Modèles)\",\n          \"baseEl\": {\n            \"title\": \"Elément de base (optionnel)\",\n            \"description\": \"Automa redémarrera l'observation de l'élément ciblé quand cet élément sera modifié\"\n          },\n          \"subtree\": {\n            \"title\": \"Inclure 'Subtree'\",\n            \"description\": \"Etendre l'observation à toute la hiérarchie descendante de l'élément ciblé\"\n          },\n          \"childList\": {\n            \"title\": \"'Child list'\",\n            \"description\": \"Observer l'ajout de nouveaux éléments-enfants ou la suppression d'éléments-enfants existants\"\n          },\n          \"attributes\": {\n            \"title\": \"'Attributes'\",\n            \"description\": \"Observer les mutations sur les valeurs des attributs de l'élément ciblé\"\n          },\n          \"attributeFilter\": {\n            \"title\": \"'Attrobute Filter' (Filtre des attributs)\",\n            \"separate\": \"Utiliser des virgules (,) entre chaque nom d'attribut\",\n            \"description\": \"Observer uniquement certains attributs (laisser vide pour tout observer)\"\n          },\n          \"characterData\": {\n            \"title\": \"'Character data' (caractères)\",\n            \"description\": \"Observe les mutations de caractères/texte au sein de l'élément ciblé\"\n          }\n        },\n        \"items\": {\n          \"manual\": \"Manuellement\",\n          \"interval\": \"Intervalle\",\n          \"cron-job\": \"Tache programmée (Cron job)\",\n          \"date\": \"À une date précise\",\n          \"context-menu\": \"Menu contextuel\",\n          \"element-change\": \"Quand un élément est modifié\",\n          \"specific-day\": \"À un jour précis\",\n          \"visit-web\": \"Lorsque vous visitez un site Web\",\n          \"on-startup\": \"Au démarrage du navigateur\",\n          \"keyboard-shortcut\": \"Raccourci clavier\"\n        }\n      },\n      \"execute-workflow\": {\n        \"name\": \"Executer le workflow\",\n        \"overwriteNote\": \"Cela écrasera les données globales du workflow sélectionné\",\n        \"select\": \"Selectionner le workflow\",\n        \"executeId\": \"Executer l'id\",\n        \"description\": \"\",\n        \"insertAllVars\": \"Utiliser toutes les variables du workflow actuel\",\n        \"insertVars\": \"Insérer les variables du workflow actuel\",\n        \"useCommas\": \"Utiliser des virgules pour séparer les noms de variable\"\n      },\n      \"google-sheets-drive\": {\n        \"name\": \"@:workflow.blocks.google-sheets.name (GDrive)\",\n        \"description\": \"@:workflow.blocks.google-sheets.description\",\n        \"connected\": \"Feuilles de calcul connectées\",\n        \"select\": \"Choisir une feuille de calcul\",\n        \"connect\": \"Connecter une feuille de calcul\"\n      },\n      \"google-drive\": {\n        \"name\": \"Google Drive\",\n        \"description\": \"Téléverser des fichiers vers Google Drive\",\n        \"actions\": {\n          \"upload\": \"Téléverser des fichiers\"\n        }\n      },\n      \"google-sheets\": {\n        \"name\": \"Google sheets\",\n        \"description\": \"Lire ou mettre à jour les données Google Sheets\",\n        \"previewData\": \"Aperçu des données\",\n        \"firstRow\": \"Utilisez la première ligne comme clés\",\n        \"keysAsFirstRow\": \"Utiliser les clés comme première ligne\",\n        \"insertData\": \"Insérer des données\",\n        \"valueInputOption\": \"Option de saisie de valeur\",\n        \"dataFrom\": {\n          \"label\": \"Données de\",\n          \"options\": {\n            \"data-columns\": \"Table\",\n            \"custom-data\": \"Données personnalisées\"\n          }\n        },\n        \"refKey\": {\n          \"label\": \"Clé de référence (facultatif)\",\n          \"placeholder\": \"Nom de la clé\"\n        },\n        \"spreadsheetId\": {\n          \"label\": \"Identifiant de la feuille de calcul\",\n          \"link\": \"Découvrez comment obtenir un identifiant de feuille de calcul\"\n        },\n        \"range\": {\n          \"label\": \"Plage\",\n          \"link\": \"Cliquez pour voir plus d'exemple\"\n        },\n        \"select\": {\n          \"get\": \"Obtenir les valeurs des cellules de la feuille de calcul\",\n          \"getRange\": \"Obtenir la plage de valeurs de la feuille de calcul\",\n          \"update\": \"Mettre à jour les valeurs des cellules de la feuille de calcul\",\n          \"append\": \"Ajouter les valeurs des cellules à la feuille de calcul\",\n          \"clear\": \"Effacer les valeurs des cellules\",\n          \"create\": \"Créer une feuille de calcul\",\n          \"add-sheet\": \"Ajouter une feuille de calcul\"\n        }\n      },\n      \"active-tab\": {\n        \"name\": \"Onglet actif\",\n        \"description\": \"Définit l'onglet actuel dans lequel vous vous trouvez en tant qu'onglet actif\"\n      },\n      \"proxy\": {\n        \"name\": \"Proxy\",\n        \"description\": \"Définit le proxy du navigateur\",\n        \"clear\": \"Effacer tous les proxys\",\n        \"bypass\": {\n          \"label\": \"Liste des contournements\",\n          \"note\": \"Utilisez des virgules (,) pour séparer les URLs\"\n        }\n      },\n      \"new-window\": {\n        \"name\": \"Nouvelle fenêtre\",\n        \"description\": \"Créer une nouvelle fenêtre\",\n        \"top\": \"Haut\",\n        \"left\": \"Gauche\",\n        \"height\": \"Hauteur\",\n        \"width\": \"Largeur\",\n        \"note\": \"Remarque : utilisez 0 pour désactiver\",\n        \"position\": \"Position de la fenêtre\",\n        \"size\": \"Taille de la fenêtre\",\n        \"windowState\": {\n          \"placeholder\": \"État de la fenêtre\",\n          \"options\": {\n            \"normal\": \"Normale\",\n            \"minimized\": \"Minimisée\",\n            \"maximized\": \"Maximisée\",\n            \"fullscreen\": \"Plein écran\"\n          }\n        },\n        \"incognito\": {\n          \"text\": \"Définir comme fenêtre de navigation privée\",\n          \"note\": \"Vous devez activer « Autoriser en navigation privée » pour que cette extension puisse utiliser l'option\"\n        }\n      },\n      \"go-back\": {\n        \"name\": \"Revenir en arrière\",\n        \"description\": \"Retourne à la page précédente\"\n      },\n      \"forward-page\": {\n        \"name\": \"Avancer\",\n        \"description\": \"Aller à la page suivante\"\n      },\n      \"close-tab\": {\n        \"name\": \"Fermer l'onglet\",\n        \"description\": \"\",\n        \"activeTab\": \"Ferme l'onglet actif\",\n        \"url\": \"URL ou modèle de correspondance\",\n        \"allWindows\": \"Fermez toutes les fenêtres\"\n      },\n      \"event-click\": {\n        \"name\": \"Cliquer sur l'élément\",\n        \"description\": \"\"\n      },\n      \"delay\": {\n        \"name\": \"Délai\",\n        \"description\": \"Ajoute un délai avant d'exécuter le bloc suivant\",\n        \"input\": {\n          \"title\": \"Délai en milliseconde\",\n          \"placeholder\": \"(milliseconde)\"\n        }\n      },\n      \"get-text\": {\n        \"name\": \"Obtenir le texte\",\n        \"description\": \"Obtenir le texte d'un élément\",\n        \"checkbox\": \"Enregistrer les données\",\n        \"includeTags\": \"Inclure les balises HTML\",\n        \"prefixText\": {\n          \"placeholder\": \"Préfixe du texte\",\n          \"title\": \"Ajouter un préfixe au texte\"\n        },\n        \"suffixText\": {\n          \"placeholder\": \"Suffixe du texte\",\n          \"title\": \"Ajouter un suffixe au texte\"\n        }\n      },\n      \"export-data\": {\n        \"name\": \"Exporter les données\",\n        \"description\": \"Exporte les colonnes de données du workflow\",\n        \"exportAs\": \"Exporter en tant que\",\n        \"refKey\": \"Clé de référence\",\n        \"bomHeader\": \"Ajouter la nomenclature UTF-8\",\n        \"dataToExport\": {\n          \"placeholder\": \"Données à exporter\",\n          \"options\": {\n            \"data-columns\": \"Table\",\n            \"google-sheets\": \"Google sheets\",\n            \"variable\": \"Variable\"\n          }\n        }\n      },\n      \"element-scroll\": {\n        \"name\": \"Défiler l'élément\",\n        \"description\": \"\",\n        \"scrollY\": \"Défilement vertical\",\n        \"scrollX\": \"Défilement horizontal\",\n        \"intoView\": \"Défiler dans la vue\",\n        \"smooth\": \"Défilement fluide\",\n        \"incScrollX\": \"Incrémenter le défilement horizontal\",\n        \"incScrollY\": \"Incrémenter le défilement vertical\"\n      },\n      \"switch-tab\": {\n        \"name\": \"Changer d'onglet\",\n        \"description\": \"Basculer entre les onglets\",\n        \"matchPattern\": \"Modèles de correspondance\",\n        \"url\": \"URL du nouvel onglet\",\n        \"createIfNoMatch\": \"Créer s'il n'y a pas de correspondance\"\n      },\n      \"new-tab\": {\n        \"name\": \"Nouvel onglet\",\n        \"description\": \"\",\n        \"url\": \"URL du nouvel onglet\",\n        \"customUserAgent\": \"Utiliser un User-Agent personnalisé\",\n        \"activeTab\": \"Définir comme onglet actif\",\n        \"tabToGroup\": \"Ajouter l'onglet au groupe\",\n        \"waitTabLoaded\": \"Attendre la fin du chargement de l'onglet\",\n        \"updatePrevTab\": {\n          \"title\": \"Utilise le nouvel onglet précédemment ouvert au lieu d'en créer un nouveau\",\n          \"text\": \"Mettre à jour l'onglet précédemment ouvert\"\n        }\n      },\n      \"link\": {\n        \"name\": \"Lien\",\n        \"description\": \"Ouvre le lien d'un élément\",\n        \"openInNewTab\": \"Ouvrir dans un nouvel onglet\"\n      },\n      \"attribute-value\": {\n        \"name\": \"Valeur de l'attribut\",\n        \"description\": \"Obtenir la valeur de l'attribut d'un élément\",\n        \"forms\": {\n          \"name\": \"Nom de l'attribut\",\n          \"checkbox\": \"Enregistrer des données\",\n          \"column\": \"Sélectionnez la colonne\",\n          \"extraRow\": {\n            \"checkbox\": \"Ajouter une ligne supplémentaire\",\n            \"placeholder\": \"Valeur\",\n            \"title\": \"Valeur de la ligne supplémentaire\"\n          }\n        }\n      },\n      \"forms\": {\n        \"name\": \"Formulaires\",\n        \"description\": \"\",\n        \"selected\": \"Selectionné\",\n        \"type\": \"Type de formulaire\",\n        \"getValue\": \"Obtenir la valeur du formulaire\",\n        \"text-field\": {\n          \"name\": \"Champ de texte\",\n          \"value\": \"Valeur\",\n          \"clearValue\": \"Effacer la valeur du formulaire\",\n          \"delay\": {\n            \"placeholder\": \"Délai\",\n            \"label\": \"Délai de frappe (milliseconde) (0 pour désactiver)\"\n          }\n        },\n        \"select\": {\n          \"name\": \"Sélection\"\n        },\n        \"radio\": {\n          \"name\": \"Radio\"\n        },\n        \"checkbox\": {\n          \"name\": \"Case à cocher\"\n        }\n      },\n      \"repeat-task\": {\n        \"name\": \"Répéter la tâche\",\n        \"description\": \"\",\n        \"times\": \"fois\",\n        \"repeatFrom\": \"Répéter depuis\"\n      },\n      \"javascript-code\": {\n        \"name\": \"Code JavaScript\",\n        \"description\": \"Exécutez votre code javascript dans la page web\",\n        \"availabeFuncs\": \"Méthodes disponibles:\",\n        \"removeAfterExec\": \"Supprimer après l'exécution du bloc\",\n        \"everyNewTab\": \"Executer dans chaque nouvel onglet\",\n        \"context\": {\n          \"name\": \"Contexte d'éxécution\",\n          \"items\": {\n            \"website\": \"Onglet actif\",\n            \"background\": \"Tâche de fond\"\n          }\n        },\n        \"modal\": {\n          \"tabs\": {\n            \"code\": \"Code JavaScript\",\n            \"preloadScript\": \"Script de préchargement\"\n          }\n        },\n        \"timeout\": {\n          \"placeholder\": \"Délai d'attente\",\n          \"title\": \"Délai d'exécution du code Javascript\"\n        }\n      },\n      \"trigger-event\": {\n        \"name\": \"Événement déclencheur\",\n        \"description\": \"\",\n        \"selectEvent\": \"Sélectionnez un événement\"\n      },\n      \"conditions\": {\n        \"name\": \"Conditions\",\n        \"add\": \"Ajouter une condition\",\n        \"retryConditions\": \"Réessayer si aucune condition n'est remplie\",\n        \"description\": \"Bloc conditionnel\",\n        \"refresh\": \"Rafraichir les connections des conditions\",\n        \"fallbackTitle\": \"Exécuté lorsque toutes les comparaisons ne répondent pas aux exigences\",\n        \"equals\": \"Égale à\",\n        \"gt\": \"Plus grand que\",\n        \"gte\": \"Plus grand que ou égal\",\n        \"lt\": \"Moins que\",\n        \"lte\": \"Moins que ou égal\",\n        \"ne\": \"Différent\",\n        \"contains\": \"Contient\"\n      },\n      \"element-exists\": {\n        \"name\": \"L'élément existe\",\n        \"description\": \"Vérifie si un élément existe\",\n        \"selector\": \"Sélecteur d'éléments\",\n        \"fallbackTitle\": \"Exécuté lorsque l'élément n'existe pas\",\n        \"throwError\": \"Activer une erreur si n'existe pas\",\n        \"tryFor\": {\n          \"title\": \"Essaye de vérifier si l'élément existe\",\n          \"label\": \"Nombre d'essai\"\n        },\n        \"timeout\": {\n          \"label\": \"Délai d'attente (millisecondes)\",\n          \"title\": \"Délai d'attente pour chaque essai\"\n        }\n      },\n      \"webhook\": {\n        \"name\": \"Requête HTTP\",\n        \"description\": \"Faire une requête HTTP\",\n        \"contentType\": \"Type de contenu\",\n        \"method\": \"Méthode de la requête\",\n        \"url\": \"URL de la requête\",\n        \"fallback\": \"Exécuter en cas d'échec ou d'erreur lors de la création de la requête HTTP\",\n        \"buttons\": {\n          \"header\": \"Ajouter un en-tête\"\n        },\n        \"timeout\": {\n          \"placeholder\": \"Délai d'exécution\",\n          \"title\": \"Délai d'exécution de la requête HTTP (ms)\"\n        },\n        \"tabs\": {\n          \"headers\": \"En-têtes\",\n          \"body\": \"Corps\",\n          \"response\": \"Réponse\"\n        }\n      },\n      \"while-loop\": {\n        \"name\": \"Boucle Tant que\",\n        \"description\": \"Exécute les blocs tant que la condition est remplie\",\n        \"editCondition\": \"Modifier les conditions\",\n        \"fallback\": \"S'éxécute quand la condition n'est pas remplie\"\n      },\n      \"loop-elements\": {\n        \"name\": \"Boucle d'éléments\",\n        \"description\": \"Itérer entre chaque éléMent\",\n        \"loadMore\": \"Charger d'autres éléments\",\n        \"scrollToBottom\": \"Scroller jusqu'en bas de page\",\n        \"scrollToTop\": \"Scroller jusqu'en haut de page\",\n        \"actions\": {\n          \"none\": \"Aucune\",\n          \"click-element\": \"Cliquer sur un élément\",\n          \"scroll\": \"Scroller vers la bas\",\n          \"click-link\": \"Cliquer sur un lien\",\n          \"scroll-up\": \"Scroller vers le haut\"\n        }\n      },\n      \"loop-data\": {\n        \"name\": \"Boucle de données\",\n        \"description\": \"Itérer depuis les colonnes de données ou vos données personnalisées\",\n        \"loopId\": \"ID de la boucle\",\n        \"refKey\": \"Clé de référence\",\n        \"startIndex\": \"Commencer à partir de l'index\",\n        \"resumeLastWorkflow\": \"Reprendre la où la dernière éxecution s'est arrêté\",\n        \"modal\": {\n          \"fileTooLarge\": \"Fichier trop volumineux pour être modifié\",\n          \"maxFile\": \"La taille maximale du fichier est de 1 Mo\",\n          \"options\": {\n            \"firstRow\": \"Utiliser la première ligne comme clé\"\n          }\n        },\n        \"buttons\": {\n          \"clear\": \"Effacer les données\",\n          \"insert\": \"Insérer des données\",\n          \"import\": \"Importer un fichier\"\n        },\n        \"maxLoop\": {\n          \"title\": \"Nombre maximum d'itérations de la boucle\",\n          \"label\": \"Nombre maximum d'itérations (0 pour désactiver)\"\n        },\n        \"loopThrough\": {\n          \"placeholder\": \"Boucler depuis\",\n          \"fromNumber\": \"Depuis le nombre\",\n          \"toNumber\": \"Vers le nombre\",\n          \"options\": {\n            \"numbers\": \"Nombres\",\n            \"variable\": \"Variable\",\n            \"data-columns\": \"Table\",\n            \"table\": \"Table\",\n            \"custom-data\": \"Données personnalisées\",\n            \"google-sheets\": \"Google sheets\",\n            \"elements\": \"Elements\"\n          }\n        }\n      },\n      \"loop-breakpoint\": {\n        \"name\": \"Point d'arrêt de la boucle\",\n        \"description\": \"Pour dire où la boucle doit s'arrêter\"\n      },\n      \"take-screenshot\": {\n        \"name\": \"Prendre une capture d'écran\",\n        \"fullPage\": \"Prendre une capture d'écran pleine page\",\n        \"description\": \"Prend une capture d'écran de l'onglet actif\",\n        \"imageQuality\": \"Qualité de l'image\",\n        \"saveToColumn\": \"Insérer une capture d'écran dans la Table\",\n        \"saveToComputer\": \"Enregistrer la capture d'écran sur l'ordinateur\",\n        \"types\": {\n          \"title\": \"Prende une capture d'écran\",\n          \"page\": \"D'une page\",\n          \"fullpage\": \"D'une page entière\",\n          \"element\": \"D'un élément\"\n        }\n      },\n      \"switch-to\": {\n        \"name\": \"Basculer de cadre\",\n        \"description\": \"Basculer entre la fenêtre principale et l'iframe\",\n        \"iframeSelector\": \"Sélecteur de l'élément iframe\",\n        \"windowTypes\": {\n          \"main\": \"Fenêtre principale\",\n          \"iframe\": \"Iframe\"\n        }\n      },\n      \"debugMode\": {\n        \"title\": \"Modalità di debug\",\n        \"description\": \"Esegui il block usando il protocollo di Chrome DevTools\"\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "src/locales/fr/common.json",
    "content": "{\n  \"common\": {\n    \"dashboard\": \"Tableau de bord\",\n    \"workflow\": \"Workflow | Workflows\",\n    \"collection\": \"Collection | Collections\",\n    \"log\": \"Log | Logs\",\n    \"block\": \"Bloc | Blocs\",\n    \"schedule\": \"Planificateur\",\n    \"folder\": \"Dossier | Dossiers\",\n    \"new\": \"Nouveau\",\n    \"docs\": \"Documentation\",\n    \"search\": \"Rechercher\",\n    \"example\": \"Example | Examples\",\n    \"import\": \"Importer\",\n    \"export\": \"Exporter\",\n    \"rename\": \"Renommer\",\n    \"execute\": \"Exécuter\",\n    \"delete\": \"Supprimer\",\n    \"cancel\": \"Annuler\",\n    \"settings\": \"Réglages\",\n    \"options\": \"Options\",\n    \"confirm\": \"Confirmer\",\n    \"name\": \"Nom\",\n    \"all\": \"Tous\",\n    \"add\": \"Ajouter\",\n    \"save\": \"Enregistrer\",\n    \"data\": \"Données\",\n    \"stop\": \"Arrêter\",\n    \"editor\": \"Éditeur\",\n    \"running\": \"En cours\",\n    \"globalData\": \"Données générales\",\n    \"fileName\": \"Nom du fichier\",\n    \"editor\": \"Editeur\",\n    \"running\": \"En cours\",\n    \"globalData\": \"Donnée Globale\",\n    \"fileName\": \"Nom de fichier\",\n    \"description\": \"Description\",\n    \"disable\": \"Désactiver\",\n    \"disabled\": \"Désactivé\",\n    \"enable\": \"Activer\",\n    \"fallback\": \"Fallback\",\n    \"update\": \"Mettre à jour\",\n    \"duplicate\": \"Dupliquer\",\n    \"password\": \"Mot de passe\",\n    \"category\": \"Catégorie\",\n    \"category\": \"Catégorie\",\n    \"optional\": \"Optionnel\",\n    \"0disable\": \"0 pour désactiver\"\n  },\n  \"message\": {\n    \"noBlock\": \"Pas de bloc\",\n    \"noData\": \"Aucune donnée à afficher\",\n    \"noTriggerBlock\": \"Impossible de trouver un bloc déclencheur\",\n    \"useDynamicData\": \"Apprenez à ajouter des données dynamiques\",\n    \"delete\": \"Voulez-vous vraiment supprimer \\\"{name}\\\"?\",\n    \"empty\": \"Oupss... Il semble que vous n'ayez aucun élément\",\n    \"maxSizeExceeded\": \"La taille maximum autorisée du fichier est dépassée\",\n    \"notSaved\": \"Voulez-vous vraiment partir ? Vous avez des changements non enregistrés !\",\n    \"somethingWrong\": \"Quelque chose s'est mal passé\",\n    \"limitExceeded\": \"Vous avez dépassé la limite\"\n  },\n  \"sort\": {\n    \"sortBy\": \"Trier par\",\n    \"name\": \"Nom\",\n    \"createdAt\": \"Date de création\"\n  },\n  \"logStatus\": {\n    \"stopped\": \"arrêté\",\n    \"error\": \"erreur\",\n    \"success\": \"succès\"\n  }\n}"
  },
  {
    "path": "src/locales/fr/newtab.json",
    "content": "{\n  \"home\": {\n    \"viewAll\": \"Voir tout\"\n  },\n  \"welcome\": {\n    \"title\": \"Bienvenue sur Automa! 🎉\",\n    \"text\": \"Commencez par lire la documentation ou parcourez workflows dans Automa Marketplace.\",\n    \"marketplace\": \"Marketplace\"\n  },\n  \"updateMessage\": {\n    \"text1\": \"Automa a été mis à jour vers v{version},\",\n    \"text2\": \"Regardez ce qu'il y a de nouveau.\"\n  },\n  \"auth\": {\n    \"title\": \"Authentification\",\n    \"signIn\": \"S'identifier\",\n    \"username\": \"Vous devez d'abord définir votre nom d'utilisateur\",\n    \"clickHere\": \"Cliquez ici\",\n    \"text\": \"Vous devez être connecté avant de pouvoir le faire\"\n  },\n  \"settings\": {\n    \"theme\": \"Thème\",\n    \"shortcuts\": {\n      \"duplicate\": \"Raccourci déjà utilisé par \\\"{name}\\\"\"\n    },\n    \"language\": {\n      \"label\": \"Langue\",\n      \"helpTranslate\": \"Vous ne trouvez pas votre langue ? Aider à traduire.\",\n      \"reloadPage\": \"Recharger la page pour appliquer\"\n    },\n    \"menu\": {\n      \"backup\": \"Sauvegarder les Workflows\",\n      \"general\": \"Général\",\n      \"shortcuts\": \"Raccourcis\",\n      \"about\": \"À propos\"\n    },\n    \"backupWorkflows\": {\n      \"title\": \"Sauvegarde locale\",\n      \"invalidPassword\": \"Mot de passe incorrect\",\n      \"workflowsAdded\": \"{count} workflows ont été ajoutés\",\n      \"name\": \"Sauvegarder les Workflows\",\n      \"backup\": {\n        \"button\": \"Sauvegarde\",\n        \"encrypt\": \"Chiffrer avec mot de passe\"\n      },\n      \"restore\": {\n        \"title\": \"Restaurer les workflows\",\n        \"button\": \"Restaurer\",\n        \"update\": \"Mettre à jour si le workflow existe\"\n      },\n      \"cloud\": {\n        \"buttons\": {\n          \"local\": \"Locale\",\n          \"cloud\": \"Cloud\"\n        },\n        \"delete\": \"Supprimer la sauvegarde\",\n        \"title\": \"Enregistrer sur le cloud\",\n        \"sync\": \"Synchroniser\",\n        \"lastSync\": \"Dernière synchronisation\",\n        \"lastBackup\": \"Dernière sauvegarde\",\n        \"select\": \"Sélectionnez les workflows\",\n        \"storedWorkflows\": \"Workflows stockés dans le cloud\",\n        \"selected\": \"Sélectionné\",\n        \"selectText\": \"Sélectionnez les workflows que vous souhaitez sauvegarder\",\n        \"selectAll\": \"Tout sélectionner\",\n        \"deselectAll\": \"Tout déselectionner\",\n        \"needSelectWorkflow\": \"Vous devez sélectionner les workflows que vous souhaitez sauvegarder\"\n      }\n    }\n  },\n  \"workflow\": {\n    \"import\": \"Importer un workflow\",\n    \"new\": \"Nouveau workflow\",\n    \"delete\": \"Supprimer le workflow\",\n    \"browse\": \"Parcourir les workflows\",\n    \"name\": \"Nom du workflow\",\n    \"rename\": \"Renommer le workflow\",\n    \"add\": \"Ajouter un workflow\",\n    \"clickToEnable\": \"Cliquer pour activer\",\n    \"toggleSidebar\": \"Afficher/Cacher la barre latérale\",\n    \"cantEdit\": \"Impossible de modifier le workflow partagé\",\n    \"host\": {\n      \"title\": \"Workflow hôte\",\n      \"set\": \"Définir comme workflow hôte\",\n      \"id\": \"Identifiant de l'hôte\",\n      \"add\": \"Ajouter un workflow hébergé\",\n      \"sync\": {\n        \"title\": \"Synchroniser\",\n        \"description\": \"Synchronisation avec le workflow de l'hôte\"\n      },\n      \"messages\": {\n        \"hostExist\": \"Vous avez déjà ajouté cet hôte\",\n        \"notFound\": \"Impossible de trouver le workflow hébergé avec l'identifiant \\\"{id}\\\"\",\n        \"successAdded\": \"Workflow hébergé ajouté avec succès avec l'identifiant \\\"{id}\\\"\"\n      }\n    },\n    \"type\": {\n      \"local\": \"Locale\",\n      \"shared\": \"Partagé\",\n      \"host\": \"Hébergé\"\n    },\n    \"unpublish\": {\n      \"title\": \"Annuler la publication du workflow\",\n      \"button\": \"Annuler la publication\",\n      \"body\": \"Voulez-vous vraiment annuler la publication du workflow \\\"{name}\\\" ?\"\n    },\n    \"share\": {\n      \"url\": \"Partager l'URL\",\n      \"publish\": \"Publier\",\n      \"sharedAs\": \"Partagé en tant que \\\"{name}\\\"\",\n      \"title\": \"Partager le workflow\",\n      \"download\": \"Télécharger le workflow\",\n      \"edit\": \"Éditer la description\",\n      \"fetchLocal\": \"Récupérer le workflow local\",\n      \"update\": \"Mettre à jour\",\n      \"unpublish\": \"Annuler la publication\",\n      \"linkCopied\": \"Lien copié dans le presse-papiers\"\n    },\n    \"variables\": {\n      \"title\": \"Variable | Variables\",\n      \"name\": \"Nom de la variable\",\n      \"assign\": \"Attribuer à la variablee\"\n    },\n    \"protect\": {\n      \"title\": \"Protéger le workflow\",\n      \"remove\": \"Supprimer la protection\",\n      \"button\": \"Protéger\",\n      \"note\": \"Remarque : vous devez vous souvenir de ce mot de passe, ce mot de passe vous sera demandé pour modifier et supprimer le workflow ultérieurement.\"\n    },\n    \"locked\": {\n      \"title\": \"Ce Workflow est protégé\",\n      \"body\": \"Entrez le mot de passe pour le déverrouiller\",\n      \"unlock\": \"Déverrouiller\",\n      \"messages\": {\n        \"incorrect-password\": \"Mot de passe incorrect\"\n      }\n    },\n    \"state\": {\n      \"executeBy\": \"Exécuté par: \\\"{name}\\\"\"\n    },\n    \"dataColumns\": {\n      \"title\": \"Tableau\",\n      \"placeholder\": \"Rechercher ou ajouter une colonne\",\n      \"select\": \"Sélectionner une colonne\",\n      \"column\": {\n        \"name\": \"Nom de la colonne\",\n        \"type\": \"Type de donnée\"\n      }\n    },\n    \"sidebar\": {\n      \"workflowIcon\": \"Icône du workflow\"\n    },\n    \"editor\": {\n      \"zoomIn\": \"Agrandir\",\n      \"zoomOut\": \"Réduire\",\n      \"resetZoom\": \"Réinitialiser le zoom\",\n      \"duplicate\": \"Dupliquer\"\n    },\n    \"settings\": {\n      \"saveLog\": \"Enregistrer les logs du workflow\",\n      \"executedBlockOnWeb\": \"Afficher le bloc exécuté sur la page Web\",\n      \"debugMode\": {\n        \"title\": \"Mode debug\",\n        \"description\": \"Exécutez le workflow à l'aide du protocole Chrome DevTools.\"\n      },\n      \"restartWorkflow\": {\n        \"for\": \"Redémarrer pour\",\n        \"times\": \"Fois\"\n      },\n      \"onError\": {\n        \"title\": \"Lors d'une erreur du workflow\",\n        \"items\": {\n          \"keepRunning\": \"Continuer\",\n          \"stopWorkflow\": \"Arrêter le workflow\",\n          \"restartWorkflow\": \"Redémarrer le workflow\"\n        }\n      },\n      \"timeout\": {\n        \"title\": \"Délai d'expiration du workflow (millisecondes)\"\n      },\n      \"blockDelay\": {\n        \"title\": \"Délai du bloc (millisecondes)\",\n        \"description\": \"Ajouter un délai avant d'exécuter chacun des blocs\"\n      }\n    }\n  },\n  \"collection\": {\n    \"description\": \"Exécutez vos workflows en séquence\",\n    \"new\": \"Nouvelle collection\",\n    \"delete\": \"Supprimer la collection\",\n    \"add\": \"Ajouter une collection\",\n    \"rename\": \"Renommer la collection\",\n    \"flow\": \"Flow\",\n    \"dragDropText\": \"Déposez un workflow ou un bloc ici\",\n    \"options\": {\n      \"atOnce\": {\n        \"title\": \"Exécuter tous les workflows de la collection en même temps\",\n        \"description\": \"Le bloc ne sera pas exécuté lors de l'utilisation de cette option\"\n      }\n    },\n    \"globalData\": {\n      \"note\": \"Cela écrasera les données globales du workflow\"\n    }\n  },\n  \"log\": {\n    \"goBack\": \"Revenir aux logs de \\\"{name}\\\"\",\n    \"startedDate\": \"Date de début\",\n    \"duration\": \"Durée\",\n    \"selectAll\": \"Tout sélectionner\",\n    \"deselectAll\": \"Tout déselectionner\",\n    \"deleteSelected\": \"Supprimer les logs sélectionnés\",\n    \"clearLogs\": {\n      \"title\": \"Effacer les logs\",\n      \"description\": \"Êtes-vous sûr de vouloir effacer tous les logs?\"\n    },\n    \"types\": {\n      \"stop\": \"Le workflow est arrêté\",\n      \"finish\": \"Finir\"\n    },\n    \"messages\": {\n      \"url-empty\": \"L'URL est vide\",\n      \"invalid-url\": \"L'URL n'est pas valide\",\n      \"conditions-empty\": \"Les conditions sont vides\",\n      \"invalid-proxy-host\": \"Hôte proxy non valide\",\n      \"workflow-disabled\": \"Le workflow est désactivé\",\n      \"selector-empty\": \"Le sélecteur d'élément est vide\",\n      \"invalid-body\": \"Le corps du contenu n'est pas en JSON valide\",\n      \"invalid-active-tab\": \"\\\"{url}\\\" est une URL invalide\",\n      \"empty-spreadsheet-id\": \"L'Id de la feuille de calcul est vide\",\n      \"invalid-loop-data\": \"Données non valides à parcourir\",\n      \"empty-workflow\": \"Vous devez d'abord sélectionner workflow\",\n      \"active-tab-removed\": \"L'onglet actif du workflow est supprimé\",\n      \"empty-spreadsheet-range\": \"La plage de la feuille de calcul est vide\",\n      \"stop-timeout\": \"Le workflow est arrêté en raison du délai d'attente\",\n      \"no-file-access\": \"Automa n'a pas accès au fichier\",\n      \"no-workflow\": \"Impossible de trouver le workflow avec l'ID \\\"{workflowId}\\\"\",\n      \"no-match-tab\": \"Impossible de trouver un onglet avec les motifs \\\"{pattern}\\\"\",\n      \"no-clipboard-acces\": \"Vous n'êtes pas autorisé à accéder au presse-papiers\",\n      \"element-not-found\": \"Impossible de trouver un élément avec le sélecteur \\\"{selector}\\\".\",\n      \"not-iframe\": \"L'élément avec le sélecteur \\\"{selector}\\\" n'est pas un élément Iframe\",\n      \"iframe-not-found\": \"Impossible de trouver un élément Iframe avec le sélecteur \\\"{selector}\\\".\",\n      \"workflow-infinite-loop\": \"Impossible d'exécuter le workflow pour éviter une boucle infinie\",\n      \"not-debug-mode\": \"Le workflow doit s'exécuter en mode débogage pour que ce bloc fonctionne correctement\",\n      \"no-iframe-id\": \"Impossible de trouver l'ID de Frame pour l'élément iframe avec le sélecteur \\\"{selector}\\\"\",\n      \"no-tab\": \"Impossible de se connecter à un onglet, utilisez le bloc \\\"Nouvel onglet\\\" ou \\\"Onglet actif\\\" avant d'utiliser le bloc \\\"{name}\\\".\"\n    },\n    \"description\": {\n      \"text\": \"{status} le {date} en {duration}\",\n      \"status\": {\n        \"success\": \"Réussi\",\n        \"error\": \"Échoué\",\n        \"stopped\": \"Arrêté\"\n      }\n    },\n    \"delete\": {\n      \"title\": \"Supprimer le log\",\n      \"description\": \"Voulez-vous vraiment supprimer tous les logs sélectionnés ?\"\n    },\n    \"exportData\": {\n      \"title\": \"Exporter les données\",\n      \"types\": {\n        \"json\": \"JSON\",\n        \"csv\": \"CSV\",\n        \"plain-text\": \"Texte brut\"\n      }\n    },\n    \"filter\": {\n      \"title\": \"Filtrer\",\n      \"byStatus\": \"Par statut\",\n      \"byDate\": {\n        \"title\": \"Par date\",\n        \"items\": {\n          \"lastDay\": \"Hier\",\n          \"last7Days\": \"La semaine dernière\",\n          \"last30Days\": \"Le mois dernier\"\n        }\n      }\n    }\n  },\n  \"components\": {\n    \"pagination\": {\n      \"text1\": \"Afficher\",\n      \"text2\": \"Éléments sur {count}\",\n      \"nextPage\": \"Page suivante\",\n      \"currentPage\": \"Page actuelle\",\n      \"prevPage\": \"Page précédente\",\n      \"of\": \"sur {page}\"\n    }\n  }\n}\n"
  },
  {
    "path": "src/locales/fr/popup.json",
    "content": "{\n  \"recording\": {\n    \"stop\": \"Arrêter d'enregistrer\",\n    \"title\": \"Enregistrer\"\n  },\n  \"home\": {\n    \"record\": {\n      \"title\": \"Enregistrer un workflow\",\n      \"button\": \"Enregistrer\",\n      \"name\": \"Nom du workflow\"\n    },\n    \"elementSelector\": {\n      \"name\": \"Sélecteur d'éléments\",\n      \"noAccess\": \"L'extension n'a pas accès à ce site\"\n    },\n    \"workflow\": {\n      \"new\": \"Nouveau workflow\",\n      \"rename\": \"Renommer le workflow\",\n      \"delete\": \"Supprimer le workflow\"\n    }\n  }\n}"
  },
  {
    "path": "src/locales/it/blocks.json",
    "content": "{\n  \"collection\": {\n    \"blocks\": {\n      \"export-result\": {\n        \"name\": \"Esporta risultato\",\n        \"description\": \"Esporta il risultato della raccolta come JSON\"\n      }\n    }\n  },\n  \"workflow\": {\n    \"blocks\": {\n      \"base\": {\n        \"title\": \"Blocchi\",\n        \"moveToGroup\": \"Sposta il blocco nel gruppo di blocchi\",\n        \"selector\": \"Selettore elemento\",\n        \"selectorOptions\": \"Opzioni del selettore\",\n        \"timeout\": \"Timeout (millisecondi)\",\n        \"noPermission\": \"Automa non ha permessi sufficienti per eseguire questa azione\",\n        \"grantPermission\": \"Autorizza\",\n        \"action\": \"Azione\",\n        \"element\": {\n          \"select\": \"Seleziona un elemento\",\n          \"verify\": \"Verifica il selettore\"\n        },\n        \"settings\": {\n          \"title\": \"Impostazioni del blocco\",\n          \"blockTimeout\": {\n            \"title\": \"Timeout di esecuzione (millisecondi)\",\n            \"description\": \"Il tempo massimo di esecuzione del blocco (0 per disabilitare)\"\n          },\n          \"line\": {\n            \"title\": \"Linee\",\n            \"label\": \"Etichetta\",\n            \"animated\": \"Animato\",\n            \"select\": \"Seleziona linea\",\n            \"to\": \"Linea al blocco {name}\",\n            \"lineColor\": \"Colore\"\n          }\n        },\n        \"toggle\": {\n          \"enable\": \"Abilita blocco\",\n          \"disable\": \"Disabilita blocco\"\n        },\n        \"onError\": {\n          \"info\": \"Le regole verranno applicate al verificarsi di un errore sul blocco\",\n          \"button\": \"In caso di errore\",\n          \"title\": \"Al verificarsi di un errore\",\n          \"retry\": \"Ritenta l'azione\",\n          \"fallbackTitle\": \"Verrà eseguito al verificarsi di un errore nel blocco\",\n          \"times\": {\n            \"name\": \"Volte\",\n            \"description\": \"Il numero di volte in cui riprovare l'azione\"\n          },\n          \"interval\": {\n            \"name\": \"Intervallo\",\n            \"description\": \"L'intervallo di tempo da attendere tra ogni tentativo\",\n            \"second\": \"secondo/i\"\n          },\n          \"toDo\": {\n            \"error\": \"Lancia un errore\",\n            \"continue\": \"Continua l'esecuzione\",\n            \"fallback\": \"Esegui il fallback\",\n            \"restart\": \"Riavvia il flusso\"\n          },\n          \"insertData\": {\n            \"name\": \"Inserisci dati\"\n          }\n        },\n        \"table\": {\n          \"checkbox\": \"Inserisci in tabella\",\n          \"select\": \"Seleziona colonna\",\n          \"extraRow\": {\n            \"checkbox\": \"Aggiungi riga extra\",\n            \"placeholder\": \"Valore\",\n            \"title\": \"Valore della riga extra\"\n          }\n        },\n        \"findElement\": {\n          \"placeholder\": \"Trova elemento per\",\n          \"options\": {\n            \"cssSelector\": \"Selettore CSS\",\n            \"xpath\": \"XPath\"\n          }\n        },\n        \"markElement\": {\n          \"title\": \"Ogni elemento verrà selezionato una volta sola\",\n          \"text\": \"Contrassegna l'elemento\"\n        },\n        \"multiple\": {\n          \"title\": \"Seleziona più elementi\",\n          \"text\": \"Multipli\"\n        },\n        \"waitSelector\": {\n          \"title\": \"Attendi il selettore\",\n          \"timeout\": \"Timeout del selettore (ms)\"\n        },\n        \"downloads\": {\n          \"onConflict\": {\n            \"uniquify\": \"Diversifica\",\n            \"overwrite\": \"Sovrascrivi\",\n            \"prompt\": \"Visualizza\"\n          }\n        }\n      },\n      \"wait-connections\": {\n        \"name\": \"Attendi connessioni\",\n        \"description\": \"Attendi tutte le connessioni prima di passare al blocco successivo\",\n        \"specificFlow\": \"Continua solo un flusso specifico\",\n        \"selectFlow\": \"Seleziona flusso\"\n      },\n      \"cookie\": {\n        \"name\": \"Cookie\",\n        \"description\": \"Ottieni, imposta o rimuovi cookie\",\n        \"types\": {\n          \"get\": \"Ottieni cookie\",\n          \"set\": \"Imposta cookie\",\n          \"remove\": \"Rimuovi cookie\",\n          \"getAll\": \"Ottieni tutti i cookie\"\n        },\n        \"useJson\": \"Usa il formato JSON\"\n      },\n      \"note\": {\n        \"name\": \"Annotazioni\"\n      },\n      \"slice-variable\": {\n        \"name\": \"Taglia variabile\",\n        \"description\": \"Estrae una sezione del valore di una variabile\",\n        \"start\": \"Indice di inizio\",\n        \"end\": \"Indice di fine\"\n      },\n      \"workflow-state\": {\n        \"name\": \"Stato workflow\",\n        \"description\": \"Gestisci gli stati dei workflow\",\n        \"actions\": {\n          \"stop\": \"Interrompi i workflow\"\n        }\n      },\n      \"regex-variable\": {\n        \"name\": \"Variabile RegEx\",\n        \"description\": \"Confronta il valore di una variabile con un'espressione regolare\"\n      },\n      \"data-mapping\": {\n        \"source\": \"Sorgente\",\n        \"destination\": \"Destinazione\",\n        \"name\": \"Mappatura dati\",\n        \"edit\": \"Modifica mappa dei dati\",\n        \"dataSource\": \"Sorgente dei dati\",\n        \"description\": \"Mappa i dati di una variabile o di una tabella\",\n        \"addSource\": \"Aggiungi sorgente\",\n        \"addDestination\": \"Aggiungi destinazione\"\n      },\n      \"sort-data\": {\n        \"name\": \"Ordina dati\",\n        \"description\": \"Ordina gli elementi dei dati\",\n        \"property\": \"Ordina per proprietà dell'elemento\",\n        \"addProperty\": \"Aggiungi proprietà\"\n      },\n      \"increase-variable\": {\n        \"name\": \"Incrementa variabile\",\n        \"description\": \"Incrementa il valore di una variabile di una quantità specifica\",\n        \"increase\": \"Incrementa di\"\n      },\n      \"notification\": {\n        \"name\": \"notifica\",\n        \"description\": \"Visualizza una notifica\",\n        \"title\": \"Titolo\",\n        \"message\": \"Messaggio\",\n        \"imageUrl\": \"URL dell'immagine (opzionale)\",\n        \"iconUrl\": \"URL dell'icona (opzionale)\"\n      },\n      \"delete-data\": {\n        \"name\": \"Elimina dati\",\n        \"description\": \"Elimina tabella o dati variabili\",\n        \"from\": \"Dati da\",\n        \"allColumns\": \"[Tutte le colonne]\"\n      },\n      \"log-data\": {\n        \"name\": \"Ottieni dati di log\",\n        \"description\": \"Ottieni i dati dei log più recenti di un workflow\",\n        \"data\": \"Dati di log\"\n      },\n      \"tab-url\": {\n        \"name\": \"Ottieni URL scheda\",\n        \"description\": \"Ottieni l'URL della scheda\",\n        \"select\": \"Seleziona scheda\",\n        \"types\": {\n          \"active-tab\": \"Scheda attiva\",\n          \"all\": \"Tutte le schede\"\n        },\n        \"query\": {\n          \"title\": \"Query\",\n          \"matchPatterns\": \"@:workflow.blocks.switch-tab.matchPattern (opzionale)\",\n          \"tabTitle\": \"Titolo della scheda (opzionale)\"\n        }\n      },\n      \"reload-tab\": {\n        \"name\": \"Ricarica scheda\",\n        \"description\": \"Ricarica la scheda attiva\"\n      },\n      \"press-key\": {\n        \"name\": \"Premi tasto\",\n        \"description\": \"Premi un tasto o una combinazione\",\n        \"target\": \"Elemento target (opzionale)\",\n        \"key\": \"Tasto\",\n        \"detect\": \"Rileva tasto\",\n        \"actions\": {\n          \"press-key\": \"Premi un tasto\",\n          \"multiple-keys\": \"Premi una combinazione di tasti\"\n        }\n      },\n      \"save-assets\": {\n        \"name\": \"Salva risorse\",\n        \"description\": \"Salva le risorse (immagine, video, audio o file) da un elemento o URL\",\n        \"filename\": \"Nome file (opzionale)\",\n        \"saveDownloadIds\": \"Salva gli ID di download\",\n        \"contentTypes\": {\n          \"title\": \"Tipo\",\n          \"element\": \"Elemento multimediale (immagine, audio o video)\",\n          \"url\": \"URL\"\n        }\n      },\n      \"handle-dialog\": {\n        \"name\": \"Gestisci dialogo\",\n        \"description\": \"Accetta o chiude una finestra di dialogo avviata da JavaScript (alert, confirm, prompt o onbeforeunload)\",\n        \"accept\": \"Accetta finestra di dialogo\",\n        \"promptText\": {\n          \"label\": \"Testo del prompt (opzionale)\",\n          \"description\": \"Il testo da inserire nella finestra di dialogo del prompt prima di accettare\"\n        }\n      },\n      \"handle-download\": {\n        \"name\": \"Gestici download\",\n        \"description\": \"Gestisci il file da scaricare\",\n        \"timeout\": \"Timeout (millisecondi)\",\n        \"noPermission\": \"Automa non dispone del permesso per accedere ai download\",\n        \"onConflict\": \"In caso di conflitto\",\n        \"waitFile\": \"Attendi il download del file\",\n        \"downloadId\": \"ID di download del file (opzionale)\",\n        \"filePath\": \"Percorso del file\"\n      },\n      \"insert-data\": {\n        \"name\": \"Inserisci dati\",\n        \"description\": \"Inserisci dati in una tabella o variabile\"\n      },\n      \"clipboard\": {\n        \"name\": \"Appunti\",\n        \"description\": \"Ottieni il testo copiato dagli appunti\",\n        \"data\": \"Dati degli appunti\",\n        \"noPermission\": \"Automa non dispone del permesso per accedere agli appunti\",\n        \"grantPermission\": \"Autorizza\",\n        \"copySelection\": \"Copia il testo selezionato sulla pagina\",\n        \"types\": {\n          \"get\": \"Ottieni i dati degli appunti\",\n          \"insert\": \"Inserisci testo negli appunti\"\n        }\n      },\n      \"hover-element\": {\n        \"name\": \"Hover elemento\",\n        \"description\": \"Passa il mouse sopra un elemento\"\n      },\n      \"create-element\": {\n        \"name\": \"Crea elemento\",\n        \"description\": \"Crea un elemento da inserire nella pagina\",\n        \"edit\": \"Modifica elemento\",\n        \"wrap\": \"Avvolgi l'elemento dentro\",\n        \"insertEl\": {\n          \"title\": \"Inserisci elemento\",\n          \"items\": {\n            \"before\": \"Come first child\",\n            \"after\": \"Come last child\",\n            \"next-sibling\": \"Come next sibling\",\n            \"prev-sibling\": \"Come previous sibling\",\n            \"replace\": \"Sostituisci l'elemento target\"\n          }\n        }\n      },\n      \"upload-file\": {\n        \"name\": \"Carica file\",\n        \"description\": \"Carica un file in un elemento <input type=\\\"file\\\">\",\n        \"filePath\": \"URL o percorso del file\",\n        \"addFile\": \"Aggiungi file\",\n        \"onlyURL\": \"Nel browser Firefox è supportato solo il caricamento di file da un URL\",\n        \"requirement\": \"Leggi i requisiti prima di usare questo blocco\",\n        \"noFileAccess\": \"Automa non ha accesso ai file\"\n      },\n      \"browser-event\": {\n        \"name\": \"Evento browser\",\n        \"description\": \"Esegue il blocco successivo quando si verifica l'evento specificato\",\n        \"events\": \"Eventi\",\n        \"timeout\": \"Timeout (millisecondi)\",\n        \"activeTabLoaded\": \"Scheda attiva\",\n        \"setAsActiveTab\": \"Imposta come scheda attiva\"\n      },\n      \"blocks-group-2\": {\n        \"name\": \"@:workflow.blocks.blocks-group.name 2\",\n        \"description\": \"@:workflow.blocks.blocks-group.description\"\n      },\n      \"blocks-group\": {\n        \"name\": \"Gruppo blocchi\",\n        \"groupName\": \"Nome gruppo\",\n        \"description\": \"Raggruppa dei blocchi insieme\",\n        \"dropText\": \"Trascina e rilascia un blocco qui\",\n        \"cantAdd\": \"Non è possibile aggiungere il blocco \\\"{blockName}\\\" al gruppo\"\n      },\n      \"trigger\": {\n        \"name\": \"Trigger\",\n        \"description\": \"Blocco da cui inizierà l'esecuzione del workflow\",\n        \"addTime\": \"Aggiungi l'ora\",\n        \"selectDay\": \"Seleziona il giorno\",\n        \"timeExist\": \"Hai già aggiunto un trigger alle ore {time} del giorno {day}\",\n        \"fixedDelay\": \"Delay fisso\",\n        \"contextMenus\": {\n          \"noPermission\": \"Questo trigger richiede il permesso \\\"contextMenus\\\" per funzionare\",\n          \"grantPermission\": \"Autorizza\",\n          \"appearIn\": \"Apparirà in\",\n          \"contextName\": \"Nome del workflow nel menu contestuale\"\n        },\n        \"days\": [\n          \"Domenica\",\n          \"Lunedì\",\n          \"Martedì\",\n          \"Mercoledì\",\n          \"Giovedì\",\n          \"Venerdì\",\n          \"Sabato\"\n        ],\n        \"useRegex\": \"Usa regex\",\n        \"shortcut\": {\n          \"tooltip\": \"Registra scorciatoia\",\n          \"stopRecord\": \"Termina registrazione\",\n          \"checkboxTitle\": \"Esegui la scorciatoia da tastiera anche all'interno di ​​un elemento di input\",\n          \"checkbox\": \"Rileva anche negli input\",\n          \"note\": \"Nota: la scorciatoia da tastiera funziona solo mentre si è su una pagina web\"\n        },\n        \"forms\": {\n          \"triggerWorkflow\": \"Attiva workflow\",\n          \"interval\": \"Intervallo (minuti)\",\n          \"delay\": \"Delay (minuti)\",\n          \"date\": \"Data\",\n          \"time\": \"Ora\",\n          \"url\": \"URL o Regex\",\n          \"shortcut\": \"Scorciatoia\",\n          \"cron-expression\": \"Espressione Cron\"\n        },\n        \"element-change\": {\n          \"target\": \"Seleziona elemento da osservare\",\n          \"optionsInfo\": \"Quale variazione dell'elemento attiverà il workflow\",\n          \"targetWebsite\": \"Il Match Pattern del sito web in cui si trova l'elemento target (clicca per vedere più esempi di Match Pattern)\",\n          \"baseEl\": {\n            \"title\": \"Elemento base (opzionale)\",\n            \"description\": \"Automa ricomincerà ad osservare l'elemento target quando questo elemento cambia\"\n          },\n          \"subtree\": {\n            \"title\": \"Includi sottoalbero\",\n            \"description\": \"Estendi il monitoraggio all'intero sottoalbero dell'elemento target\"\n          },\n          \"childList\": {\n            \"title\": \"Elenco elementi figlio\",\n            \"description\": \"Monitora l'aggiunta di nuovi elementi figlio o la rimozione di quelli esistenti\"\n          },\n          \"attributes\": {\n            \"title\": \"Attributi\",\n            \"description\": \"Osserva le modifiche ai valori degli attributi dell'elemento target\"\n          },\n          \"attributeFilter\": {\n            \"title\": \"Filtro attributi\",\n            \"separate\": \"Usa le virgole (,) per separare i nomi degli attributi\",\n            \"description\": \"Monitora solo attributi specifici (lascia vuoto per monitorare tutti)\"\n          },\n          \"characterData\": {\n            \"title\": \"Character data\",\n            \"description\": \"Monitora modifiche ai character data/testo all'interno dell'elemento target\"\n          }\n        },\n        \"items\": {\n          \"manual\": \"Manualmente\",\n          \"interval\": \"Intervallo\",\n          \"cron-job\": \"Cron job\",\n          \"date\": \"In una data specifica\",\n          \"context-menu\": \"Menu contestuale\",\n          \"element-change\": \"Alla variazione di un elemento\",\n          \"specific-day\": \"In un giorno specifico\",\n          \"visit-web\": \"Quando si visita un sito web\",\n          \"on-startup\": \"All'avvio del browser\",\n          \"keyboard-shortcut\": \"Scorciatoia da tastiera\"\n        }\n      },\n      \"execute-workflow\": {\n        \"name\": \"Esegui workflow\",\n        \"overwriteNote\": \"Questo sovrascriverà i dati globali del workflow selezionato\",\n        \"select\": \"Seleziona workflow\",\n        \"executeId\": \"Esegui ID (opzionale)\",\n        \"description\": \"\",\n        \"insertAllVars\": \"Usa tutte le variabili del workflow corrente\",\n        \"insertVars\": \"Inserisci le variabili del workflow corrente\",\n        \"useCommas\": \"Usa le virgole per separare i nomi delle variabili\",\n        \"insertAllGlobalData\": \"Utilizza tutto il flusso di lavoro corrente globalData\"\n      },\n      \"google-sheets\": {\n        \"name\": \"Google Sheets\",\n        \"description\": \"Leggi o aggiorna dati di Google Sheets\",\n        \"previewData\": \"Anteprima dei dati\",\n        \"firstRow\": \"Usa la prima riga come chiavi\",\n        \"keysAsFirstRow\": \"Usa le chiavi come prima riga\",\n        \"insertData\": \"Inserisci dati\",\n        \"valueInputOption\": \"Opzione di immissione del valore\",\n        \"insertDataOption\": \"Opzione di inserimento dati\",\n        \"rangeToSearch\": \"Intervallo da ricercare\",\n        \"dataFrom\": {\n          \"label\": \"Dati da\",\n          \"options\": {\n            \"data-columns\": \"Tabella\",\n            \"custom\": \"Personalizzato\"\n          }\n        },\n        \"refKey\": {\n          \"label\": \"Chiave di riferimento (opzionale)\",\n          \"placeholder\": \"Nome chiave\"\n        },\n        \"spreadsheetId\": {\n          \"label\": \"ID foglio di calcolo\",\n          \"link\": \"Scopri come ottenere l'ID del foglio di calcolo\"\n        },\n        \"range\": {\n          \"label\": \"Intervallo\",\n          \"link\": \"Clicca per vedere più esempi\"\n        },\n        \"select\": {\n          \"get\": \"Ottieni i valori delle celle\",\n          \"getRange\": \"Ottieni un intervallo\",\n          \"update\": \"Aggiorna i valori delle celle\",\n          \"append\": \"Aggiungi valori alle celle\",\n          \"clear\": \"Cancella i valori delle celle\"\n        }\n      },\n      \"active-tab\": {\n        \"name\": \"Scheda attiva\",\n        \"description\": \"Imposta la scheda in cui ti trovi come scheda attiva\"\n      },\n      \"proxy\": {\n        \"name\": \"Proxy\",\n        \"description\": \"Imposta il proxy del browser\",\n        \"clear\": \"Cancella tutti i proxy\",\n        \"bypass\": {\n          \"label\": \"Elenco dei bypass\",\n          \"note\": \"Usa le virgole (,) per separare gli URL\"\n        }\n      },\n      \"new-window\": {\n        \"name\": \"Nuova finestra\",\n        \"description\": \"Crea una nuova finestra\",\n        \"top\": \"Top\",\n        \"left\": \"Left\",\n        \"height\": \"Altezza\",\n        \"width\": \"Larghezza\",\n        \"note\": \"Nota: usa 0 per disabilitare\",\n        \"position\": \"Posizione finestra\",\n        \"size\": \"Dimensione finestra\",\n        \"windowState\": {\n          \"placeholder\": \"Stato finestra\",\n          \"options\": {\n            \"normal\": \"Normale\",\n            \"minimized\": \"Minimizzato\",\n            \"maximized\": \"Massimizzato\",\n            \"fullscreen\": \"Schermo intero\"\n          }\n        },\n        \"incognito\": {\n          \"text\": \"Apri come finestra in incognito\",\n          \"note\": \"Devi prima abilitare 'Consenti modalità di navigazione in incognito' per questa estensione\"\n        }\n      },\n      \"go-back\": {\n        \"name\": \"Torna indietro\",\n        \"description\": \"Torna alla pagina precedente\"\n      },\n      \"forward-page\": {\n        \"name\": \"Vai avanti\",\n        \"description\": \"Vai alla pagina successiva\"\n      },\n      \"close-tab\": {\n        \"name\": \"Chiudi scheda/finestra\",\n        \"description\": \"\",\n        \"url\": \"Match Pattern\",\n        \"activeTab\": \"Chiudi scheda attiva\",\n        \"allWindows\": \"Chiudi tutte le finestre\"\n      },\n      \"event-click\": {\n        \"name\": \"Clicca elemento\",\n        \"description\": \"\"\n      },\n      \"delay\": {\n        \"name\": \"Delay\",\n        \"description\": \"Aggiungi un delay per ritardare l'esecuzione del blocco successivo\",\n        \"input\": {\n          \"title\": \"Delay in millisecondi\",\n          \"placeholder\": \"(millisecondi)\"\n        }\n      },\n      \"parameter-prompt\": {\n        \"name\": \"Prompt parametri\"\n      },\n      \"get-text\": {\n        \"name\": \"Ottieni testo\",\n        \"description\": \"Ottieni testo da un elemento\",\n        \"checkbox\": \"Inserisci in tabella\",\n        \"includeTags\": \"Includi tag HTML\",\n        \"prefixText\": {\n          \"placeholder\": \"Prefisso del testo\",\n          \"title\": \"Aggiungi un prefisso al testo\"\n        },\n        \"suffixText\": {\n          \"placeholder\": \"Suffisso del testo\",\n          \"title\": \"Aggiungi un suffisso al testo\"\n        }\n      },\n      \"export-data\": {\n        \"name\": \"Esporta dati\",\n        \"description\": \"Esporta i dati del workflow\",\n        \"exportAs\": \"Esporta come\",\n        \"refKey\": \"Chiave di riferimento\",\n        \"bomHeader\": \"Aggiungi UTF-8 BOM\",\n        \"dataToExport\": {\n          \"placeholder\": \"Dati da esportare\",\n          \"options\": {\n            \"data-columns\": \"Tabella\",\n            \"google-sheets\": \"Google Sheets\",\n            \"variable\": \"Variabile\"\n          }\n        }\n      },\n      \"element-scroll\": {\n        \"name\": \"Scorri elemento\",\n        \"description\": \"\",\n        \"scrollY\": \"Scorri verticalmente\",\n        \"scrollX\": \"Scorri orizzontalmente\",\n        \"intoView\": \"Scorri in vista\",\n        \"smooth\": \"Scorrimento fluido\",\n        \"incScrollX\": \"Incrementa lo scorrimento orizzontale\",\n        \"incScrollY\": \"Incrementa lo scorrimento verticale\"\n      },\n      \"switch-tab\": {\n        \"name\": \"Cambia scheda\",\n        \"description\": \"Passa da una scheda all'altra\",\n        \"matchPattern\": \"Match Pattern\",\n        \"url\": \"URL della nuova scheda\",\n        \"createIfNoMatch\": \"Crea se non c'è corrispondenza\"\n      },\n      \"new-tab\": {\n        \"name\": \"Nuova scheda\",\n        \"description\": \"\",\n        \"url\": \"URL della nuova scheda\",\n        \"customUserAgent\": \"Usa un User-Agent personalizzato\",\n        \"activeTab\": \"Imposta come scheda attiva\",\n        \"tabToGroup\": \"Aggiungi scheda a un gruppo\",\n        \"waitTabLoaded\": \"Attendi il caricamento\",\n        \"updatePrevTab\": {\n          \"title\": \"Usa la scheda aperta in precedenza invece di crearne una nuova\",\n          \"text\": \"Aggiorna la scheda aperta prima\"\n        }\n      },\n      \"link\": {\n        \"name\": \"Link\",\n        \"description\": \"Apri elemento di collegamento\",\n        \"openInNewTab\": \"Apri in una nuova scheda\"\n      },\n      \"attribute-value\": {\n        \"name\": \"Valore attributo\",\n        \"description\": \"Ottieni il valore dell'attributo di un elemento\",\n        \"forms\": {\n          \"name\": \"Nome attributo\",\n          \"checkbox\": \"Inserisci in tabella\",\n          \"column\": \"Seleziona colonna\",\n          \"extraRow\": {\n            \"checkbox\": \"Aggiungi riga extra\",\n            \"placeholder\": \"Valore\",\n            \"title\": \"Valore della riga extra\"\n          }\n        }\n      },\n      \"forms\": {\n        \"name\": \"Moduli\",\n        \"description\": \"\",\n        \"selected\": \"Selezionato\",\n        \"type\": \"Tipo di modulo\",\n        \"getValue\": \"Ottini il valore del modulo\",\n        \"text-field\": {\n          \"name\": \"Campo di testo\",\n          \"value\": \"Valore\",\n          \"clearValue\": \"Cancella il valore del modulo\",\n          \"delay\": {\n            \"placeholder\": \"Delay\",\n            \"label\": \"Delay di battitura (ms) (0 per disabilitare)\"\n          }\n        },\n        \"select\": {\n          \"name\": \"Select\"\n        },\n        \"radio\": {\n          \"name\": \"Radio\"\n        },\n        \"checkbox\": {\n          \"name\": \"Checkbox\"\n        }\n      },\n      \"repeat-task\": {\n        \"name\": \"Ripeti operazione\",\n        \"description\": \"\",\n        \"times\": \"volte\",\n        \"repeatFrom\": \"Ripeti da\"\n      },\n      \"javascript-code\": {\n        \"name\": \"Codice JavaScript\",\n        \"description\": \"Esegui il tuo codice JavaScript nella pagina web\",\n        \"availabeFuncs\": \"Metodi disponibili:\",\n        \"removeAfterExec\": \"Rimuovi dopo l'esecuzione del blocco\",\n        \"everyNewTab\": \"Esegui in ogni nuova scheda\",\n        \"context\": {\n          \"name\": \"Contesto di esecuzione\",\n          \"items\": {\n            \"website\": \"Scheda attiva\",\n            \"background\": \"Background\"\n          }\n        },\n        \"modal\": {\n          \"tabs\": {\n            \"code\": \"Codice JavaScript\",\n            \"preloadScript\": \"Script di precaricamento\"\n          }\n        },\n        \"timeout\": {\n          \"placeholder\": \"Timeout (millisecondi)\",\n          \"title\": \"Timeout di esecuzione del codice JavaScript\"\n        }\n      },\n      \"trigger-event\": {\n        \"name\": \"Innesca evento\",\n        \"description\": \"\",\n        \"selectEvent\": \"Seleziona evento\"\n      },\n      \"conditions\": {\n        \"name\": \"Condizioni\",\n        \"add\": \"Aggiungi percorso\",\n        \"retryConditions\": \"Riprova se non si verifica nessuna condizione\",\n        \"description\": \"Blocco condizionale\",\n        \"refresh\": \"Aggiorna le connessioni delle condizioni\",\n        \"fallbackTitle\": \"Viene eseguito quando tutti i confronti non soddisfano il requisito\",\n        \"equals\": \"Equivale\",\n        \"gt\": \"Maggiore di\",\n        \"gte\": \"Maggiore o uguale\",\n        \"lt\": \"Minore di\",\n        \"lte\": \"Minore o uguale\",\n        \"ne\": \"Non equivale\",\n        \"contains\": \"Contiene\"\n      },\n      \"element-exists\": {\n        \"name\": \"Esiste elemento\",\n        \"description\": \"Controlla se esiste un elemento\",\n        \"selector\": \"Selettore elemento\",\n        \"fallbackTitle\": \"Viene eseguito quando l'elemento non esiste\",\n        \"throwError\": \"Lancia un errore se non esiste\",\n        \"tryFor\": {\n          \"title\": \"Quante volte provare a verificare se l'elemento esiste\",\n          \"label\": \"Prova per\"\n        },\n        \"timeout\": {\n          \"label\": \"Timeout (millisecondi)\",\n          \"title\": \"Timeout per ogni tentativo\"\n        }\n      },\n      \"webhook\": {\n        \"name\": \"Richiesta HTTP\",\n        \"description\": \"Effettua una richiesta HTTP\",\n        \"contentType\": \"Tipo di contenuto\",\n        \"method\": \"Metodo di richiesta\",\n        \"url\": \"URL di richiesta\",\n        \"fallback\": \"Viene eseguito quando la richiesta HTTP ha esito negativo\",\n        \"buttons\": {\n          \"header\": \"Aggiungi header\"\n        },\n        \"timeout\": {\n          \"placeholder\": \"Timeout\",\n          \"title\": \"Timeout di esecuzione della richiesta HTTP (ms)\"\n        },\n        \"tabs\": {\n          \"headers\": \"Intestazioni\",\n          \"body\": \"Corpo\",\n          \"response\": \"Risposta\"\n        }\n      },\n      \"while-loop\": {\n        \"name\": \"Ciclo while\",\n        \"description\": \"Esegue i blocchi fintantoché si verifica la condizione\",\n        \"editCondition\": \"Modifica condizione\",\n        \"fallback\": \"Viene eseguito quando la condizione risulta falsa\"\n      },\n      \"loop-elements\": {\n        \"name\": \"Itera elementi\",\n        \"description\": \"Itera gli elementi\",\n        \"loadMore\": \"Carica altri elementi\",\n        \"scrollToBottom\": \"Scorri fino in fondo\",\n        \"scrollToTop\": \"Scorri fino in cima\",\n        \"actions\": {\n          \"none\": \"Nessuna azione\",\n          \"click-element\": \"Clicca un elemento\",\n          \"scroll\": \"Scorri verso il basso\",\n          \"click-link\": \"Clicca un link\",\n          \"scroll-up\": \"Scorri verso l'alto\"\n        }\n      },\n      \"loop-data\": {\n        \"name\": \"Itera dati\",\n        \"description\": \"Itera una tabella o dei dati personalizzati\",\n        \"loopId\": \"ID ciclo\",\n        \"refKey\": \"Chiave di riferimento\",\n        \"startIndex\": \"Inizia dall'indice\",\n        \"resumeLastWorkflow\": \"Riprendi l'ultimo workflow\",\n        \"reverse\": \"Inverti l'ordine del ciclo\",\n        \"modal\": {\n          \"fileTooLarge\": \"File troppo grande per essere modificato\",\n          \"maxFile\": \"La dimensione massima del file è di 1MB\",\n          \"options\": {\n            \"firstRow\": \"Usa la prima riga come chiavi\"\n          }\n        },\n        \"buttons\": {\n          \"clear\": \"Cancella dati\",\n          \"insert\": \"Inserisci dati\",\n          \"import\": \"Importa file\"\n        },\n        \"maxLoop\": {\n          \"title\": \"Numero massimo di dati da iterare\",\n          \"label\": \"Numero massimo di cicli (0 per disabilitare)\"\n        },\n        \"loopThrough\": {\n          \"placeholder\": \"Itera attraverso\",\n          \"fromNumber\": \"Dal numero\",\n          \"toNumber\": \"Al numero\",\n          \"options\": {\n            \"numbers\": \"Numeri\",\n            \"variable\": \"Variabile\",\n            \"data-columns\": \"Tabella\",\n            \"table\": \"Tabella\",\n            \"custom-data\": \"Dati personalizzati\",\n            \"google-sheets\": \"Google Sheets\",\n            \"elements\": \"Elementi\"\n          }\n        }\n      },\n      \"loop-breakpoint\": {\n        \"name\": \"Breakpoint ciclo\",\n        \"description\": \"Per indicare il punto di interruzione del blocco Itera Dati\"\n      },\n      \"take-screenshot\": {\n        \"name\": \"Effettua screenshot\",\n        \"fullPage\": \"Cattura la schermata dell'intera pagina\",\n        \"description\": \"Cattura la schermata della scheda attiva\",\n        \"imageQuality\": \"Qualità dell'immagine\",\n        \"saveToColumn\": \"Inserisci in tabella\",\n        \"saveToComputer\": \"Salva sul computer\",\n        \"types\": {\n          \"title\": \"Cattura\",\n          \"page\": \"Una pagina\",\n          \"fullpage\": \"Una pagina intera\",\n          \"element\": \"Un elemento\"\n        }\n      },\n      \"switch-to\": {\n        \"name\": \"Cambia frame\",\n        \"description\": \"Passa dalla finestra principale a un iframe\",\n        \"iframeSelector\": \"Selettore elemento\",\n        \"windowTypes\": {\n          \"main\": \"Finestra principale\",\n          \"iframe\": \"Iframe\"\n        }\n      },\n      \"debugMode\": {\n        \"description\": \"Esegui il modulo corrente utilizzando il protocollo Chrome DevTools\"\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "src/locales/it/common.json",
    "content": "{\n  \"common\": {\n    \"dashboard\": \"Dashboard\",\n    \"workflow\": \"Workflow | Workflow\",\n    \"collection\": \"Raccolta | Raccolte\",\n    \"log\": \"Log | Log\",\n    \"block\": \"Blocco | Blocchi\",\n    \"schedule\": \"Programma\",\n    \"folder\": \"Cartella | Cartelle\",\n    \"new\": \"Nuovo\",\n    \"docs\": \"Documentazione\",\n    \"search\": \"Cerca\",\n    \"example\": \"Esempio | Esempi\",\n    \"import\": \"Importa\",\n    \"export\": \"Esporta\",\n    \"rename\": \"Rinomina\",\n    \"execute\": \"Esegui\",\n    \"delete\": \"Elimina\",\n    \"cancel\": \"Annulla\",\n    \"settings\": \"Impostazioni\",\n    \"options\": \"Opzioni\",\n    \"confirm\": \"Conferma\",\n    \"name\": \"Nome\",\n    \"all\": \"Tutti\",\n    \"add\": \"Aggiungi\",\n    \"save\": \"Salva\",\n    \"data\": \"dati\",\n    \"stop\": \"Interrompi\",\n    \"action\": \"Azione | Azioni\",\n    \"packages\": \"Pacchetti\",\n    \"storage\": \"Archiviazione\",\n    \"editor\": \"Editor\",\n    \"running\": \"In esecuzione\",\n    \"globalData\": \"Dati globali\",\n    \"fileName\": \"Nome file\",\n    \"description\": \"Descrizione\",\n    \"disable\": \"Disabilita\",\n    \"disabled\": \"Disabilitato\",\n    \"enable\": \"Abilita\",\n    \"fallback\": \"Fallback\",\n    \"update\": \"Aggiorna\",\n    \"feature\": \"Funzione\",\n    \"duplicate\": \"Duplica\",\n    \"password\": \"Password\",\n    \"category\": \"Categoria\",\n    \"optional\": \"Opzionale\",\n    \"0disable\": \"0 per disabilitare\"\n  },\n  \"message\": {\n    \"noBlock\": \"Nessun blocco\",\n    \"noData\": \"Nessun dato da mostrare\",\n    \"noTriggerBlock\": \"Impossibile trovare un blocco trigger\",\n    \"useDynamicData\": \"Scopri come aggiungere dati dinamici\",\n    \"delete\": \"Sei sicuro di voler eliminare \\\"{name}\\\"?\",\n    \"empty\": \"Ops... Sembra che tu non abbia alcun oggetto\",\n    \"maxSizeExceeded\": \"La dimensione del file ha superato il massimo consentito\",\n    \"notSaved\": \"Sei sicuro di voler uscire? Ci sono modifiche non salvate!\",\n    \"somethingWrong\": \"Qualcosa è andato storto\",\n    \"limitExceeded\": \"Hai superato il limite\"\n  },\n  \"sort\": {\n    \"sortBy\": \"Ordina per\",\n    \"name\": \"Nome\",\n    \"createdAt\": \"Data di creazione\",\n    \"mostUsed\": \"Più usato\"\n  },\n  \"logStatus\": {\n    \"stopped\": \"interrotto\",\n    \"error\": \"errore\",\n    \"success\": \"successo\"\n  }\n}\n"
  },
  {
    "path": "src/locales/it/newtab.json",
    "content": "{\n  \"home\": {\n    \"viewAll\": \"Mostra tutto\",\n    \"communities\": \"Comunità\"\n  },\n  \"welcome\": {\n    \"title\": \"Benvenuto su Automa! 🎉\",\n    \"text\": \"Puoi cominciare leggendo la documentazione o esplorando i workflow nel marketplace di Automa.\",\n    \"marketplace\": \"Marketplace\"\n  },\n  \"packages\": {\n    \"name\": \"Pacchetto | Pacchetti\",\n    \"add\": \"Aggiungi pacchetto\",\n    \"icon\": \"Icona pacchetto\",\n    \"open\": \"Apri pacchetti\",\n    \"new\": \"Nuovo pacchetto\",\n    \"import\": \"Importa pacchetto\",\n    \"set\": \"Imposta come pacchetto\",\n    \"settings\": {\n      \"asBlock\": \"Imposta pacchetto come blocco\"\n    },\n    \"categories\": {\n      \"my\": \"I miei pacchetti\",\n      \"installed\": \"Pacchetti installati\"\n    }\n  },\n  \"scheduledWorkflow\": {\n    \"title\": \"Workflow programmati\",\n    \"nextRun\": \"Prossima esecuzione\",\n    \"active\": \"Attivo\",\n    \"refresh\": \"Ricarica\",\n    \"schedule\":{\n      \"title\": \"Regola\",\n      \"types\": {\n        \"everyDay\": \"Ogni giorno\",\n        \"general\": \"Ogni {time}\",\n        \"interval\": \"Ogni {time} minuti\"\n      }\n    }\n  },\n  \"storage\": {\n    \"title\": \"Archiviazione\",\n    \"table\": {\n      \"add\": \"Aggiungi tabella\",\n      \"createdAt\": \"Creato alle\",\n      \"modifiedAt\": \"Modificato alle\",\n      \"rowsCount\": \"Numero di righe\",\n      \"delete\": \"Elimina tabella\"\n    }\n  },\n  \"credential\": {\n    \"title\": \"Credenziale | Credenziali\",\n    \"add\": \"Aggiungi credenziale\",\n    \"use\": {\n      \"title\": \"Credenziali usate\",\n      \"description\": \"Questo workflow utilizza le seguenti credenziali\"\n    }\n  },\n  \"workflowPermissions\": {\n    \"title\": \"Permessi del workflow\",\n    \"description\": \"Questo workflow richiede i seguenti permessi per funzionare correttamente\",\n    \"contextMenus\": {\n      \"title\": \"Menu contestuale\",\n      \"description\": \"Per eseguire il workflow tramite il menu contestuale\"\n    },\n    \"clipboardRead\": {\n      \"title\": \"Appunti\",\n      \"description\": \"Per accedere ai dati degli appunti\"\n    },\n    \"notifications\": {\n      \"title\": \"Notifiche\",\n      \"description\": \"Per visualizzare le notifiche\"\n    },\n    \"downloads\": {\n      \"title\": \"Download\",\n      \"description\": \"Per salvare e rinominare le risorse delle pagine web\"\n    },\n    \"cookies\": {\n      \"title\": \"Cookie\",\n      \"description\": \"Per leggere, impostare o rimuovere i cookie\"\n    }\n  },\n  \"updateMessage\": {\n    \"text1\": \"Automa è stato aggiorno alla versione v{version},\",\n    \"text2\": \"scopri le novità.\"\n  },\n  \"workflows\": {\n    \"folder\": {\n      \"new\": \"Nuova cartella\",\n      \"name\": \"Nome cartella\",\n      \"delete\": \"Elimina cartella\",\n      \"rename\": \"Rinomina cartella\"\n    }\n  },\n  \"auth\": {\n    \"title\": \"Autenticazione\",\n    \"signIn\": \"Accedi\",\n    \"username\": \"Devi prima impostare il tuo nome utente\",\n    \"clickHere\": \"Clicca qui\",\n    \"text\": \"Per poterlo fare, è necessario aver effettuato l'accesso.\"\n  },\n  \"running\": {\n    \"start\": \"Iniziato il {date}\",\n    \"message\": \"Questo visualizza solo gli ultimi 5 log\"\n  },\n  \"settings\": {\n    \"theme\": \"Tema\",\n    \"shortcuts\": {\n      \"duplicate\": \"Shortcut già utilizzata da \\\"{name}\\\"\"\n    },\n    \"editor\": {\n      \"title\": \"Titolo\",\n      \"curvature\": {\n        \"title\": \"Curvatura Linea\",\n        \"line\": \"Linea\",\n        \"reroute\": \"Reindirizza\",\n        \"rerouteFirstLast\": \"Reindirizza il primo e l'ultimo punto\"\n      },\n      \"arrow\": {\n        \"title\": \"Freccia della linea\",\n        \"description\": \"Aggiungi una freccia alla fine della linea\"\n      },\n      \"snapGrid\": {\n        \"title\": \"Aggancia alla griglia\",\n        \"description\": \"Aggancia alla griglia quando si sposta un blocco\"\n      }\n    },\n    \"deleteLog\": {\n      \"title\": \"Elimina in automatico i log dei workflow\",\n      \"after\": \"Elimina dopo\",\n      \"deleteAfter\": {\n        \"never\": \"Mai\",\n        \"days\": \"{day} giorni\"\n      }\n    },\n    \"language\": {\n      \"label\": \"Lingua\",\n      \"helpTranslate\": \"Non trovi la tua lingua? Aiutaci a tradurre.\",\n      \"reloadPage\": \"Ricarica la pagina per rendere effettiva la modifica\"\n    },\n    \"menu\": {\n      \"backup\": \"Backup\",\n      \"editor\": \"Editor\",\n      \"general\": \"Generale\",\n      \"shortcuts\": \"Scorciatoie\",\n      \"about\": \"Informazioni\"\n    },\n    \"backupWorkflows\": {\n      \"title\": \"Backup Locale\",\n      \"invalidPassword\": \"Password invalida\",\n      \"workflowsAdded\": \"Sono stati aggiunti {count} workflow\",\n      \"name\": \"Backup dei workflow\",\n      \"needSignin\": \"Devi prima effettuare l'accesso\",\n      \"backup\": {\n        \"button\": \"Backup\",\n        \"encrypt\": \"Cifra con password\"\n      },\n      \"restore\": {\n        \"title\": \"Ripristina workflow\",\n        \"button\": \"Ripristina\",\n        \"update\": \"Aggiorna se il workflow esiste\"\n      },\n      \"cloud\": {\n        \"buttons\": {\n          \"local\": \"Locale\",\n          \"cloud\": \"Cloud\"\n        },\n        \"location\": \"Posizione\",\n        \"delete\": \"Elimina backup\",\n        \"title\": \"Backup Cloud\",\n        \"sync\": \"Sincronizza\",\n        \"lastSync\": \"Ultima sincronizzazione\",\n        \"lastBackup\": \"Ultimo backup\",\n        \"select\": \"Seleziona i workflow\",\n        \"storedWorkflows\": \"Workflow archiviati nel cloud\",\n        \"selected\": \"Selezionati\",\n        \"selectText\": \"Seleziona i workflow di cui vuoi eseguire il backup\",\n        \"selectAll\": \"Seleziona tutto\",\n        \"deselectAll\": \"Deseleziona tutto\",\n        \"needSelectWorkflow\": \"Devi selezionare i workflow di cui vuoi eseguire il backup\"\n      }\n    }\n  },\n  \"workflow\": {\n    \"previewMode\": {\n      \"title\": \"Modalità anteprima\",\n      \"description\": \"Sei in modalità anteprima, le modifiche apportate non verranno salvate\"\n    },\n    \"pinWorkflow\": {\n      \"pin\": \"Fissa in alto\",\n      \"unpin\": \"Rimuovi da in alto\",\n      \"pinned\": \"Fissati in alto\"\n    },\n    \"parameters\": {\n      \"add\": \"Aggiungi parametro\",\n      \"preferInTab\": \"Preferisci i parametri di input nella scheda\"\n    },\n    \"my\": \"I miei workflow\",\n    \"import\": \"Importa workflow\",\n    \"new\": \"Nuovo workflow\",\n    \"delete\": \"Elimina workflow\",\n    \"browse\": \"Esplora workflow\",\n    \"name\": \"Nome workflow\",\n    \"rename\": \"Rinomina workflow\",\n    \"backupCloud\": \"Esegui backup nel cloud\",\n    \"add\": \"Aggiungi workflow\",\n    \"clickToEnable\": \"Clicca per abilitare\",\n    \"toggleSidebar\": \"Apri/chiudi barra laterale\",\n    \"cantEdit\": \"Impossibile modificare il workflow condiviso\",\n    \"undo\": \"Annulla\",\n    \"redo\": \"Ripeti\",\n    \"autoAlign\": {\n      \"title\": \"Allinea blocchi\"\n    },\n    \"blocksFolder\": {\n      \"title\": \"Cartella dei blocchi\",\n      \"add\": \"Aggiungi blocchi alla cartella\",\n      \"save\": \"Salva nella cartella\"\n    },\n    \"searchBlocks\": {\n      \"title\": \"Cerca blocchi nell'editor\"\n    },\n    \"conditionBuilder\": {\n      \"title\": \"Costruttore di condizioni\",\n      \"add\": \"Aggiungi condizione\",\n      \"and\": \"AND\",\n      \"or\": \"OR\",\n      \"topAwait\": \"Supporta l'await di primo livello e il metodo \\\"automaRefData\\\"\"\n    },\n    \"host\": {\n      \"title\": \"Ospita workflow\",\n      \"set\": \"Imposta il workflow come host\",\n      \"id\": \"ID host\",\n      \"add\": \"Aggiungi workflow ospitato\",\n      \"sync\": {\n        \"title\": \"Sincronizza\",\n        \"description\": \"Sincronizza col workflow dell'host\"\n      },\n      \"messages\": {\n        \"hostExist\": \"Hai già aggiunto questo host\",\n        \"notFound\": \"Impossibile trovare un workflow ospitato con l'ID \\\"{id}\\\"\",\n        \"successAdded\": \"Workflow ospitato aggiunto con successo con l'ID \\\"{id}\\\"\"\n      }\n    },\n    \"type\": {\n      \"local\": \"Locale\",\n      \"shared\": \"Condiviso\",\n      \"host\": \"Host\"\n    },\n    \"unpublish\": {\n      \"title\": \"Annulla pubblicazione\",\n      \"button\": \"Conferma\",\n      \"body\": \"Sei sicuro di voler annullare la pubblicazione del workflow \\\"{name}\\\"?\"\n    },\n    \"share\": {\n      \"url\": \"Condividi URL\",\n      \"publish\": \"Pubblica\",\n      \"sharedAs\": \"Condiviso come \\\"{name}\\\"\",\n      \"title\": \"Condividi workflow\",\n      \"download\": \"Salva workflow in locale\",\n      \"edit\": \"Modifica descrizione\",\n      \"fetchLocal\": \"Recupera workflow locale\",\n      \"update\": \"Aggiorna\",\n      \"unpublish\": \"Annulla pubblicazione\",\n      \"linkCopied\": \"Link copiato negli appunti\"\n    },\n    \"variables\": {\n      \"title\": \"Variabile | Variabili\",\n      \"name\": \"Nome variabile\",\n      \"assign\": \"Assegna a variabile\"\n    },\n    \"protect\": {\n      \"title\": \"Proteggi workflow\",\n      \"remove\": \"Rimuovi protezione\",\n      \"button\": \"Proteggi\",\n      \"note\": \"Nota: la password verrà richiesta in seguito per modificare o eliminare il workflow.\"\n    },\n    \"locked\": {\n      \"title\": \"Questo workflow è protetto\",\n      \"body\": \"Inserisci la password per sbloccarlo\",\n      \"unlock\": \"Sblocca\",\n      \"messages\": {\n        \"incorrect-password\": \"Password errata\"\n      }\n    },\n    \"state\": {\n      \"executeBy\": \"Eseguito da: \\\"{name}\\\"\"\n    },\n    \"table\": {\n      \"title\": \"Tabella | Tabelle\",\n      \"placeholder\": \"Cerca o aggiungi una colonna\",\n      \"select\": \"Seleziona colonna\",\n      \"column\": {\n        \"name\": \"Nome colonna\",\n        \"type\": \"Tipo di dato\"\n      }\n    },\n    \"sidebar\": {\n      \"workflowIcon\": \"Icona workflow\"\n    },\n    \"editor\": {\n      \"zoomIn\": \"Ingrandisci\",\n      \"zoomOut\": \"Rimpicciolisci\",\n      \"resetZoom\": \"Reimposta lo zoom\",\n      \"duplicate\": \"Duplica\",\n      \"copy\": \"Copia\",\n      \"paste\": \"Incolla\",\n      \"group\": \"Raggruppa blocchi\",\n      \"ungroup\": \"Separa blocchi\"\n    },\n    \"settings\": {\n      \"saveLog\": \"Salva i log del workflow\",\n      \"executedBlockOnWeb\": \"Mostra il blocco eseguito sulla pagina web\",\n      \"notification\": {\n        \"title\": \"Notifica del workflow\",\n        \"description\": \"Mostra lo stato del workflow (successo o fallito) dopo l'esecuzione\",\n        \"noPermission\": \"Questa opzione richiede il permesso \\\"notifications\\\" per funzionare\"\n      },\n      \"publicId\": {\n        \"title\": \"ID pubblico del workflow\",\n        \"description\": \"Imposta un'ID pubblico per eseguire il workflow tramite un evento personalizzato di JavaScript\"\n      },\n      \"defaultColumn\": {\n        \"title\": \"Inserisci nella colonna predefinita\",\n        \"description\": \"Inserisci i dati nella colonna predefinita se nel blocco non è selezionata alcuna colonna\",\n        \"name\": \"Nome colonna predefinita\"\n      },\n      \"autocomplete\": {\n        \"title\": \"Autocompletamento\",\n        \"description\": \"Abilita l'autocompletamento nel blocco di input (disabilita se rende Automa instabile)\"\n      },\n      \"clearCache\": {\n        \"title\": \"Svuota cache\",\n        \"description\": \"Svuota la cache (stato e indice di ciclo) del workflow\",\n        \"info\": \"Cache del workflow svuotata correttamente\",\n        \"btn\": \"Svuota\"\n      },\n      \"reuseLastState\": {\n        \"title\": \"Riprendi lo stato del workflow precedente\",\n        \"description\": \"Usa i dati (tabella, variabili e dati globali) dell'ultimo workflow eseguito\"\n      },\n      \"debugMode\": {\n        \"title\": \"Modalità di debug\",\n        \"description\": \"Esegui il workflow usando il protocollo di Chrome DevTools\"\n      },\n      \"restartWorkflow\": {\n        \"for\": \"Riavvia per\",\n        \"times\": \"Volte\",\n        \"description\": \"Numero massimo di volte in cui si riavvierà il workflow\"\n      },\n      \"onError\": {\n        \"title\": \"In caso di errore nel workflow\",\n        \"description\": \"Imposta l'azione da eseguire se si verifica un errore nel workflow\",\n        \"items\": {\n          \"keepRunning\": \"Prosegui\",\n          \"stopWorkflow\": \"Interrompi\",\n          \"restartWorkflow\": \"Riavvia\"\n        }\n      },\n      \"timeout\": {\n        \"title\": \"Timeout workflow (millisecondi)\"\n      },\n      \"blockDelay\": {\n        \"title\": \"Delay blocco (millisecondi)\",\n        \"description\": \"Aggiungi un delay prima dell'esecuzione di ogni blocco\"\n      },\n      \"tabLoadTimeout\": {\n        \"title\": \"Timeout caricamento schede\",\n        \"description\": \"Tempo massimo di attesa in millisecondi per il caricamento di una scheda (0 per disabilitare)\"\n      }\n    }\n  },\n  \"collection\": {\n    \"description\": \"Esegui i tuoi workflow in sequenza\",\n    \"new\": \"Nuova raccolta\",\n    \"delete\": \"Elimina raccolta\",\n    \"add\": \"Aggiungi raccolta\",\n    \"rename\": \"Rinomina raccolta\",\n    \"flow\": \"Flusso\",\n    \"dragDropText\": \"Trascina un workflow o blocco qui\",\n    \"options\": {\n      \"atOnce\": {\n        \"title\": \"Esegui tutti i workflow nella raccolta insieme\",\n        \"description\": \"I blocchi non verranno eseguiti quando si usa questa opzione\"\n      }\n    },\n    \"globalData\": {\n      \"note\": \"Questo sovrascriverà i dati globali del workflow\"\n    }\n  },\n  \"log\": {\n    \"flowId\": \"ID flusso\",\n    \"goBack\": \"Torna ai log di \\\"{name}\\\"\",\n    \"goWorkflow\": \"Vai al workflow\",\n    \"startedDate\": \"Data di inizio\",\n    \"duration\": \"Durata\",\n    \"selectAll\": \"Seleziona tutto\",\n    \"deselectAll\": \"Deseleziona tutto\",\n    \"deleteSelected\": \"Elimina i log selezionati\",\n    \"clearLogs\": {\n      \"title\": \"Svuota log\",\n      \"description\": \"Sei sicuro di voler cancellare tutti i log?\"\n    },\n    \"types\": {\n      \"stop\": \"Workflow is stopped\",\n      \"finish\": \"Finish\"\n    },\n    \"messages\": {\n      \"url-empty\": \"L'URL è vuoto\",\n      \"invalid-url\": \"L'URL non è valido\",\n      \"conditions-empty\": \"Le condizioni sono vuote\",\n      \"invalid-proxy-host\": \"Host proxy invalido\",\n      \"workflow-disabled\": \"Il workflow è disabilitato\",\n      \"selector-empty\": \"Il selettore dell'elemento è vuoto\",\n      \"invalid-body\": \"Il corpo del contenuto non è un JSON valido\",\n      \"invalid-active-tab\": \"\\\"{url}\\\" non è un URL valido\",\n      \"empty-spreadsheet-id\": \"L'ID del foglio di calcolo è vuoto\",\n      \"invalid-loop-data\": \"I dati da iterare non sono validi\",\n      \"empty-workflow\": \"Devi prima selezionare un workflow\",\n      \"active-tab-removed\": \"La scheda attiva del workflow è stata chiusa\",\n      \"empty-spreadsheet-range\": \"L'intervallo del foglio di calcolo è vuoto\",\n      \"stop-timeout\": \"Il workflow è stato interrotto a causa dello scadere del timeout\",\n      \"no-file-access\": \"Automa non ha accesso al file\",\n      \"no-workflow\": \"Impossibile trovare un workflow con l'ID \\\"{workflowId}\\\"\",\n      \"no-match-tab\": \"Impossibile trovare una scheda che corrisponda al pattern \\\"{pattern}\\\"\",\n      \"no-clipboard-acces\": \"Automa non dispone del permesso per accedere agli appunti\",\n      \"browser-not-supported\": \"Questa funzione non è supportata in un browser {browser}\",\n      \"element-not-found\": \"Impossibile trovare un elemento con il selettore \\\"{selector}\\\"\",\n      \"no-permission\": \"Automa non dispone del permesso \\\"{permission}\\\" per eseguire questa azione\",\n      \"not-iframe\": \"L'elemento con il selettore \\\"{selettore}\\\" non è un elemento iframe\",\n      \"iframe-not-found\": \"Impossibile trovare un elemento iframe con il selettore \\\"{selector}\\\"\",\n      \"workflow-infinite-loop\": \"Impossibile eseguire il workflow per evitare di ripetere un ciclo infinito\",\n      \"not-debug-mode\": \"Il workflow deve essere eseguito in modalità di debug affinché questo blocco funzioni correttamente\",\n      \"no-iframe-id\": \"Impossibile trovare l'ID frame per l'elemento iframe con il selettore \\\"{selector}\\\"\",\n      \"no-tab\": \"Impossibile connettersi ad una scheda, usa il blocchi \\\"Nuova scheda\\\" o \\\"Scheda attiva\\\" prima di usare il blocco \\\"{name}\\\"\"\n    },\n    \"description\": {\n      \"text\": \"{status} il {date} in {duration}\",\n      \"status\": {\n        \"success\": \"Riuscito\",\n        \"error\": \"Fallito\",\n        \"stopped\": \"Interrotto\"\n      }\n    },\n    \"delete\": {\n      \"title\": \"Elimina log\",\n      \"description\": \"Sei sicuro di voler eliminare tutti i log selezionati?\"\n    },\n    \"exportData\": {\n      \"title\": \"Esporta dati\",\n      \"types\": {\n        \"json\": \"JSON\",\n        \"csv\": \"CSV\",\n        \"plain-text\": \"Testo semplice\"\n      }\n    },\n    \"filter\": {\n      \"title\": \"Filtra\",\n      \"byStatus\": \"Per stato\",\n      \"byDate\": {\n        \"title\": \"Per data\",\n        \"items\": {\n          \"lastDay\": \"Ultimo giorno\",\n          \"last7Days\": \"Ultimi sette giorni\",\n          \"last30Days\": \"Ultimi trenta giorni\"\n        }\n      }\n    }\n  },\n  \"components\": {\n    \"pagination\": {\n      \"text1\": \"Visualizzazione di \",\n      \"text2\": \"elementi su {count}\",\n      \"nextPage\": \"Pagina successiva\",\n      \"currentPage\": \"Pagina corrente\",\n      \"prevPage\": \"Pagina precedente\",\n      \"of\": \"di {page}\"\n    }\n  }\n}\n"
  },
  {
    "path": "src/locales/it/popup.json",
    "content": "{\n  \"recording\": {\n    \"stop\": \"Termina registrazione\",\n    \"title\": \"Registrando\"\n  },\n  \"home\": {\n    \"record\": {\n      \"title\": \"Registra workflow\",\n      \"button\": \"Registra\",\n      \"name\": \"Nome workflow\",\n      \"selectBlock\": \"Seleziona un blocco da cui iniziare\",\n      \"anotherBlock\": \"Impossibile iniziare da questo blocco\",\n      \"tabs\": {\n        \"new\": \"Nuovo workflow\",\n        \"existing\": \"Workflow esistente\"\n      }\n    },\n    \"elementSelector\": {\n      \"name\": \"Selettore elemento\",\n      \"noAccess\": \"Automa non ha accesso a questo sito\"\n    },\n    \"workflow\": {\n      \"new\": \"Nuovo workflow\",\n      \"rename\": \"Rinomina workflow\",\n      \"delete\": \"Elimina workflow\",\n      \"type\": {\n        \"host\": \"Host\",\n        \"local\": \"Locale\"\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "src/locales/pt-BR/blocks.json",
    "content": "{\n  \"collection\": {\n    \"blocks\": {\n      \"export-result\": {\n        \"name\": \"Exportar resultado\",\n        \"description\": \"Exportar o resultado da coleta como JSON\"\n      }\n    }\n  },\n  \"workflow\": {\n    \"blocks\": {\n      \"base\": {\n        \"title\": \"Blocos\",\n        \"moveToGroup\": \"Mover bloco para o grupo de blocos\",\n        \"selector\": \"Seletor de elemento\",\n        \"selectorOptions\": \"Opções de seletor\",\n        \"timeout\": \"Timeout (milissegundos)\",\n        \"noPermission\": \"O Automa não tem permissões suficientes para realizar esta ação\",\n        \"grantPermission\": \"Conceder permissão\",\n        \"action\": \"Ação\",\n        \"element\": {\n          \"select\": \"Selecionar um elemento\",\n          \"verify\": \"Verificar seletor\"\n        },\n        \"settings\": {\n          \"title\": \"Configurações do bloco\",\n          \"blockTimeout\": {\n            \"title\": \"Tempo limite de execução do bloco (milissegundos)\",\n            \"description\": \"O tempo máximo de execução do bloco (0 para desabilitar)\"\n          },\n          \"line\": {\n            \"title\": \"Linhas\",\n            \"label\": \"Rótulo\",\n            \"animated\": \"Animado\",\n            \"select\": \"Selecionar linha\",\n            \"to\": \"Linha para o bloco {name}\",\n            \"lineColor\": \"Cor\"\n          }\n        },\n        \"toggle\": {\n          \"enable\": \"Habilitar bloco\",\n          \"disable\": \"Desabilitar bloco\"\n        },\n        \"onError\": {\n          \"info\": \"Estas regras serão aplicadas quando ocorrer um erro no bloco\",\n          \"button\": \"Em caso de erro\",\n          \"title\": \"Quando ocorrer um erro\",\n          \"retry\": \"Tentar novamente a ação\",\n          \"fallbackTitle\": \"Será executado quando ocorrer um erro no bloco\",\n          \"times\": {\n            \"name\": \"Vezes\",\n            \"description\": \"O número de vezes para tentar a ação novamente\"\n          },\n          \"interval\": {\n            \"name\": \"Intervalo\",\n            \"description\": \"O intervalo de tempo para aguardar entre cada tentativa\",\n            \"second\": \"segundo\"\n          },\n          \"toDo\": {\n            \"error\": \"Lançar erro\",\n            \"continue\": \"Continuar fluxo\",\n            \"fallback\": \"Executar fallback\",\n            \"restart\": \"Reiniciar fluxo\"\n          },\n          \"insertData\": {\n            \"name\": \"Inserir dados\"\n          }\n        },\n        \"table\": {\n          \"checkbox\": \"Inserir na tabela\",\n          \"select\": \"Selecionar coluna\",\n          \"extraRow\": {\n            \"checkbox\": \"Adicionar linha extra\",\n            \"placeholder\": \"Valor\",\n            \"title\": \"Valor da linha extra\"\n          }\n        },\n        \"findElement\": {\n          \"placeholder\": \"Encontrar elemento por\",\n          \"options\": {\n            \"cssSelector\": \"Seletor CSS\",\n            \"xpath\": \"XPath\"\n          }\n        },\n        \"markElement\": {\n          \"title\": \"Um elemento não será selecionado se já foi selecionado antes\",\n          \"text\": \"Marcar elemento\"\n        },\n        \"multiple\": {\n          \"title\": \"Selecionar múltiplos elementos\",\n          \"text\": \"Múltiplos\"\n        },\n        \"waitSelector\": {\n          \"title\": \"Aguardar seletor\",\n          \"timeout\": \"Tempo limite do seletor (ms)\"\n        },\n        \"downloads\": {\n          \"onConflict\": {\n            \"uniquify\": \"Tornar único\",\n            \"overwrite\": \"Sobrescrever\",\n            \"prompt\": \"Solicitar\"\n          }\n        }\n      },\n  \"wait-connections\": {\n    \"name\": \"Aguardar conexões\",\n    \"description\": \"Aguardar todas as conexões antes de continuar para o próximo bloco\",\n    \"specificFlow\": \"Continuar apenas um fluxo específico\",\n    \"selectFlow\": \"Selecionar fluxo\"\n  },\n  \"cookie\": {\n    \"name\": \"Cookie\",\n    \"description\": \"Obter, definir ou remover cookies\",\n    \"types\": {\n      \"get\": \"Obter cookies\",\n      \"set\": \"Definir cookie\",\n      \"remove\": \"Remover cookies\",\n      \"getAll\": \"Obter todos os cookies\"\n    },\n    \"useJson\": \"Usar formato JSON\"\n  },\n  \"note\": {\n    \"name\": \"Nota\"\n  },\n  \"slice-variable\": {\n    \"name\": \"Fatiar variável\",\n    \"description\": \"Extrai uma seção do valor de uma variável\",\n    \"start\": \"Índice inicial\",\n    \"end\": \"Índice final\"\n  },\n  \"workflow-state\": {\n    \"name\": \"Estado do Workflow\",\n    \"description\": \"Gerenciar estados dos workflows\",\n    \"actions\": {\n      \"stop\": \"Parar workflows\"\n    },\n    \"error\": {\n      \"throwError\": \"Lançar erro\",\n      \"message\": \"Mensagem de erro\"\n    }\n  },\n  \"regex-variable\": {\n    \"name\": \"Variável RegEx\",\n    \"description\": \"Comparar o valor de uma variável com uma expressão regular\"\n  },\n  \"data-mapping\": {\n    \"source\": \"Origem\",\n    \"destination\": \"Destino\",\n    \"name\": \"Mapeamento de dados\",\n    \"edit\": \"Editar mapa de dados\",\n    \"dataSource\": \"Fonte de dados\",\n    \"description\": \"Mapear os dados de uma variável ou tabela\",\n    \"addSource\": \"Adicionar origem\",\n    \"addDestination\": \"Adicionar destino\"\n  },\n  \"sort-data\": {\n    \"name\": \"Ordenar dados\",\n    \"description\": \"Ordenar os itens dos dados\",\n    \"property\": \"Ordenar pela propriedade do item\",\n    \"addProperty\": \"Adicionar propriedade\"\n  },\n  \"increase-variable\": {\n    \"name\": \"Aumentar variável\",\n    \"description\": \"Aumentar o valor de uma variável por um valor específico\",\n    \"increase\": \"Aumentar em\"\n  },\n  \"notification\": {\n    \"name\": \"notificação\",\n    \"description\": \"Exibir uma notificação\",\n    \"title\": \"Título\",\n    \"message\": \"Mensagem\",\n    \"imageUrl\": \"URL da imagem (opcional)\",\n    \"iconUrl\": \"URL do ícone (opcional)\"\n  },\n  \"delete-data\": {\n    \"name\": \"Excluir dados\",\n    \"description\": \"Excluir dados de tabela ou variável\",\n    \"from\": \"Dados de\",\n    \"allColumns\": \"[Todas as colunas]\"\n  },\n  \"log-data\": {\n    \"name\": \"Obter dados de registro\",\n    \"description\": \"Obter os dados de registro mais recentes de um workflow\",\n    \"data\": \"Dados de registro\"\n  },\n  \"tab-url\": {\n    \"name\": \"Obter URL da aba\",\n    \"description\": \"Obter a URL da aba\",\n    \"select\": \"Selecionar aba\",\n    \"types\": {\n      \"active-tab\": \"Aba ativa\",\n      \"all\": \"Todas as abas\"\n    },\n    \"query\": {\n      \"title\": \"Consulta\",\n      \"matchPatterns\": \"@:workflow.blocks.switch-tab.matchPattern (opcional)\",\n      \"tabTitle\": \"Título da aba (opcional)\"\n    }\n  },\n  \"reload-tab\": {\n    \"name\": \"Recarregar aba\",\n    \"description\": \"Recarregar a aba ativa\"\n  },\n  \"press-key\": {\n    \"name\": \"Pressionar tecla\",\n    \"description\": \"Pressionar uma tecla ou uma combinação\",\n    \"target\": \"Elemento alvo (opcional)\",\n    \"key\": \"Tecla\",\n    \"detect\": \"Detectar tecla\",\n    \"actions\": {\n      \"press-key\": \"Pressionar uma tecla\",\n      \"multiple-keys\": \"Pressionar múltiplas teclas\"\n    },\n    \"press-time\": \"Tempo de pressão (milissegundos)\"\n  },\n  \"save-assets\": {\n    \"name\": \"Salvar ativos\",\n    \"description\": \"Salvar ativos (imagem, vídeo, áudio ou arquivo) de um elemento ou URL\",\n    \"filename\": \"Nome do arquivo (opcional)\",\n    \"saveDownloadIds\": \"Salvar IDs de download dos itens\",\n    \"contentTypes\": {\n      \"title\": \"Tipo\",\n      \"element\": \"Elemento de mídia (imagem, áudio ou vídeo)\",\n      \"url\": \"URL\"\n    }\n  },\n  \"handle-dialog\": {\n    \"name\": \"Manipular diálogo\",\n    \"description\": \"Aceitar ou rejeitar um diálogo iniciado pelo JavaScript (alerta, confirmação, prompt ou onbeforeunload)\",\n    \"accept\": \"Aceitar diálogo\",\n    \"promptText\": {\n      \"label\": \"Texto do prompt (opcional)\",\n      \"description\": \"O texto a ser inserido no diálogo de prompt antes de aceitar\"\n    }\n  },\n  \"handle-download\": {\n    \"name\": \"Manipular download\",\n    \"description\": \"Tratar o arquivo baixado\",\n    \"timeout\": \"Tempo limite (milissegundos)\",\n    \"noPermission\": \"Não tem permissão para acessar os downloads\",\n    \"onConflict\": \"Em caso de conflito\",\n    \"waitFile\": \"Aguardar o download do arquivo\",\n    \"downloadId\": \"ID de download do arquivo (opcional)\",\n    \"filePath\": \"Caminho do arquivo\"\n  },\n  \"insert-data\": {\n    \"name\": \"Inserir dados\",\n    \"description\": \"Inserir dados em uma tabela ou variável\"\n  },\n  \"clipboard\": {\n    \"name\": \"Área de transferência\",\n    \"description\": \"Obter o texto copiado da área de transferência\",\n    \"data\": \"Dados da área de transferência\",\n    \"noPermission\": \"Não tem permissão para acessar a área de transferência\",\n    \"grantPermission\": \"Conceder permissão\",\n    \"copySelection\": \"Copiar o texto selecionado na página\",\n    \"types\": {\n      \"get\": \"Obter dados da área de transferência\",\n      \"insert\": \"Inserir texto na área de transferência\"\n    }\n  },\n  \"hover-element\": {\n    \"name\": \"Passar o mouse sobre o elemento\",\n    \"description\": \"Passar o mouse sobre um elemento\"\n  },\n  \"create-element\": {\n    \"name\": \"Criar elemento\",\n    \"description\": \"Criar um elemento e inseri-lo na página\",\n    \"edit\": \"Editar elemento\",\n    \"wrap\": \"Envolver o elemento dentro de\",\n    \"insertEl\": {\n      \"title\": \"Inserir elemento\",\n      \"items\": {\n        \"before\": \"Como primeiro filho\",\n        \"after\": \"Como último filho\",\n        \"next-sibling\": \"Como próximo irmão\",\n        \"prev-sibling\": \"Como irmão anterior\",\n        \"replace\": \"Substituir o elemento alvo\"\n      }\n    }\n  },\n  \"upload-file\": {\n    \"name\": \"Enviar arquivo\",\n    \"description\": \"Enviar arquivo para o elemento <input type=\\\"file\\\">\",\n    \"filePath\": \"URL ou caminho do arquivo\",\n    \"addFile\": \"Adicionar arquivo\",\n    \"onlyURL\": \"Somente o envio de arquivos a partir de uma URL é suportado no navegador Firefox\",\n    \"requirement\": \"Leia os requisitos antes de usar este bloco\",\n    \"noFileAccess\": \"O Automa não tem acesso a arquivos\"\n  },\n  \"browser-event\": {\n    \"name\": \"Evento do navegador\",\n    \"description\": \"Executa o próximo bloco quando o evento especificado é acionado\",\n    \"events\": \"Eventos\",\n    \"timeout\": \"Tempo limite (milissegundos)\",\n    \"activeTabLoaded\": \"Aba ativa\",\n    \"setAsActiveTab\": \"Definir como aba ativa\"\n  },\n  \"blocks-group-2\": {\n    \"name\": \"@:workflow.blocks.blocks-group.name 2\",\n    \"description\": \"@:workflow.blocks.blocks-group.description\"\n  },\n  \"blocks-group\": {\n    \"name\": \"Grupo de blocos\",\n    \"groupName\": \"Nome do grupo\",\n    \"description\": \"Agrupamento de blocos\",\n    \"dropText\": \"Arraste e solte um bloco aqui\",\n    \"cantAdd\": \"Não é possível adicionar o bloco \\\"{blockName}\\\" ao grupo\"\n      },\n      \"trigger\": {\n    \"name\": \"Gatilho\",\n    \"description\": \"Bloco onde o workflow começará a ser executado\",\n    \"addTime\": \"Adicionar horário\",\n    \"selectDay\": \"Selecionar dia\",\n    \"timeExist\": \"Você já adicionou um gatilho às {time} em {day}\",\n    \"fixedDelay\": \"Atraso fixo\",\n    \"contextMenus\": {\n      \"noPermission\": \"Este gatilho requer a permissão \\\"contextMenus\\\" para funcionar\",\n      \"grantPermission\": \"Conceder permissão\",\n      \"appearIn\": \"Aparecerá em\",\n      \"contextName\": \"Nome do workflow no menu de contexto\"\n    },\n    \"days\": [\n      \"Domingo\",\n      \"Segunda-feira\",\n      \"Terça-feira\",\n      \"Quarta-feira\",\n      \"Quinta-feira\",\n      \"Sexta-feira\",\n      \"Sábado\"\n    ],\n    \"useRegex\": \"Usar regex\",\n    \"shortcut\": {\n      \"tooltip\": \"Gravar atalho\",\n      \"stopRecord\": \"Parar gravação\",\n      \"checkboxTitle\": \"Executar atalho mesmo quando você estiver em um elemento de entrada\",\n      \"checkbox\": \"Ativo enquanto em entrada\",\n      \"note\": \"Nota: o atalho de teclado só funciona quando você está em uma página web\"\n    },\n    \"forms\": {\n      \"triggerWorkflow\": \"Acionar workflow\",\n      \"interval\": \"Intervalo (minutos)\",\n      \"delay\": \"Atraso (minutos)\",\n      \"date\": \"Data\",\n      \"time\": \"Hora\",\n      \"url\": \"URL ou Regex\",\n      \"shortcut\": \"Atalho\",\n      \"cron-expression\": \"Expressão cron\"\n    },\n    \"element-change\": {\n      \"target\": \"Elemento alvo a ser observado\",\n      \"optionsInfo\": \"Qual mutação do elemento acionará o workflow\",\n      \"targetWebsite\": \"O Padrão de Correspondência do site onde o elemento alvo está (clique para ver mais exemplos de Padrão de Correspondência)\",\n      \"baseEl\": {\n        \"title\": \"Elemento base (opcional)\",\n        \"description\": \"O Automa reiniciará a observação do elemento alvo quando este elemento mudar\"\n      },\n      \"subtree\": {\n        \"title\": \"Incluir subárvore\",\n        \"description\": \"Estender a monitoração para toda a subárvore do elemento alvo\"\n      },\n      \"childList\": {\n        \"title\": \"Lista de filhos\",\n        \"description\": \"Monitorar a adição de novos elementos filhos ou a remoção dos existentes\"\n      },\n      \"attributes\": {\n        \"title\": \"Atributos\",\n        \"description\": \"Observar mudanças nos valores dos atributos do elemento alvo\"\n      },\n      \"attributeFilter\": {\n        \"title\": \"Filtro de atributos\",\n        \"separate\": \"Use vírgulas (,) para separar os nomes dos atributos\",\n        \"description\": \"Monitorar apenas atributos específicos (deixe em branco para monitorar todos)\"\n      },\n      \"characterData\": {\n        \"title\": \"Dados de caracteres\",\n        \"description\": \"Monitorar mudanças nos dados de caracteres/texto dentro do elemento alvo\"\n      }\n    },\n    \"items\": {\n      \"manual\": \"Manualmente\",\n      \"interval\": \"Intervalo\",\n      \"cron-job\": \"Tarefa cron\",\n      \"date\": \"Em uma data específica\",\n      \"context-menu\": \"Menu de contexto\",\n      \"element-change\": \"Na mudança de elemento\",\n      \"specific-day\": \"Em um dia específico\",\n      \"visit-web\": \"Ao visitar um site\",\n      \"on-startup\": \"Na inicialização do navegador\",\n      \"keyboard-shortcut\": \"Atalho de teclado\"\n    }\n  },\n  \"execute-workflow\": {\n    \"name\": \"Executar workflow\",\n    \"overwriteNote\": \"Isso irá sobrescrever os dados globais do workflow selecionado\",\n    \"select\": \"Selecionar workflow\",\n    \"executeId\": \"ID de execução (opcional)\",\n    \"description\": \"\",\n    \"insertAllVars\": \"Usar todas as variáveis atuais do workflow\",\n    \"insertVars\": \"Inserir variáveis atuais do workflow\",\n    \"useCommas\": \"Use vírgulas para separar o nome da variável\",\n    \"insertAllGlobalData\": \"Usar todos os dados globais atuais do workflow\"\n      },\n      \"google-sheets-drive\": {\n    \"name\": \"@:workflow.blocks.google-sheets.name (GDrive)\",\n    \"description\": \"@:workflow.blocks.google-sheets.description\",\n    \"connected\": \"Planilhas conectadas\",\n    \"select\": \"Selecionar planilha\",\n    \"connect\": \"Conectar planilha\"\n  },\n  \"google-drive\": {\n    \"name\": \"Google Drive\",\n    \"description\": \"Enviar arquivos para o Google Drive\",\n    \"actions\": {\n      \"upload\": \"Enviar arquivos\"\n    }\n  },\n  \"google-sheets\": {\n    \"name\": \"Google Sheets\",\n    \"description\": \"Ler ou atualizar dados do Google Sheets\",\n    \"previewData\": \"Visualizar dados\",\n    \"firstRow\": \"Usar a primeira linha como chaves\",\n    \"keysAsFirstRow\": \"Usar chaves como a primeira linha\",\n    \"insertData\": \"Inserir dados\",\n    \"valueInputOption\": \"Opção de inserção de valor\",\n    \"insertDataOption\": \"Opção de inserção de dados\",\n    \"rangeToSearch\": \"Intervalo para iniciar a busca\",\n    \"dataFrom\": {\n      \"label\": \"Dados de\",\n      \"options\": {\n        \"data-columns\": \"Tabela\",\n        \"custom\": \"Personalizado\"\n      }\n    },\n    \"refKey\": {\n      \"label\": \"Chave de referência (opcional)\",\n      \"placeholder\": \"Nome da chave\"\n    },\n    \"spreadsheetId\": {\n      \"label\": \"ID da planilha\",\n      \"link\": \"Veja como obter o ID da planilha\"\n    },\n    \"range\": {\n      \"label\": \"Intervalo\",\n      \"link\": \"Clique para ver mais exemplos\"\n    },\n    \"select\": {\n      \"get\": \"Obter valores das células da planilha\",\n      \"getRange\": \"Obter intervalo da planilha\",\n      \"update\": \"Atualizar valores das células da planilha\",\n      \"append\": \"Adicionar valores às células da planilha\",\n      \"clear\": \"Limpar valores das células da planilha\",\n      \"create\": \"Criar uma planilha\",\n      \"add-sheet\": \"Adicionar planilha\"\n    }\n  },\n  \"active-tab\": {\n    \"name\": \"Aba ativa\",\n    \"description\": \"Definir a aba atual como a aba ativa\"\n  },\n  \"proxy\": {\n    \"name\": \"Proxy\",\n    \"description\": \"Definir o proxy do navegador\",\n    \"clear\": \"Limpar todos os proxies\",\n    \"bypass\": {\n      \"label\": \"Lista de exceções\",\n      \"note\": \"Use vírgulas (,) para separar URLs\"\n    }\n  },\n  \"new-window\": {\n    \"name\": \"Nova janela\",\n    \"description\": \"Criar uma nova janela\",\n    \"top\": \"Topo\",\n    \"left\": \"Esquerda\",\n    \"height\": \"Altura\",\n    \"width\": \"Largura\",\n    \"note\": \"Nota: use 0 para desativar\",\n    \"position\": \"Posição da janela\",\n    \"size\": \"Tamanho da janela\",\n    \"windowState\": {\n      \"placeholder\": \"Estado da janela\",\n      \"options\": {\n        \"normal\": \"Normal\",\n        \"minimized\": \"Minimizada\",\n        \"maximized\": \"Maximizada\",\n        \"fullscreen\": \"Tela cheia\"\n      }\n    },\n    \"incognito\": {\n      \"text\": \"Definir como janela anônima\",\n      \"note\": \"Você deve ativar 'Permitir no modo anônimo' para esta extensão primeiro\"\n    }\n  },\n  \"go-back\": {\n    \"name\": \"Voltar\",\n    \"description\": \"Voltar para a página anterior\"\n  },\n  \"forward-page\": {\n    \"name\": \"Avançar\",\n    \"description\": \"Avançar para a próxima página\"\n  },\n  \"close-tab\": {\n    \"name\": \"Fechar aba/janela\",\n    \"description\": \"\",\n    \"url\": \"Padrões de correspondência\",\n    \"activeTab\": \"Fechar aba ativa\",\n    \"allWindows\": \"Fechar todas as janelas\"\n  },\n  \"event-click\": {\n    \"name\": \"Clicar no elemento\",\n    \"description\": \"\"\n      },\n      \"delay\": {\n        \"name\": \"Atraso\",\n        \"description\": \"Adicionar um atraso antes de executar o próximo bloco\",\n        \"input\": {\n          \"title\": \"Atraso em milissegundos\",\n          \"placeholder\": \"(milissegundos)\"\n        }\n      },\n      \"parameter-prompt\": {\n        \"name\": \"Prompt de Parâmetro\"\n      },\n      \"get-text\": {\n        \"name\": \"Obter texto\",\n        \"description\": \"Obter texto de um elemento\",\n        \"checkbox\": \"Inserir na tabela\",\n        \"includeTags\": \"Incluir tags HTML\",\n        \"prefixText\": {\n          \"placeholder\": \"Prefixo de texto\",\n          \"title\": \"Adicionar prefixo ao texto\"\n        },\n        \"suffixText\": {\n          \"placeholder\": \"Sufixo de texto\",\n          \"title\": \"Adicionar sufixo ao texto\"\n        }\n      },\n      \"export-data\": {\n        \"name\": \"Exportar dados\",\n        \"description\": \"Exportar dados do workflow\",\n        \"exportAs\": \"Exportar como\",\n        \"refKey\": \"Chave de referência\",\n        \"bomHeader\": \"Adicionar BOM UTF-8\",\n        \"dataToExport\": {\n          \"placeholder\": \"Dados para exportar\",\n          \"options\": {\n            \"data-columns\": \"Tabela\",\n            \"google-sheets\": \"Google Sheets\",\n            \"variable\": \"Variável\"\n          }\n        }\n      },\n      \"element-scroll\": {\n        \"name\": \"Deslocar elemento\",\n        \"description\": \"\",\n        \"scrollY\": \"Rolagem vertical\",\n        \"scrollX\": \"Rolagem horizontal\",\n        \"intoView\": \"Rolar para visualização\",\n        \"smooth\": \"Rolagem suave\",\n        \"incScrollX\": \"Incrementar rolagem horizontal\",\n        \"incScrollY\": \"Incrementar rolagem vertical\"\n      },\n      \"switch-tab\": {\n        \"name\": \"Alternar aba\",\n        \"description\": \"Alternar entre abas\",\n        \"matchPattern\": \"Padrões de correspondência\",\n        \"url\": \"URL da nova aba\",\n        \"createIfNoMatch\": \"Criar se não houver correspondência\"\n      },\n      \"new-tab\": {\n        \"name\": \"Nova aba\",\n        \"description\": \"\",\n        \"url\": \"URL da nova aba\",\n        \"tab-zoom\": \"Zoom da aba\",\n        \"customUserAgent\": \"Usar User-Agent personalizado\",\n        \"activeTab\": \"Definir como aba ativa\",\n        \"tabToGroup\": \"Adicionar aba a um grupo\",\n        \"waitTabLoaded\": \"Aguardar até que a aba seja carregada\",\n        \"updatePrevTab\": {\n          \"title\": \"Usar a aba nova previamente aberta em vez de criar uma nova\",\n          \"text\": \"Atualizar aba previamente aberta\"\n        }\n      },\n      \"link\": {\n        \"name\": \"Link\",\n        \"description\": \"Abrir elemento de link\",\n        \"openInNewTab\": \"Abrir em nova aba\"\n      },\n      \"attribute-value\": {\n        \"name\": \"Valor do atributo\",\n        \"description\": \"Obter o valor de um atributo do elemento\",\n        \"forms\": {\n          \"name\": \"Nome do atributo\",\n          \"checkbox\": \"Inserir na tabela\",\n          \"column\": \"Selecionar coluna\",\n          \"value\": \"Valor do atributo\",\n          \"action\": {\n            \"get\": \"Obter valor do atributo\",\n            \"set\": \"Definir valor do atributo\"\n          },\n          \"extraRow\": {\n            \"checkbox\": \"Adicionar linha extra\",\n            \"placeholder\": \"Valor\",\n            \"title\": \"Valor da linha extra\"\n          }\n        }\n      },\n      \"forms\": {\n        \"name\": \"Formulários\",\n        \"description\": \"\",\n        \"selected\": \"Selecionado\",\n        \"type\": \"Tipo de formulário\",\n        \"getValue\": \"Obter valor do formulário\",\n        \"text-field\": {\n          \"name\": \"Campo de texto\",\n          \"value\": \"Valor\",\n          \"clearValue\": \"Limpar valor do formulário\",\n          \"delay\": {\n            \"placeholder\": \"Atraso\",\n            \"label\": \"Atraso de digitação (milissegundo)(0 para desativar)\"\n          }\n        },\n        \"select\": {\n    \"name\": \"Selecionar\"\n  },\n  \"radio\": {\n    \"name\": \"Botão de rádio\"\n  },\n  \"checkbox\": {\n    \"name\": \"Caixa de seleção\"\n  },\n  \"repeat-task\": {\n    \"name\": \"Repetir tarefa\",\n    \"description\": \"\",\n    \"times\": \"vezes\",\n    \"repeatFrom\": \"Repetir a partir de\"\n  },\n  \"javascript-code\": {\n    \"name\": \"Código JavaScript\",\n    \"description\": \"Execute seu código JavaScript na página web\",\n    \"availabeFuncs\": \"Funções disponíveis:\",\n    \"removeAfterExec\": \"Remover após a execução do bloco\",\n    \"everyNewTab\": \"Executar em cada nova aba\",\n    \"context\": {\n      \"name\": \"Contexto de execução\",\n      \"items\": {\n        \"website\": \"Aba ativa\",\n        \"background\": \"Segundo plano\"\n      }\n    },\n    \"modal\": {\n      \"tabs\": {\n        \"code\": \"Código JavaScript\",\n        \"preloadScript\": \"Script de pré-carregamento\"\n      }\n    },\n    \"timeout\": {\n      \"placeholder\": \"Tempo limite (milissegundos)\",\n      \"title\": \"Tempo limite de execução do código JavaScript\"\n    }\n  },\n  \"trigger-event\": {\n    \"name\": \"Acionar evento\",\n    \"description\": \"\",\n    \"selectEvent\": \"Selecionar evento\"\n  },\n  \"conditions\": {\n    \"name\": \"Condições\",\n    \"add\": \"Adicionar caminho\",\n    \"retryConditions\": \"Tentar novamente se nenhuma condição for atendida\",\n    \"description\": \"Bloco condicional\",\n    \"refresh\": \"Atualizar conexões de condição\",\n    \"fallbackTitle\": \"Executa quando todas as comparações não atendem ao requisito\",\n    \"equals\": \"Igual a\",\n    \"gt\": \"Maior que\",\n    \"gte\": \"Maior ou igual a\",\n    \"lt\": \"Menor que\",\n    \"lte\": \"Menor ou igual a\",\n    \"ne\": \"Diferente de\",\n    \"contains\": \"Contém\"\n  }\n},\n  \"element-exists\": {\n    \"name\": \"Elemento existe\",\n    \"description\": \"Verificar se um elemento existe\",\n    \"selector\": \"Seletor de elemento\",\n    \"fallbackTitle\": \"Executa quando o elemento não existe\",\n    \"throwError\": \"Lançar um erro se não existir\",\n    \"tryFor\": {\n      \"title\": \"Quantas vezes tentar verificar se o elemento existe\",\n      \"label\": \"Tentar por\"\n    },\n    \"timeout\": {\n      \"label\": \"Tempo limite (milissegundos)\",\n      \"title\": \"Tempo limite para cada tentativa\"\n    }\n  },\n  \"webhook\": {\n    \"name\": \"Requisição HTTP\",\n    \"description\": \"Fazer uma requisição HTTP\",\n    \"contentType\": \"Tipo de conteúdo\",\n    \"method\": \"Método da requisição\",\n    \"url\": \"URL da requisição\",\n    \"fallback\": \"Executa quando a requisição HTTP falha\",\n    \"buttons\": {\n      \"header\": \"Adicionar cabeçalho\"\n    },\n    \"timeout\": {\n      \"placeholder\": \"Tempo limite\",\n      \"title\": \"Tempo limite de execução da requisição HTTP (ms)\"\n    },\n    \"tabs\": {\n      \"headers\": \"Cabeçalhos\",\n      \"body\": \"Corpo\",\n      \"response\": \"Resposta\"\n    }\n  },\n  \"while-loop\": {\n    \"name\": \"Loop while\",\n    \"description\": \"Executa blocos enquanto a condição for atendida\",\n    \"editCondition\": \"Editar condição\",\n    \"fallback\": \"Executa quando a condição é falsa\"\n      },\n      \"loop-elements\": {\n    \"name\": \"Percorrer elementos\",\n    \"description\": \"Iterar pelos elementos\",\n    \"loadMore\": \"Carregar mais elementos\",\n    \"scrollToBottom\": \"Rolar para o final\",\n    \"scrollToTop\": \"Rolar para o topo\",\n    \"actions\": {\n      \"none\": \"Nenhum\",\n      \"click-element\": \"Clicar em um elemento\",\n      \"scroll\": \"Rolar para baixo\",\n      \"click-link\": \"Clicar em um link\",\n      \"scroll-up\": \"Rolar para cima\"\n    }\n  },\n  \"loop-data\": {\n    \"name\": \"Percorrer dados\",\n    \"description\": \"Iterar por uma tabela ou seus dados personalizados\",\n    \"loopId\": \"ID do loop\",\n    \"refKey\": \"Chave de referência\",\n    \"startIndex\": \"Iniciar a partir do índice\",\n    \"resumeLastWorkflow\": \"Retomar o último workflow\",\n    \"reverse\": \"Inverter a ordem do loop\",\n    \"modal\": {\n      \"fileTooLarge\": \"Arquivo muito grande para editar\",\n      \"maxFile\": \"Tamanho máximo do arquivo é 1MB\",\n      \"options\": {\n        \"firstRow\": \"Usar a primeira linha como chaves\"\n      }\n    },\n    \"buttons\": {\n      \"clear\": \"Limpar dados\",\n      \"insert\": \"Inserir dados\",\n      \"import\": \"Importar arquivo\"\n    },\n    \"maxLoop\": {\n      \"title\": \"Número máximo de dados para iterar\",\n      \"label\": \"Máximo de dados para iterar (0 para desativar)\"\n    },\n    \"loopThrough\": {\n      \"placeholder\": \"Percorrer\",\n      \"fromNumber\": \"De número\",\n      \"toNumber\": \"Até número\",\n      \"options\": {\n        \"numbers\": \"Números\",\n        \"variable\": \"Variável\",\n        \"data-columns\": \"Tabela\",\n        \"table\": \"Tabela\",\n        \"custom-data\": \"Dados personalizados\",\n        \"google-sheets\": \"Google Sheets\",\n        \"elements\": \"Elementos\"\n      }\n    }\n  },\n  \"loop-breakpoint\": {\n    \"name\": \"Ponto de interrupção do loop\",\n    \"description\": \"Para indicar onde o bloco de loop de dados deve parar\"\n  },\n  \"take-screenshot\": {\n    \"name\": \"Tirar captura de tela\",\n    \"fullPage\": \"Tirar captura de tela de página inteira\",\n    \"description\": \"Tirar uma captura de tela da aba ativa\",\n    \"imageQuality\": \"Qualidade da imagem\",\n    \"saveToColumn\": \"Inserir captura de tela na tabela\",\n    \"saveToComputer\": \"Salvar captura de tela no computador\",\n    \"types\": {\n      \"title\": \"Tirar uma captura de tela de\",\n      \"page\": \"Uma página\",\n      \"fullpage\": \"Página inteira\",\n      \"element\": \"Um elemento\"\n    }\n  },\n  \"switch-to\": {\n    \"name\": \"Alternar frame\",\n    \"description\": \"Alternar entre a janela principal e um iframe\",\n    \"iframeSelector\": \"Seletor de elemento\",\n    \"windowTypes\": {\n      \"main\": \"Janela principal\",\n      \"iframe\": \"Iframe\"\n      }\n    },\n      \"debugMode\": {\n      \"title\": \"Modo de depuração\",\n      \"description\": \"Executar o bloco usando o Chrome DevTools Protocol\"\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "src/locales/pt-BR/common.json",
    "content": "{\n  \"common\": {\n    \"dashboard\": \"Painel\",\n    \"workflow\": \"Workflow | Workflows\",\n    \"collection\": \"Coleção | Coleções\",\n    \"log\": \"Registro | Registros\",\n    \"block\": \"Bloco | Blocos\",\n    \"schedule\": \"Agendamento\",\n    \"folder\": \"Pasta | Pastas\",\n    \"new\": \"Novo\",\n    \"docs\": \"Documentação\",\n    \"search\": \"Pesquisar\",\n    \"example\": \"Exemplo | Exemplos\",\n    \"import\": \"Importar\",\n    \"export\": \"Exportar\",\n    \"rename\": \"Renomear\",\n    \"execute\": \"Executar\",\n    \"delete\": \"Excluir\",\n    \"cancel\": \"Cancelar\",\n    \"settings\": \"Configurações\",\n    \"options\": \"Opções\",\n    \"confirm\": \"Confirmar\",\n    \"name\": \"Nome\",\n    \"all\": \"Todos\",\n    \"add\": \"Adicionar\",\n    \"save\": \"Salvar\",\n    \"data\": \"dados\",\n    \"stop\": \"Parar\",\n    \"sheet\": \"Planilha\",\n    \"pause\": \"Pausar\",\n    \"resume\": \"Retomar\",\n    \"action\": \"Ação | Ações\",\n    \"packages\": \"Pacotes\",\n    \"storage\": \"Armazenamento\",\n    \"editor\": \"Editor\",\n    \"running\": \"Executando\",\n    \"globalData\": \"Dados globais\",\n    \"fileName\": \"Nome do arquivo\",\n    \"description\": \"Descrição\",\n    \"disable\": \"Desativar\",\n    \"disabled\": \"Desativado\",\n    \"enable\": \"Ativar\",\n    \"fallback\": \"Fallback\",\n    \"update\": \"Atualizar\",\n    \"feature\": \"Recurso\",\n    \"duplicate\": \"Duplicar\",\n    \"password\": \"Senha\",\n    \"category\": \"Categoria\",\n    \"optional\": \"Opcional\",\n    \"0disable\": \"0 para desativar\",\n    \"millisecond\": \"milissegundo | milissegundos\"\n  },\n  \"message\": {\n    \"noBlock\": \"Nenhum bloco\",\n    \"noData\": \"Nenhum dado para exibir\",\n    \"noTriggerBlock\": \"Não foi possível encontrar um bloco de gatilho\",\n    \"useDynamicData\": \"Saiba como adicionar dados dinâmicos\",\n    \"delete\": \"Tem certeza de que deseja excluir \\\"{name}\\\"?\",\n    \"empty\": \"Ops... Parece que você não tem nenhum item\",\n    \"maxSizeExceeded\": \"O tamanho do arquivo excedeu o máximo permitido\",\n    \"notSaved\": \"Você realmente deseja sair? Você tem alterações não salvas!\",\n    \"somethingWrong\": \"Algo deu errado\",\n    \"limitExceeded\": \"Você excedeu o limite\"\n  },\n  \"sort\": {\n    \"sortBy\": \"Ordenar por\",\n    \"name\": \"Nome\",\n    \"createdAt\": \"Data de criação\",\n    \"updatedAt\": \"Última atualização\",\n    \"mostUsed\": \"Mais usados\"\n  },\n  \"logStatus\": {\n    \"stopped\": \"parado\",\n    \"error\": \"erro\",\n    \"success\": \"sucesso\"\n  }\n}\n"
  },
  {
    "path": "src/locales/pt-BR/newtab.json",
    "content": "{\n  \"home\": {\n    \"viewAll\": \"Ver tudo\",\n    \"communities\": \"Comunidades\"\n  },\n  \"welcome\": {\n    \"title\": \"Bem-vindo ao Automa! 🎉\",\n    \"text\": \"Comece lendo a documentação ou navegando pelos Workflows no Automa Marketplace.\",\n    \"marketplace\": \"Marketplace\"\n  },\n  \"packages\": {\n    \"name\": \"Pacote | Pacotes\",\n    \"add\": \"Adicionar pacote\",\n    \"icon\": \"Ícone do pacote\",\n    \"open\": \"Abrir pacotes\",\n    \"new\": \"Novo pacote\",\n    \"import\": \"Importar pacote\",\n    \"set\": \"Definir como pacote\",\n    \"settings\": {\n      \"asBlock\": \"Definir pacote como bloco\"\n    },\n    \"categories\": {\n      \"my\": \"Meus pacotes\",\n      \"installed\": \"Pacotes instalados\"\n    }\n  },\n  \"scheduledWorkflow\": {\n    \"title\": \"Workflows agendados\",\n    \"nextRun\": \"Próxima execução\",\n    \"active\": \"Ativo\",\n    \"refresh\": \"Atualizar\",\n    \"schedule\": {\n      \"title\": \"Agendamento\",\n      \"types\": {\n        \"everyDay\": \"Todos os dias\",\n        \"general\": \"A cada {time}\",\n        \"interval\": \"A cada {time} minutos\"\n      }\n    }\n  },\n  \"storage\": {\n    \"title\": \"Armazenamento\",\n    \"table\": {\n      \"add\": \"Adicionar tabela\",\n      \"edit\": \"Editar tabela\",\n      \"createdAt\": \"Criado em\",\n      \"modifiedAt\": \"Modificado em\",\n      \"rowsCount\": \"Número de linhas\",\n      \"delete\": \"Excluir tabela\"\n    }\n  },\n  \"credential\": {\n    \"title\": \"Credencial | Credenciais\",\n    \"add\": \"Adicionar credencial\",\n    \"use\": {\n      \"title\": \"Credenciais utilizadas\",\n      \"description\": \"Este Workflow utiliza estas credenciais\"\n    }\n  },\n  \"workflowPermissions\": {\n    \"title\": \"Permissões do Workflow\",\n    \"description\": \"Este Workflow requer estas permissões para funcionar corretamente\",\n    \"contextMenus\": {\n      \"title\": \"Menu de contexto\",\n      \"description\": \"Para executar o Workflow através do menu de contexto\"\n    },\n    \"clipboardRead\": {\n      \"title\": \"Área de transferência\",\n      \"description\": \"Para acessar os dados da área de transferência\"\n    },\n    \"notifications\": {\n      \"title\": \"Notificação\",\n      \"description\": \"Para exibir uma notificação\"\n    },\n    \"downloads\": {\n      \"title\": \"Baixar\",\n      \"description\": \"Salvar os recursos da página e renomear o arquivo baixado\"\n    },\n    \"cookies\": {\n      \"title\": \"Cookies\",\n      \"description\": \"Ler, definir ou remover cookies\"\n    }\n  },\n  \"updateMessage\": {\n    \"text1\": \"O Automa foi atualizado para a v{version},\",\n    \"text2\": \"veja o que há de novo.\"\n  },\n  \"workflows\": {\n    \"folder\": {\n      \"new\": \"Nova pasta\",\n      \"name\": \"Nome da pasta\",\n      \"delete\": \"Excluir pasta\",\n      \"rename\": \"Renomear pasta\"\n    }\n  },\n  \"auth\": {\n    \"title\": \"Autenticação\",\n    \"signIn\": \"Entrar\",\n    \"username\": \"Você precisa definir seu nome de usuário primeiro\",\n    \"clickHere\": \"Clique aqui\",\n    \"text\": \"Você precisa estar conectado antes de poder fazer isso\"\n  },\n  \"running\": {\n    \"start\": \"Iniciado em {date}\",\n    \"message\": \"Isso exibe apenas os últimos 5 registros\"\n  },\n  \"settings\": {\n    \"theme\": \"Tema\",\n    \"shortcuts\": {\n      \"duplicate\": \"Atalho já utilizado por \\\"{name}\\\"\"\n    },\n    \"editor\": {\n      \"title\": \"Título\",\n      \"curvature\": {\n        \"title\": \"Curvatura da linha\",\n        \"line\": \"Linha\",\n        \"reroute\": \"Redirecionar\",\n        \"rerouteFirstLast\": \"Redirecionar primeiro e último ponto\"\n      },\n      \"arrow\": {\n        \"title\": \"Seta da linha\",\n        \"description\": \"Adicionar uma seta ao final da linha\"\n      },\n      \"snapGrid\": {\n        \"title\": \"Ajustar à grade\",\n        \"description\": \"Ajustar à grade ao mover um bloco\"\n      },\n      \"saveWhenExecute\": {\n        \"title\": \"Salvar automaticamente ao executar o Workflow\",\n        \"description\": \"As alterações do Workflow serão salvas ao executar o Workflow\"\n      }\n    },\n    \"deleteLog\": {\n      \"title\": \"Excluir automaticamente os registros do Workflow\",\n      \"after\": \"Excluir após\",\n      \"deleteAfter\": {\n        \"never\": \"Nunca\",\n        \"days\": \"{day} dias\"\n      }\n    },\n    \"language\": {\n      \"label\": \"Idioma\",\n      \"helpTranslate\": \"Não encontrou seu idioma? Ajude a traduzir.\",\n      \"reloadPage\": \"Recarregue a página para que a alteração tenha efeito\"\n    },\n    \"menu\": {\n      \"backup\": \"Backup Workflows\",\n      \"editor\": \"Editor\",\n      \"general\": \"Geral\",\n      \"shortcuts\": \"Atalhos\",\n      \"about\": \"Sobre\"\n    },\n    \"backupWorkflows\": {\n      \"title\": \"Backup Local\",\n      \"invalidPassword\": \"Senha inválida\",\n      \"workflowsAdded\": \"{count} Workflows foram adicionados\",\n      \"name\": \"Backup Workflow\",\n      \"needSignin\": \"Você precisa entrar primeiro\",\n      \"backup\": {\n        \"button\": \"Backup\",\n        \"settings\": \"Configurações de Backup\",\n        \"encrypt\": \"Criptografar com senha\",\n        \"schedule\": \"Agendar backup local\"\n      },\n      \"restore\": {\n        \"title\": \"Restaurar Workflows\",\n        \"button\": \"Restaurar\",\n        \"update\": \"Atualizar se o Workflow existir\"\n      },\n      \"cloud\": {\n        \"buttons\": {\n          \"local\": \"Local\",\n          \"cloud\": \"Nuvem\"\n        },\n        \"location\": \"Localização\",\n        \"delete\": \"Excluir backup\",\n        \"title\": \"Backup na Nuvem\",\n        \"sync\": \"Sincronizar\",\n        \"lastSync\": \"Última sincronização\",\n        \"lastBackup\": \"Último backup\",\n        \"select\": \"Selecionar Workflows\",\n        \"storedWorkflows\": \"Workflows armazenados na nuvem\",\n        \"selected\": \"Selecionados\",\n        \"selectText\": \"Selecione os Workflows que você deseja fazer backup\",\n        \"selectAll\": \"Selecionar todos\",\n        \"deselectAll\": \"Desselecionar todos\",\n        \"needSelectWorkflow\": \"Você precisa selecionar os Workflows que deseja fazer backup\"\n      }\n    }\n  },\n  \"workflow\": {\n    \"events\": {\n      \"title\": \"Eventos do Workflow\",\n      \"add-action\": \"Adicionar ação\",\n      \"description\": \"Executar ações quando o evento ocorrer.\",\n      \"event\": \"Evento | Eventos\",\n      \"action\": \"Ação\",\n      \"actions\": {\n        \"js-code\": {\n          \"title\": \"Executar código JS\"\n        },\n        \"http-request\": {\n          \"title\": \"Requisição HTTP\"\n        }\n      },\n      \"types\": {\n    \"finish:success\": {\n      \"name\": \"Finalizar (sucesso)\",\n      \"description\": \"Execução do Workflow finalizada com sucesso\"\n    },\n    \"finish:failed\": {\n      \"name\": \"Finalizar (falha)\",\n      \"description\": \"Execução do Workflow finalizada com erro\"\n     }\n    }\n  },\n  \"previewMode\": {\n    \"title\": \"Modo de pré-visualização\",\n    \"description\": \"Você está no modo de pré-visualização, as alterações feitas não serão salvas\"\n  },\n  \"pinWorkflow\": {\n    \"pin\": \"Fixar Workflow\",\n    \"unpin\": \"Desfixar Workflow\",\n    \"pinned\": \"Workflows fixados\"\n  },\n  \"parameters\": {\n    \"add\": \"Adicionar parâmetro\",\n    \"preferInTab\": \"Preferir parâmetros de entrada na aba\"\n  },\n  \"my\": \"Meus Workflows\",\n  \"testing\": {\n    \"title\": \"Modo de teste\",\n    \"nextBlock\": \"Próximo bloco\",\n    \"startRun\": \"Iniciar execução em\",\n    \"disabled\": \"Salve as alterações primeiro\"\n  },\n  \"import\": \"Importar Workflow\",\n  \"new\": \"Novo Workflow\",\n  \"delete\": \"Excluir Workflow\",\n  \"browse\": \"Navegar pelos Workflows\",\n  \"name\": \"Nome do Workflow\",\n  \"rename\": \"Renomear Workflow\",\n  \"backupCloud\": \"Backup do Workflow para a nuvem\",\n  \"add\": \"Adicionar Workflow\",\n  \"clickToEnable\": \"Clique para habilitar\",\n  \"toggleSidebar\": \"Alternar barra lateral\",\n  \"cantEdit\": \"Não é possível editar Workflow compartilhado\",\n  \"undo\": \"Desfazer\",\n  \"redo\": \"Refazer\",\n  \"autoAlign\": {\n    \"title\": \"Alinhar automaticamente\"\n  },\n  \"blocksFolder\": {\n    \"title\": \"Pasta de Blocos\",\n    \"add\": \"Adicionar blocos à pasta\",\n    \"save\": \"Salvar na pasta\"\n  },\n  \"searchBlocks\": {\n    \"title\": \"Pesquisar blocos no editor\"\n  },\n  \"conditionBuilder\": {\n    \"title\": \"Construtor de condições\",\n    \"add\": \"Adicionar condição\",\n    \"and\": \"E\",\n    \"or\": \"OU\",\n    \"topAwait\": \"Suporta await em nível superior e a função \\\"automaRefData\\\"\"\n  },\n  \"host\": {\n    \"title\": \"Host Workflow\",\n    \"set\": \"Definir como Host Workflow\",\n    \"id\": \"ID do Host\",\n    \"add\": \"Adicionar Host Workflow\",\n    \"sync\": {\n      \"title\": \"Sincronizar\",\n      \"description\": \"Sincronizar com Host Workflow\"\n    },\n    \"messages\": {\n      \"hostExist\": \"Você já adicionou este host\",\n      \"notFound\": \"Não foi possível encontrar um Host Workflow com o ID \\\"{id}\\\"\"\n    }\n  },\n  \"type\": {\n    \"local\": \"Local\",\n    \"shared\": \"Compartilhado\",\n    \"host\": \"Host\"\n  },\n  \"unpublish\": {\n    \"title\": \"Despublicar Workflow\",\n    \"button\": \"Despublicar\",\n    \"body\": \"Tem certeza de que deseja despublicar o Workflow \\\"{name}\\\"?\"\n  },\n  \"share\": {\n    \"url\": \"Compartilhar URL\",\n    \"publish\": \"Publicar\",\n    \"sharedAs\": \"Compartilhado como \\\"{name}\\\"\",\n    \"title\": \"Compartilhar Workflow\",\n    \"download\": \"Salvar Workflow localmente\",\n    \"edit\": \"Editar descrição\",\n    \"fetchLocal\": \"Buscar Workflow local\",\n    \"update\": \"Atualizar\",\n    \"unpublish\": \"Despublicar\"\n  },\n  \"variables\": {\n    \"title\": \"Variável | Variáveis\",\n    \"name\": \"Nome da variável\",\n    \"assign\": \"Atribuir à variável\"\n    },\n    \"protect\": {\n      \"title\": \"Proteger workflow\",\n      \"remove\": \"Remover proteção\",\n      \"button\": \"Proteger\",\n      \"note\": \"Observação: esta senha será necessária posteriormente para editar ou excluir o workflow.\"\n    },\n    \"locked\": {\n      \"title\": \"Este workflow está protegido\",\n      \"body\": \"Digite a senha para desbloqueá-lo\",\n      \"unlock\": \"Desbloquear\",\n      \"messages\": {\n        \"incorrect-password\": \"Senha incorreta\"\n      }\n    },\n    \"state\": {\n      \"executeBy\": \"Executado por: \\\"{name}\\\"\"\n    },\n    \"table\": {\n      \"title\": \"Tabela | Tabelas\",\n      \"placeholder\": \"Pesquisar ou adicionar uma coluna\",\n      \"select\": \"Selecionar coluna\",\n      \"column\": {\n        \"name\": \"Nome da coluna\",\n        \"type\": \"Tipo de dado\"\n      }\n    },\n    \"sidebar\": {\n      \"workflowIcon\": \"Ícone do Workflow\"\n    },\n    \"editor\": {\n      \"zoomIn\": \"Ampliar\",\n      \"zoomOut\": \"Reduzir zoom\",\n      \"resetZoom\": \"Restaurar zoom\",\n      \"duplicate\": \"Duplicar\",\n      \"copy\": \"Copiar\",\n      \"paste\": \"Colar\",\n      \"group\": \"Agrupar blocos\",\n      \"ungroup\": \"Desagrupar blocos\"\n    },\n    \"settings\": {\n      \"saveLog\": \"Salvar registro do workflow\",\n      \"executedBlockOnWeb\": \"Mostrar bloco executado na página web\",\n      \"notification\": {\n        \"title\": \"Notificação do workflow\",\n        \"description\": \"Mostrar status do workflow (sucesso ou falha) após sua execução\",\n        \"noPermission\": \"Esta opção requer a permissão \\\"notifications\\\" para funcionar\"\n      },\n      \"publicId\": {\n        \"title\": \"ID público do workflow\",\n        \"description\": \"Defina um ID público para executar o workflow via um evento customizado JavaScript\"\n      },\n      \"defaultColumn\": {\n        \"title\": \"Inserir na coluna padrão\",\n        \"description\": \"Inserir dados na coluna padrão se nenhuma coluna estiver selecionada no bloco\",\n        \"name\": \"Nome da coluna padrão\"\n      },\n      \"autocomplete\": {\n        \"title\": \"Autocompletar\",\n        \"description\": \"Ativar autocompletar no bloco de entrada (desative se causar instabilidade no Automa)\"\n      },\n      \"clearCache\": {\n        \"title\": \"Limpar cache\",\n        \"description\": \"Limpar cache (estado e índice de loop) do workflow\",\n        \"info\": \"Cache do workflow limpo com sucesso\",\n        \"btn\": \"Limpar\"\n      },\n      \"reuseLastState\": {\n        \"title\": \"Reutilizar o estado do último workflow\",\n        \"description\": \"Utilize os dados de estado (tabela, variáveis e dados globais) do último workflow executado\"\n      },\n      \"debugMode\": {\n        \"title\": \"Modo de depuração\",\n        \"description\": \"Execute o workflow usando o protocolo do Chrome DevTools\"\n      },\n      \"restartWorkflow\": {\n        \"for\": \"Reiniciar por\",\n        \"times\": \"vezes\",\n        \"description\": \"Número máximo de vezes que o workflow será reiniciado\"\n      },\n      \"onError\": {\n        \"title\": \"Em caso de erro no workflow\",\n        \"description\": \"Defina a ação a ser tomada se ocorrer um erro no workflow\",\n        \"items\": {\n          \"keepRunning\": \"Continuar executando\",\n          \"stopWorkflow\": \"Parar workflow\",\n          \"restartWorkflow\": \"Reiniciar workflow\"\n        }\n      },\n      \"timeout\": {\n        \"title\": \"Tempo limite do workflow (milissegundos)\"\n      },\n      \"blockDelay\": {\n        \"title\": \"Atraso do bloco (milissegundos)\",\n        \"description\": \"Adicionar atraso antes de executar cada um dos blocos\"\n      },\n      \"tabLoadTimeout\": {\n        \"title\": \"Tempo limite de carregamento da aba\",\n        \"description\": \"Tempo máximo para carregar uma aba em milissegundos, insira 0 para desativar o tempo limite\"\n      }\n    }\n  },\n  \"collection\": {\n    \"description\": \"Execute seus Workflows em sequência\",\n    \"new\": \"Nova coleção\",\n    \"delete\": \"Excluir coleção\",\n    \"add\": \"Adicionar coleção\",\n    \"rename\": \"Renomear coleção\",\n    \"flow\": \"Fluxo\",\n    \"dragDropText\": \"Arraste e solte um Workflow ou bloco aqui\",\n    \"options\": {\n      \"atOnce\": {\n        \"title\": \"Execute todos os Workflows da coleção de uma vez\",\n        \"description\": \"Os blocos não serão executados quando esta opção for utilizada\"\n      }\n    },\n    \"globalData\": {\n      \"note\": \"Isso irá sobrescrever os dados globais do Workflow\"\n    }\n  },\n  \"log\": {\n    \"flowId\": \"ID do Fluxo\",\n    \"goBack\": \"Voltar para os registros de \\\"{name}\\\"\",\n    \"goWorkflow\": \"Ir para o Workflow\",\n    \"startedDate\": \"Data de início\",\n    \"duration\": \"Duração\",\n    \"selectAll\": \"Selecionar tudo\",\n    \"deselectAll\": \"Desselecionar tudo\",\n    \"deleteSelected\": \"Excluir registros selecionados\",\n    \"clearLogs\": {\n      \"title\": \"Limpar registros\",\n      \"description\": \"Tem certeza de que deseja limpar todos os registros?\"\n    },\n    \"types\": {\n      \"stop\": \"Workflow está parado\",\n      \"finish\": \"Finalizar\"\n    },\n    \"messages\": {\n      \"url-empty\": \"A URL está vazia\",\n      \"invalid-url\": \"A URL não é válida\",\n      \"conditions-empty\": \"As condições estão vazias\",\n      \"invalid-proxy-host\": \"Host do proxy inválido\",\n      \"workflow-disabled\": \"O Workflow está desativado\",\n      \"selector-empty\": \"O seletor de elemento está vazio\",\n      \"invalid-body\": \"O corpo do conteúdo não é um JSON válido\",\n      \"invalid-active-tab\": \"\\\"{url}\\\" é uma URL inválida\",\n      \"empty-spreadsheet-id\": \"O ID da planilha está vazio\",\n      \"invalid-loop-data\": \"Dados inválidos para iterar\",\n      \"empty-workflow\": \"Você deve selecionar um Workflow primeiro\",\n      \"active-tab-removed\": \"A aba ativa do Workflow foi removida\",\n      \"empty-spreadsheet-range\": \"O intervalo da planilha está vazio\",\n      \"stop-timeout\": \"O Workflow foi interrompido devido ao tempo limite\",\n      \"no-file-access\": \"O Automa não tem acesso ao arquivo\",\n      \"no-workflow\": \"Não foi possível encontrar um Workflow com o ID \\\"{workflowId}\\\"\",\n      \"no-match-tab\": \"Não foi possível encontrar uma aba que corresponda ao padrão \\\"{pattern}\\\"\",\n      \"no-clipboard-acces\": \"Sem permissão para acessar a área de transferência\",\n      \"browser-not-supported\": \"Este recurso não é suportado no navegador {browser}\",\n      \"element-not-found\": \"Não foi possível encontrar um elemento com o seletor \\\"{selector}\\\"\",\n      \"no-permission\": \"Sem permissão \\\"{permission}\\\" para realizar esta ação\",\n      \"not-iframe\": \"O elemento com o seletor \\\"{selector}\\\" não é um elemento iframe\",\n      \"iframe-not-found\": \"Não foi possível encontrar um elemento iframe com o seletor \\\"{selector}\\\"\",\n      \"workflow-infinite-loop\": \"Não é possível executar o Workflow para evitar um loop infinito\",\n      \"not-debug-mode\": \"O Workflow deve ser executado em modo de depuração para que este bloco funcione corretamente\",\n      \"no-iframe-id\": \"Não foi possível encontrar o ID do Frame para o elemento iframe com o seletor \\\"{selector}\\\"\",\n      \"no-tab\": \"Não foi possível conectar a uma aba, use o bloco \\\"Nova aba\\\" ou \\\"Aba ativa\\\" antes de usar o bloco \\\"{name}\\\"\"\n    },\n    \"description\": {\n    \"text\": \"{status} em {date} durante {duration}\",\n    \"status\": {\n      \"success\": \"Concluído\",\n      \"error\": \"Falhou\",\n      \"stopped\": \"Parou\"\n    }\n  },\n  \"delete\": {\n    \"title\": \"Excluir registro\",\n    \"description\": \"Tem certeza de que deseja excluir todos os registros selecionados?\"\n  },\n  \"exportData\": {\n    \"title\": \"Exportar dados\",\n    \"types\": {\n      \"json\": \"JSON\",\n      \"csv\": \"CSV\",\n      \"plain-text\": \"Texto simples\"\n    }\n  },\n  \"filter\": {\n    \"title\": \"Filtro\",\n    \"byStatus\": \"Por status\",\n    \"byDate\": {\n      \"title\": \"Por data\",\n      \"items\": {\n        \"lastDay\": \"Último dia\",\n        \"last7Days\": \"Últimos sete dias\",\n        \"last30Days\": \"Últimos 30 dias\"\n      }\n    }\n  }\n},\n  \"components\": {\n    \"pagination\": {\n      \"text1\": \"Mostrando\",\n      \"text2\": \"itens de {count}\",\n      \"nextPage\": \"Próxima página\",\n      \"currentPage\": \"Página atual\",\n      \"prevPage\": \"Página anterior\",\n      \"of\": \"de {page}\"\n    }\n  }\n}\n"
  },
  {
    "path": "src/locales/pt-BR/popup.json",
    "content": "{\n  \"recording\": {\n    \"stop\": \"Parar gravação\",\n    \"title\": \"Gravando\"\n  },\n  \"home\": {\n    \"record\": {\n      \"title\": \"Gravar Workflow\",\n      \"button\": \"Gravar\",\n      \"name\": \"Nome do Workflow\",\n      \"selectBlock\": \"Selecione um bloco para iniciar\",\n      \"anotherBlock\": \"Não é possível iniciar a partir deste bloco\",\n      \"tabs\": {\n        \"new\": \"Novo Workflow\",\n        \"existing\": \"Workflow existente\"\n      }\n    },\n    \"elementSelector\": {\n      \"name\": \"Seletor de elemento\",\n      \"noAccess\": \"Não tem acesso a este site\"\n    },\n    \"workflow\": {\n      \"new\": \"Novo Workflow\",\n      \"rename\": \"Renomear Workflow\",\n      \"delete\": \"Excluir Workflow\",\n      \"type\": {\n        \"host\": \"Host\",\n        \"local\": \"Local\"\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "src/locales/tr/blocks.json",
    "content": "{\n  \"collection\": {\n    \"blocks\": {\n      \"export-result\": {\n        \"name\": \"Sonucu Dışa Aktar\",\n        \"description\": \"Koleksiyon sonucunu JSON olarak dışa aktar\"\n      }\n    }\n  },\n  \"workflow\": {\n    \"blocks\": {\n      \"base\": {\n        \"title\": \"Bloklar\",\n        \"moveToGroup\": \"Bloğu bloklar grubuna taşı\",\n        \"selector\": \"Öğe seçici\",\n        \"selectorOptions\": \"Seçici seçenekleri\",\n        \"timeout\": \"Zaman aşımı (milisaniye)\",\n        \"noPermission\": \"Automa, bu işlemi gerçekleştirmek için yeterli izne sahip değil\",\n        \"grantPermission\": \"İzni ver\",\n        \"action\": \"Eylem\",\n        \"element\": {\n          \"select\": \"Bir Öğe seç\",\n          \"verify\": \"Seçiciyi doğrula\"\n        },\n        \"settings\": {\n          \"title\": \"Bloğun Ayarları\",\n          \"blockTimeout\": {\n            \"title\": \"Bloğun yürütme zaman aşımı (milisaniye)\",\n            \"description\": \"Bloğun maksimum yürütme süresi (0 devre dışı bırakmak için)\"\n          },\n          \"line\": {\n            \"title\": \"Satırlar\",\n            \"label\": \"Etiket\",\n            \"animated\": \"Animasyonlu\",\n            \"select\": \"Satırı seç\",\n            \"to\": \"{name} bloğuna giden satır\",\n            \"lineColor\": \"Renk\"\n          }\n        },\n        \"toggle\": {\n          \"enable\": \"Bloğu etkinleştir\",\n          \"disable\": \"Bloğu devre dışı bırak\"\n        },\n        \"onError\": {\n          \"info\": \"Bu kurallar, blokta bir hata oluştuğunda uygulanacaktır\",\n          \"button\": \"Hata durumunda\",\n          \"title\": \"Hata oluştuğunda\",\n          \"retry\": \"Eylemi tekrar dene\",\n          \"fallbackTitle\": \"Blokte hata oluştuğunda çalıştırılacak\",\n          \"times\": {\n            \"name\": \"Kez\",\n            \"description\": \"Eylemi tekrar denemek için kaç kez\"\n          },\n          \"interval\": {\n            \"name\": \"Aralık\",\n            \"description\": \"Her deneme arasında beklenen süre\",\n            \"second\": \"saniye\"\n          },\n          \"toDo\": {\n            \"error\": \"Hata fırlat\",\n            \"continue\": \"Akışa devam et\",\n            \"fallback\": \"Alternatif çalıştır\",\n            \"restart\": \"Akışı yeniden başlat\"\n          },\n          \"insertData\": {\n            \"name\": \"Veri ekle\"\n          }\n        },\n        \"table\": {\n          \"checkbox\": \"Tabloya ekle\",\n          \"select\": \"Sütunu seç\",\n          \"extraRow\": {\n            \"checkbox\": \"Ek satır ekle\",\n            \"placeholder\": \"Değer\",\n            \"title\": \"Ek satırın değeri\"\n          }\n        },\n        \"findElement\": {\n          \"placeholder\": \"Öğeyi şunun ile bul\",\n          \"options\": {\n            \"cssSelector\": \"CSS Seçici\",\n            \"xpath\": \"XPath\"\n          }\n        },\n        \"markElement\": {\n          \"title\": \"Daha önce seçilmişse bir Öğe seçilmeyecek\",\n          \"text\": \"Öğe işaretle\"\n        },\n        \"multiple\": {\n          \"title\": \"Birden çok öğe seç\",\n          \"text\": \"Çoklu\"\n        },\n        \"waitSelector\": {\n          \"title\": \"Seçiciyi bekle\",\n          \"timeout\": \"Seçici zaman aşımı (ms)\"\n        },\n        \"downloads\": {\n          \"onConflict\": {\n            \"uniquify\": \"Benzersiz hale getir\",\n            \"overwrite\": \"Üzerine yaz\",\n            \"prompt\": \"Sorma\"\n          }\n        }\n      },\n      \"wait-connections\": {\n        \"name\": \"Bağlantıları Bekle\",\n        \"description\": \"Devam etmeden önce tüm bağlantıları bekleyin\",\n        \"specificFlow\": \"Yalnızca belirli bir akışa devam et\",\n        \"selectFlow\": \"Akışı seç\"\n      },\n      \"cookie\": {\n        \"name\": \"Çerez\",\n        \"description\": \"Çerezleri al, ayarla veya kaldır\",\n        \"types\": {\n          \"get\": \"Çerezleri al\",\n          \"set\": \"Çerez ayarla\",\n          \"remove\": \"Çerezleri kaldır\",\n          \"getAll\": \"Tüm çerezleri al\"\n        },\n        \"useJson\": \"JSON formatını kullan\"\n      },\n      \"note\": {\n        \"name\": \"Not\"\n      },\n      \"slice-variable\": {\n        \"name\": \"Değişkeni Dilimle\",\n        \"description\": \"Bir değişken değerinin bir bölümünü çıkarır\",\n        \"start\": \"Başlangıç dizini\",\n        \"end\": \"Bitiş dizini\"\n      },\n      \"workflow-state\": {\n        \"name\": \"İş Akışı Durumu\",\n        \"description\": \"İş akışı durumlarını yönet\",\n        \"actions\": {\n          \"stop\": \"İş akışlarını durdur\"\n        }\n      },\n      \"regex-variable\": {\n        \"name\": \"RegEx Değişkeni\",\n        \"description\": \"Bir değişken değerini bir düzenli ifadeye karşı eşle\"\n      },\n      \"data-mapping\": {\n        \"source\": \"Kaynak\",\n        \"destination\": \"Hedef\",\n        \"name\": \"Veri Eşleme\",\n        \"edit\": \"Veri haritasını düzenle\",\n        \"dataSource\": \"Veri kaynağı\",\n        \"description\": \"Bir değişkenin veya tablonun verisini eşle\",\n        \"addSource\": \"Kaynak ekle\",\n        \"addDestination\": \"Hedef ekle\"\n      },\n      \"sort-data\": {\n          \"name\": \"Veriyi Sırala\",\n          \"description\": \"Veri öğelerini sırala\",\n          \"property\": \"Öğenin özelliğine göre sırala\",\n          \"addProperty\": \"Özellik ekle\"\n        },\n        \"increase-variable\": {\n          \"name\": \"Değişkeni Artır\",\n          \"description\": \"Bir değişkenin değerini belirli bir miktar artır\",\n          \"increase\": \"Artış miktarı\"\n        },\n        \"notification\": {\n          \"name\": \"Bildirim\",\n          \"description\": \"Bir bildirim görüntüle\",\n          \"title\": \"Başlık\",\n          \"message\": \"Mesaj\",\n          \"imageUrl\": \"Resim URL'si (isteğe bağlı)\",\n          \"iconUrl\": \"Simge URL'si (isteğe bağlı)\"\n        },\n        \"delete-data\": {\n          \"name\": \"Veriyi Sil\",\n          \"description\": \"Tablo veya değişken verisini sil\",\n          \"from\": \"Şu veriden\",\n          \"allColumns\": \"[Tüm sütunlar]\"\n        },\n        \"log-data\": {\n          \"name\": \"Günlük Veriyi Al\",\n          \"description\": \"Bir iş akışının en son günlük verisini al\",\n          \"data\": \"Günlük veri\"\n        },\n        \"tab-url\": {\n          \"name\": \"Sekme URL'sini Al\",\n          \"description\": \"Sekmenin URL'sini al\",\n          \"select\": \"Sekme seç\",\n          \"types\": {\n            \"active-tab\": \"Aktif sekme\",\n            \"all\": \"Tüm sekmeler\"\n          },\n          \"query\": {\n            \"title\": \"Sorgu\",\n            \"matchPatterns\": \"@:workflow.blocks.switch-tab.matchPattern (isteğe bağlı)\",\n            \"tabTitle\": \"Sekme başlığı (isteğe bağlı)\"\n          }\n        },\n        \"reload-tab\": {\n          \"name\": \"Sekmeyi Yeniden Yükle\",\n          \"description\": \"Aktif sekme üzerindeki yeniden yükle\"\n        },\n        \"press-key\": {\n          \"name\": \"Tuşa Bas\",\n          \"description\": \"Bir tuşa veya kombinasyona bas\",\n          \"target\": \"Hedef element (isteğe bağlı)\",\n          \"key\": \"Tuş\",\n          \"detect\": \"Tuşu Algıla\",\n          \"actions\": {\n            \"press-key\": \"Tuşa bas\",\n            \"multiple-keys\": \"Birden çok tuşa bas\"\n          },\n          \"press-time\": \"Basma süresi (milisaniye)\"\n        },\n        \"save-assets\": {\n          \"name\": \"Varlıkları Kaydet\",\n          \"description\": \"Bir öğeden veya URL'den varlıkları (resim, video, ses veya dosya) kaydet\",\n          \"filename\": \"Dosya adı (isteğe bağlı)\",\n          \"saveDownloadIds\": \"Öğelerin indirme kimliklerini kaydet\",\n          \"contentTypes\": {\n            \"title\": \"Tür\",\n            \"element\": \"Medya öğesi (resim, ses veya video)\",\n            \"url\": \"URL\"\n          }\n        },\n        \"handle-dialog\": {\n          \"name\": \"Diyalogu İşle\",\n          \"description\": \"JavaScript başlatılmış bir iletişim kutusunu (uyarı, onay, prompt veya onbeforeunload) kabul eder veya reddeder\",\n          \"accept\": \"Diyalogu Kabul Et\",\n          \"promptText\": {\n            \"label\": \"Prompt metni (isteğe bağlı)\",\n            \"description\": \"Kabul etmeden önce prompt ile gireceğiniz metin\"\n          }\n        },\n        \"handle-download\": {\n          \"name\": \"İndirmeyi İşle\",\n          \"description\": \"İndirilen dosyayı işle\",\n          \"timeout\": \"Zaman aşımı (milisaniye)\",\n          \"noPermission\": \"İndirmelere erişim izni yok\",\n          \"onConflict\": \"Çakışma durumunda\",\n          \"waitFile\": \"Dosyanın indirilmesini bekle\",\n          \"downloadId\": \"Dosya indirme kimliği (isteğe bağlı)\",\n          \"filePath\": \"Dosya yolu\"\n        },\n        \"insert-data\": {\n          \"name\": \"Veri Ekleyin\",\n          \"description\": \"Veriyi tabloya veya değişkene ekleyin\"\n        },\n        \"clipboard\": {\n          \"name\": \"Pano\",\n          \"description\": \"Panodan kopyalanan metni al\",\n          \"data\": \"Pano verisi\",\n          \"noPermission\": \"Panoya erişim izni yok\",\n          \"grantPermission\": \"İzni ver\",\n          \"copySelection\": \"Sayfadaki seçilen metni kopyala\",\n          \"types\": {\n            \"get\": \"Panodan veri al\",\n            \"insert\": \"Metni panoya ekle\"\n          }\n        },\n        \"hover-element\": {\n          \"name\": \"Öğenin Üzerine Gel\",\n          \"description\": \"Bir öğenin üzerine gel\"\n        },\n        \"create-element\": {\n          \"name\": \"Öğe Oluştur\",\n          \"description\": \"Bir öğe oluşturun ve sayfaya ekleyin\",\n          \"edit\": \"Öğeyi Düzenle\",\n          \"wrap\": \"Öğeyi içine al\",\n          \"insertEl\": {\n            \"title\": \"Öğe Ekle\",\n            \"items\": {\n              \"before\": \"İlk çocuk olarak\",\n              \"after\": \"Son çocuk olarak\",\n              \"next-sibling\": \"Sonraki kardeş olarak\",\n              \"prev-sibling\": \"Önceki kardeş olarak\",\n              \"replace\": \"Hedef Öğeyi değiştir\"\n            }\n          }\n        },\n        \"upload-file\": {\n          \"name\": \"Dosya Yükle\",\n          \"description\": \"<input type=\\\"file\\\"> Öğesine dosya yükle\",\n          \"filePath\": \"URL veya Dosya yolu\",\n          \"addFile\": \"Dosya ekle\",\n          \"onlyURL\": \"Firefox tarayıcısında yalnızca URL'den dosya yükleme desteklenir\",\n          \"requirement\": \"Bu bloku kullanmadan önce gereksinimleri okuyun\",\n          \"noFileAccess\": \"Automa dosyalara erişim izni bulunmamaktadır\"\n      },\n      \"browser-event\": {\n        \"name\": \"Tarayıcı Olayı\",\n        \"description\": \"Belirtilen olay tetiklendiğinde bir sonraki bloğu yürütür\",\n        \"events\": \"Olaylar\",\n        \"timeout\": \"Zaman aşımı (milisaniye)\",\n        \"activeTabLoaded\": \"Aktif sekme\",\n        \"setAsActiveTab\": \"Aktif sekme olarak ayarla\"\n      },\n      \"blocks-group-2\": {\n        \"name\": \"@:workflow.blocks.blocks-group.name 2\",\n        \"description\": \"@:workflow.blocks.blocks-group.description\"\n      },\n      \"blocks-group\": {\n        \"name\": \"Bloklar Grubu\",\n        \"groupName\": \"Grup adı\",\n        \"description\": \"Blokları gruplama\",\n        \"dropText\": \"Bir bloğu buraya sürükleyin\",\n        \"cantAdd\": \"\\\"{blockName}\\\" bloğu gruba eklenemiyor\"\n      },\n      \"trigger\": {\n        \"name\": \"Tetikleyici\",\n        \"description\": \"İş akışının yürütmesine başlayacağı blok\",\n        \"addTime\": \"Zaman ekle\",\n        \"selectDay\": \"Gün seç\",\n        \"timeExist\": \"{day} günü saat {time}'de zaten bir tetikleyici eklediniz\",\n        \"fixedDelay\": \"Sabit gecikme\",\n        \"contextMenus\": {\n          \"noPermission\": \"Bu tetikleyici çalışması için \\\"contextMenus\\\" iznine ihtiyaç duyar\",\n          \"grantPermission\": \"İzni ver\",\n          \"appearIn\": \"Şurada görünecek\",\n          \"contextName\": \"Bağlam menüsündeki iş akışı adı\"\n        },\n        \"days\": [\n          \"Pazar\",\n          \"Pazartesi\",\n          \"Salı\",\n          \"Çarşamba\",\n          \"Perşembe\",\n          \"Cuma\",\n          \"Cumartesi\"\n        ],\n        \"useRegex\": \"Regex kullan\",\n        \"shortcut\": {\n          \"tooltip\": \"Kısayol kaydı\",\n          \"stopRecord\": \"Kaydı durdur\",\n          \"checkboxTitle\": \"Giriş öğesindeyken kısayolu çalıştır\",\n          \"checkbox\": \"Girişteyken etkin\",\n          \"note\": \"Not: Klavye kısayolu sadece bir web sayfasında olduğunuzda çalışır\"\n        },\n        \"forms\": {\n          \"triggerWorkflow\": \"İş akışını tetikle\",\n          \"interval\": \"Aralık (dakika)\",\n          \"delay\": \"Gecikme (dakika)\",\n          \"date\": \"Tarih\",\n          \"time\": \"Saat\",\n          \"url\": \"URL veya Regex\",\n          \"shortcut\": \"Kısayol\",\n          \"cron-expression\": \"Cron ifadesi\"\n        },\n        \"element-change\": {\n          \"target\": \"İzlenecek hedef öğe\",\n          \"optionsInfo\": \"İş akışını tetikleyecek öğe mutasyonu\",\n          \"targetWebsite\": \"Hedef öğenin bulunduğu web sitesinin Eşleşme Deseni (Daha fazla Eşleşme Deseni örneğini görmek için tıklayın)\",\n          \"baseEl\": {\n            \"title\": \"Temel öğe (isteğe bağlı)\",\n            \"description\": \"Bu öğe değiştiğinde Automa hedef öğeyi izlemeyi yeniden başlatır\"\n          },\n          \"subtree\": {\n            \"title\": \"Alt ağacı dahil et\",\n            \"description\": \"İzlemeyi hedef öğenin tüm alt ağacına genişlet\"\n          },\n          \"childList\": {\n            \"title\": \"Çocuk listesi\",\n            \"description\": \"Yeni çocuk öğelerin eklenmesini veya mevcut olanların kaldırılmasını izle\"\n          },\n          \"attributes\": {\n            \"title\": \"Özellikler\",\n            \"description\": \"Hedef öğenin özellik değerlerindeki değişiklikleri izle\"\n          },\n          \"attributeFilter\": {\n            \"title\": \"Özellik filtresi\",\n            \"separate\": \"Özellik adlarını ayırmak için virgül (,) kullanın\",\n            \"description\": \"Yalnızca belirli özellikleri izle (tümünü izlemek için boş bırakın)\"\n          },\n          \"characterData\": {\n            \"title\": \"Karakter verisi\",\n            \"description\": \"Hedef öğe içindeki karakter verisinin değişikliklerini izle\"\n          }\n        },\n        \"items\": {\n          \"manual\": \"Manuel olarak\",\n          \"interval\": \"Aralık\",\n          \"cron-job\": \"Cron işi\",\n          \"date\": \"Belirli bir tarihte\",\n          \"context-menu\": \"Bağlam menüsü\",\n          \"element-change\": \"öğe değişikliğinde\",\n          \"specific-day\": \"Belirli bir günde\",\n          \"visit-web\": \"Bir web sitesini ziyaret ederken\",\n          \"on-startup\": \"Tarayıcı başlatıldığında\",\n          \"keyboard-shortcut\": \"Klavye kısayolu\"\n        }\n      },\n      \"execute-workflow\": {\n        \"name\": \"İş Akışını Yürüt\",\n        \"overwriteNote\": \"Bu, seçilen iş akışının genel verilerini üzerine yazar\",\n        \"select\": \"İş akışını seç\",\n        \"executeId\": \"Yürütme Kimliği (isteğe bağlı)\",\n        \"description\": \"\",\n        \"insertAllVars\": \"Tüm mevcut iş akışı değişkenlerini kullan\",\n        \"insertVars\": \"Mevcut iş akışı değişkenlerini ekle\",\n        \"useCommas\": \"Değişken adını ayırmak için virgül kullan\",\n        \"insertAllGlobalData\": \"Tüm mevcut iş akışı genel verilerini kullan\"\n      },\n      \"google-sheets-drive\": {\n        \"name\": \"@:workflow.blocks.google-sheets.name (GDrive)\",\n        \"description\": \"@:workflow.blocks.google-sheets.description\",\n        \"connected\": \"Bağlı tablolar\",\n        \"select\": \"Tabloyu seç\",\n        \"connect\": \"Tabloyu bağla\"\n      },\n      \"google-drive\": {\n        \"name\": \"Google Drive\",\n        \"description\": \"Dosyaları Google Drive'a yükle\",\n        \"actions\": {\n          \"upload\": \"Dosyaları yükle\"\n        }\n      },\n      \"google-sheets\": {\n        \"name\": \"Google Sheets\",\n        \"description\": \"Google Sheets verilerini oku veya güncelle\",\n        \"previewData\": \"Verileri önizle\",\n        \"firstRow\": \"İlk satırı anahtar olarak kullan\",\n        \"keysAsFirstRow\": \"Anahtarları ilk satır olarak kullan\",\n        \"insertData\": \"Veri ekle\",\n        \"valueInputOption\": \"Değer giriş seçeneği\",\n        \"insertDataOption\": \"Veri ekleme seçeneği\",\n        \"rangeToSearch\": \"Aramaya başlamak için aralık\",\n        \"dataFrom\": {\n          \"label\": \"Veri kaynağı\",\n          \"options\": {\n            \"data-columns\": \"Tablo\",\n            \"custom\": \"Özel\"\n          }\n        },\n        \"refKey\": {\n          \"label\": \"Referans anahtarı (isteğe bağlı)\",\n          \"placeholder\": \"Anahtar adı\"\n        },\n        \"spreadsheetId\": {\n          \"label\": \"Tablo Kimliği\",\n          \"link\": \"Tablo Kimliği nasıl alınır\"\n        },\n        \"range\": {\n          \"label\": \"Aralık\",\n          \"link\": \"Daha fazla örnek görmek için tıklayın\"\n        },\n        \"select\": {\n          \"get\": \"Tablo hücre değerlerini al\",\n          \"getRange\": \"Tablo aralığını al\",\n          \"update\": \"Tablo hücre değerlerini güncelle\",\n          \"append\": \"Tablo hücre değerlerini ekleyin\",\n          \"clear\": \"Tablo hücre değerlerini temizle\",\n          \"create\": \"Tablo oluştur\",\n          \"add-sheet\": \"Sayfa ekle\"\n        }\n      },\n      \"active-tab\": {\n        \"name\": \"Aktif sekme\",\n        \"description\": \"Şu anda bulunduğunuz sekme olarak ayarla\"\n      },\n      \"proxy\": {\n        \"name\": \"Proxy\",\n        \"description\": \"Tarayıcının proxy'sini ayarla\",\n        \"clear\": \"Tüm proxy'leri temizle\",\n        \"bypass\": {\n          \"label\": \"Atlatma listesi\",\n          \"note\": \"URL'leri ayırmak için virgül (,) kullanın\"\n        }\n      },\n      \"new-window\": {\n        \"name\": \"Yeni pencere\",\n        \"description\": \"Yeni bir pencere oluştur\",\n        \"top\": \"Üst\",\n        \"left\": \"Sol\",\n        \"height\": \"Yükseklik\",\n        \"width\": \"Genişlik\",\n        \"note\": \"Not: Devre dışı bırakmak için 0 kullanın\",\n        \"position\": \"Pencere konumu\",\n        \"size\": \"Pencere boyutu\",\n        \"windowState\": {\n          \"placeholder\": \"Pencere durumu\",\n          \"options\": {\n            \"normal\": \"Normal\",\n            \"küçültülmüş\": \"Küçültülmüş\",\n            \"büyütülmüş\": \"Büyütülmüş\",\n            \"tam ekran\": \"Tam Ekran\"\n          }\n        },\n        \"incognito\": {\n          \"text\": \"Gizli pencere olarak ayarla\",\n          \"note\": \"Bu uzantı için önce 'Gizli modda izin ver' seçeneğini etkinleştirmelisiniz\"\n        }\n      },\n      \"go-back\": {\n        \"name\": \"Geri git\",\n        \"description\": \"Önceki sayfaya geri git\"\n      },\n      \"forward-page\": {\n        \"name\": \"İleri git\",\n        \"description\": \"Sonraki sayfaya ilerle\"\n      },\n      \"close-tab\": {\n        \"name\": \"Sekmeyi/pencereyi kapat\",\n        \"description\": \"\",\n        \"url\": \"Eşleşme Desenleri\",\n        \"activeTab\": \"Aktif sekmeyi kapat\",\n        \"allWindows\": \"Tüm pencereleri kapat\"\n      },\n      \"event-click\": {\n        \"name\": \"Öğeye tıkla\",\n        \"description\": \"\"\n      },\n      \"delay\": {\n        \"name\": \"Gecikme\",\n        \"description\": \"Bir sonraki bloğu yürütmeye geçmeden önce bir gecikme ekler\",\n        \"input\": {\n          \"title\": \"Milisaniye cinsinden gecikme\",\n          \"placeholder\": \"(milisaniye)\"\n        }\n      },\n      \"parameter-prompt\": {\n        \"name\": \"Parametre İstemi\"\n      },\n      \"get-text\": {\n        \"name\": \"Metin Al\",\n        \"description\": \"Bir öğeden metin al\",\n        \"checkbox\": \"Tabloya ekle\",\n        \"includeTags\": \"HTML etiketlerini içer\",\n        \"prefixText\": {\n          \"placeholder\": \"Metin öneki\",\n          \"title\": \"Metine önek ekle\"\n        },\n        \"suffixText\": {\n          \"placeholder\": \"Metin son eki\",\n          \"title\": \"Metine son ek ekle\"\n        }\n      },\n      \"export-data\": {\n        \"name\": \"Veriyi Dışa Aktar\",\n        \"description\": \"İş akışı verisini dışa aktar\",\n        \"exportAs\": \"Olarak dışa aktar\",\n        \"refKey\": \"Referans anahtarı\",\n        \"bomHeader\": \"UTF-8 BOM ekle\",\n        \"dataToExport\": {\n          \"placeholder\": \"Dışa aktarılacak veri\",\n          \"options\": {\n            \"data-columns\": \"Tablo\",\n            \"google-sheets\": \"Google Sheets\",\n            \"variable\": \"Değişken\"\n          }\n        }\n      },\n      \"element-scroll\": {\n        \"name\": \"Öğe Kaydır\",\n        \"description\": \"\",\n        \"scrollY\": \"Dikey kaydır\",\n        \"scrollX\": \"Yatay kaydır\",\n        \"intoView\": \"Görünüme kaydır\",\n        \"smooth\": \"Yumuşak kaydırma\",\n        \"incScrollX\": \"Yatay kaydırmayı artır\",\n        \"incScrollY\": \"Dikey kaydırmayı artır\"\n      },\n      \"switch-tab\": {\n        \"name\": \"Sekme Değiştir\",\n        \"description\": \"Sekmeler arasında geçiş yap\",\n        \"matchPattern\": \"Eşleşme Desenleri\",\n        \"url\": \"Yeni sekme URL'si\",\n        \"createIfNoMatch\": \"Eşleşme yoksa oluştur\"\n      },\n      \"new-tab\": {\n        \"name\": \"Yeni Sekme\",\n        \"description\": \"\",\n        \"url\": \"Yeni sekme URL'si\",\n        \"tab-zoom\": \"Sekme yakınlaştırma\",\n        \"customUserAgent\": \"Özel Kullanıcı Aracısı kullan\",\n        \"activeTab\": \"Aktif sekme olarak ayarla\",\n        \"tabToGroup\": \"Sekmeyi bir gruba ekle\",\n        \"waitTabLoaded\": \"Sekme yüklenene kadar bekle\",\n        \"updatePrevTab\": {\n          \"title\": \"Yeni sekme oluşturmak yerine önceki açılan yeni sekme kullanılsın\",\n          \"text\": \"Daha önce açılan sekme güncelle\"\n        }\n      },\n      \"link\": {\n        \"name\": \"Bağlantı\",\n        \"description\": \"Bağlantı öğesini aç\",\n        \"openInNewTab\": \"Yeni sekmede aç\"\n      },\n      \"attribute-value\": {\n        \"name\": \"Özellik Değeri\",\n        \"description\": \"Bir öğe özelliğinin değerini al\",\n        \"forms\": {\n          \"name\": \"Özellik adı\",\n          \"checkbox\": \"Tabloya ekle\",\n          \"column\": \"Sütun seç\",\n          \"value\": \"Özellik değeri\",\n          \"action\": {\n            \"get\": \"Özellik değerini al\",\n            \"set\": \"Özellik değerini ayarla\"\n          },\n          \"extraRow\": {\n            \"checkbox\": \"Ek satır ekle\",\n            \"placeholder\": \"Değer\",\n            \"title\": \"Ek satırın değeri\"\n          }\n        }\n      },\n      \"forms\": {\n        \"name\": \"Formlar\",\n        \"description\": \"\",\n        \"selected\": \"Seçilen\",\n        \"type\": \"Form türü\",\n        \"getValue\": \"Form değerini al\",\n        \"text-field\": {\n          \"name\": \"Metin alanı\",\n          \"value\": \"Değer\",\n          \"clearValue\": \"Form değerini temizle\",\n          \"delay\": {\n            \"placeholder\": \"Gecikme\",\n            \"label\": \"Yazma gecikmesi (milisaniye)(Devre dışı bırakmak için 0)\"\n          }\n        },\n        \"select\": {\n          \"name\": \"Seçim\"\n        },\n        \"radio\": {\n          \"name\": \"Radyo\"\n        },\n        \"checkbox\": {\n          \"name\": \"Onay kutusu\"\n        }\n      },\n      \"repeat-task\": {\n        \"name\": \"Görevi Yinele\",\n        \"description\": \"\",\n        \"times\": \"kez\",\n        \"repeatFrom\": \"Şuradan tekrarla\"\n      },\n      \"javascript-code\": {\n        \"name\": \"JavaScript Kodu\",\n        \"description\": \"Web sayfasında JavaScript kodunuzu yürütün\",\n        \"availabeFuncs\": \"Kullanılabilir fonksiyonlar:\",\n        \"removeAfterExec\": \"Bloğu yürütme sonrasında kaldır\",\n        \"everyNewTab\": \"Her yeni sekmede yürüt\",\n        \"context\": {\n          \"name\": \"Yürütme bağlamı\",\n          \"items\": {\n            \"website\": \"Aktif sekme\",\n            \"background\": \"Arka plan\"\n          }\n        },\n        \"modal\": {\n          \"tabs\": {\n            \"code\": \"JavaScript kodu\",\n            \"preloadScript\": \"Ön yükleme script'i\"\n          }\n        },\n        \"timeout\": {\n          \"placeholder\": \"Zaman aşımı (milisaniye)\",\n          \"title\": \"JavaScript kodu yürütme zaman aşımı\"\n        }\n      },\n      \"trigger-event\": {\n        \"name\": \"Olay Tetikleyici\",\n        \"description\": \"\",\n        \"selectEvent\": \"Olayı seç\"\n      },\n      \"conditions\": {\n        \"name\": \"Koşullar\",\n        \"add\": \"Yol ekle\",\n        \"retryConditions\": \"Koşullar karşılanmazsa tekrar dene\",\n        \"description\": \"Koşullu blok\",\n        \"refresh\": \"Koşul bağlantılarını yenile\",\n        \"fallbackTitle\": \"Tüm karşılaştırmalar gereksinimi karşılamadığında yürütülür\",\n        \"equals\": \"Eşittir\",\n        \"gt\": \"Büyüktür\",\n        \"gte\": \"Büyük veya eşittir\",\n        \"lt\": \"Küçüktür\",\n        \"lte\": \"Küçük veya eşittir\",\n        \"ne\": \"Eşit değildir\",\n        \"contains\": \"İçerir\"\n      },\n      \"element-exists\": {\n        \"name\": \"Öğe Var Mı?\",\n        \"description\": \"Bir öğenin var olup olmadığını kontrol et\",\n        \"selector\": \"Öğe seçici\",\n        \"fallbackTitle\": \"Öğe var olmadığında yürütülür\",\n        \"throwError\": \"Var olmadığında hata fırlat\",\n        \"tryFor\": {\n          \"title\": \"Öğenin var olup olmadığını kontrol etmek için kaç kez deneneceği\",\n          \"label\": \"Deneme süresi\"\n        },\n        \"timeout\": {\n          \"label\": \"Zaman aşımı (milisaniye)\",\n          \"title\": \"Her deneme için zaman aşımı\"\n        }\n      },\n      \"webhook\": {\n        \"name\": \"HTTP İsteği\",\n        \"description\": \"Bir HTTP İsteği yap\",\n        \"contentType\": \"İçerik türü\",\n        \"method\": \"İstek yöntemi\",\n        \"url\": \"İstek URL'si\",\n        \"fallback\": \"HTTP isteği başarısız olduğunda yürütülür\",\n        \"buttons\": {\n          \"header\": \"Başlık ekle\"\n        },\n        \"timeout\": {\n          \"placeholder\": \"Zaman aşımı\",\n          \"title\": \"HTTP isteği yürütme zaman aşımı (ms)\"\n        },\n        \"tabs\": {\n          \"headers\": \"Başlıklar\",\n          \"body\": \"Gövde\",\n          \"response\": \"Yanıt\"\n        }\n      },\n      \"while-loop\": {\n        \"name\": \"While Döngüsü\",\n        \"description\": \"Koşul karşılanırken blokları yürütür, aksi takdirde yürütme\",\n        \"editCondition\": \"Koşulu düzenle\",\n        \"fallback\": \"Koşul yanlış olduğunda yürütülür\"\n      },\n      \"loop-elements\": {\n        \"name\": \"Öğeleri Döngüle\",\n        \"description\": \"Öğeler arasında yinele\",\n        \"loadMore\": \"Daha fazla öğe yükle\",\n        \"scrollToBottom\": \"En alta kaydır\",\n        \"scrollToTop\": \"En üste kaydır\",\n        \"actions\": {\n          \"none\": \"Hiçbiri\",\n          \"click-element\": \"Bir öğeye tıkla\",\n          \"scroll\": \"Aşağı kaydır\",\n          \"click-link\": \"Bir bağlantıya tıkla\",\n          \"scroll-up\": \"Yukarı kaydır\"\n        }\n      },\n      \"loop-data\": {\n        \"name\": \"Verileri Döngüle\",\n        \"description\": \"Bir tablo veya özel veri kümesi üzerinde yinele\",\n        \"loopId\": \"Döngü Kimliği\",\n        \"refKey\": \"Referans anahtarı\",\n        \"startIndex\": \"İndexten başla\",\n        \"resumeLastWorkflow\": \"Son iş akışını devam ettir\",\n        \"reverse\": \"Döngü sırasını tersine çevir\",\n        \"modal\": {\n          \"fileTooLarge\": \"Dosya düzenlemek için çok büyük\",\n          \"maxFile\": \"Maksimum dosya boyutu 1 MB\",\n          \"options\": {\n            \"firstRow\": \"İlk satırı anahtar olarak kullan\"\n          }\n        },\n        \"buttons\": {\n          \"clear\": \"Veriyi temizle\",\n          \"insert\": \"Veriyi ekle\",\n          \"import\": \"Dosyayı içe aktar\"\n        },\n        \"maxLoop\": {\n          \"title\": \"Yinelemek için maksimum veri sayısı\",\n          \"label\": \"Yinelemek için maksimum veri (Devre dışı bırakmak için 0)\"\n        },\n        \"loopThrough\": {\n          \"placeholder\": \"Şunun üzerinden döngüle\",\n          \"fromNumber\": \"Numaradan\",\n          \"toNumber\": \"Numaraya\",\n          \"options\": {\n            \"numbers\": \"Sayılar\",\n            \"variable\": \"Değişken\",\n            \"data-columns\": \"Tablo\",\n            \"table\": \"Tablo\",\n            \"custom-data\": \"Özel veri\",\n            \"google-sheets\": \"Google Sheets\",\n            \"elements\": \"Öğeler\"\n          }\n        }\n      },\n      \"loop-breakpoint\": {\n        \"name\": \"Döngü Kırılma Noktası\",\n        \"description\": \"Döngü Veri bloğunun nerede durması gerektiğini belirtmek için\"\n      },\n      \"take-screenshot\": {\n        \"name\": \"Ekran Görüntüsü Al\",\n        \"fullPage\": \"Tam sayfa ekran görüntüsü al\",\n        \"description\": \"Şu anda aktif olan sekmenin ekran görüntüsünü al\",\n        \"imageQuality\": \"Resim kalitesi\",\n        \"saveToColumn\": \"Ekran görüntüsünü tabloya ekle\",\n        \"saveToComputer\": \"Ekran görüntüsünü bilgisayara kaydet\",\n        \"types\": {\n          \"title\": \"Şunun ekran görüntüsünü al\",\n          \"page\": \"Bir sayfa\",\n          \"fullpage\": \"Tam sayfa\",\n          \"element\": \"Bir öğe\"\n        }\n      },\n      \"switch-to\": {\n        \"name\": \"Çerçeve Değiştir\",\n        \"description\": \"Ana pencere ile bir iframe arasında geçiş yap\",\n        \"iframeSelector\": \"Öğe seçici\",\n        \"windowTypes\": {\n          \"main\": \"Ana pencere\",\n          \"iframe\": \"Iframe\"\n        }\n      },\n      \"debugMode\": {\n        \"title\": \"Hata Ayıklama Modu\",\n        \"description\": \"Bloğu Chrome DevTools Protokolü kullanarak yürüt\"\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "src/locales/tr/common.json",
    "content": "{\n  \"common\": {\n    \"dashboard\": \"Kontrol Paneli\",\n    \"workflow\": \"İş Akışı | İş Akışları\",\n    \"collection\": \"Koleksiyon | Koleksiyonlar\",\n    \"log\": \"Kayıt | Kayıtlar\",\n    \"block\": \"Blok | Bloklar\",\n    \"schedule\": \"Zamanlama\",\n    \"folder\": \"Klasör | Klasörler\",\n    \"new\": \"Yeni\",\n    \"docs\": \"Belgeler\",\n    \"search\": \"Ara\",\n    \"example\": \"Örnek | Örnekler\",\n    \"import\": \"İçe Aktar\",\n    \"export\": \"Dışa Aktar\",\n    \"rename\": \"Yeniden Adlandır\",\n    \"execute\": \"Çalıştır\",\n    \"delete\": \"Sil\",\n    \"cancel\": \"İptal\",\n    \"settings\": \"Ayarlar\",\n    \"options\": \"Seçenekler\",\n    \"confirm\": \"Onayla\",\n    \"name\": \"Ad\",\n    \"all\": \"Tümü\",\n    \"add\": \"Ekle\",\n    \"save\": \"Kaydet\",\n    \"data\": \"Veri\",\n    \"stop\": \"Durdur\",\n    \"sheet\": \"Sayfa\",\n    \"pause\": \"Duraklat\",\n    \"resume\": \"Devam Et\",\n    \"action\": \"Eylem | Eylemler\",\n    \"packages\": \"Paketler\",\n    \"storage\": \"Depolama\",\n    \"editor\": \"Düzenleyici\",\n    \"running\": \"Çalışıyor\",\n    \"globalData\": \"Global veri\",\n    \"fileName\": \"Dosya adı\",\n    \"description\": \"Açıklama\",\n    \"disable\": \"Devre Dışı Bırak\",\n    \"disabled\": \"Devre Dışı\",\n    \"enable\": \"Etkinleştir\",\n    \"fallback\": \"Yedek\",\n    \"update\": \"Güncelle\",\n    \"feature\": \"Özellik\",\n    \"duplicate\": \"Çoğalt\",\n    \"password\": \"Şifre\",\n    \"category\": \"Kategori\",\n    \"optional\": \"İsteğe Bağlı\",\n    \"0disable\": \"0 ile devre dışı bırak\",\n    \"millisecond\": \"milisaniye | milisaniyeler\"\n  },\n  \"message\": {\n    \"noBlock\": \"Blok yok\",\n    \"noData\": \"Gösterilecek veri yok\",\n    \"noTriggerBlock\": \"Tetikleyici blok bulunamıyor\",\n    \"useDynamicData\": \"Dinamik veri eklemeyi öğrenin\",\n    \"delete\": \"\\\"{name}\\\"'i silmek istediğinizden emin misiniz?\",\n    \"empty\": \"Ups... Herhangi bir öğeniz olmadığı görünüyor\",\n    \"maxSizeExceeded\": \"Dosya boyutu maksimum izin verilen sınırı aştı\",\n    \"notSaved\": \"Gerçekten ayrılmak istiyor musunuz? Kaydedilmemiş değişiklikleriniz var!\",\n    \"somethingWrong\": \"Bir şeyler yanlış gitti\",\n    \"limitExceeded\": \"Sınırı aştınız\"\n  },\n  \"sort\": {\n    \"sortBy\": \"Sırala\",\n    \"name\": \"Ad\",\n    \"createdAt\": \"Oluşturulma tarihi\",\n    \"updatedAt\": \"Son güncelleme\",\n    \"mostUsed\": \"En çok kullanılan\"\n  },\n  \"logStatus\": {\n    \"stopped\": \"durduruldu\",\n    \"error\": \"hata\",\n    \"success\": \"başarı\"\n  }\n}\n"
  },
  {
    "path": "src/locales/tr/newtab.json",
    "content": "{\n  \"home\": {\n    \"viewAll\": \"Hepsini Görüntüle\",\n    \"communities\": \"Topluluklar\"\n  },\n  \"welcome\": {\n    \"title\": \"Automa'ya Hoş Geldiniz! 🎉\",\n    \"text\": \"Belgelendirmeyi okuyarak veya Automa Marketplace'deki iş akışlarını göz atarak başlayın.\",\n    \"marketplace\": \"Marketplace\"\n  },\n  \"packages\": {\n    \"name\": \"Paket | Paketler\",\n    \"add\": \"Paket Ekle\",\n    \"icon\": \"Paket simgesi\",\n    \"open\": \"Paketleri Aç\",\n    \"new\": \"Yeni Paket\",\n    \"import\": \"Paket İçe Aktar\",\n    \"set\": \"Paket Olarak Ayarla\",\n    \"settings\": {\n      \"asBlock\": \"Paketi blok olarak ayarla\"\n    },\n    \"categories\": {\n      \"my\": \"Paketlerim\",\n      \"installed\": \"Yüklenmiş Paketler\"\n    }\n  },\n  \"scheduledWorkflow\": {\n    \"title\": \"Zamanlanmış iş akışları\",\n    \"nextRun\": \"Sonraki çalıştırma\",\n    \"active\": \"Aktif\",\n    \"refresh\": \"Yenile\",\n    \"schedule\":{\n      \"title\": \"Zamanlama\",\n      \"types\": {\n        \"everyDay\": \"Her gün\",\n        \"general\": \"Her {time}\",\n        \"interval\": \"{time} dakikada bir\"\n      }\n    }\n  },\n  \"storage\": {\n    \"title\": \"Depolama\",\n    \"table\": {\n      \"add\": \"Tablo Ekle\",\n      \"createdAt\": \"Oluşturulma Tarihi\",\n      \"modifiedAt\": \"Değiştirilme Tarihi\",\n      \"rowsCount\": \"Satır Sayısı\",\n      \"delete\": \"Tabloyu Sil\"\n    }\n  },\n  \"credential\": {\n    \"title\": \"Kimlik Bilgisi | Kimlik Bilgileri\",\n    \"add\": \"Kimlik Bilgisi Ekle\",\n    \"use\": {\n      \"title\": \"Kullanılan Kimlik Bilgileri\",\n      \"description\": \"Bu iş akışı bu kimlik bilgilerini kullanıyor\"\n    }\n  },\n  \"workflowPermissions\": {\n    \"title\": \"İş Akışı İzinleri\",\n    \"description\": \"Bu iş akışının düzgün çalışması için bu izinlere ihtiyaç duyar\",\n    \"contextMenus\": {\n      \"title\": \"Bağlam Menüsü\",\n      \"description\": \"İş akışını bağlam menüsü aracılığıyla yürütmek için\"\n    },\n    \"clipboardRead\": {\n      \"title\": \"Pano\",\n      \"description\": \"Panodaki verilere erişim sağlamak için\"\n    },\n    \"notifications\": {\n      \"title\": \"Bildirim\",\n      \"description\": \"Bir bildirim göstermek için\"\n    },\n    \"downloads\": {\n      \"title\": \"İndir\",\n      \"description\": \"Sayfa varlıklarını kaydetme ve indirilen dosyayı yeniden adlandırma için\"\n    },\n    \"cookies\": {\n      \"title\": \"Çerezler\",\n      \"description\": \"Çerezleri okuma, ayarlama veya kaldırmak için\"\n    }\n  },\n  \"updateMessage\": {\n    \"text1\": \"Automa v{version} sürümüne güncellendi,\",\n    \"text2\": \"yeniliklere göz atın.\"\n  },\n  \"workflows\": {\n    \"folder\": {\n      \"new\": \"Yeni Klasör\",\n      \"name\": \"Klasör Adı\",\n      \"delete\": \"Klasörü Sil\",\n      \"rename\": \"Klasörü Yeniden Adlandır\"\n    }\n  },\n  \"auth\": {\n    \"title\": \"Kimlik Doğrulama\",\n    \"signIn\": \"Oturum aç\",\n    \"username\": \"Önce kullanıcı adınızı ayarlamanız gerekiyor\",\n    \"clickHere\": \"Buraya tıklayın\",\n    \"text\": \"Bunu yapmadan önce oturum açmanız gerekiyor\"\n  },\n  \"running\": {\n    \"start\": \"Başlangıç tarihi: {date}\",\n    \"message\": \"Bu yalnızca son 5 günlüğü gösterir\"\n  },\n  \"settings\": {\n    \"theme\": \"Tema\",\n    \"shortcuts\": {\n      \"duplicate\": \"\\\"{name}\\\" tarafından zaten kullanılan kısayol\"\n    },\n    \"editor\": {\n      \"title\": \"Başlık\",\n      \"curvature\": {\n        \"title\": \"Çizgi Kıvrımı\",\n        \"line\": \"Çizgi\",\n        \"reroute\": \"Yeniden Yönlendirme\",\n        \"rerouteFirstLast\": \"İlk ve son noktayı yeniden yönlendir\"\n      },\n      \"arrow\": {\n        \"title\": \"Çizgi oku\",\n        \"description\": \"Çizginin sonuna bir ok ekleyin\"\n      },\n      \"snapGrid\": {\n        \"title\": \"Izgaraya tutun\",\n        \"description\": \"Bir bloğu taşıdığınızda ızgaraya tutunur\"\n      },\n      \"saveWhenExecute\": {\n        \"title\": \"İş akışını yürütürken otomatik olarak kaydet\",\n        \"description\": \"İş akışı yürütüldüğünde iş akışı değişiklikleri kaydedilecektir\"\n      }\n    },\n    \"deleteLog\": {\n      \"title\": \"İş akışı günlüklerini otomatik sil\",\n      \"after\": \"Şu tarihten sonra sil\",\n      \"deleteAfter\": {\n        \"never\": \"Hiçbir Zaman\",\n        \"days\": \"{day} gün\"\n      }\n    },\n    \"language\": {\n      \"label\": \"Dil\",\n      \"helpTranslate\": \"Dilinizi bulamıyor musunuz? Çeviriye yardımcı olun.\",\n      \"reloadPage\": \"Değişikliğin etkili olması için sayfayı yenileyin\"\n    },\n    \"menu\": {\n      \"backup\": \"İş Akışlarını Yedekle\",\n      \"editor\": \"Düzenleyici\",\n      \"general\": \"Genel\",\n      \"shortcuts\": \"Kısayollar\",\n      \"about\": \"Hakkında\"\n    },\n    \"backupWorkflows\": {\n      \"title\": \"Yerel Yedek\",\n      \"invalidPassword\": \"Geçersiz şifre\",\n      \"workflowsAdded\": \"{count} iş akışı eklenmiştir\",\n      \"name\": \"İş akışları Yedekle\",\n      \"needSignin\": \"İlk önce oturum açmanız gerekiyor\",\n      \"backup\": {\n        \"button\": \"Yedekle\",\n        \"settings\": \"Yedekleme ayarları\",\n        \"encrypt\": \"Şifre ile koru\",\n        \"schedule\": \"Yerel yedeklemeyi zamanla\"\n      },\n      \"restore\": {\n        \"title\": \"İş akışlarını geri yükle\",\n        \"button\": \"Geri Yükle\",\n        \"update\": \"Eğer iş akışı mevcutsa güncelle\"\n      },\n      \"cloud\": {\n        \"buttons\": {\n          \"local\": \"Yerel\",\n          \"cloud\": \"Bulut\"\n        },\n        \"location\": \"Konum\",\n        \"delete\": \"Yedeklemeyi Sil\",\n        \"title\": \"Bulut Yedekleme\",\n        \"sync\": \"Senkronize et\",\n        \"lastSync\": \"Son senkronizasyon\",\n        \"lastBackup\": \"Son yedekleme\",\n        \"select\": \"İş akışlarını seçin\",\n        \"storedWorkflows\": \"Bulutta depolanan iş akışları\",\n        \"selected\": \"Seçilen\",\n        \"selectText\": \"Yedeklemek istediğiniz iş akışlarını seçin\",\n        \"selectAll\": \"Hepsini Seç\",\n        \"deselectAll\": \"Hepsinin Seçimini Kaldır\",\n        \"needSelectWorkflow\": \"Yedeklemek istediğiniz iş akışlarını seçmelisiniz\"\n      }\n    }\n  },\n  \"workflow\": {\n    \"events\": {\n      \"title\": \"İş Akışı Etkinlikleri\",\n      \"add-action\": \"Eylem Ekle\",\n      \"description\": \"Etkinlik gerçekleştiğinde eylemleri gerçekleştirin.\",\n      \"event\": \"Etkinlik | Etkinlikler\",\n      \"action\": \"Eylem\",\n      \"actions\": {\n        \"js-code\": {\n          \"title\": \"JS Kodu Çalıştır\"\n        },\n        \"http-request\": {\n          \"title\": \"HTTP İsteği\"\n        }\n      },\n      \"types\": {\n        \"finish:success\": {\n          \"name\": \"Tamamlanma (başarılı)\",\n          \"description\": \"İş akışı yürütmesi başarıyla tamamlandı\"\n        },\n        \"finish:failed\": {\n          \"name\": \"Tamamlanma (hata)\",\n          \"description\": \"İş akışı yürütmesi hata ile tamamlandı\"\n        }\n      }\n    },\n    \"previewMode\": {\n      \"title\": \"Önizleme modu\",\n      \"description\": \"Önizleme modundasınız, yaptığınız değişiklikler kaydedilmeyecek\"\n    },\n    \"pinWorkflow\": {\n      \"pin\": \"İş Akışını Sabitle\",\n      \"unpin\": \"İş Akışının Sabitlemesini Kaldır\",\n      \"pinned\": \"Sabitlemiş İş Akışları\"\n    },\n    \"parameters\": {\n      \"add\": \"Parametre Ekle\",\n      \"preferInTab\": \"Giriş parametrelerini sekmede tercih edin\"\n    },\n    \"my\": \"İş akışlarım\",\n    \"testing\": {\n      \"title\": \"Test Modu\",\n      \"nextBlock\": \"Sonraki Blok\",\n      \"startRun\": \"Çalıştırmayı Başlat\",\n      \"disabled\": \"Önce değişiklikleri kaydedin\"\n    },\n    \"import\": \"İş Akışını İçe Aktar\",\n    \"new\": \"Yeni İş Akışı\",\n    \"delete\": \"İş Akışını Sil\",\n    \"browse\": \"İş Akışlarına Göz At\",\n    \"name\": \"İş Akışı Adı\",\n    \"rename\": \"İş Akışını Yeniden Adlandır\",\n    \"backupCloud\": \"İş Akışını Buluta Yedekle\",\n    \"add\": \"İş Akışı Ekle\",\n    \"clickToEnable\": \"Etkinleştirmek için tıklayın\",\n    \"toggleSidebar\": \"Kenar Çubuğunu Aç/Kapat\",\n    \"cantEdit\": \"Paylaşılan iş akışını düzenleyemezsiniz\",\n    \"undo\": \"Geri Al\",\n    \"redo\": \"Yinele\",\n    \"autoAlign\": {\n      \"title\": \"Otomatik hizala\"\n    },\n    \"blocksFolder\": {\n      \"title\": \"Blok Klasörü\",\n      \"add\": \"Klasöre Blok Ekle\",\n      \"save\": \"Klasöre Kaydet\"\n    },\n    \"searchBlocks\": {\n      \"title\": \"Düzenleyicide Blokları Ara\"\n    },\n    \"conditionBuilder\": {\n      \"title\": \"Koşul Oluşturucu\",\n      \"add\": \"Koşul Ekle\",\n      \"and\": \"VE\",\n      \"or\": \"VEYA\",\n      \"topAwait\": \"Üst düzey bekleme ve \\\"automaRefData\\\" fonksiyonunu destekle\"\n    },\n    \"host\": {\n      \"title\": \"İş Akışını Barındır\",\n      \"set\": \"Ana İş Akışı olarak ayarla\",\n      \"id\": \"Ana Bilgisayar Kimliği\",\n      \"add\": \"Barındırılan İş Akışı Ekle\",\n      \"sync\": {\n        \"title\": \"Senkronize Et\",\n        \"description\": \"Ana iş akışı ile senkronize et\"\n      },\n      \"messages\": {\n        \"hostExist\": \"Bu ana bilgisayarı zaten eklediniz\",\n        \"notFound\": \"Kimliği \\\"{id}\\\" olan barındırılan bir iş akışı bulunamıyor\",\n        \"successAdded\": \"Kimliği \\\"{id}\\\" olan barındırılan iş akışı başarıyla eklendi\"\n      }\n    },\n    \"type\": {\n      \"local\": \"Yerel\",\n      \"shared\": \"Paylaşılan\",\n      \"host\": \"Ana Bilgisayar\"\n    },\n    \"unpublish\": {\n      \"title\": \"İş Akışını Yayından Kaldır\",\n      \"button\": \"Yayından Kaldır\",\n      \"body\": \"İş akışını \\\"{name}\\\" yayından kaldırmak istediğinizden emin misiniz?\"\n    },\n    \"share\": {\n      \"url\": \"Paylaş URL\",\n      \"publish\": \"Yayınla\",\n      \"sharedAs\": \"Şu şekilde paylaşıldı: \\\"{name}\\\"\",\n      \"title\": \"İş Akışını Paylaş\",\n      \"download\": \"İş akışını yerel olarak kaydet\",\n      \"edit\": \"Açıklamayı düzenle\",\n      \"fetchLocal\": \"Yerel iş akışını getir\",\n      \"update\": \"Güncelle\",\n      \"unpublish\": \"Yayından kaldır\",\n      \"linkCopied\": \"Bağlantı kopyalandı\"\n    },\n    \"variables\": {\n      \"title\": \"Değişken | Değişkenler\",\n      \"name\": \"Değişken adı\",\n      \"assign\": \"Değişken ata\"\n    },\n    \"protect\": {\n      \"title\": \"İş akışını koru\",\n      \"remove\": \"Korumayı kaldır\",\n      \"button\": \"Koruma\",\n      \"note\": \"Not: Bu şifre, iş akışını daha sonra düzenlemek veya silmek için gereklidir.\"\n    },\n    \"locked\": {\n      \"title\": \"Bu İş Akışı Korunuyor\",\n      \"body\": \"Kilidini açmak için şifreyi girin\",\n      \"unlock\": \"Kilidi aç\",\n      \"messages\": {\n        \"incorrect-password\": \"Yanlış şifre\"\n      }\n    },\n    \"state\": {\n      \"executeBy\": \"\\\"{name}\\\" tarafından yürütülen\"\n    },\n    \"table\": {\n      \"title\": \"Tablo | Tablolar\",\n      \"placeholder\": \"Arama veya sütun ekle\",\n      \"select\": \"Sütun seç\",\n      \"column\": {\n        \"name\": \"Sütun adı\",\n        \"type\": \"Veri türü\"\n      }\n    },\n    \"sidebar\": {\n      \"workflowIcon\": \"İş akışı simgesi\"\n    },\n    \"editor\": {\n      \"zoomIn\": \"Yaklaştır\",\n      \"zoomOut\": \"Uzaklaştır\",\n      \"resetZoom\": \"Yakınlaştırmayı sıfırla\",\n      \"duplicate\": \"Çoğalt\",\n      \"copy\": \"Kopyala\",\n      \"paste\": \"Yapıştır\",\n      \"group\": \"Blokları grupla\",\n      \"ungroup\": \"Blokları gruplamayı kaldır\"\n    },\n    \"settings\": {\n      \"saveLog\": \"İş akışı günlüğünü kaydet\",\n      \"executedBlockOnWeb\": \"Web sayfasında yürütülen blokları göster\",\n      \"notification\": {\n        \"title\": \"İş akışı bildirimi\",\n        \"description\": \"İş akışı durumunu (başarılı veya başarısız) yürütüldükten sonra göster\",\n        \"noPermission\": \"Bu seçenek, çalışması için \\\"bildirimler\\\" iznine ihtiyaç duyar\"\n      },\n      \"publicId\": {\n        \"title\": \"İş akışı genel kimliği\",\n        \"description\": \"İş akışını bir JavaScript özel etkinlik aracılığıyla yürütmek için genel bir kimlik belirleyin\"\n      },\n      \"defaultColumn\": {\n        \"title\": \"Varsayılan sütuna ekle\",\n        \"description\": \"Bloktaki sütun seçilmediyse veriyi varsayılan sütuna ekleyin\",\n        \"name\": \"Varsayılan sütun adı\"\n      },\n      \"autocomplete\": {\n        \"title\": \"Otomatik tamamlama\",\n        \"description\": \"Giriş bloğunda otomatik tamamlamayı etkinleştirin (Automa'nın kararsız hale gelirse devre dışı bırakın)\"\n      },\n      \"clearCache\": {\n        \"title\": \"Önbelleği temizle\",\n        \"description\": \"İş akışının önbelleğini (durum ve döngü indeksi) temizleme\",\n        \"info\": \"İş akışı önbelleği başarıyla temizlendi\",\n        \"btn\": \"Temizle\"\n      },\n      \"reuseLastState\": {\n        \"title\": \"Son iş akışının durumunu yeniden kullan\",\n        \"description\": \"En son yürütülen iş akışındaki durum verilerini (tablo, değişkenler ve genel veri) kullanma\"\n      },\n      \"debugMode\": {\n        \"title\": \"Hata ayıklama modu\",\n        \"description\": \"İş akışını Chrome DevTools Protokolü kullanarak yürütme\"\n      },\n      \"restartWorkflow\": {\n        \"for\": \"Yeniden başlatma süresi\",\n        \"times\": \"Zamanlar\",\n        \"description\": \"İş akışının kaç kez yeniden başlatılacağının maksimum sayısı\"\n      },\n      \"onError\": {\n        \"title\": \"İş akışında hata durumunda\",\n        \"description\": \"İş akışında bir hata oluştuğunda alınacak aksiyonu ayarlayın\",\n        \"items\": {\n          \"keepRunning\": \"Çalışmaya devam et\",\n          \"stopWorkflow\": \"İş akışını durdur\",\n          \"restartWorkflow\": \"İş akışını yeniden başlat\"\n        }\n      },\n      \"timeout\": {\n        \"title\": \"İş akışı zaman aşımı (milisaniye)\"\n      },\n      \"blockDelay\": {\n        \"title\": \"Bloklar arası gecikme (milisaniye)\",\n        \"description\": \"Her bloğu yürütmeden önce gecikme ekle\"\n      },\n      \"tabLoadTimeout\": {\n        \"title\": \"Sekme yükleme zaman aşımı\",\n        \"description\": \"Bir sekmenin yüklenme süresi için maksimum süre milisaniye cinsinden, zaman aşımını devre dışı bırakmak için 0 girin\"\n        }\n    }\n  },\n  \"collection\": {\n    \"description\": \"İş akışlarınızı sırayla yürütün\",\n    \"new\": \"Yeni koleksiyon\",\n    \"delete\": \"Koleksiyonu sil\",\n    \"add\": \"Koleksiyon ekle\",\n    \"rename\": \"Koleksiyonu yeniden adlandır\",\n    \"flow\": \"Akış\",\n    \"dragDropText\": \"Bir iş akışını veya bloğu buraya bırakın\",\n    \"options\": {\n      \"atOnce\": {\n        \"title\": \"Koleksiyondaki tüm iş akışlarını aynı anda yürüt\",\n        \"description\": \"Bu seçenek kullanıldığında bloklar çalışmaz\"\n      }\n    },\n    \"globalData\": {\n      \"note\": \"Bu, iş akışının genel verisini üzerine yazar\"\n    }\n  },\n  \"log\": {\n    \"flowId\": \"Akış Kimliği\",\n    \"goBack\": \"\\\"{name}\\\"'in günlüklerine geri dön\",\n    \"goWorkflow\": \"İş akışına git\",\n    \"startedDate\": \"Başlangıç tarihi\",\n    \"duration\": \"Süre\",\n    \"selectAll\": \"Tümünü seç\",\n    \"deselectAll\": \"Tümünü iptal et\",\n    \"deleteSelected\": \"Seçilen günlükleri sil\",\n    \"clearLogs\": {\n      \"title\": \"Günlükleri temizle\",\n      \"description\": \"Tüm günlükleri silmek istediğinizden emin misiniz?\"\n    },\n    \"types\": {\n      \"stop\": \"İş akışı durduruldu\",\n      \"finish\": \"Tamamlandı\"\n    },\n    \"messages\": {\n      \"url-empty\": \"URL boş\",\n      \"invalid-url\": \"URL geçerli değil\",\n      \"conditions-empty\": \"Koşullar boş\",\n      \"invalid-proxy-host\": \"Geçersiz proxy ana bilgisayar\",\n      \"workflow-disabled\": \"İş akışı devre dışı\",\n      \"selector-empty\": \"Eleman seçici boş\",\n      \"invalid-body\": \"İçerik gövdesi geçerli bir JSON değil\",\n      \"invalid-active-tab\": \"\\\"{url}\\\" geçerli bir URL değil\",\n      \"empty-spreadsheet-id\": \"Tablo Kimliği boş\",\n      \"invalid-loop-data\": \"Döngü için geçersiz veri\",\n      \"empty-workflow\": \"İlk önce bir iş akışı seçmelisiniz\",\n      \"active-tab-removed\": \"İş akışı etkin sekmesi kaldırıldı\",\n      \"empty-spreadsheet-range\": \"Tablo aralığı boş\",\n      \"stop-timeout\": \"İş akışı zaman aşımı nedeniyle durduruldu\",\n      \"no-file-access\": \"Automa'nın dosyaya erişimi yok\",\n      \"no-workflow\": \"ID \\\"{workflowId}\\\" ile bir iş akışı bulunamıyor\",\n      \"no-match-tab\": \"\\\"{pattern}\\\" deseniyle eşleşen bir sekme bulunamıyor\",\n      \"no-clipboard-acces\": \"Pano erişim izni yok\",\n      \"browser-not-supported\": \"Bu özellik {browser} tarayıcısında desteklenmiyor\",\n      \"element-not-found\": \"\\\"{selector}\\\" seçici ile bir öğe bulunamıyor\",\n      \"no-permission\": \"\\\"{permission}\\\" iznine sahip değil, bu işlemi gerçekleştirmek için iznin yok\",\n      \"not-iframe\": \"\\\"{selector}\\\" seçiciye sahip öğe bir iframe öğesi değil\",\n      \"iframe-not-found\": \"\\\"{selector}\\\" seçici ile bir iframe öğesi bulunamıyor\",\n      \"workflow-infinite-loop\": \"Sonsuz döngüyü önlemek için iş akışı yürütülemedi\",\n      \"not-debug-mode\": \"Bu bloğun düzgün çalışması için iş akışının hata ayıklama modunda çalışması gerekir\",\n      \"no-iframe-id\": \"\\\"{selector}\\\" seçiciye sahip iframe öğesi için Çerçeve ID bulunamıyor\",\n      \"no-tab\": \"Bir sekme ile bağlantı kurulamıyor, \\\"{name}\\\" bloğunu kullanmadan önce \\\"Yeni sekme\\\" veya \\\"Etkin sekme\\\" bloğunu kullanın\"\n    },\n    \"description\": {\n      \"text\": \"{status} tarihinde {date} süresince {duration}\",\n      \"status\": {\n        \"success\": \"Başarılı\",\n        \"error\": \"Başarısız\",\n        \"stopped\": \"Durduruldu\"\n      }\n    },\n    \"delete\": {\n      \"title\": \"Günlüğü sil\",\n      \"description\": \"Tüm seçilen günlükleri silmek istediğinizden emin misiniz?\"\n    },\n    \"exportData\": {\n      \"title\": \"Veriyi dışa aktar\",\n      \"types\": {\n        \"json\": \"JSON\",\n        \"csv\": \"CSV\",\n        \"plain-text\": \"Düz metin\"\n      }\n    },\n    \"filter\": {\n      \"title\": \"Filtrele\",\n      \"byStatus\": \"Duruma göre\",\n      \"byDate\": {\n        \"title\": \"Tarihe göre\",\n        \"items\": {\n          \"lastDay\": \"Son gün\",\n          \"last7Days\": \"Son yedi gün\",\n          \"last30Days\": \"Son otuz gün\"\n        }\n      }\n    }\n  },\n  \"components\": {\n    \"pagination\": {\n      \"text1\": \"Gösteriliyor\",\n      \"text2\": \"{count} öğe dışında\",\n      \"nextPage\": \"Sonraki sayfa\",\n      \"currentPage\": \"Geçerli sayfa\",\n      \"prevPage\": \"Önceki sayfa\",\n      \"of\": \"toplam {page}\"\n    }\n  }\n\n\n}\n"
  },
  {
    "path": "src/locales/tr/popup.json",
    "content": "{\n  \"recording\": {\n    \"stop\": \"Kaydı Durdur\",\n    \"title\": \"Kayıt\"\n  },\n  \"home\": {\n    \"record\": {\n      \"title\": \"İş Akışı Kaydı\",\n      \"button\": \"Kayıt\",\n      \"name\": \"İş Akışı Adı\",\n      \"selectBlock\": \"Başlamak için bir blok seçin\",\n      \"anotherBlock\": \"Bu bloktan başlatılamaz\",\n      \"tabs\": {\n        \"new\": \"Yeni İş Akışı\",\n        \"existing\": \"Mevcut İş Akışı\"\n      }\n    },\n    \"elementSelector\": {\n      \"name\": \"EÖğe Seçici\",\n      \"noAccess\": \"Bu siteye erişim izniniz yok\"\n    },\n    \"workflow\": {\n      \"new\": \"Yeni İş Akışı\",\n      \"rename\": \"İş Akışını Yeniden Adlandır\",\n      \"delete\": \"İş Akışını Sil\",\n      \"type\": {\n        \"host\": \"Ana Bilgisayar\",\n        \"local\": \"Yerel\"\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "src/locales/uk/blocks.json",
    "content": "{\n  \"collection\": {\n    \"blocks\": {\n      \"export-result\": {\n        \"name\": \"Експортувати результат\",\n        \"description\": \"Експортуйте результат збору як JSON\"\n      }\n    }\n  },\n  \"workflow\": {\n    \"blocks\": {\n      \"base\": {\n        \"title\": \"Блоки\",\n        \"moveToGroup\": \"Перемістити блок до групи блоків\",\n        \"selector\": \"Вибір елемента\",\n        \"selectorOptions\": \"Опції вибору\",\n        \"timeout\": \"Час очікування (мілісекунди)\",\n        \"noPermission\": \"У Automa недостатньо прав для виконання цієї дії\",\n        \"grantPermission\": \"Надати дозвіл\",\n        \"action\": \"Дія\",\n        \"element\": {\n          \"select\": \"Виберіть елемент\",\n          \"verify\": \"Підтвердити селектор\"\n        },\n        \"settings\": {\n          \"title\": \"Налаштування блоку\",\n          \"blockTimeout\": {\n            \"title\": \"Час очікування виконання блоку (мілісекунди)\",\n            \"description\": \"Максимальний час виконання блоку (0 для вимкнення)\"\n          },\n          \"line\": {\n            \"title\": \"Лінії\",\n            \"label\": \"Мітка\",\n            \"animated\": \"Анімований\",\n            \"select\": \"Виберіть лінію\",\n            \"to\": \"Лінія до блоку {name}\",\n            \"lineColor\": \"Колір\"\n          }\n        },\n        \"toggle\": {\n          \"enable\": \"Увімкнути блок\",\n          \"disable\": \"Вимкнути блок\"\n        },\n        \"onError\": {\n          \"info\": \"Ці правила застосовуватимуться, коли в блоці виникає помилка\",\n          \"button\": \"При помилці\",\n          \"title\": \"При виникненні помилки\",\n          \"retry\": \"Повторити дію\",\n          \"fallbackTitle\": \"Виконується, коли в блоці виникає помилка\",\n          \"times\": {\n            \"name\": \"Разів\",\n            \"description\": \"Кількість повторів дії\"\n          },\n          \"interval\": {\n            \"name\": \"Інтервал\",\n            \"description\": \"Інтервал часу між кожною спробою\",\n            \"second\": \"секунд\"\n          },\n          \"toDo\": {\n            \"error\": \"Викликати помилку\",\n            \"continue\": \"Продовжити flow\",\n            \"fallback\": \"Виконати резервний варіант\",\n            \"restart\": \"Перезапустити flow\"\n          },\n          \"insertData\": {\n            \"name\": \"Вставити дані\"\n          }\n        },\n        \"table\": {\n          \"checkbox\": \"Вставити в таблицю\",\n          \"select\": \"Виберіть стовпець\",\n          \"extraRow\": {\n            \"checkbox\": \"Додати додатковий ряд\",\n            \"placeholder\": \"Значення\",\n            \"title\": \"Значення додаткового ряду\"\n          }\n        },\n        \"findElement\": {\n          \"placeholder\": \"Знайти елемент по\",\n          \"options\": {\n            \"cssSelector\": \"Селектор CSS\",\n            \"xpath\": \"XPath\"\n          }\n        },\n        \"markElement\": {\n          \"title\": \"Елемент не буде вибрано, якщо він був вибраний раніше\",\n          \"text\": \"Позначити елемент\"\n        },\n        \"multiple\": {\n          \"title\": \"Виберати кілька елементів\",\n          \"text\": \"Кілька\"\n        },\n        \"waitSelector\": {\n          \"title\": \"Зачекайте на селектор\",\n          \"timeout\": \"Час очікування селектора (мс)\"\n        },\n        \"downloads\": {\n          \"onConflict\": {\n            \"uniquify\": \"Уніфікувати\",\n            \"overwrite\": \"Перезаписати\",\n            \"prompt\": \"Підказати\"\n          }\n        }\n      },\n      \"wait-connections\": {\n        \"name\": \"Зачекати підключення\",\n        \"description\": \"Зачекати на всі підключення, перш ніж перейти до наступного блоку\",\n        \"specificFlow\": \"Продовжити лише певний flow\",\n        \"selectFlow\": \"Вибрати flow\"\n      },\n      \"cookie\": {\n        \"name\": \"Cookie\",\n        \"description\": \"Отримати, встановити або видалити файли куки\",\n        \"types\": {\n          \"get\": \"Отримати куки\",\n          \"set\": \"Встановити куки\",\n          \"remove\": \"Видалити куки\",\n          \"getAll\": \"Отримати всі куки\"\n        },\n        \"useJson\": \"Використовувати формат JSON\"\n      },\n      \"note\": {\n        \"name\": \"Примітка\"\n      },\n      \"slice-variable\": {\n        \"name\": \"Зріз змінної\",\n        \"description\": \"Виділяє частину значення змінної\",\n        \"start\": \"Початковий індекс\",\n        \"end\": \"Кінцевий індекс\"\n      },\n      \"workflow-state\": {\n        \"workflow-state\": {\n          \"name\": \"Стан робочого процесу\",\n          \"description\": \"Управління станами робочих процесів\",\n          \"actions\": {\n            \"stop\": \"Зупинити робочі процеси\"\n          }\n        },\n        \"regex-variable\": {\n          \"name\": \"RegEx змінна\",\n          \"description\": \"Зіставлення значення змінної з регулярним виразом\"\n        },\n        \"data-mapping\": {\n          \"source\": \"Джерело\",\n          \"destination\": \"Призначення\",\n          \"name\": \"Відображення даних\",\n          \"edit\": \"Редагувати відображення даних\",\n          \"dataSource\": \"Джерело даних\",\n          \"description\": \"Відображення даних змінної або таблиці\",\n          \"addSource\": \"Додати джерело\",\n          \"addDestination\": \"Додати призначення\"\n        },\n        \"sort-data\": {\n          \"name\": \"Сортування даних\",\n          \"description\": \"Сортувати елементи даних\",\n          \"property\": \"Сортувати за властивістю елементів\",\n          \"addProperty\": \"Додати властивість\"\n        },\n        \"increase-variable\": {\n          \"name\": \"Збільшити змінну\",\n          \"description\": \"Збільшити значення змінної на певну величину\",\n          \"increase\": \"Збільшити на\"\n        },\n        \"notification\": {\n          \"name\": \"сповіщення\",\n          \"description\": \"Відобразити сповіщення\",\n          \"title\": \"Заголовок\",\n          \"message\": \"Повідомлення\",\n          \"imageUrl\": \"URL зображення (необов'язково)\",\n          \"iconUrl\": \"URL значка (необов’язково)\"\n        },\n        \"delete-data\": {\n          \"name\": \"Видалити дані\",\n          \"description\": \"Видалити дані таблиці або змінної\",\n          \"from\": \"Дані з\",\n          \"allColumns\": \"[Усі колонки]\"\n        },\n        \"log-data\": {\n          \"name\": \"Отримати дані журналу\",\n          \"description\": \"Отримати останні дані журналу робочого процесу\",\n          \"data\": \"Дані журналу\"\n        },\n        \"tab-url\": {\n          \"name\": \"Отримати URL вкладки\",\n          \"description\": \"Отримати URL вкладки\",\n          \"select\": \"Вибір вкладки\",\n          \"types\": {\n            \"active-tab\": \"Активна вкладка\",\n            \"all\": \"Усі вкладки\"\n          },\n          \"query\": {\n            \"title\": \"Запит\",\n            \"matchPatterns\": \"@:workflow.blocks.switch-tab.matchPattern (необов'язково)\",\n            \"tabTitle\": \"Назва вкладки (необов'язково)\"\n          }\n        },\n        \"reload-tab\": {\n          \"name\": \"Перезавантажити вкладку\",\n          \"description\": \"Перезавантажити активну вкладку\"\n        },\n        \"press-key\": {\n          \"name\": \"Натиснути клавішу\",\n          \"description\": \"Натиснути клавішу або комбінацію\",\n          \"target\": \"Цільовий елемент (необов'язково)\",\n          \"key\": \"Клавіша\",\n          \"detect\": \"Визначити клавішу\",\n          \"actions\": {\n            \"press-key\": \"Натисніть клавішу\",\n            \"multiple-keys\": \"Натиснути декілька клавіш\"\n          }\n        },\n        \"save-assets\": {\n          \"name\": \"Зберегти ресурси\",\n          \"description\": \"Зберегти ресурси (зображення, відео, аудіо або файл) з елемента або URL-адреси\",\n          \"filename\": \"Ім'я файлу (необов'язково)\",\n          \"saveDownloadIds\": \"Зберегти ідентифікатори завантажень елементів\",\n          \"contentTypes\": {\n            \"title\": \"Тип\",\n            \"element\": \"Медіа-елемент (зображення, аудіо або відео)\",\n            \"url\": \"URL\"\n          }\n        },\n        \"handle-dialog\": {\n          \"name\": \"Опрацювати діалог\",\n          \"description\": \"Приймає або відхиляє діалогове вікно, ініційоване JavaScript (сповіщення, підтвердження, підказка або перед завантаженням)\",\n          \"accept\": \"Діалогове вікно прийняття\",\n          \"promptText\": {\n            \"label\": \"Текст підказки (необов'язково)\",\n            \"description\": \"Текст, який потрібно ввести в діалоговому вікні підказки перед прийняттям\"\n          }\n        },\n        \"handle-download\": {\n          \"name\": \"Керувати завантаженням\",\n          \"description\": \"Керувати завантаженим файлом\",\n          \"timeout\": \"Час очікування (мілісекунди)\",\n          \"noPermission\": \"Немає дозволу на доступ до завантажень\",\n          \"onConflict\": \"При конфлікті\",\n          \"waitFile\": \"Зачекати, поки файл буде завантажений\",\n          \"downloadId\": \"Ідентифікатор завантаження файлу (необов'язково)\",\n          \"filePath\": \"Шлях до файлу\"\n        },\n        \"insert-data\": {\n          \"name\": \"Вставити дані\",\n          \"description\": \"Вставити дані в таблицю або змінну\"\n        },\n        \"clipboard\": {\n          \"name\": \"Буфер обміну\",\n          \"description\": \"Отримати скопійований текст із буфера обміну\",\n          \"data\": \"Дані буфера обміну\",\n          \"noPermission\": \"Немає дозволу на доступ до буфера обміну\",\n          \"grantPermission\": \"Надати дозвіл\",\n          \"copySelection\": \"Скопіювати виділений текст на сторінці\",\n          \"types\": {\n            \"get\": \"Отримати дані буфера обміну\",\n            \"insert\": \"Вставити текст у буфер обміну\"\n          }\n        },\n        \"hover-element\": {\n          \"name\": \"Навестись на елемент\",\n          \"description\": \"Навести курсор на елемент\"\n        },\n        \"create-element\": {\n          \"name\": \"Створити елемент\",\n          \"description\": \"Створити елемент і вставити його на сторінку\",\n          \"edit\": \"Редагувати елемент\",\n          \"wrap\": \"Загорнути елемент всередину\",\n          \"insertEl\": {\n            \"title\": \"Вставити елемент\",\n            \"items\": {\n              \"before\": \"Як перша дитина\",\n              \"after\": \"Як остання дитина\",\n              \"next-sibling\": \"Як наступний родич\",\n              \"prev-sibling\": \"Як попередній родич\",\n              \"replace\": \"Замінити цільовий елемент\"\n            }\n          }\n        },\n        \"upload-file\": {\n          \"name\": \"Завантажити файл\",\n          \"description\": \"Завантажити файл в елемент <input type=\\\"file\\\">\",\n          \"filePath\": \"URL або шлях до файлу\",\n          \"addFile\": \"Додати файл\",\n          \"onlyURL\": \"У браузері Firefox підтримується лише завантаження файлів з URL-адреси\",\n          \"requirement\": \"Прочитайте вимоги перед використанням цього блоку\",\n          \"noFileAccess\": \"Automa не має доступу до файлів\"\n        },\n        \"browser-event\": {\n          \"name\": \"Події браузера\",\n          \"description\": \"Виконує наступний блок, коли спрацьовує зазначена подія\",\n          \"events\": \"Події\",\n          \"timeout\": \"Час очікування (мілісекунди)\",\n          \"activeTabLoaded\": \"Активна вкладка\",\n          \"setAsActiveTab\": \"Установити як активну вкладку\"\n        },\n        \"blocks-group-2\": {\n          \"name\": \"@:workflow.blocks.blocks-group.name 2\",\n          \"description\": \"@:workflow.blocks.blocks-group.description\"\n        },\n        \"blocks-group\": {\n          \"name\": \"Група блоків\",\n          \"groupName\": \"Назва групи\",\n          \"description\": \"Групування блоків\",\n          \"dropText\": \"Перетягніть блок сюди\",\n          \"cantAdd\": \"Неможливо додати блок \\\"{blockName}\\\" до групи\"\n        },\n        \"trigger\": {\n          \"name\": \"Тригер\",\n          \"description\": \"Блок, де почнеться виконання робочого процесу\",\n          \"addTime\": \"Додати час\",\n          \"selectDay\": \"Виберіть день\",\n          \"timeExist\": \"Ви вже додали тригер о {time} {day}\",\n          \"fixedDelay\": \"Фіксована затримка\",\n          \"contextMenus\": {\n            \"noPermission\": \"Для роботи цього тригера потрібен дозвіл \\\"contextMenus\\\"\",\n            \"grantPermission\": \"Надати дозвіл\",\n            \"appearIn\": \"З'явиться в\",\n            \"contextName\": \"Назва робочого процесу в контекстному меню\"\n          },\n          \"days\": [\n            \"Понеділок\",\n            \"Вівторок\",\n            \"Середа\",\n            \"Четвер\",\n            \"П`ятниця\",\n            \"Субота\",\n            \"Неділя\"\n          ],\n          \"useRegex\": \"Використовувати регулярний вираз\",\n          \"shortcut\": {\n            \"tooltip\": \"Відстежити комбінацію\",\n            \"stopRecord\": \"Зупинити запис\",\n            \"checkboxTitle\": \"Виконувати ярлик, навіть коли ви перебуваєте в елементі введення\",\n            \"checkbox\": \"Активний під час введення\",\n            \"note\": \"Примітка: комбінація клавіш працює, лише коли ви перебуваєте на веб-сторінці\"\n          },\n          \"forms\": {\n            \"triggerWorkflow\": \"Активувати робочі процеси\",\n            \"interval\": \"Інтервал (хвилини)\",\n            \"delay\": \"Затримка (хвилини)\",\n            \"date\": \"Дата\",\n            \"time\": \"Час\",\n            \"url\": \"URL або регулярний вираз\",\n            \"shortcut\": \"Комбінація клавіш\",\n            \"cron-expression\": \"Вираз Cron\"\n          },\n          \"element-change\": {\n            \"target\": \"Цільовий елемент для спостереження\",\n            \"optionsInfo\": \"Мутація якого елемента запустить робочий процес\",\n            \"targetWebsite\": \"Шаблон відповідності веб-сайту, де знаходиться цільовий елемент (клацніть, щоб побачити більше прикладів шаблонів відповідності)\",\n            \"baseEl\": {\n              \"title\": \"Базовий елемент (необов'язково)\",\n              \"description\": \"Automa перезапустить спостереження за цільовим елементом, коли цей елемент зміниться\"\n            },\n            \"subtree\": {\n              \"title\": \"Включити піддерево\",\n              \"description\": \"Поширити моніторинг на все піддерево цільового елемента\"\n            },\n            \"childList\": {\n              \"title\": \"Список дітей\",\n              \"description\": \"Моніторинг додавання нових дочірніх елементів або видалення існуючих\"\n            },\n            \"attributes\": {\n              \"title\": \"Атрибути\",\n              \"description\": \"Стежте за змінами значень атрибутів цільового елемента\"\n            },\n            \"attributeFilter\": {\n              \"title\": \"Фільтр атрибутів\",\n              \"separate\": \"Використовуйте коми (,) для розділення імен атрибутів\",\n              \"description\": \"Моніторинг лише певних атрибутів (залиште порожнім, щоб контролювати всі)\"\n            },\n            \"characterData\": {\n              \"title\": \"Дані символів\",\n              \"description\": \"Моніторинг змін даних/тексту у цільовому елементі\"\n            }\n          },\n          \"items\": {\n            \"manual\": \"Вручну\",\n            \"interval\": \"Інтервал\",\n            \"cron-job\": \"Завдання Cron\",\n            \"date\": \"В певну дату\",\n            \"context-menu\": \"Контекстне меню\",\n            \"element-change\": \"При зміні елемента\",\n            \"specific-day\": \"В певний день\",\n            \"visit-web\": \"Під час відвідування веб-сайту\",\n            \"on-startup\": \"Під час запуску браузера\",\n            \"keyboard-shortcut\": \"Комбінація клавіш\"\n          }\n        },\n        \"execute-workflow\": {\n          \"name\": \"Виконати робочий процес\",\n          \"overwriteNote\": \"Це перезапише глобальні дані вибраного робочого процесу\",\n          \"select\": \"Виберіть робочий процес\",\n          \"executeId\": \"Ідентифікатор виконання (необов'язково)\",\n          \"description\": \"Виконати сторонній робочий процес\",\n          \"insertAllVars\": \"Використовувати всі змінні робочого процесу\",\n          \"insertVars\": \"Вставити змінні робочого процесу\",\n          \"useCommas\": \"Використовуйте коми для відокремлення імені змінної\"\n        },\n        \"google-sheets\": {\n          \"name\": \"Google Таблиці\",\n          \"description\": \"Читати або оновлювати дані Google Таблиць\",\n          \"previewData\": \"Попередній перегляд даних\",\n          \"firstRow\": \"Використовувати перший рядок як ключі\",\n          \"keysAsFirstRow\": \"Використовувати клавіші як перший рядок\",\n          \"insertData\": \"Вставити дані\",\n          \"valueInputOption\": \"Параметр введення значення\",\n          \"insertDataOption\": \"Вставити параметр даних\",\n          \"rangeToSearch\": \"Діапазон для початку пошуку\",\n          \"dataFrom\": {\n            \"label\": \"Дані з\",\n            \"options\": {\n              \"data-columns\": \"Таблиця\",\n              \"custom\": \"Спеціальний\"\n            }\n          },\n          \"refKey\": {\n            \"label\": \"Довідковий ключ (необов'язково)\",\n            \"placeholder\": \"Назва ключа\"\n          },\n          \"spreadsheetId\": {\n            \"label\": \"Ідентифікатор електронної таблиці\",\n            \"link\": \"Дивіться, як отримати ідентифікатор електронної таблиці\"\n          },\n          \"range\": {\n            \"label\": \"Діапазон\",\n            \"link\": \"Натисніть, щоб переглянути більше прикладів\"\n          },\n          \"select\": {\n            \"get\": \"Отримати значення клітинок електронної таблиці\",\n            \"getRange\": \"Отримати діапазон електронної таблиці\",\n            \"update\": \"Оновити значення клітинок електронної таблиці\",\n            \"append\": \"Додати значення клітинок електронної таблиці\",\n            \"clear\": \"Очистити значення клітинок електронної таблиці\"\n          }\n        },\n        \"active-tab\": {\n          \"name\": \"Активна вкладка\",\n          \"description\": \"Установити вкладку, на якій ви перебуваєте, як активну\"\n        },\n        \"proxy\": {\n          \"name\": \"Проксі\",\n          \"description\": \"Установити проксі браузера\",\n          \"clear\": \"Очистити всі проксі\",\n          \"bypass\": {\n            \"label\": \"Список обходу\",\n            \"note\": \"Використовуйте коми (,) для розділення URL\"\n          }\n        },\n        \"new-window\": {\n          \"name\": \"Нове вікно\",\n          \"description\": \"Створити нове вікно\",\n          \"top\": \"Вгорі\",\n          \"left\": \"Ліворуч\",\n          \"height\": \"Висота\",\n          \"width\": \"Ширина\",\n          \"note\": \"Примітка: використовуйте 0 для вимкнення\",\n          \"position\": \"Позиція вікна\",\n          \"size\": \"Розмір вікна\",\n          \"windowState\": {\n            \"placeholder\": \"Стан вікна\",\n            \"options\": {\n              \"normal\": \"Звичайний\",\n              \"minimized\": \"Згорнутий\",\n              \"maximized\": \"Розгорнутий\",\n              \"fullscreen\": \"На весь екран\"\n            }\n          },\n          \"incognito\": {\n            \"text\": \"Установити як вікно анонімного перегляду\",\n            \"note\": \"Ви повинні спочатку ввімкнути 'Дозволити в анонімному режимі' для цього додатка\"\n          }\n        },\n        \"go-back\": {\n          \"name\": \"Повернутися\",\n          \"description\": \"Повернутися до попередньої сторінки\"\n        },\n        \"forward-page\": {\n          \"name\": \"Ідти вперед\",\n          \"description\": \"Перейти до наступної сторінки\"\n        },\n        \"close-tab\": {\n          \"name\": \"Закрити вкладку/вікно\",\n          \"description\": \"\",\n          \"url\": \"Відповідність шаблонам\",\n          \"activeTab\": \"Закрити активну вкладку\",\n          \"allWindows\": \"Закрити всі вікна\"\n        },\n        \"event-click\": {\n          \"name\": \"Натиснути на елемент\",\n          \"description\": \"\"\n        },\n        \"delay\": {\n          \"name\": \"Затримка\",\n          \"description\": \"Додати затримку перед виконанням наступного блоку\",\n          \"input\": {\n            \"title\": \"Затримка в мілісекундах\",\n            \"placeholder\": \"(мілісекунди)\"\n          }\n        },\n        \"parameter-prompt\": {\n          \"name\": \"Запросити параметри\"\n        },\n        \"get-text\": {\n          \"name\": \"Отримати текст\",\n          \"description\": \"Отримати текст з елемента\",\n          \"checkbox\": \"Вставити в таблицю\",\n          \"includeTags\": \"Включити теги HTML\",\n          \"prefixText\": {\n            \"placeholder\": \"Текстовий префікс\",\n            \"title\": \"Додати префікс до тексту\"\n          },\n          \"suffixText\": {\n            \"placeholder\": \"Текстовий суфікс\",\n            \"title\": \"Додати суфікс до тексту\"\n          }\n        },\n        \"export-data\": {\n          \"name\": \"Експорт даних\",\n          \"description\": \"Експорт даних робочого процесу\",\n          \"exportAs\": \"Експортувати як\",\n          \"refKey\": \"Довідковий ключ\",\n          \"bomHeader\": \"Додати UTF-8 BOM\",\n          \"dataToExport\": {\n            \"placeholder\": \"Дані для експорту\",\n            \"options\": {\n              \"data-columns\": \"Таблиця\",\n              \"google-sheets\": \"Google Таблиці\",\n              \"variable\": \"Змінна\"\n            }\n          }\n        },\n        \"element-scroll\": {\n          \"name\": \"Елемент прокручування\",\n          \"description\": \"\",\n          \"scrollY\": \"Прокрутити по вертикалі\",\n          \"scrollX\": \"Прокрутити по горизонталі\",\n          \"intoView\": \"Прокрутити до перегляду\",\n          \"smooth\": \"Плавне прокручування\",\n          \"incScrollX\": \"Збільшити горизонтальне прокручування\",\n          \"incScrollY\": \"Збільшити вертикальне прокручування\"\n        },\n        \"switch-tab\": {\n          \"name\": \"Переключити вкладку\",\n          \"description\": \"Перемикатися між вкладками\",\n          \"matchPattern\": \"Відповідність шаблонам\",\n          \"url\": \"URL нової вкладки\",\n          \"createIfNoMatch\": \"Створити, якщо немає відповідності\"\n        },\n        \"new-tab\": {\n          \"name\": \"Нова вкладка\",\n          \"description\": \"\",\n          \"url\": \"URL нової вкладки\",\n          \"customUserAgent\": \"Використовувати спеціальний User-Agent\",\n          \"activeTab\": \"Встановити як активну вкладку\",\n          \"tabToGroup\": \"Додати вкладку до групи\",\n          \"waitTabLoaded\": \"Зачекати, поки вкладка завантажиться\",\n          \"updatePrevTab\": {\n            \"title\": \"Використовувати раніше відкриту нову вкладку замість створення нової\",\n            \"text\": \"Оновити раніше відкриту вкладку\"\n          }\n        },\n        \"link\": {\n          \"name\": \"Посилання\",\n          \"description\": \"Відкрити елемент посилання\",\n          \"openInNewTab\": \"Відкрити в новій вкладці\"\n        },\n        \"attribute-value\": {\n          \"name\": \"Значення атрибута\",\n          \"description\": \"Отримати значення атрибута елемента\",\n          \"forms\": {\n            \"name\": \"Ім'я атрибута\",\n            \"checkbox\": \"Вставити в таблицю\",\n            \"column\": \"Вибрати стовпець\",\n            \"extraRow\": {\n              \"checkbox\": \"Додати додатковий рядок\",\n              \"placeholder\": \"Значення\",\n              \"title\": \"Значення додаткового рядка\"\n            }\n          }\n        },\n        \"forms\": {\n          \"name\": \"Форми\",\n          \"description\": \"\",\n          \"selected\": \"Вибрано\",\n          \"type\": \"Тип форми\",\n          \"getValue\": \"Отримати значення форми\",\n          \"text-field\": {\n            \"name\": \"Текстове поле\",\n            \"value\": \"Значення\",\n            \"clearValue\": \"Очистити значення форми\",\n            \"delay\": {\n              \"placeholder\": \"Затримка\",\n              \"label\": \"Затримка введення (мілісекунди) (0 для вимкнення)\"\n            }\n          },\n          \"select\": {\n            \"name\": \"Вибрати\"\n          },\n          \"radio\": {\n            \"name\": \"Радіо\"\n          },\n          \"checkbox\": {\n            \"name\": \"Прапорець\"\n          }\n        },\n        \"repeat-task\": {\n          \"name\": \"Повторити завдання\",\n          \"description\": \"\",\n          \"times\": \"разів\",\n          \"repeatFrom\": \"Повторити з\"\n        },\n        \"javascript-code\": {\n          \"name\": \"Код JavaScript\",\n          \"description\": \"Виконайте свій код JavaScript на веб-сторінці\",\n          \"availabeFuncs\": \"Доступні функції:\",\n          \"removeAfterExec\": \"Видалити після виконання блоку\",\n          \"everyNewTab\": \"Виконувати в кожній новій вкладці\",\n          \"context\": {\n            \"name\": \"Контекст виконання\",\n            \"items\": {\n              \"website\": \"Активна вкладка\",\n              \"background\": \"Тло\"\n            }\n          },\n          \"modal\": {\n            \"tabs\": {\n              \"code\": \"Код JavaScript\",\n              \"preloadScript\": \"Попереднє завантаження сценарію\"\n            }\n          },\n          \"timeout\": {\n            \"placeholder\": \"Час очікування (мілісекунди)\",\n            \"title\": \"Час очікування виконання коду JavaScript\"\n          }\n        },\n        \"trigger-event\": {\n          \"name\": \"Викликати подію\",\n          \"description\": \"Викликати подію єлемента\",\n          \"selectEvent\": \"Вибрати подію\"\n        },\n        \"conditions\": {\n          \"name\": \"Умови\",\n          \"add\": \"Додати шлях\",\n          \"retryConditions\": \"Повторити, якщо не виконано жодних умов\",\n          \"description\": \"Умовний блок\",\n          \"refresh\": \"Оновити підключення умов\",\n          \"fallbackTitle\": \"Виконується, якщо всі порівняння не відповідають вимогам\",\n          \"equals\": \"Дорівнює\",\n          \"gt\": \"Більше ніж\",\n          \"gte\": \"Більше або дорівнює\",\n          \"lt\": \"Менше ніж\",\n          \"lte\": \"Менше або дорівнює\",\n          \"ne\": \"Не дорівнює\",\n          \"contains\": \"Містить\"\n        },\n        \"element-exists\": {\n          \"name\": \"Елемент існує\",\n          \"description\": \"Перевірити, чи існує елемент\",\n          \"selector\": \"Селектор елемента\",\n          \"fallbackTitle\": \"Виконується, коли елемент не існує\",\n          \"throwError\": \"Виклакати помилку, якщо не існує\",\n          \"tryFor\": {\n            \"title\": \"Скільки разів перевірити, чи існує елемент\",\n            \"label\": \"Спробувати\"\n          },\n          \"timeout\": {\n            \"label\": \"Час очікування (мілісекунди)\",\n            \"title\": \"Час очікування для кожної спроби\"\n          }\n        },\n        \"webhook\": {\n          \"name\": \"HTTP запит\",\n          \"description\": \"Зробити HTTP-запит\",\n          \"contentType\": \"Тип вмісту\",\n          \"method\": \"Метод запиту\",\n          \"url\": \"URL запиту\",\n          \"fallback\": \"Виконується, коли HTTP-запит не вдається\",\n          \"buttons\": {\n            \"header\": \"Додати заголовок\"\n          },\n          \"timeout\": {\n            \"placeholder\": \"Час очікування\",\n            \"title\": \"Час очікування виконання запиту HTTP (мс)\"\n          },\n          \"tabs\": {\n            \"headers\": \"Заголовки\",\n            \"body\": \"Тіло\",\n            \"response\": \"Відповідь\"\n          }\n        },\n        \"while-loop\": {\n          \"name\": \"Цикл While\",\n          \"description\": \"Виконує блоки, поки виконується умова\",\n          \"editCondition\": \"Редагувати умову\",\n          \"fallback\": \"Виконується, коли умова хибна\"\n        },\n        \"loop-elements\": {\n          \"name\": \"Елементи циклу\",\n          \"description\": \"Ітерація по елементах\",\n          \"loadMore\": \"Завантажити більше елементів\",\n          \"scrollToBottom\": \"Прокрутити вниз\",\n          \"scrollToTop\": \"Прокрутити вгору\",\n          \"actions\": {\n            \"none\": \"Жодного\",\n            \"click-element\": \"Натисніть елемент\",\n            \"scroll\": \"Прокрутити вниз\",\n            \"click-link\": \"Натисніть посилання\",\n            \"scroll-up\": \"Прокрутити вгору\"\n          }\n        },\n        \"loop-data\": {\n          \"name\": \"Дані циклу\",\n          \"description\": \"Перехід по таблиці або вашим даним\",\n          \"loopId\": \"Ідентифікатор циклу\",\n          \"refKey\": \"Довідковий ключ\",\n          \"startIndex\": \"Почати з індексу\",\n          \"resumeLastWorkflow\": \"Відновити останній робочий процес\",\n          \"reverse\": \"Зворотний порядок циклу\",\n          \"modal\": {\n            \"fileTooLarge\": \"Файл завеликий для редагування\",\n            \"maxFile\": \"Максимальний розмір файлу 1 Мб\",\n            \"options\": {\n              \"firstRow\": \"Використовувати перший рядок як ключі\"\n            }\n          },\n          \"buttons\": {\n            \"clear\": \"Очистити дані\",\n            \"insert\": \"Вставити дані\",\n            \"import\": \"Імпортувати файл\"\n          },\n          \"maxLoop\": {\n            \"title\": \"Максимальна кількість даних для циклу\",\n            \"label\": \"Максимум даних для циклу (0 для вимкнення)\"\n          },\n          \"loopThrough\": {\n            \"placeholder\": \"Пройтись по\",\n            \"fromNumber\": \"З числа\",\n            \"toNumber\": \"У число\",\n            \"options\": {\n              \"numbers\": \"Числа\",\n              \"variable\": \"Змінна\",\n              \"data-columns\": \"Таблиця\",\n              \"table\": \"Таблиця\",\n              \"custom-data\": \"Користувацькі дані\",\n              \"google-sheets\": \"Google Таблиці\",\n              \"elements\": \"Елементи\"\n            }\n          }\n        },\n        \"loop-breakpoint\": {\n          \"name\": \"Точка зупинки циклу\",\n          \"description\": \"Щоб вказати, де має зупинитися блок циклу\"\n        },\n        \"take-screenshot\": {\n          \"name\": \"Зробити знімок екрана\",\n          \"fullPage\": \"Зробити знімок екрана на всю сторінку\",\n          \"description\": \"Зробити знімок екрана активної вкладки\",\n          \"imageQuality\": \"Якість зображення\",\n          \"saveToColumn\": \"Вставити знімок екрана в таблицю\",\n          \"saveToComputer\": \"Зберегти знімок екрана на комп'ютер\",\n          \"types\": {\n            \"title\": \"Зробити знімок екрана\",\n            \"page\": \"Сторінка\",\n            \"fullpage\": \"Повна сторінка\",\n            \"element\": \"Елемент\"\n          }\n        },\n        \"switch-to\": {\n          \"name\": \"Переключити frame\",\n          \"description\": \"Переключатися між головним вікном та iframe\",\n          \"iframeSelector\": \"Селектор елементів\",\n          \"windowTypes\": {\n            \"main\": \"Головне вікно\",\n            \"iframe\": \"Iframe\"\n          }\n        }\n      },\n      \"execute-workflow\": {\n        \"insertAllGlobalData\": \"Використовувати весь поточний робочий процес globalData\"\n      },\n      \"debugMode\": {\n        \"title\": \"Режим налагодження\",\n        \"description\": \"Виконати поточний Блоки за допомогою протоколу Chrome DevTools\"\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "src/locales/uk/common.json",
    "content": "{\n  \"common\": {\n    \"dashboard\": \"Dashboard\",\n    \"workflow\": \"Workflow | Workflows\",\n    \"collection\": \"Колекція | Колекції\",\n    \"log\": \"Журнал | Журнали\",\n    \"block\": \"Блок | Блоки\",\n    \"schedule\": \"Графік\",\n    \"folder\": \"Папка | Папки\",\n    \"new\": \"Новий\",\n    \"docs\": \"Документація\",\n    \"search\": \"Пошук\",\n    \"example\": \"Приклад | Приклади\",\n    \"import\": \"Імпорт\",\n    \"export\": \"Експорт\",\n    \"rename\": \"Перейменувати\",\n    \"execute\": \"Виконати\",\n    \"delete\": \"Видалити\",\n    \"cancel\": \"Скасувати\",\n    \"settings\": \"Налаштування\",\n    \"options\": \"Опції\",\n    \"confirm\": \"Підтвердити\",\n    \"name\": \"Ім'я\",\n    \"all\": \"Всі\",\n    \"add\": \"Додати\",\n    \"save\": \"Зберегти\",\n    \"data\": \"дані\",\n    \"stop\": \"Стоп\",\n    \"action\": \"Дія | Дії\",\n    \"packages\": \"Пакети\",\n    \"storage\": \"Зберігання\",\n    \"editor\": \"Редактор\",\n    \"running\": \"Запущений\",\n    \"globalData\": \"Глобальні дані\",\n    \"fileName\": \"Ім'я файлу\",\n    \"description\": \"Опис\",\n    \"disable\": \"Вимкнути\",\n    \"disabled\": \"Вимкнено\",\n    \"enable\": \"Увімкнути\",\n    \"fallback\": \"Резервний\",\n    \"update\": \"Оновити\",\n    \"feature\": \"Функція\",\n    \"duplicate\": \"Дубліковати\",\n    \"password\": \"Пароль\",\n    \"category\": \"Категорія\",\n    \"optional\": \"Необов'язково\"\n  },\n  \"message\": {\n    \"noBlock\": \"Жодного блоку\",\n    \"noData\": \"Дані для відображення відсутні\",\n    \"noTriggerBlock\": \"Не вдається знайти тригерний блок\",\n    \"useDynamicData\": \"Дізнайтеся, як додавати динамічні дані\",\n    \"delete\": \"Ви впевнені, що хочете видалити \\\"{name}\\\"?\",\n    \"empty\": \"Ой... Схоже, у вас немає жодного елемента\",\n    \"maxSizeExceeded\": \"Розмір файлу перевищив максимально допустимий\",\n    \"notSaved\": \"Ви дійсно хочете піти? У вас є незбережені зміни!\",\n    \"somethingWrong\": \"Щось пішло не так\",\n    \"limitExceeded\": \"Ви перевищили ліміт\"\n  },\n  \"sort\": {\n    \"sortBy\": \"Сортувати за\",\n    \"name\": \"Ім'я\",\n    \"createdAt\": \"Дата створення\",\n    \"mostUsed\": \"Найбільш використовувані\"\n  },\n  \"logStatus\": {\n    \"stopped\": \"зупинено\",\n    \"error\": \"помилка\",\n    \"success\": \"успіх\"\n  }\n}\n"
  },
  {
    "path": "src/locales/uk/newtab.json",
    "content": "{\n  \"home\": {\n    \"viewAll\": \"Переглянути все\",\n    \"communities\": \"Спільноти\"\n  },\n  \"welcome\": {\n    \"title\": \"Ласкаво просимо до Automa! 🎉\",\n    \"text\": \"Почніть з перегляду документації або з робочих процесів в Automa Marketplace.\",\n    \"marketplace\": \"Marketplace\"\n  },\n  \"packages\": {\n    \"name\": \"Пакет | Пакети\",\n    \"add\": \"Додати пакет\",\n    \"icon\": \"Піктограма пакета\",\n    \"open\": \"Відкрити пакети\",\n    \"new\": \"Новий пакет\",\n    \"set\": \"Установити як пакет\",\n    \"settings\": {\n      \"asBlock\": \"Установити пакет як блок\"\n    },\n    \"categories\": {\n      \"my\": \"Мої пакети\",\n      \"installed\": \"Встановлені пакети\"\n    }\n  },\n  \"scheduledWorkflow\": {\n    \"title\": \"Заплановані робочі процеси\",\n    \"nextRun\": \"Наступний запуск\",\n    \"active\": \"Активний\",\n    \"refresh\": \"Оновити\",\n    \"schedule\":{\n      \"title\": \"Розклад\",\n      \"types\": {\n        \"everyDay\": \"Щодня\",\n        \"general\": \"Кожного {time}\",\n        \"interval\": \"Кожні {time} хвилин\"\n      }\n    }\n  },\n  \"storage\": {\n    \"title\": \"Сховище\",\n    \"table\": {\n      \"add\": \"Додати таблицю\",\n      \"createdAt\": \"Створено о\",\n      \"modifiedAt\": \"Змінено о\",\n      \"rowsCount\": \"Кількість рядків\",\n      \"delete\": \"Видалити таблицю\"\n    }\n  },\n  \"credential\": {\n    \"title\": \"Облікові дані | Облікові дані\",\n    \"add\": \"Додати облікові дані\",\n    \"use\": {\n      \"title\": \"Використані облікові дані\",\n      \"description\": \"Цей робочий процес використовує ці облікові дані\"\n    }\n  },\n  \"workflowPermissions\": {\n    \"title\": \"Дозволи робочого процесу\",\n    \"description\": \"Для належної роботи цього робочого процесу потрібні ці дозволи\",\n    \"contextMenus\": {\n      \"title\": \"Контекстне меню\",\n      \"description\": \"Щоб виконати робочий процес через контекстне меню\"\n    },\n    \"clipboardRead\": {\n      \"title\": \"Буфер обміну\",\n      \"description\": \"Для доступу до даних буфера обміну\"\n    },\n    \"notifications\": {\n      \"title\": \"Сповіщення\",\n      \"description\": \"Для відображення сповіщення\"\n    },\n    \"downloads\": {\n      \"title\": \"Завантажити\",\n      \"description\": \"Збереження ресурсів сторінки та перейменування завантаженого файлу\"\n    },\n    \"cookies\": {\n      \"title\": \"Файли cookie\",\n      \"description\": \"Прочитати, встановити або видалити файли cookie\"\n    }\n  },\n  \"updateMessage\": {\n    \"text1\": \"Automa оновлена до v{version},\",\n    \"text2\": \"подивитися, що нового.\"\n  },\n  \"workflows\": {\n    \"folder\": {\n      \"new\": \"Нова папка\",\n      \"name\": \"Назва папки\",\n      \"delete\": \"Видалити папку\",\n      \"rename\": \"Перейменувати папку\"\n    }\n  },\n  \"auth\": {\n    \"title\": \"Аутентифікація\",\n    \"signIn\": \"Увійти\",\n    \"username\": \"Вам потрібно спочатку встановити ім'я користувача\",\n    \"clickHere\": \"Натисніть тут\",\n    \"text\": \"Вам потрібно ввійти, перш ніж ви зможете це зробити\"\n  },\n  \"running\": {\n    \"start\": \"Розпочато {date}\",\n    \"message\": \"Відображаються лише останні 5 журналів\"\n  },\n  \"settings\": {\n    \"theme\": \"Тема\",\n    \"shortcuts\": {\n      \"duplicate\": \"Комбінація вже використовується \\\"{name}\\\"\"\n    },\n    \"editor\": {\n      \"title\": \"Назва\",\n      \"curvature\": {\n        \"title\": \"Кривизна лінії\",\n        \"line\": \"Лінія\",\n        \"reroute\": \"Перенаправити\",\n        \"rerouteFirstLast\": \"Змінити перший і останній пункт\"\n      },\n      \"arrow\": {\n        \"title\": \"Стрілка лінії\",\n        \"description\": \"Додати стрілку в кінці рядка\"\n      },\n      \"snapGrid\": {\n        \"title\": \"Прив'язати до сітки\",\n        \"description\": \"Прив'язка до сітки під час переміщення блоку\"\n      }\n    },\n    \"deleteLog\": {\n      \"title\": \"Автоматичне видалення журналів робочого процесу\",\n      \"after\": \"Видалити після\",\n      \"deleteAfter\": {\n        \"never\": \"Ніколи\",\n        \"days\": \"{day} днів\"\n      }\n    },\n    \"language\": {\n      \"label\": \"Мова\",\n      \"helpTranslate\": \"Не можете знайти свою мову? Допоможіть перекласти.\",\n      \"reloadPage\": \"Перезавантажте сторінку, щоб зміни набули чинності\"\n    },\n    \"menu\": {\n      \"backup\": \"Резервна копія робочих процесів\",\n      \"editor\": \"Редактор\",\n      \"general\": \"Загальні\",\n      \"shortcuts\": \"Комбінації клавіш\",\n      \"about\": \"Про додаток\"\n    },\n    \"backupWorkflows\": {\n      \"title\": \"Локальна резервна копія\",\n      \"invalidPassword\": \"Невірний пароль\",\n      \"workflowsAdded\": \"додано {count} робочих процесів\",\n      \"name\": \"Резервна копія робочих процесів\",\n      \"needSignin\": \"Вам потрібно спочатку ввійти\",\n      \"backup\": {\n        \"button\": \"Резервне копіювання\",\n        \"encrypt\": \"Шифрувати паролем\"\n      },\n      \"restore\": {\n        \"title\": \"Відновити робочі процеси\",\n        \"button\": \"Відновити\",\n        \"update\": \"Оновити, якщо робочий процес існує\"\n      },\n      \"cloud\": {\n        \"buttons\": {\n          \"local\": \"Місцевий\",\n          \"cloud\": \"Хмара\"\n        },\n        \"location\": \"Розташування\",\n        \"delete\": \"Видалити резервну копію\",\n        \"title\": \"Резервне копіювання у хмару\",\n        \"sync\": \"Синхронізувати\",\n        \"lastSync\": \"Остання синхронізація\",\n        \"lastBackup\": \"Останнє резервне копіювання\",\n        \"select\": \"Виберіть робочі процеси\",\n        \"storedWorkflows\": \"Робочі процеси, які зберігаються у хмарі\",\n        \"selected\": \"Вибрано\",\n        \"selectText\": \"Виберіть робочі процеси, для яких потрібно створити резервну копію\",\n        \"selectAll\": \"Вибрати все\",\n        \"deselectAll\": \"Скасувати вибір усіх\",\n        \"needSelectWorkflow\": \"Вам потрібно вибрати робочі процеси, для яких ви хочете створити резервну копію\"\n      }\n    }\n  },\n  \"workflow\": {\n    \"previewMode\": {\n      \"title\": \"Режим попереднього перегляду\",\n      \"description\": \"Ви перебуваєте в режимі попереднього перегляду, внесені вами зміни не будуть збережені\"\n    },\n    \"pinWorkflow\": {\n      \"pin\": \"Закріпити робочий процес\",\n      \"unpin\": \"Відкріпити робочий процес\",\n      \"pinned\": \"Закріплені робочі процеси\"\n    },\n    \"parameters\": {\n      \"add\": \"Додати параметр\",\n      \"preferInTab\": \"Надавати перевагу параметрам введення на вкладці\"\n    },\n    \"my\": \"Мої робочі процеси\",\n    \"import\": \"Імпортувати робочий процес\",\n    \"new\": \"Новий робочий процес\",\n    \"delete\": \"Видалити робочий процес\",\n    \"browse\": \"Перегляд робочих процесів\",\n    \"name\": \"Назва робочого процесу\",\n    \"rename\": \"Перейменувати робочий процес\",\n    \"backupCloud\": \"Резервне копіювання робочого процесу у хмару\",\n    \"add\": \"Додати робочий процес\",\n    \"clickToEnable\": \"Натисніть, щоб увімкнути\",\n    \"toggleSidebar\": \"Перемкнути стан бічної панелі\",\n    \"cantEdit\": \"Неможливо редагувати спільний робочий процес\",\n    \"undo\": \"Скасувати\",\n    \"redo\": \"Повторити\",\n    \"autoAlign\": {\n      \"title\": \"Автоматичне вирівнювання\"\n    },\n    \"blocksFolder\": {\n      \"title\": \"Папка блоків\",\n      \"add\": \"Додати блоки до папки\",\n      \"save\": \"Зберегти до папки\"\n    },\n    \"searchBlocks\": {\n      \"title\": \"Пошук блоків у редакторі\"\n    },\n    \"conditionBuilder\": {\n      \"title\": \"Конструктор умов\",\n      \"add\": \"Додати умову\",\n      \"and\": \"І\",\n      \"or\": \"АБО\",\n      \"topAwait\": \"Підтримка високорівневого очікування та функції \\\"automaRefData\\\"\"\n    },\n    \"host\": {\n      \"title\": \"Розмістити робочий процес\",\n      \"set\": \"Установити як робочий процес хоста\",\n      \"id\": \"Ідентифікатор хоста\",\n      \"add\": \"Додати розміщений робочий процес\",\n      \"sync\": {\n        \"title\": \"Синхронізувати\",\n        \"description\": \"Синхронізувати з робочим процесом хоста\"\n      },\n      \"messages\": {\n        \"hostExist\": \"Ви вже додали цей хост\",\n        \"notFound\": \"Не вдається знайти розміщений робочий процес з ідентифікатором \\\"{id}\\\"\",\n        \"successAdded\": \"Розміщений робочий процес успішно доданий з ідентифікатором \\\"{id}\\\"\"\n      }\n    },\n    \"type\": {\n      \"local\": \"Місцевий\",\n      \"shared\": \"Спільний\",\n      \"host\": \"Хост\"\n    },\n    \"unpublish\": {\n      \"title\": \"Скасувати публікацію робочого процесу\",\n      \"button\": \"Скасувати публікацію\",\n      \"body\": \"Ви впевнені, що хочете скасувати публікацію робочого процесу \\\"{name}\\\"?\"\n    },\n    \"share\": {\n      \"url\": \"Поділитися URL\",\n      \"publish\": \"Опублікувати\",\n      \"sharedAs\": \"Спільно доступний як \\\"{name}\\\"\",\n      \"title\": \"Поділитися робочим процесом\",\n      \"download\": \"Зберегти робочий процес локально\",\n      \"edit\": \"Редагувати опис\",\n      \"fetchLocal\": \"Отримати локальний робочий процес\",\n      \"update\": \"Оновити\",\n      \"unpublish\": \"Скасувати публікацію\",\n      \"linkCopied\": \"Посилання скопійовано в буфер обміну\"\n    },\n    \"variables\": {\n      \"title\": \"Змінна | Змінні\",\n      \"name\": \"Ім'я змінної\",\n      \"assign\": \"Призначити змінній\"\n    },\n    \"protect\": {\n      \"title\": \"Захист робочого процесу\",\n      \"remove\": \"Зняти захист\",\n      \"button\": \"Захистити\",\n      \"note\": \"Примітка: цей пароль знадобиться пізніше для редагування або видалення робочого процесу\"\n    },\n    \"locked\": {\n      \"title\": \"Цей робочий процес захищено паролем\",\n      \"body\": \"Введіть пароль, щоб розблокувати\",\n      \"unlock\": \"Розблокувати\",\n      \"messages\": {\n        \"incorrect-password\": \"Неправильний пароль\"\n      }\n    },\n    \"state\": {\n      \"executeBy\": \"Виконано: \\\"{name}\\\"\"\n    },\n    \"table\": {\n      \"title\": \"Таблиця | Таблиці\",\n      \"placeholder\": \"Пошук або додавання стовпця\",\n      \"select\": \"Вибрати стовпець\",\n      \"column\": {\n        \"name\": \"Назва стовпця\",\n        \"type\": \"Тип даних\"\n      }\n    },\n    \"sidebar\": {\n      \"workflowIcon\": \"Піктограма робочого процесу\"\n    },\n    \"editor\": {\n      \"zoomIn\": \"Збільшити\",\n      \"zoomOut\": \"Зменшити\",\n      \"resetZoom\": \"Скинути масштаб\",\n      \"duplicate\": \"Дублювати\",\n      \"copy\": \"Копіювати\",\n      \"paste\": \"Вставити\",\n      \"group\": \"Блоки групи\",\n      \"ungroup\": \"Розгрупувати блоки\"\n    },\n    \"settings\": {\n      \"saveLog\": \"Зберегти журнал робочого процесу\",\n      \"executedBlockOnWeb\": \"Показати виконаний блок на веб-сторінці\",\n      \"notification\": {\n        \"title\": \"Повідомлення робочого процесу\",\n        \"description\": \"Показати статус робочого процесу (успішно або невдало) після його виконання\",\n        \"noPermission\": \"Для роботи цієї опції додатку потрібен дозвіл на \\\"повідомлення\\\"\"\n      },\n      \"publicId\": {\n        \"title\": \"Публічний ідентифікатор робочого процесу\",\n        \"description\": \"Встановіть загальнодоступний ідентифікатор для виконання робочого процесу через спеціальну подію JavaScript\"\n      },\n      \"defaultColumn\": {\n        \"title\": \"Вставити в стовпець за замовчуванням\",\n        \"description\": \"Вставити дані в стовпець за замовчуванням, якщо в блоці не вибрано жодного стовпця\",\n        \"name\": \"Назва стовпця за замовчуванням\"\n      },\n      \"autocomplete\": {\n        \"title\": \"Автозаповнення\",\n        \"description\": \"Увімкнути автозаповнення у блоці введення (вимкнути, якщо воно робить Automa нестабільним)\"\n      },\n      \"clearCache\": {\n        \"title\": \"Очистити кеш\",\n        \"description\": \"Очистити кеш (стан та індекс циклу) робочого процесу\",\n        \"info\": \"Кеш робочого циклу успішно очищено\",\n        \"btn\": \"Очистити\"\n      },\n      \"reuseLastState\": {\n        \"title\": \"Повторне використання стану останнього робочого процесу\",\n        \"description\": \"Використовувати дані стану (таблицю, змінні та глобальні дані) з останнього виконаного робочого процесу\"\n      },\n      \"debugMode\": {\n        \"title\": \"Режим налагодження\",\n        \"description\": \"Виконати робочий процес за допомогою протоколу Chrome DevTools\"\n      },\n      \"restartWorkflow\": {\n        \"for\": \"Перезапустити\",\n        \"times\": \"Разів\",\n        \"description\": \"Максимальна кількість перезапусків робочого процесу\"\n      },\n      \"onError\": {\n        \"title\": \"При помилці у робочому процесі\",\n        \"description\": \"Встановити дію, яку потрібно виконати, якщо в робочому процесі станеться помилка\",\n        \"items\": {\n          \"keepRunning\": \"Продовжити працювати\",\n          \"stopWorkflow\": \"Зупинити робочий процес\",\n          \"restartWorkflow\": \"Перезапустити робочий процес\"\n        }\n      },\n      \"timeout\": {\n        \"title\": \"Час очікування робочого процесу (мілісекунди)\"\n      },\n      \"blockDelay\": {\n        \"title\": \"Затримка блоку (мілісекунди)\",\n        \"description\": \"Додати затримку перед виконанням кожного з блоків\"\n      },\n      \"tabLoadTimeout\": {\n        \"title\": \"Час очікування завантаження вкладки\",\n        \"description\": \"Максимальний час очікування завантаження вкладки в мілісекундах(0, щоб вимкнути очікування)\"\n      }\n    }\n  },\n  \"collection\": {\n    \"description\": \"Послідовно виконувати ваші робочі процеси\",\n    \"new\": \"Нова колекція\",\n    \"delete\": \"Видалити колекцію\",\n    \"add\": \"Додати колекцію\",\n    \"rename\": \"Перейменувати колекцію\",\n    \"flow\": \"Flow\",\n    \"dragDropText\": \"Перетягніть робочий процес або блок сюди\",\n    \"options\": {\n      \"atOnce\": {\n        \"title\": \"Виконати всі робочі процеси в колекції одночасно\",\n        \"description\": \"Блоки не виконуватимуться, коли використовується цей параметр\"\n      }\n    },\n    \"globalData\": {\n      \"note\": \"Це перезапише глобальні дані робочого процесу\"\n    }\n  },\n  \"log\": {\n    \"flowId\": \"Ідентифікатор процесу\",\n    \"goBack\": \"Повернутися до журналів \\\"{name}\\\"\",\n    \"goWorkflow\": \"Перейти до робочого процесу\",\n    \"startedDate\": \"Дата початку\",\n    \"duration\": \"Тривалість\",\n    \"selectAll\": \"Вибрати все\",\n    \"deselectAll\": \"Скасувати вибір усіх\",\n    \"deleteSelected\": \"Видалити вибрані журнали\",\n    \"clearLogs\": {\n      \"title\": \"Очистити журнали\",\n      \"description\": \"Ви впевнені, що бажаєте очистити всі журнали?\"\n    },\n    \"types\": {\n      \"stop\": \"Робочий процес зупинено\",\n      \"finish\": \"Закінчити\"\n    },\n    \"messages\": {\n      \"url-empty\": \"URL порожня\",\n      \"invalid-url\": \"URL недійсний\",\n      \"conditions-empty\": \"Умови порожні\",\n      \"invalid-proxy-host\": \"Недійсний хост проксі\",\n      \"workflow-disabled\": \"Робочий процес вимкнено\",\n      \"selector-empty\": \"Селектор елемента порожній\",\n      \"invalid-body\": \"Тіло вмісту не є дійсним JSON\",\n      \"invalid-active-tab\": \"\\\"{url}\\\" є недійсною URL\",\n      \"empty-spreadsheet-id\": \"Ідентифікатор електронної таблиці порожній\",\n      \"invalid-loop-data\": \"Недійсні дані для циклу\",\n      \"empty-workflow\": \"Ви повинні спочатку вибрати робочий процес\",\n      \"active-tab-removed\": \"Активну вкладку робочого процесу видалено\",\n      \"empty-spreadsheet-range\": \"Діапазон таблиці порожній\",\n      \"stop-timeout\": \"Робочий процес зупинено через тайм-аут\",\n      \"no-file-access\": \"Automa не має доступу до файлу\",\n      \"no-workflow\": \"Не вдається знайти робочий цикл з ідентифікатором \\\"{workflowId}\\\"\",\n      \"no-match-tab\": \"Не вдається знайти вкладку, що відповідає шаблону \\\"{pattern}\\\"\",\n      \"no-clipboard-acces\": \"Немає дозволу на доступ до буфера обміну\",\n      \"browser-not-supported\": \"Ця функція не підтримується у браузері {browser}\",\n      \"element-not-found\": \"Не вдається знайти елемент за допомогою селектора \\\"{selector}\\\"\",\n      \"no-permission\": \"Немає дозволу \\\"{permission}\\\" для виконання цієї дії\",\n      \"not-iframe\": \"Елемент із селектором \\\"{selector}\\\" не є елементом iframe\",\n      \"iframe-not-found\": \"Не вдається знайти елемент iframe за допомогою селектора \\\"{selector}\\\"\",\n      \"workflow-infinite-loop\": \"Неможливо виконати робочий процес, щоб запобігти нескінченному циклу\",\n      \"not-debug-mode\": \"Робочий процес має працювати в режимі налагодження, щоб цей блок працював належним чином\",\n      \"no-iframe-id\": \"Не вдається знайти ID фрейму для елемента iframe за допомогою селектора \\\"{selector}\\\"\",\n      \"no-tab\": \"Не вдається підключитися до вкладки, скористайтеся блоком \\\"Нова вкладка\\\" або \\\"Активна вкладка\\\" перед використанням блоку \\\"{name}\\\"\"\n    },\n    \"description\": {\n      \"text\": \"{status} {date} протягом {duration}\",\n      \"status\": {\n        \"success\": \"Успіх\",\n        \"error\": \"Помилка\",\n        \"stopped\": \"Зупинено\"\n      }\n    },\n    \"delete\": {\n      \"title\": \"Видалити журнал\",\n      \"description\": \"Ви впевнені, що хочете видалити всі вибрані журнали?\"\n    },\n    \"exportData\": {\n      \"title\": \"Експорт даних\",\n      \"types\": {\n        \"json\": \"JSON\",\n        \"csv\": \"CSV\",\n        \"plain-text\": \"Звичайний текст\"\n      }\n    },\n    \"filter\": {\n      \"title\": \"Фільтр\",\n      \"byStatus\": \"За статусом\",\n      \"byDate\": {\n        \"title\": \"За датою\",\n        \"items\": {\n          \"lastDay\": \"Останній день\",\n          \"last7Days\": \"Останні сім днів\",\n          \"last30Days\": \"Останні тридцять днів\"\n        }\n      }\n    }\n  },\n  \"components\": {\n    \"pagination\": {\n      \"text1\": \"Показ\",\n      \"text2\": \"елементи з {count}\",\n      \"nextPage\": \"Наступна сторінка\",\n      \"currentPage\": \"Поточна сторінка\",\n      \"prevPage\": \"Попередня сторінка\",\n      \"of\": \"з {page}\"\n    }\n  }\n}\n"
  },
  {
    "path": "src/locales/uk/popup.json",
    "content": "{\n  \"recording\": {\n    \"stop\": \"Зупинити запис\",\n    \"title\": \"Запис\"\n  },\n  \"home\": {\n    \"record\": {\n      \"title\": \"Запис workflow\",\n      \"button\": \"Запис\",\n      \"name\": \"Назва workflow\",\n      \"selectBlock\": \"Виберіть блок, з якого потрібно розпочати\",\n      \"anotherBlock\": \"Неможливо почати з цього блоку\",\n      \"tabs\": {\n        \"new\": \"Новий workflow\",\n        \"existing\": \"Існуючий workflow\"\n      }\n    },\n    \"elementSelector\": {\n      \"name\": \"Вибір елемента\",\n      \"noAccess\": \"Немає доступу до цього сайту\"\n    },\n    \"workflow\": {\n      \"new\": \"Новий workflow\",\n      \"rename\": \"Перейменувати workflow\",\n      \"delete\": \"Видалити workflow\",\n      \"type\": {\n        \"host\": \"Хост\",\n        \"local\": \"Локальний\"\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "src/locales/vi/blocks.json",
    "content": "{\n  \"collection\": {\n    \"blocks\": {\n      \"export-result\": {\n        \"name\": \"Xuất kết quả\",\n        \"description\": \"Xuất kết quả thu thập dưới dạng JSON\"\n      }\n    }\n  },\n  \"workflow\": {\n    \"blocks\": {\n      \"base\": {\n        \"title\": \"Khối\",\n        \"moveToGroup\": \"Di chuyển khối sang nhóm khối\",\n        \"selector\": \"Bộ chọn phần tử\",\n        \"selectorOptions\": \"Tùy chọn bộ chọn\",\n        \"timeout\": \"Thời gian chờ (mili giây)\",\n        \"noPermission\": \"Automa không có đủ quyền để thực hiện hành động này\",\n        \"grantPermission\": \"Cấp phép\",\n        \"action\": \"Hoạt động\",\n        \"element\": {\n          \"select\": \"Chọn một phần tử\",\n          \"verify\": \"Xác minh bộ chọn\"\n        },\n        \"settings\": {\n          \"title\": \"Cài đặt chặn\",\n          \"line\": {\n            \"title\": \"Dòng\",\n            \"label\": \"Nhãn dòng\",\n            \"animated\": \"Hoạt hình\",\n            \"select\": \"Chọn dòng\",\n            \"to\": \"Dòng tới khối {name}\",\n            \"lineColor\": \"Màu đường kẻ\"\n          }\n        },\n        \"toggle\": {\n          \"enable\": \"Bật khối\",\n          \"disable\": \"Tắt khối\"\n        },\n        \"onError\": {\n          \"info\": \"Các quy tắc này sẽ được áp dụng khi có lỗi xảy ra trên khối\",\n          \"button\": \"Có lỗi\",\n          \"title\": \"Khi xảy ra lỗi\",\n          \"retry\": \"Thử lại hành động\",\n          \"fallbackTitle\": \"Sẽ thực thi khi có lỗi xảy ra trong khối\",\n          \"times\": {\n            \"name\": \"Thời gian\",\n            \"description\": \"Số lần thử lại hành động\"\n          },\n          \"interval\": {\n            \"name\": \"Khoảng thời gian\",\n            \"description\": \"Khoảng thời gian chờ giữa mỗi lần thử\",\n            \"second\": \"thứ hai\"\n          },\n          \"toDo\": {\n            \"error\": \"Ném lỗi\",\n            \"continue\": \"Tiếp tục dòng chảy\",\n            \"fallback\": \"Thực thi dự phòng\",\n            \"restart\": \"Khởi động lại quy trình\"\n          },\n          \"insertData\": {\n            \"name\": \"Chèn dữ liệu\"\n          }\n        },\n        \"table\": {\n          \"checkbox\": \"Chèn vào bảng\",\n          \"select\": \"Chọn cột\",\n          \"extraRow\": {\n            \"checkbox\": \"Thêm hàng bổ sung\",\n            \"placeholder\": \"Giá trị\",\n            \"title\": \"Giá trị của hàng phụ\"\n          }\n        },\n        \"findElement\": {\n          \"placeholder\": \"Tìm phần tử bằng\",\n          \"options\": {\n            \"cssSelector\": \"Bộ chọn CSS\",\n            \"xpath\": \"XPath\"\n          }\n        },\n        \"markElement\": {\n          \"title\": \"Một phần tử sẽ không được chọn nếu nó đã được chọn trước đó\",\n          \"text\": \"Đánh dấu phần tử\"\n        },\n        \"multiple\": {\n          \"title\": \"Chọn nhiều phần tử\",\n          \"text\": \"Nhiều\"\n        },\n        \"waitSelector\": {\n          \"title\": \"Chờ bộ chọn\",\n          \"timeout\": \"Thời gian chờ của bộ chọn (mili giây)\"\n        },\n        \"downloads\": {\n          \"onConflict\": {\n            \"uniquify\": \"Thống nhất\",\n            \"overwrite\": \"Ghi đè\",\n            \"prompt\": \"Lời nhắc\"\n          }\n        }\n      },\n      \"wait-connections\": {\n        \"name\": \"Chờ kết nối\",\n        \"description\": \"Chờ tất cả các kết nối trước khi tiếp tục khối tiếp theo\",\n        \"specificFlow\": \"Chỉ tiếp tục một quy trình cụ thể\",\n        \"selectFlow\": \"Chọn luồng\"\n      },\n      \"cookie\": {\n        \"name\": \"Cookie\",\n        \"description\": \"Nhận, đặt hoặc xóa cookie\",\n        \"types\": {\n          \"get\": \"Nhận cookie\",\n          \"set\": \"Đặt cookie\",\n          \"remove\": \"Xóa cookie\",\n          \"getAll\": \"Nhận tất cả cookie\"\n        }\n      },\n      \"note\": {\n        \"name\": \"Ghi chú\"\n      },\n      \"slice-variable\": {\n        \"name\": \"Biến Slice\",\n        \"description\": \"Trích xuất một phần của một giá trị biến\",\n        \"start\": \"Bắt đầu lập chỉ mục\",\n        \"end\": \"Chỉ mục kết thúc\"\n      },\n      \"workflow-state\": {\n        \"name\": \"Trạng thái quy trình làm việc\",\n        \"description\": \"Quản lý trạng thái quy trình công việc\",\n        \"actions\": {\n          \"stop\": \"Dừng quy trình công việc\"\n        }\n      },\n      \"regex-variable\": {\n        \"name\": \"Biến RegEx\",\n        \"description\": \"Khớp một giá trị biến với một biểu thức chính quy\"\n      },\n      \"data-mapping\": {\n        \"source\": \"Nguồn\",\n        \"destination\": \"Điểm đến\",\n        \"name\": \"Ánh xạ dữ liệu\",\n        \"edit\": \"Chỉnh sửa bản đồ dữ liệu\",\n        \"dataSource\": \"Nguồn dữ liệu\",\n        \"description\": \"Ánh xạ dữ liệu của một biến hoặc bảng\",\n        \"addSource\": \"Thêm nguồn\",\n        \"addDestination\": \"Thêm điểm đến\"\n      },\n      \"sort-data\": {\n        \"name\": \"Sắp xếp dữ liệu\",\n        \"description\": \"Sắp xếp các mục dữ liệu\",\n        \"property\": \"Sắp xếp theo thuộc tính của mặt hàng\",\n        \"addProperty\": \"Thêm tài sản\"\n      },\n      \"increase-variable\": {\n        \"name\": \"Tăng biến\",\n        \"description\": \"Tăng giá trị của một biến bằng số tiền cụ thể\",\n        \"increase\": \"Tăng bởi\"\n      },\n      \"notification\": {\n        \"name\": \"thông báo\",\n        \"description\": \"Hiển thị thông báo\",\n        \"title\": \"Tiêu đề\",\n        \"message\": \"Thông điệp\",\n        \"imageUrl\": \"URL hình ảnh (tùy chọn)\",\n        \"iconUrl\": \"URL biểu tượng (tùy chọn)\"\n      },\n      \"delete-data\": {\n        \"name\": \"Xóa dữ liệu\",\n        \"description\": \"Xóa bảng hoặc dữ liệu biến\",\n        \"from\": \"Dữ liệu từ\",\n        \"allColumns\": \"[Tất cả các cột]\"\n      },\n      \"log-data\": {\n        \"name\": \"Get log data\",\n        \"description\": \"Nhận dữ liệu nhật ký mới nhất của quy trình làm việc\",\n        \"data\": \"Ghi dữ liệu\"\n      },\n      \"tab-url\": {\n        \"name\": \"Nhận URL tab\",\n        \"description\": \"Lấy URL của tab\",\n        \"select\": \"Chọn tab\",\n        \"types\": {\n          \"active-tab\": \"Tab hoạt động\",\n          \"all\": \"Tất cả các tab\"\n        }\n      },\n      \"reload-tab\": {\n        \"name\": \"Tải lại tab\",\n        \"description\": \"Tải lại tab đang hoạt động\"\n      },\n      \"press-key\": {\n        \"name\": \"Nhấn phím\",\n        \"description\": \"Nhấn một phím hoặc một tổ hợp\",\n        \"target\": \"Yếu tố mục tiêu (tùy chọn)\",\n        \"key\": \"Chìa khóa\",\n        \"detect\": \"Phát hiện khóa\",\n        \"actions\": {\n          \"press-key\": \"Nhấn một phím\",\n          \"multiple-keys\": \"Nhấn nhiều phím\"\n        }\n      },\n      \"save-assets\": {\n        \"name\": \"Tiết kiệm tài sản\",\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\",\n        \"filename\": \"Tên tệp (tùy chọn)\",\n        \"contentTypes\": {\n          \"title\": \"Loại hình\",\n          \"element\": \"Phần tử phương tiện (hình ảnh, âm thanh hoặc video)\",\n          \"url\": \"URL\"\n        }\n      },\n      \"handle-dialog\": {\n        \"name\": \"Xử lý hộp thoại\",\n        \"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).\",\n        \"accept\": \"Hộp thoại chấp nhận\",\n        \"promptText\": {\n          \"label\": \"Văn bản nhắc nhở (tùy chọn)\",\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\"\n        }\n      },\n      \"handle-download\": {\n        \"name\": \"Xử lý tải xuống\",\n        \"description\": \"Xử lý tệp đã tải xuống\",\n        \"timeout\": \"Thời gian chờ (mili giây)\",\n        \"noPermission\": \"Không có quyền truy cập các bản tải xuống\",\n        \"onConflict\": \"Xung đột\",\n        \"waitFile\": \"Chờ tệp được tải xuống\"\n      },\n      \"insert-data\": {\n        \"name\": \"Chèn dữ liệu\",\n        \"description\": \"Chèn dữ liệu vào bảng hoặc biến\"\n      },\n      \"clipboard\": {\n        \"name\": \"Bộ nhớ tạm\",\n        \"description\": \"Lấy văn bản đã sao chép từ khay nhớ tạm\",\n        \"data\": \"Dữ liệu bảng tạm\",\n        \"noPermission\": \"Không có quyền truy cập khay nhớ tạm\",\n        \"grantPermission\": \"Cấp phép\",\n        \"copySelection\": \"Sao chép văn bản đã chọn trên trang\",\n        \"types\": {\n          \"get\": \"Nhận dữ liệu khay nhớ tạm\",\n          \"insert\": \"Chèn văn bản vào khay nhớ tạm\"\n        }\n      },\n      \"hover-element\": {\n        \"name\": \"Hover element\",\n        \"description\": \"Hover over an element\"\n      },\n      \"create-element\": {\n        \"name\": \"Tạo phần tử\",\n        \"description\": \"Tạo một phần tử và chèn nó vào trang\",\n        \"edit\": \"Chỉnh sửa phần tử\",\n        \"wrap\": \"Bọc phần tử bên trong\",\n        \"insertEl\": {\n          \"title\": \"Chèn phần tử\",\n          \"items\": {\n            \"before\": \"Là đứa con đầu lòng\",\n            \"after\": \"Là đứa con cuối cùng\",\n            \"next-sibling\": \"Là anh chị em tiếp theo\",\n            \"prev-sibling\": \"Là anh chị em trước\",\n            \"replace\": \"Thay thế phần tử mục tiêu\"\n          }\n        }\n      },\n      \"upload-file\": {\n        \"name\": \"Cập nhật dử liệu\",\n        \"description\": \"Tải tệp lên phần tử <input type=\\\"file\\\">\",\n        \"filePath\": \"URL hoặc đường dẫn tệp\",\n        \"addFile\": \"Thêm tập tin\",\n        \"onlyURL\": \"Chỉ hỗ trợ tải tệp lên từ một URL trong trình duyệt Firefox\",\n        \"requirement\": \"Xem yêu cầu trước khi sử dụng khối này\",\n        \"noFileAccess\": \"Automa không có quyền truy cập tệp\"\n      },\n      \"browser-event\": {\n        \"name\": \"Sự kiện trình duyệt\",\n        \"description\": \"Thực thi khối tiếp theo khi sự kiện được kích hoạt\",\n        \"events\": \"Sự kiện\",\n        \"timeout\": \"Thời gian chờ (mili giây)\",\n        \"activeTabLoaded\": \"Tab hoạt động\",\n        \"setAsActiveTab\": \"Đặt làm tab hoạt động\"\n      },\n      \"blocks-group-2\": {\n        \"name\": \"@:workflow.blocks.blocks-group.name 2\",\n        \"description\": \"@:workflow.blocks.blocks-group.description\"\n      },\n      \"blocks-group\": {\n        \"name\": \"Nhóm khối\",\n        \"groupName\": \"Tên nhóm\",\n        \"description\": \"Nhóm các khối\",\n        \"dropText\": \"Kéo và thả một khối ở đây\",\n        \"cantAdd\": \"Không thể thêm khối \\\"{blockName}\\\" vào nhóm.\"\n      },\n      \"trigger\": {\n        \"name\": \"Kích hoạt\",\n        \"description\": \"Quy trình bắt đầu được thực thi ở đây\",\n        \"addTime\": \"Thêm thời gian\",\n        \"selectDay\": \"Chọn ngày\",\n        \"timeExist\": \"Bạn đã thêm {time} vào {day}\",\n        \"fixedDelay\": \"Cố định độ trễ\",\n        \"contextMenus\": {\n          \"noPermission\": \"Trình kích hoạt này yêu cầu quyền \\\"contextMenus\\\" để hoạt động\",\n          \"grantPermission\": \"Cấp phép\",\n          \"appearIn\": \"Sẽ xuất hiện trong\",\n          \"contextName\": \"Tên quy trình làm việc trong menu ngữ cảnh\"\n        },\n        \"days\": [\n          \"Chủ nhật\",\n          \"Thứ hai\",\n          \"Thứ ba\",\n          \"Thứ tư\",\n          \"Thứ năm\",\n          \"Thứ sáu\",\n          \"Thứ bảy\"\n        ],\n        \"useRegex\": \"Dùng regex\",\n        \"shortcut\": {\n          \"tooltip\": \"Ghi lại lối tắt\",\n          \"stopRecord\": \"Dừng ghi\",\n          \"checkboxTitle\": \"Execute shortcut even when you're in an input element\",\n          \"checkbox\": \"Hoạt động khi nhập liệu\",\n          \"note\": \"Lưu ý: phím tắt chỉ hoạt động khi bạn đang truy cập một trang web\"\n        },\n        \"forms\": {\n          \"triggerWorkflow\": \"Quy trình kích hoạt\",\n          \"interval\": \"Chu kỳ (phút)\",\n          \"delay\": \"Độ trễ (phút)\",\n          \"date\": \"Ngày\",\n          \"time\": \"Giờ\",\n          \"url\": \"URL hoặc Regex\",\n          \"shortcut\": \"Phim tắt\",\n          \"cron-expression\": \"Biểu thức cron\"\n        },\n        \"element-change\": {\n          \"target\": \"Mục tiêu yếu tố để quan sát\",\n          \"optionsInfo\": \"Đột biến phần tử nào sẽ kích hoạt quy trình làm việc\",\n          \"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)\",\n          \"baseEl\": {\n            \"title\": \"Phần tử cơ sở (tùy chọn)\",\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\"\n          },\n          \"subtree\": {\n            \"title\": \"Bao gồm cây con\",\n            \"description\": \"Mở rộng giám sát cho toàn bộ cây con của phần tử mục tiêu\"\n          },\n          \"childList\": {\n            \"title\": \"Danh sách con\",\n            \"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ó\"\n          },\n          \"attributes\": {\n            \"title\": \"Thuộc tính\",\n            \"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\"\n          },\n          \"attributeFilter\": {\n            \"title\": \"Bộ lọc thuộc tính\",\n            \"separate\": \"Sử dụng dấu phẩy (,) để phân tách tên thuộc tính\",\n            \"description\": \"Chỉ theo dõi các thuộc tính cụ thể (để trống để theo dõi tất cả)\"\n          },\n          \"characterData\": {\n            \"title\": \"Dữ liệu ký tự\",\n            \"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\"\n          }\n        },\n        \"items\": {\n          \"manual\": \"Thủ công\",\n          \"interval\": \"Chu kỳ\",\n          \"cron-job\": \"Lập lịch công việc\",\n          \"date\": \"Vào một ngày cụ thể\",\n          \"context-menu\": \"Danh mục\",\n          \"element-change\": \"Khi phần tử thay đổi\",\n          \"specific-day\": \"Vào một ngày cụ thể\",\n          \"visit-web\": \"Khi truy cập một trang web\",\n          \"on-startup\": \"Khi khởi động trình duyệt\",\n          \"keyboard-shortcut\": \"Phim tắt\"\n        }\n      },\n      \"execute-workflow\": {\n        \"name\": \"Thực thi quy trình\",\n        \"overwriteNote\": \"Thao tác này sẽ ghi đè lên dữ liệu chung của quy trình đã chọn\",\n        \"select\": \"Chọn quy trình\",\n        \"executeId\": \"Thực thi ID (tùy chọn)\",\n        \"description\": \"\",\n        \"insertAllVars\": \"Chèn tất cả các biến quy trình làm việc hiện tại\",\n        \"insertVars\": \"Chèn các biến quy trình công việc hiện tại\",\n        \"useCommas\": \"Sử dụng dấu phẩy để phân tách tên biến\",\n        \"insertAllGlobalData\": \"Sử dụng tất cả quy trình công việc hiện tại GlobalData\"\n      },\n      \"google-sheets\": {\n        \"name\": \"Google sheets\",\n        \"description\": \"Đọc hoặc cập nhật dữ liệu Google Trang tính\",\n        \"previewData\": \"Xem trước dữ liệu\",\n        \"firstRow\": \"Sử dụng hàng đầu tiên làm khóa\",\n        \"keysAsFirstRow\": \"Sử dụng các phím làm hàng đầu tiên\",\n        \"insertData\": \"Chèn dữ liệu\",\n        \"valueInputOption\": \"Tùy chọn nhập giá trị\",\n        \"insertDataOption\": \"Chèn tùy chọn dữ liệu\",\n        \"rangeToSearch\": \"Phạm vi bắt đầu tìm kiếm\",\n        \"dataFrom\": {\n          \"label\": \"Dữ liệu từ\",\n          \"options\": {\n            \"data-columns\": \"Bảng\",\n            \"custom\": \"Tùy chỉnh\"\n          }\n        },\n        \"refKey\": {\n          \"label\": \"Khóa tham chiếu (tùy chọn)\",\n          \"placeholder\": \"Tên khóa\"\n        },\n        \"spreadsheetId\": {\n          \"label\": \"Id bảng tính\",\n          \"link\": \"Xem cách lấy ID bảng tính\"\n        },\n        \"range\": {\n          \"label\": \"Phạm vi\",\n          \"link\": \"Bấm để xem thêm ví dụ\"\n        },\n        \"select\": {\n          \"get\": \"Nhận giá trị ô bảng tính\",\n          \"getRange\": \"Nhận phạm vi bảng tính\",\n          \"update\": \"Cập nhật giá trị ô bảng tính\",\n          \"append\": \"Nối các giá trị ô bảng tính\",\n          \"clear\": \"Xóa giá trị ô bảng tính\"\n        }\n      },\n      \"active-tab\": {\n        \"name\": \"Tab hoạt động\",\n        \"description\": \"Chỉ định tab hiện tại mà bạn đang truy cập thành tab đang hoạt động\"\n      },\n      \"proxy\": {\n        \"name\": \"Proxy\",\n        \"description\": \"Thiết lập proxy của trình duyệt\",\n        \"clear\": \"Xóa tất cả proxy\",\n        \"bypass\": {\n          \"label\": \"Danh sách bỏ qua\",\n          \"note\": \"Dùng dấu phẩy (,) để tách biệt URL\"\n        }\n      },\n      \"new-window\": {\n        \"name\": \"Cửa sổ mới\",\n        \"description\": \"Tạo cửa sổ mới\",\n        \"top\": \"Trên\",\n        \"left\": \"Trái\",\n        \"height\": \"Chiều cao\",\n        \"width\": \"Chiều rộng\",\n        \"note\": \"Lưu ý: sử dụng số 0 để tắt\",\n        \"position\": \"Vị trí cửa sổ\",\n        \"size\": \"Kích thước cửa sổ\",\n        \"windowState\": {\n          \"placeholder\": \"Trạng thái cửa sổ\",\n          \"options\": {\n            \"normal\": \"Bình thường\",\n            \"minimized\": \"Thu gọn\",\n            \"maximized\": \"Mở rộng tối đa\",\n            \"fullscreen\": \"Toàn màn hình\"\n          }\n        },\n        \"incognito\": {\n          \"text\": \"Đặt làm cửa sổ ẩn danh\",\n          \"note\": \"Bạn cần bật 'Cho phép ở chế độ ẩn danh' cho tiện ích mở rộng này để sử dụng tùy chọn\"\n        }\n      },\n      \"go-back\": {\n        \"name\": \"Quay lại\",\n        \"description\": \"Quay trở lại trang trước\"\n      },\n      \"forward-page\": {\n        \"name\": \"Về trước\",\n        \"description\": \"Đi tới trang tiếp theo\"\n      },\n      \"close-tab\": {\n        \"name\": \"Đóng tab\",\n        \"description\": \"\",\n        \"url\": \"URL hoặc match pattern\",\n        \"activeTab\": \"Đóng activeTab\",\n        \"allWindows\": \"Đóng tất cả các cửa sổ\"\n      },\n      \"event-click\": {\n        \"name\": \"Nhấp vào phần tử\",\n        \"description\": \"\"\n      },\n      \"delay\": {\n        \"name\": \"Độ trễ\",\n        \"description\": \"Thêm độ trễ trước khi thực hiện khối tiếp theo\",\n        \"input\": {\n          \"title\": \"Độ trễ trong mili giây\",\n          \"placeholder\": \"(mili giây)\"\n        }\n      },\n      \"parameter-prompt\": {\n        \"name\": \"Nhắc tham số\"\n      },\n      \"get-text\": {\n        \"name\": \"Trích văn bản\",\n        \"description\": \"Trích văn bản từ một phần tử\",\n        \"checkbox\": \"Chèn vào bảng\",\n        \"includeTags\": \"Bao gồm các thẻ HTML\",\n        \"prefixText\": {\n          \"placeholder\": \"Tiền tố văn bản\",\n          \"title\": \"Thêm tiền tố vào văn bản\"\n        },\n        \"suffixText\": {\n          \"placeholder\": \"Hậu tố văn bản\",\n          \"title\": \"Thêm hậu tố vào văn bản\"\n        }\n      },\n      \"export-data\": {\n        \"name\": \"Xuất dữ liệu\",\n        \"description\": \"Xuất dữ liệu của quy trình\",\n        \"exportAs\": \"Xuất file tại\",\n        \"refKey\": \"Khóa liên kết\",\n        \"bomHeader\": \"Thêm UTF-8 BOM\",\n        \"dataToExport\": {\n          \"placeholder\": \"Dữ liệu để xuất\",\n          \"options\": {\n            \"data-columns\": \"Bảng\",\n            \"google-sheets\": \"Google sheets\",\n            \"variable\": \"Biến\"\n          }\n        }\n      },\n      \"element-scroll\": {\n        \"name\": \"Cuộn\",\n        \"description\": \"\",\n        \"scrollY\": \"Cuộn thẳng\",\n        \"scrollX\": \"Cuộn ngang\",\n        \"intoView\": \"Scroll into view\",\n        \"smooth\": \"Cuộn mượt\",\n        \"incScrollX\": \"Cuộn ngang tăng dần\",\n        \"incScrollY\": \"Cuộn dọc tăng dần\"\n      },\n      \"new-tab\": {\n        \"name\": \"Tab mới\",\n        \"description\": \"\",\n        \"url\": \"URL tab mới\",\n        \"customUserAgent\": \"Sử dụng tác nhân người dùng tùy chỉnh\",\n        \"activeTab\": \"Đặt làm tab hoạt động\",\n        \"tabToGroup\": \"Thêm tab vào nhóm\",\n        \"waitTabLoaded\": \"Chờ cho đến khi tab được tải\",\n        \"updatePrevTab\": {\n          \"title\": \"Sử dụng tab mới đã mở trước đó thay vì tạo tab mới\",\n          \"text\": \"Cập nhật tab đã mở trước đó\"\n        }\n      },\n      \"link\": {\n        \"name\": \"Link\",\n        \"description\": \"Mở phần tử link\"\n      },\n      \"attribute-value\": {\n        \"name\": \"Giá trị thuộc tính\",\n        \"description\": \"Trích xuất giá trị từ một thuộc tính của phần tử\",\n        \"forms\": {\n          \"name\": \"Tên thuộc tính\",\n          \"checkbox\": \"Lưu dữ liệu\",\n          \"column\": \"Chọn cột\",\n          \"extraRow\": {\n            \"checkbox\": \"Thêm hàng bổ sung\",\n            \"placeholder\": \"Giá trị\",\n            \"title\": \"Giá trị của hàng phụ\"\n          }\n        }\n      },\n      \"forms\": {\n        \"name\": \"Biểu mẫu\",\n        \"description\": \"\",\n        \"selected\": \"Đã chọn\",\n        \"type\": \"Loại biểu mẫu\",\n        \"getValue\": \"Trích xuất giá trị từ biểu mẫu\",\n        \"text-field\": {\n          \"name\": \"Trường văn bản\",\n          \"value\": \"Giá trị\",\n          \"clearValue\": \"Xóa giá trị biểu mẫu\",\n          \"delay\": {\n            \"placeholder\": \"Độ trễ\",\n            \"label\": \"Nhập độ trễ (mili giây)(0 là vô hiệu hóa)\"\n          }\n        },\n        \"select\": {\n          \"name\": \"Select\"\n        },\n        \"radio\": {\n          \"name\": \"Radio\"\n        },\n        \"checkbox\": {\n          \"name\": \"Checkbox\"\n        }\n      },\n      \"repeat-task\": {\n        \"name\": \"Nhiệm vụ lặp lại\",\n        \"description\": \"\",\n        \"times\": \"lần\",\n        \"repeatFrom\": \"Lặp lại từ\"\n      },\n      \"javascript-code\": {\n        \"name\": \"JavaScript code\",\n        \"description\": \"Thực thi code Javascript trong trang web\",\n        \"availabeFuncs\": \"Các hàm có sẵn:\",\n        \"removeAfterExec\": \"Xóa sau khi khối được thực thi\",\n        \"everyNewTab\": \"Thực thi mọi tab mới\",\n        \"context\": {\n          \"name\": \"Bối cảnh thực thi\",\n          \"items\": {\n            \"website\": \"Tab hoạt động\",\n            \"background\": \"Nền\"\n          }\n        },\n        \"modal\": {\n          \"tabs\": {\n            \"code\": \"JavaScript code\",\n            \"preloadScript\": \"Preload script\"\n          }\n        },\n        \"timeout\": {\n          \"placeholder\": \"Thời gian chờ\",\n          \"title\": \"Thời gian thực thi code Javascript\"\n        }\n      },\n      \"trigger-event\": {\n        \"name\": \"Sự kiện kích hoạt\",\n        \"description\": \"\",\n        \"selectEvent\": \"Chọn sự kiện\"\n      },\n      \"conditions\": {\n        \"name\": \"Điều kiện\",\n        \"add\": \"Thêm điều kiện\",\n        \"description\": \"Khối có điều kiện\",\n        \"retryConditions\": \"Thử lại nếu tất cả các điều kiện không được đáp ứng\",\n        \"refresh\": \"Làm mới các kết nối điều kiện\",\n        \"fallbackTitle\": \"Thực thi khi tất cả các phép so sánh không đáp ứng yêu cầu\",\n        \"equals\": \"Ngang bằng\",\n        \"gt\": \"Lớn hơn\",\n        \"gte\": \"Lớn hơn hoặc ngang bằng\",\n        \"lt\": \"Nhỏ hơn\",\n        \"lte\": \"Nhỏ hơn hoặc ngang bằng\",\n        \"ne\": \"Không bằng\",\n        \"contains\": \"Bao hàm\"\n      },\n      \"element-exists\": {\n        \"name\": \"Phần tử tồn tại\",\n        \"description\": \"Kiểm tra xem một phần tử có tồn tại không\",\n        \"selector\": \"Bộ chọn phần tử\",\n        \"fallbackTitle\": \"Thực thi khi phần tử không tồn tại\",\n        \"throwError\": \"Ném lỗi nếu không tồn tại\",\n        \"tryFor\": {\n          \"title\": \"Cố gắng kiểm tra xem phần tử có tồn tại không\",\n          \"label\": \"Số lần thử\"\n        },\n        \"timeout\": {\n          \"label\": \"Giới hạn thời gian (mili giây)\",\n          \"title\": \"Thời gian cho mỗi lần thử\"\n        }\n      },\n      \"webhook\": {\n        \"name\": \"Webhook\",\n        \"description\": \"Webhook cho phép dịch vụ bên ngoài được thông báo\",\n        \"url\": \"The Post receive URL\",\n        \"contentType\": \"Chọn một loại nội dung\",\n        \"method\": \"Yêu cầu phương thức\",\n        \"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\",\n        \"buttons\": {\n          \"header\": \"Thêm header\"\n        },\n        \"timeout\": {\n          \"placeholder\": \"Thời gian chờ\",\n          \"title\": \"Thời gian chờ thực hiện yêu cầu Http(ms)\"\n        },\n        \"tabs\": {\n          \"headers\": \"Headers\",\n          \"body\": \"Nội dung\",\n          \"response\": \"Phản hồi\"\n        }\n      },\n      \"while-loop\": {\n        \"name\": \"Trong khi lặp lại\",\n        \"description\": \"Thực thi các khối khi điều kiện được đáp ứng\",\n        \"editCondition\": \"Chỉnh sửa điều kiện\",\n        \"fallback\": \"Thực thi khi điều kiện sai\"\n      },\n      \"loop-elements\": {\n        \"name\": \"Yếu tố vòng lặp\",\n        \"description\": \"Lặp lại qua các phần tử\",\n        \"loadMore\": \"Tải thêm các phần tử\",\n        \"scrollToBottom\": \"Cuộn xuống dưới cùng\",\n        \"actions\": {\n          \"none\": \"Không có\",\n          \"click-element\": \"Nhấp vào một phần tử để tải thêm\",\n          \"scroll\": \"Cuộn xuống để tải thêm\",\n          \"click-link\": \"Nhấp vào liên kết để tải thêm\"\n        }\n      },\n      \"loop-data\": {\n        \"name\": \"Dữ liệu vòng lặp\",\n        \"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\",\n        \"loopId\": \"ID Vòng lặp\",\n        \"refKey\": \"Khóa liên kết\",\n        \"startIndex\": \"Bắt đầu từ chỉ mục\",\n        \"resumeLastWorkflow\": \"Tiếp tục quy trình làm việc cuối cùng\",\n        \"reverse\": \"Thứ tự vòng lặp đảo ngược\",\n        \"modal\": {\n          \"fileTooLarge\": \"Tệp quá lớn để chỉnh sửa\",\n          \"maxFile\": \"Kích thước tệp tối đa là 1MB\",\n          \"options\": {\n            \"firstRow\": \"Use the first row as keys\"\n          }\n        },\n        \"buttons\": {\n          \"clear\": \"Xóa dữ liệu\",\n          \"insert\": \"Chèn dữ liệu\",\n          \"import\": \"Nhập tệp\"\n        },\n        \"maxLoop\": {\n          \"title\": \"Số lượng dữ liệu tối đa để lặp lại\",\n          \"label\": \"Dữ liệu tối đa cho vòng lặp (0 là vô hiệu hóa)\"\n        },\n        \"loopThrough\": {\n          \"placeholder\": \"Lặp lại\",\n          \"fromNumber\": \"Từ số\",\n          \"toNumber\": \"Đến số\",\n          \"options\": {\n            \"numbers\": \"Số liệu\",\n            \"variable\": \"Biến\",\n            \"data-columns\": \"Cột dữ liệu\",\n            \"table\": \"Bảng\",\n            \"custom-data\": \"Dữ liệu tùy chỉnh\",\n            \"google-sheets\": \"Google sheets\",\n            \"elements\": \"Các yếu tố\"\n          }\n        }\n      },\n      \"loop-breakpoint\": {\n        \"name\": \"Điểm ngắt vòng lặp\",\n        \"description\": \"Để cho biết khối dữ liệu vòng lặp phải dừng ở đâu\"\n      },\n      \"take-screenshot\": {\n        \"name\": \"Chụp màn hình\",\n        \"fullPage\": \"Chụp ảnh màn hình toàn trang\",\n        \"description\": \"Chụp màn hình của tab đang hoạt động\",\n        \"imageQuality\": \"Chất lượng hình ảnh\",\n        \"saveToColumn\": \"Chèn ảnh chụp màn hình vào bảng\",\n        \"saveToComputer\": \"Lưu ảnh chụp màn hình vào máy tính\",\n        \"types\": {\n          \"title\": \"Chụp ảnh màn hình của\",\n          \"page\": \"Trang\",\n          \"fullpage\": \"Toàn trang\",\n          \"element\": \"Một yếu tố\"\n        }\n      },\n      \"switch-to\": {\n        \"name\": \"Chuyển đổi frame\",\n        \"description\": \"Chuyển đổi giữa cửa sổ chính và iframe\",\n        \"iframeSelector\": \"Bộ chọn phần tử Iframe\",\n        \"windowTypes\": {\n          \"main\": \"Cửa sổ chính\",\n          \"iframe\": \"Iframe\"\n        }\n      },\n      \"debugMode\": {\n        \"title\": \"Chế độ kiểm tra sửa lỗi\",\n        \"description\": \"Sử dụng giao thức Chrome Devtools để chạy khối\"\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "src/locales/vi/common.json",
    "content": "{\n  \"common\": {\n    \"dashboard\": \"Bảng điều khiển\",\n    \"workflow\": \"Quy trình | Danh sách quy trình\",\n    \"collection\": \"Bộ sưu tập | Danh sách bộ sưu tập\",\n    \"log\": \"Nhật ký | Danh sách nhật ký\",\n    \"block\": \"Khối | Danh sách khối\",\n    \"schedule\": \"Lịch trình\",\n    \"folder\": \"Thư mục | Danh sách thư mục\",\n    \"new\": \"Mới\",\n    \"docs\": \"Tài liệu\",\n    \"search\": \"Tìm kiếm\",\n    \"example\": \"Ví dụ | Danh sách ví dụ\",\n    \"import\": \"Nhập\",\n    \"export\": \"Xuất\",\n    \"rename\": \"Đổi tên\",\n    \"execute\": \"Thực thi\",\n    \"delete\": \"Xóa\",\n    \"cancel\": \"Hủy\",\n    \"settings\": \"Cài đặt\",\n    \"options\": \"Tùy chọn\",\n    \"confirm\": \"Xác nhận\",\n    \"name\": \"Tên\",\n    \"all\": \"Tất cả\",\n    \"add\": \"Thêm\",\n    \"save\": \"Lưu\",\n    \"data\": \"Dữ liệu\",\n    \"stop\": \"Dừng lại\",\n    \"action\": \"Hành động | Danh sách jành động\",\n    \"packages\": \"Gói\",\n    \"storage\": \"Kho\",\n    \"editor\": \"Trình biên tập\",\n    \"running\": \"Đang chạy\",\n    \"globalData\": \"Dữ liệu chung\",\n    \"fileName\": \"Tên file\",\n    \"description\": \"Mô tả\",\n    \"disable\": \"Vô hiệu hóa\",\n    \"disabled\": \"Đã vô hiệu hóa\",\n    \"enable\": \"Kích hoạt\",\n    \"fallback\": \"Dự phòng\",\n    \"update\": \"Cập nhật\",\n    \"feature\": \"Tính năng\",\n    \"duplicate\": \"Nhân bản\",\n    \"password\": \"Mật khẩu\",\n    \"category\": \"Loại\",\n    \"optional\": \"Không bắt buộc\"\n  },\n  \"message\": {\n    \"noBlock\": \"Không có khối\",\n    \"noData\": \"Không có dữ liệu để hiển thị\",\n    \"noTriggerBlock\": \"Không thể tìm thấy khối kích hoạt\",\n    \"useDynamicData\": \"Tìm hiểu cách thêm dữ liệu động\",\n    \"delete\": \"Bạn có chắc chắn muốn xóa \\\"{name}\\\"?\",\n    \"empty\": \"Mục này của bạn có vẻ đang bị trống\",\n    \"maxSizeExceeded\": \"Kích thước tệp vượt quá mức tối đa cho phép\",\n    \"notSaved\": \"Bạn thực sự muốn thoát? Bạn có một số thay đổi chưa được lưu!\",\n    \"somethingWrong\": \"Đã xảy ra sự cố\",\n    \"limitExceeded\": \"Bạn đã vượt quá giới hạn\"\n  },\n  \"sort\": {\n    \"sortBy\": \"Sắp xếp theo\",\n    \"name\": \"Tên\",\n    \"createdAt\": \"Ngày tạo\"\n  },\n  \"logStatus\": {\n    \"stopped\": \"đã dừng lại\",\n    \"error\": \"bị lỗi\",\n    \"success\": \"thành công\"\n  },\n}\n"
  },
  {
    "path": "src/locales/vi/newtab.json",
    "content": "{\n  \"home\": {\n    \"viewAll\": \"Tất cả\",\n    \"communities\": \"Cộng đồng\"\n  },\n  \"welcome\": {\n    \"title\": \"Chào mừng bạn đến với Automa! 🎉\",\n    \"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.\",\n    \"marketplace\": \"Cửa hàng\"\n  },\n  \"packages\": {\n    \"name\": \"Gói hàng | Danh sách gói\",\n    \"add\": \"Thêm gói\",\n    \"icon\": \"Biểu tượng gói\",\n    \"open\": \"Mở gói\",\n    \"new\": \"Gói mới\",\n    \"set\": \"Đặt dưới dạng một gói\",\n    \"settings\": {\n      \"asBlock\": \"Đặt gói dưới dạng khối\"\n    },\n    \"categories\": {\n      \"my\": \"Gói của tôi\",\n      \"installed\": \"Các gói đã cài đặt\"\n    }\n  },\n  \"scheduledWorkflow\": {\n    \"title\": \"Quy trình làm việc đã lên lịch\",\n    \"nextRun\": \"Lần chạy tiếp theo\",\n    \"active\": \"Kích hoạt\",\n    \"refresh\": \"Tải lại\",\n    \"schedule\":{\n      \"title\": \"Lịch trình\",\n      \"types\": {\n        \"everyDay\": \"Hằng ngày\",\n        \"general\": \"Mỗi {time}\",\n        \"interval\": \"Cứ {time} phút một lần\"\n      }\n    }\n  },\n  \"storage\": {\n    \"title\": \"Kho\",\n    \"table\": {\n      \"add\": \"Thêm bảng\",\n      \"createdAt\": \"Được tạo lúc\",\n      \"modifiedAt\": \"Đã sửa đổi lúc\",\n      \"rowsCount\": \"Số hàng\",\n      \"delete\": \"Xóa bảng\"\n    }\n  },\n  \"credential\": {\n    \"title\": \"Thông tin xác thực | Danh sách thông tin xác thực\",\n    \"add\": \"Thêm thông tin đăng nhập\",\n    \"use\": {\n      \"title\": \"Thông tin đăng nhập đã sử dụng\",\n      \"description\": \"Quy trình làm việc này sử dụng các thông tin xác thực này\"\n    }\n  },\n  \"workflowPermissions\": {\n    \"title\": \"Quyền quy trình làm việc\",\n    \"description\": \"Dòng công việc này yêu cầu các quyền này chạy đúng cách\",\n    \"contextMenus\": {\n      \"title\": \"Danh mục\",\n      \"description\": \"Để thực thi quy trình làm việc qua menu ngữ cảnh\"\n    },\n    \"clipboardRead\": {\n      \"title\": \"Bảng tạm\",\n      \"description\": \"Để truy cập dữ liệu khay nhớ tạm\"\n    },\n    \"notifications\": {\n      \"title\": \"Thông báo\",\n      \"description\": \"Để hiển thị một thông báo\"\n    },\n    \"downloads\": {\n      \"title\": \"Tải xuống\",\n      \"description\": \"Lưu nội dung trang và đổi tên tệp đã tải xuống\"\n    },\n    \"cookies\": {\n      \"title\": \"Cookies\",\n      \"description\": \"Đọc, đặt hoặc xóa cookie\"\n    }\n  },\n  \"updateMessage\": {\n    \"text1\": \"Automa đã được cập nhật lên v{version},\",\n    \"text2\": \"xem có gì mới.\"\n  },\n  \"workflows\": {\n    \"folder\": {\n      \"new\": \"Thư mục mới\",\n      \"name\": \"Tên thư mục\",\n      \"delete\": \"Xóa thư mục\",\n      \"rename\": \"Đổi tên thư mục\"\n    }\n  },\n  \"auth\": {\n    \"title\": \"Xác thực\",\n    \"signIn\": \"Đăng nhập\",\n    \"username\": \"Bạn cần đặt tên người dùng của mình trước\",\n    \"clickHere\": \"Bấm vào đây\",\n    \"text\": \"Bạn cần phải đăng nhập trước khi có thể làm điều đó\"\n  },\n  \"running\": {\n    \"start\": \"Bắt đầu vào {date}\",\n    \"message\": \"Điều này chỉ hiển thị 5 bản ghi cuối cùng\"\n  },\n  \"settings\": {\n    \"theme\": \"Chủ đề\",\n    \"shortcuts\": {\n      \"duplicate\": \"Lối tắt đã được sử dụng bởi \\\"{name}\\\"\"\n    },\n     \"editor\": {\n      \"title\": \"Tiêu đề\",\n      \"curvature\": {\n        \"title\": \"Đường cong\",\n        \"line\": \"Hàng\",\n        \"reroute\": \"Định tuyến\",\n        \"rerouteFirstLast\": \"Định tuyến lại điểm đầu tiên và điểm cuối cùng\"\n      },\n      \"arrow\": {\n        \"title\": \"Mũi tên dòng\",\n        \"description\": \"Thêm mũi tên vào cuối dòng\"\n      },\n      \"snapGrid\": {\n        \"title\": \"Bám vào lưới\",\n        \"description\": \"Bám vào lưới khi di chuyển một khối\"\n      }\n    },\n    \"deleteLog\": {\n      \"title\": \"Tự động xóa nhật ký quy trình làm việc\",\n      \"after\": \"Xóa sau\",\n      \"deleteAfter\": {\n        \"never\": \"Không bao giờ\",\n        \"days\": \"{day} ngày\"\n      }\n    },\n    \"language\": {\n      \"label\": \"Ngôn ngữ\",\n      \"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.\",\n      \"reloadPage\": \"Tải lại trang để hoàn tất thao tác\"\n    },\n    \"menu\": {\n      \"backup\": \"Sao lưu quy trình làm việc\",\n      \"editor\": \"Biên tập viên\",\n      \"general\": \"Chung\",\n      \"shortcuts\": \"Các phím tắt\",\n      \"about\": \"Thông tin\"\n    },\n    \"backupWorkflows\": {\n      \"title\": \"Sao lưu cục bộ\",\n      \"invalidPassword\": \"Mật khẩu không hợp lệ\",\n      \"workflowsAdded\": \"{count} quy trình công việc đã được thêm vào\",\n      \"name\": \"Sao lưu quy trình công việc\",\n      \"needSignin\": \"Trước tiên bạn cần đăng nhập vào tài khoản của mình\",\n      \"backup\": {\n        \"button\": \"Sao lưu\",\n        \"encrypt\": \"Mã hóa bằng mật khẩu\"\n      },\n      \"restore\": {\n        \"title\": \"Khôi phục quy trình làm việc\",\n        \"button\": \"Khôi phục\",\n        \"update\": \"Cập nhật nếu quy trình làm việc tồn tại\"\n      },\n      \"cloud\": {\n        \"buttons\": {\n          \"local\": \"Cục bộ\",\n          \"cloud\": \"Đám mây\"\n        },\n        \"location\": \"Địa điểm\",\n        \"delete\": \"Xóa bản sao lưu\",\n        \"title\": \"Sao lưu dữ liệu đám mây\",\n        \"sync\": \"Đồng bộ\",\n        \"lastSync\": \"Lần đồng bộ cuối cùng\",\n        \"lastBackup\": \"Sao lưu cuối cùng\",\n        \"select\": \"Chọn quy trình làm việc\",\n        \"storedWorkflows\": \"Quy trình làm việc được lưu trữ trên đám mây\",\n        \"selected\": \"Đã chọn\",\n        \"selectText\": \"Chọn quy trình công việc mà bạn muốn sao lưu\",\n        \"selectAll\": \"Chọn tất cả\",\n        \"deselectAll\": \"Bỏ chọn tất cả\",\n        \"needSelectWorkflow\": \"Bạn cần chọn quy trình công việc mà bạn muốn sao lưu\"\n      }\n    }\n  },\n  \"workflow\": {\n    \"previewMode\": {\n      \"title\": \"Chế độ xem trước\",\n      \"description\": \"Bạn đang ở chế độ xem trước, những thay đổi bạn đã thực hiện sẽ không được lưu\"\n    },\n    \"pinWorkflow\": {\n      \"pin\": \"Ghim quy trình làm việc\",\n      \"unpin\": \"Bỏ ghim quy trình làm việc\",\n      \"pinned\": \"Quy trình công việc được ghim\"\n    },\n    \"my\": \"Quy trình làm việc của tôi\",\n    \"import\": \"Nhập quy trình\",\n    \"new\": \"Tạo quy trình mới\",\n    \"delete\": \"Xóa quy trình\",\n    \"browse\": \"Duyệt quy trình công việc\",\n    \"name\": \"Tên quy trình\",\n    \"rename\": \"Sửa tên quy trình\",\n    \"backupCloud\": \"Sao lưu quy trình công việc lên đám mây\",\n    \"add\": \"Thêm quy trình\",\n    \"clickToEnable\": \"Nhấn để kích hoạt\",\n    \"toggleSidebar\": \"Chuyển đổi thanh bên\",\n    \"cantEdit\": \"Không thể chỉnh sửa quy trình làm việc được chia sẻ\",\n    \"undo\": \"Hoàn tác\",\n    \"redo\": \"Làm lại\",\n    \"autoAlign\": {\n      \"title\": \"Tự động căn chỉnh\"\n    },\n    \"blocksFolder\": {\n      \"title\": \"Thư mục khối\",\n      \"add\": \"Thêm khối vào thư mục\",\n      \"save\": \"Lưu vào thư mục\"\n    },\n    \"searchBlocks\": {\n      \"title\": \"Các khối tìm kiếm trong trình chỉnh sửa\"\n    },\n    \"conditionBuilder\": {\n      \"title\": \"Trình tạo điều kiện\",\n      \"add\": \"Add Thêm điều kiện\",\n      \"and\": \"Và\",\n      \"or\": \"Hoặc\",\n      \"topAwait\": \"Hỗ trợ chức năng chờ đợi cấp cao nhất và \\\"automaRefData \\\"\"\n    },\n    \"host\": {\n      \"title\": \"Lưu trữ quy trình làm việc\",\n      \"set\": \"Đặt làm quy trình làm việc trên máy chủ lưu trữ\",\n      \"id\": \"ID Máy chủ\",\n      \"add\": \"Thêm quy trình làm việc được lưu trữ\",\n      \"sync\": {\n        \"title\": \"Đồng bộ\",\n        \"description\": \"Đồng bộ hóa với quy trình làm việc trên máy chủ\"\n      },\n      \"messages\": {\n        \"hostExist\": \"Bạn đã thêm máy chủ này\",\n        \"notFound\": \"Không thể tìm thấy quy trình làm việc được lưu trữ với id \\\"{id} \\\"\",\n        \"successAdded\": \"Quy trình làm việc được lưu trữ với id \\\"{id} \\\" đã được thêm thành công\"\n      }\n    },\n    \"type\": {\n      \"local\": \"Cục bộ\",\n      \"shared\": \"Được chia sẻ\",\n      \"host\": \"Máy chủ\"\n    },\n    \"unpublish\": {\n      \"title\": \"Hủy xuất bản quy trình làm việc\",\n      \"button\": \"Hủy xuất bản\",\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?\"\n    },\n    \"share\": {\n      \"url\": \"Chia sẻ URL\",\n      \"publish\": \"Xuất bản\",\n      \"sharedAs\": \"Được chia sẻ với tên \\\"{name} \\\"\",\n      \"title\": \"Chia sẻ quy trình làm việc\",\n      \"download\": \"Thêm quy trình làm việc vào cục bộ\",\n      \"edit\": \"Chỉnh sửa Mô tả\",\n      \"fetchLocal\": \"Tìm nạp quy trình làm việc cục bộ\",\n      \"update\": \"Cập nhật\",\n      \"unpublish\": \"Hủy xuất bản\",\n      \"linkCopied\": \"Liên kết đã được sao chép vào bộ nhớ\"\n    },\n    \"variables\": {\n      \"title\": \"Biến | Danh sách biến\",\n      \"name\": \"Tên biến\",\n      \"assign\": \"Gán cho biến\"\n    },\n    \"protect\": {\n      \"title\": \"Bảo vệ quy trình làm việc \",\n      \"remove\": \"Loại bỏ bảo vệ\",\n      \"button\": \"Bảo vệ\",\n      \"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.\"\n    },\n    \"locked\": {\n      \"title\": \"Dòng công việc này được bảo vệ\",\n      \"body\": \"Nhập mật khẩu để mở khóa\",\n      \"unlock\": \"Mở khóa\",\n      \"messages\": {\n        \"incorrect-password\": \"Mật khẩu không đúng\"\n      }\n    },\n    \"state\": {\n      \"executeBy\": \"Thực hiện bởi: \\\"{name}\\\"\"\n    },\n    \"table\": {\n      \"title\": \"Bảng | Danh sách bảng\",\n      \"placeholder\": \"Tìm kiếm hoặc thêm cột\",\n      \"select\": \"Chọn cột\",\n      \"column\": {\n        \"name\": \"Tên cột dọc\",\n        \"type\": \"Loại dữ liệu\"\n      }\n    },\n    \"sidebar\": {\n      \"workflowIcon\": \"Biểu tượng quy trình làm việc\"\n    },\n    \"editor\": {\n      \"zoomIn\": \"Phóng to\",\n      \"zoomOut\": \"Thu nhỏ\",\n      \"resetZoom\": \"Về mặc định\",\n      \"duplicate\": \"Nhân bản\",\n      \"copy\": \"Sao chép\",\n      \"paste\": \"Dán\",\n      \"group\": \"Nhóm khối\",\n      \"ungroup\": \"Bỏ nhóm các khối\"\n    },\n    \"settings\": {\n      \"saveLog\": \"Lưu nhật ký quy trình làm việc\",\n      \"executedBlockOnWeb\": \"Hiển thị khối đã thực thi trên trang web\",\n      \"notification\": {\n        \"title\": \"Thông báo quy trình làm việc\",\n        \"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\",\n        \"noPermission\": \"Automa yêu cầu quyền \\\"thông báo \\\" để làm cho điều này hoạt động\"\n      },\n      \"publicId\": {\n        \"title\": \"ID công khai quy trình làm việc\",\n        \"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\"\n      },\n      \"defaultColumn\": {\n        \"title\": \"Chèn vào cột mặc định\",\n        \"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\",\n        \"name\": \"Tên cột mặc định\"\n      },\n      \"autocomplete\": {\n        \"title\": \"Tự động hoàn thành\",\n        \"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)\"\n      },\n      \"clearCache\": {\n        \"title\": \"Xóa bộ nhớ cache\",\n        \"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\",\n        \"info\": \"Xóa thành công bộ nhớ cache của quy trình làm việc\",\n        \"btn\": \"Xóa\"\n      },\n      \"reuseLastState\": {\n        \"title\": \"Sử dụng lại trạng thái quy trình làm việc cuối cùng\",\n        \"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 \"\n      },\n      \"debugMode\": {\n        \"title\": \"Chế độ kiểm tra sửa lỗi\",\n        \"description\": \"Thực thi quy trình làm việc bằng Giao thức Chrome DevTools\"\n      },\n      \"restartWorkflow\": {\n        \"for\": \"Khởi động lại cho\",\n        \"times\": \"Times\",\n        \"description\": \"Tối đa bao nhiêu lần quy trình làm việc sẽ khởi động lại\"\n      },\n      \"onError\": {\n        \"title\": \"Khi quy trình gặp lỗi\",\n        \"description\": \"Đặt những việc cần làm khi xảy ra lỗi trong quy trình làm việc\",\n        \"items\": {\n          \"keepRunning\": \"Tiếp tục chạy\",\n          \"stopWorkflow\": \"Dừng quy trình\",\n          \"restartWorkflow\": \"Khởi động lại quy trình làm việc\"\n        }\n      },\n      \"timeout\": {\n        \"title\": \"Thời lượng thực thi tối đa (Mili giây)\"\n      },\n      \"blockDelay\": {\n        \"title\": \"Chậm trễ khối (mili giây)\",\n        \"description\": \"Thêm độ trễ trước khi thực hiện từng khối\"\n      },\n      \"tabLoadTimeout\": {\n        \"title\": \"Thời gian chờ tải tab\",\n        \"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ờ.\"\n      }\n    }\n  },\n  \"collection\": {\n    \"description\": \"Thực thi quy trình của bạn theo trình tự\",\n    \"new\": \"Bộ sưu tập mới\",\n    \"delete\": \"Xóa bộ sưu tập\",\n    \"add\": \"Thêm bộ sưu tập\",\n    \"rename\": \"Đổi tên bộ sưu tập\",\n    \"flow\": \"Trình tự\",\n    \"dragDropText\": \"Thả một quy trình hoặc khối vào đây\",\n    \"options\": {\n      \"atOnce\": {\n        \"title\": \"Thực thi tất cả quy trình trong bộ sưu tập cùng một lúc\",\n        \"description\": \"Khối sẽ không được thực thi khi kích hoạt tùy chọn này\"\n      }\n    },\n    \"globalData\": {\n      \"note\": \"Điều này sẽ ghi đè lên dữ liệu chung của quy trình\"\n    }\n  },\n  \"log\": {\n    \"flowId\": \"ID Luồng\",\n    \"goBack\": \"Trở lại nhật ký \\\"{name}\\\"\",\n    \"goWorkflow\": \"Đi tới quy trình làm việc\",\n    \"startedDate\": \"Ngày bắt đầu\",\n    \"duration\": \"Thời lượng\",\n    \"selectAll\": \"Chọn tất cả\",\n    \"deselectAll\": \"Bỏ chọn tất cả\",\n    \"deleteSelected\": \"Xóa nhật ký đã chọn\",\n    \"types\": {\n      \"stop\": \"Quy trình đã bị dừng\",\n      \"finish\": \"Hoàn thành\"\n    },\n    \"messages\": {\n      \"url-empty\": \"URL trống\",\n      \"invalid-url\": \"URL không hợp lệ\",\n      \"conditions-empty\": \"Điều kiện trống\",\n      \"workflow-disabled\": \"Quy trình đã được vô hiệu hóa\",\n      \"selector-empty\": \"Bộ chọn phần tử trống\",\n      \"invalid-body\": \"Nội dung không phải là JSON hợp lệ\",\n      \"invalid-active-tab\": \"\\\"{url} \\\" là URL không hợp lệ\",\n      \"empty-spreadsheet-id\": \"Id bảng tính trống\",\n      \"invalid-loop-data\": \"Dữ liệu không hợp lệ để lặp lại\",\n      \"empty-workflow\": \"Đầu tiên, bạn phải chọn một quy trình\",\n      \"active-tab-removed\": \"Tab hoạt động của quy trình làm việc bị xóa\",\n      \"empty-spreadsheet-range\": \"Phạm vi bảng tính trống\",\n      \"stop-timeout\": \"Quy trình đã bị dừng vì hết thời lượng thực thi\",\n      \"invalid-proxy-host\": \"Máy chủ proxy không hợp lệ\",\n      \"no-file-access\": \"Automa không có quyền truy cập vào tệp \",\n      \"no-workflow\": \"Không tìm thấy quy trình với ID \\\"{workflowId}\\\"\",\n      \"no-match-tab\": \"Không thể tìm thấy tab có các mẫu \\\"{pattern} \\\"\",\n      \"no-clipboard-acces\": \"Không có quyền truy cập khay nhớ tạm\",\n      \"browser-not-supported\": \"Tính năng này không được hỗ trợ trong trình duyệt {browser}\",\n      \"element-not-found\": \"Không thể tìm thấy phần tử có bộ chọn \\\"{selector} \\\".\",\n      \"no-permission\": \"Không có quyền \\\"{allow} \\\" để thực hiện tác vụ này\",\n      \"not-iframe\": \"Phần tử có bộ chọn \\\"{selector} \\\" không phải là phần tử Iframe\",\n      \"iframe-not-found\": \"Không thể tìm thấy phần tử Iframe bằng bộ chọn \\\"{selector} \\\".\",\n      \"workflow-infinite-loop\": \"Quy trình không được thực thi để ngăn vòng lặp vô hạn\",\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\",\n      \"no-iframe-id\": \"Không tìm thấy Frame ID cho iframe element với bộ chọn \\\"{selector}\\\"\",\n      \"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}\\\".\"\n    },\n    \"description\": {\n      \"text\": \"{status} vào {date} trong {duration}\",\n      \"status\": {\n        \"success\": \"Thành công\",\n        \"error\": \"Thất bại\",\n        \"stopped\": \"Đã dừng lại\"\n      }\n    },\n    \"delete\": {\n      \"title\": \"Xóa nhật ký\",\n      \"description\": \"Bạn muốn xóa tất cả các nhật ký đã chọn?\"\n    },\n    \"exportData\": {\n      \"title\": \"Xuất dữ liệu\",\n      \"types\": {\n        \"json\": \"JSON\",\n        \"csv\": \"CSV\",\n        \"plain-text\": \"Văn bản thuần túy\"\n      }\n    },\n    \"filter\": {\n      \"title\": \"Bộ lọc\",\n      \"byStatus\": \"Theo trang thái\",\n      \"byDate\": {\n        \"title\": \"Theo ngày\",\n        \"items\": {\n          \"lastDay\": \"Hôm nay\",\n          \"last7Days\": \"7 ngày qua\",\n          \"last30Days\": \"30 ngày qua\"\n        }\n      }\n    }\n  },\n  \"components\": {\n    \"pagination\": {\n      \"text1\": \"Hiển thị\",\n      \"text2\": \"trong tổng số {count}\",\n      \"nextPage\": \"Trang tiếp theo\",\n      \"currentPage\": \"Trang hiện tại\",\n      \"prevPage\": \"Trang trước\",\n      \"of\": \"trong tổng số {page}\"\n    }\n  }\n}\n"
  },
  {
    "path": "src/locales/vi/popup.json",
    "content": "{\n  \"recording\": {\n    \"stop\": \"Dừng ghi\",\n    \"title\": \"Ghi âm\"\n  },\n  \"home\": {\n    \"record\": {\n      \"title\": \"Ghi lại quy trình làm việc\",\n      \"button\": \"Ghi lại\",\n      \"name\": \"Tên quy trình làm việc\",\n      \"selectBlock\": \"Chọn một khối để bắt đầu\",\n      \"anotherBlock\": \"Không thể bắt đầu từ khối này\",\n      \"tabs\": {\n        \"new\": \"Quy trình làm việc mới\",\n        \"existing\": \"Quy trình làm việc hiện tại\"\n      }\n    },\n    \"elementSelector\": {\n      \"name\": \"Bộ chọn phần tử\",\n      \"noAccess\": \"Không có quyền truy cập vào trang web này\"\n    },\n    \"workflow\": {\n      \"new\": \"Tạo quy trình mới\",\n      \"rename\": \"Đổi tên quy trình\",\n      \"delete\": \"Xóa quy trình\",\n      \"type\": {\n        \"host\": \"Máy chủ\",\n        \"local\": \"Cục bộ\",\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "src/locales/zh/blocks.json",
    "content": "{\n  \"collection\": {\n    \"blocks\": {\n      \"export-result\": {\n        \"name\": \"导出结果\",\n        \"description\": \"将集合结果导出为 JSON\"\n      }\n    }\n  },\n  \"workflow\": {\n    \"blocks\": {\n      \"base\": {\n        \"title\": \"模块\",\n        \"moveToGroup\": \"移动模块到模块组\",\n        \"selector\": \"元素选择器\",\n        \"selectorOptions\": \"选择器选项\",\n        \"timeout\": \"超时 (毫秒)\",\n        \"noPermission\": \"Automa 没有足够的权限执行此操作\",\n        \"grantPermission\": \"授予权限\",\n        \"action\": \"操作\",\n        \"element\": {\n          \"select\": \"选择一个元素\",\n          \"verify\": \"验证选择器\"\n        },\n        \"settings\": {\n          \"title\": \"模块设置\",\n          \"blockTimeout\": {\n            \"title\": \"模块执行超时（毫秒）\",\n            \"description\": \"模块的最大执行时间（0表示禁用）\"\n          },\n          \"line\": {\n            \"title\": \"线段\",\n            \"label\": \"线段标签\",\n            \"animated\": \"动画\",\n            \"select\": \"选择线段\",\n            \"to\": \"连线到 {name} 模块\",\n            \"lineColor\": \"线段颜色\"\n          }\n        },\n        \"toggle\": {\n          \"enable\": \"启用模块\",\n          \"disable\": \"禁用模块\"\n        },\n        \"onError\": {\n          \"info\": \"这些规则适用于模块发生错误时\",\n          \"button\": \"出错时\",\n          \"title\": \"错误发生时\",\n          \"retry\": \"重试操作\",\n          \"fallbackTitle\": \"当模块中发生错误时执行\",\n          \"times\": {\n            \"name\": \"次数\",\n            \"description\": \"重试操作的次数\"\n          },\n          \"interval\": {\n            \"name\": \"间隔\",\n            \"description\": \"每次尝试等待的时间\",\n            \"second\": \"秒\"\n          },\n          \"toDo\": {\n            \"error\": \"抛出错误\",\n            \"continue\": \"继续流程\",\n            \"fallback\": \"执行回退\",\n            \"restart\": \"重启流程\"\n          },\n          \"insertData\": {\n            \"name\": \"插入数据\"\n          }\n        },\n        \"table\": {\n          \"checkbox\": \"插入表格\",\n          \"select\": \"选择列\",\n          \"extraRow\": {\n            \"checkbox\": \"添加额外的行\",\n            \"placeholder\": \"值\",\n            \"title\": \"额外行的值\"\n          }\n        },\n        \"findElement\": {\n          \"placeholder\": \"查找元素依据\",\n          \"options\": {\n            \"cssSelector\": \"CSS 选择器\",\n            \"xpath\": \"XPath\"\n          }\n        },\n        \"markElement\": {\n          \"title\": \"如果之前已经选择过元素，则不会被选择\",\n          \"text\": \"标记元素\"\n        },\n        \"multiple\": {\n          \"title\": \"选择多个元素\",\n          \"text\": \"多选\"\n        },\n        \"waitSelector\": {\n          \"title\": \"等待选择器\",\n          \"timeout\": \"选择器超时 (ms)\"\n        },\n        \"downloads\": {\n          \"onConflict\": {\n            \"uniquify\": \"唯一\",\n            \"overwrite\": \"覆盖\",\n            \"prompt\": \"询问\"\n          }\n        }\n      },\n      \"wait-connections\": {\n        \"name\": \"等待连接\",\n        \"description\": \"在继续下一个模块之前等待所有连接\",\n        \"specificFlow\": \"只继续一个特定的流程\",\n        \"selectFlow\": \"选择流程\"\n      },\n      \"cookie\": {\n        \"name\": \"Cookie\",\n        \"description\": \"获取, 设置, 或移除 cookies\",\n        \"types\": {\n          \"get\": \"获取 cookies\",\n          \"set\": \"设置 cookie\",\n          \"remove\": \"移除 cookies\",\n          \"getAll\": \"获取所有 cookies\"\n        },\n        \"useJson\": \"使用 JSON 格式\"\n      },\n      \"note\": {\n        \"name\": \"注释\"\n      },\n      \"slice-variable\": {\n        \"name\": \"切片变量\",\n        \"description\": \"提取变量值的一部分\",\n        \"start\": \"开始序列\",\n        \"end\": \"结束序列\"\n      },\n      \"workflow-state\": {\n        \"name\": \"工作流状态\",\n        \"description\": \"管理工作流状态\",\n        \"actions\": {\n          \"stop\": \"停止工作流\"\n        },\n        \"error\": {\n          \"throwError\": \"抛出错误\",\n          \"message\": \"错误消息\"\n        }\n      },\n      \"regex-variable\": {\n        \"name\": \"RegEx 变量\",\n        \"description\": \"将变量值与正则表达式匹配\"\n      },\n      \"data-mapping\": {\n        \"source\": \"源\",\n        \"destination\": \"目的\",\n        \"name\": \"数据映射\",\n        \"edit\": \"编辑数据图\",\n        \"dataSource\": \"数据源\",\n        \"description\": \"映射变量或表的数据\",\n        \"addSource\": \"添加源\",\n        \"addDestination\": \"添加目的\"\n      },\n      \"sort-data\": {\n        \"name\": \"排序数据\",\n        \"description\": \"对数据项进行排序\",\n        \"property\": \"根据属性排序数据项\",\n        \"addProperty\": \"添加属性\"\n      },\n      \"increase-variable\": {\n        \"name\": \"增加变量\",\n        \"description\": \"将变量的值增加特定数量\",\n        \"increase\": \"增加自\"\n      },\n      \"notification\": {\n        \"name\": \"通知\",\n        \"description\": \"显示通知\",\n        \"title\": \"标题\",\n        \"message\": \"消息\",\n        \"imageUrl\": \"图片 URL (可选项)\",\n        \"iconUrl\": \"图标 URL (可选项)\"\n      },\n      \"delete-data\": {\n        \"name\": \"删除数据\",\n        \"description\": \"删除表格或变量\",\n        \"from\": \"数据来自\",\n        \"allColumns\": \"[全部列]\"\n      },\n      \"log-data\": {\n        \"name\": \"获取日志数据\",\n        \"description\": \"获取工作流的最新日志数据\",\n        \"data\": \"日志数据\"\n      },\n      \"tab-url\": {\n        \"name\": \"获取标签页 URL\",\n        \"description\": \"获取标签页 URL\",\n        \"select\": \"选择标签页\",\n        \"types\": {\n          \"active-tab\": \"活动标签页\",\n          \"all\": \"所有标签页\"\n        },\n        \"query\": {\n          \"title\": \"查询\",\n          \"matchPatterns\": \"@:workflow.blocks.switch-tab.matchPattern (可选项)\",\n          \"tabTitle\": \"表格标题 (可选项)\"\n        }\n      },\n      \"reload-tab\": {\n        \"name\": \"重载标签页\",\n        \"description\": \"重新加载激活的标签页\"\n      },\n      \"press-key\": {\n        \"name\": \"按键\",\n        \"description\": \"按键或组合键\",\n        \"target\": \"目标元素 (可选项)\",\n        \"key\": \"按键\",\n        \"detect\": \"检测按键\",\n        \"actions\": {\n          \"press-key\": \"按下一个按键\",\n          \"multiple-keys\": \"按下多个按键\"\n        },\n        \"press-time\": \"按下时间 (毫秒)\"\n      },\n      \"save-assets\": {\n        \"name\": \"保存资源\",\n        \"description\": \"保存资源 (图像, 视频, 音频, 或文件) 从一个元素或 URL\",\n        \"filename\": \"文件名 (可选项)\",\n        \"saveDownloadIds\": \"保存条目'下载 id\",\n        \"contentTypes\": {\n          \"title\": \"类型\",\n          \"element\": \"多媒体元素 (图像, 音频, 或视频)\",\n          \"url\": \"URL\"\n        }\n      },\n      \"handle-dialog\": {\n        \"name\": \"处理对话框\",\n        \"description\": \"接受或取消JavaScript启动的对话框(警报、确认、提示或卸载前)。\",\n        \"accept\": \"接受对话框\",\n        \"promptText\": {\n          \"label\": \"提示文本 (可选项)\",\n          \"description\": \"接受前要输入对话框提示的文本\"\n        }\n      },\n      \"handle-download\": {\n        \"name\": \"处理下载\",\n        \"description\": \"处理下载文件\",\n        \"timeout\": \"超时 (毫秒)\",\n        \"noPermission\": \"没有访问下载的权限\",\n        \"onConflict\": \"冲突时\",\n        \"waitFile\": \"等待下载文件\",\n        \"downloadId\": \"文件下载 id (可选项)\",\n        \"filePath\": \"文件路径\"\n      },\n      \"insert-data\": {\n        \"name\": \"插入数据\",\n        \"description\": \"将数据插入表或变量\"\n      },\n      \"clipboard\": {\n        \"name\": \"剪贴板\",\n        \"description\": \"从剪贴板获取复制的文本\",\n        \"data\": \"剪贴板数据\",\n        \"noPermission\": \"没有访问剪贴板的权限\",\n        \"grantPermission\": \"授予权限\",\n        \"copySelection\": \"复制页面上的选定文本\",\n        \"types\": {\n          \"get\": \"获取剪贴板数据\",\n          \"insert\": \"将文本插入剪贴板\"\n        }\n      },\n      \"hover-element\": {\n        \"name\": \"悬停元素\",\n        \"description\": \"悬停在一个元素上\"\n      },\n      \"create-element\": {\n        \"name\": \"创建元素\",\n        \"description\": \"创建一个元素并插入到页面\",\n        \"edit\": \"编辑元素\",\n        \"wrap\": \"将元素包裹在里面\",\n        \"insertEl\": {\n          \"title\": \"插入元素\",\n          \"items\": {\n            \"before\": \"为首个子元素\",\n            \"after\": \"为最末子元素\",\n            \"next-sibling\": \"为下一个同级元素\",\n            \"prev-sibling\": \"为上一个同级元素\",\n            \"replace\": \"替换目标元素\"\n          }\n        }\n      },\n      \"upload-file\": {\n        \"name\": \"上传文件\",\n        \"description\": \"上传文件到 <input type=\\\"file\\\"> 元素\",\n        \"filePath\": \"URL 或 文件路径\",\n        \"addFile\": \"添加文件\",\n        \"onlyURL\": \"仅支持从 Firefox 浏览器中的 URL 上传文件\",\n        \"requirement\": \"使用本模块前，请参看要求\",\n        \"noFileAccess\": \"Automa 没有文件访问权限\"\n      },\n      \"browser-event\": {\n        \"name\": \"浏览器事件\",\n        \"description\": \"触发事件时执行下一个模块\",\n        \"events\": \"事件\",\n        \"timeout\": \"超时 (毫秒)\",\n        \"activeTabLoaded\": \"活动标签页\",\n        \"setAsActiveTab\": \"设为活动标签页\"\n      },\n      \"blocks-group-2\": {\n        \"name\": \"@:workflow.blocks.blocks-group.name 2\",\n        \"description\": \"@:workflow.blocks.blocks-group.description\"\n      },\n      \"blocks-group\": {\n        \"name\": \"模块组\",\n        \"groupName\": \"组名\",\n        \"description\": \"分组模块\",\n        \"dropText\": \"拖放模块到此处\",\n        \"cantAdd\": \"无法添加 \\\"{blockName}\\\" 模块到此组.\"\n      },\n      \"trigger\": {\n        \"name\": \"触发器\",\n        \"description\": \"工作流从这里开始执行\",\n        \"addTime\": \"添加时间\",\n        \"selectDay\": \"选择星期几\",\n        \"timeExist\": \"你已经添加 {time} 在 {day}\",\n        \"fixedDelay\": \"固定延迟\",\n        \"contextMenus\": {\n          \"noPermission\": \"此触发器需要 \\\"contextMenus\\\" 权限才能工作\",\n          \"grantPermission\": \"授予权限\",\n          \"appearIn\": \"会使\",\n          \"contextName\": \"工作流名称出现在上下文菜单中\"\n        },\n        \"days\": [\n          \"星期日\",\n          \"星期一\",\n          \"星期二\",\n          \"星期三\",\n          \"星期四\",\n          \"星期五\",\n          \"星期六\"\n        ],\n        \"useRegex\": \"使用正则表达式\",\n        \"shortcut\": {\n          \"tooltip\": \"录制快捷键\",\n          \"stopRecord\": \"停止录制\",\n          \"checkboxTitle\": \"即使在输入元素中也执行快捷键\",\n          \"checkbox\": \"输入时激活\",\n          \"note\": \"注意：键盘快捷键仅在您访问网页时有效\"\n        },\n        \"forms\": {\n          \"triggerWorkflow\": \"触发工作流\",\n          \"interval\": \"间隔 (分钟)\",\n          \"delay\": \"延迟 (分钟)\",\n          \"date\": \"日期\",\n          \"time\": \"时间\",\n          \"url\": \"URL 或 正则表达式\",\n          \"shortcut\": \"快捷键\",\n          \"cron-expression\": \"Cron 表达式\"\n        },\n        \"element-change\": {\n          \"target\": \"要监测的目标元素\",\n          \"optionsInfo\": \"哪个元素突变会触发工作流\",\n          \"targetWebsite\": \"目标元素所在网站的匹配模式（点击查看更多匹配模式示例)\",\n          \"baseEl\": {\n            \"title\": \"基础元素 (可选项)\",\n            \"description\": \"当此元素发生变化时，Automa 将重新开始监测目标元素\"\n          },\n          \"subtree\": {\n            \"title\": \"包含子树\",\n            \"description\": \"将监测扩展到目标元素的整个子树\"\n          },\n          \"childList\": {\n            \"title\": \"子列表\",\n            \"description\": \"监测新的子元素的增加或现有子元素的删除\"\n          },\n          \"attributes\": {\n            \"title\": \"属性\",\n            \"description\": \"监测目标元素属性值的变化\"\n          },\n          \"attributeFilter\": {\n            \"title\": \"属性过滤器\",\n            \"separate\": \"使用逗号(,)来分隔属性名称\",\n            \"description\": \"只监测特定的属性（留空以监测所有属性）。\"\n          },\n          \"characterData\": {\n            \"title\": \"字符数据\",\n            \"description\": \"监测目标元素内的字符数据/文本的变化\"\n          }\n        },\n        \"items\": {\n          \"manual\": \"手动\",\n          \"interval\": \"间隔\",\n          \"cron-job\": \"Cron 任务\",\n          \"date\": \"在特定日期\",\n          \"context-menu\": \"上下文菜单\",\n          \"element-change\": \"元素更改时\",\n          \"specific-day\": \"在特定的星期几\",\n          \"visit-web\": \"访问网站时\",\n          \"on-startup\": \"浏览器启动时\",\n          \"keyboard-shortcut\": \"键盘快捷键\"\n        }\n      },\n      \"execute-workflow\": {\n        \"name\": \"执行工作流\",\n        \"overwriteNote\": \"这将覆盖所选工作流的全局数据\",\n        \"select\": \"选择工作流\",\n        \"executeId\": \"执行 Id\",\n        \"description\": \"\",\n        \"insertAllVars\": \"插入所有当前工作流变量\",\n        \"insertVars\": \"插入当前工作流变量\",\n        \"useCommas\": \"使用逗号分隔变量名\",\n        \"insertAllGlobalData\": \"插入当前工作流的 GlobalData\"\n      },\n      \"google-sheets-drive\": {\n        \"name\": \"@:workflow.blocks.google-sheets.name (GDrive)\",\n        \"description\": \"@:workflow.blocks.google-sheets.description\",\n        \"connected\": \"已连接工作簿\",\n        \"select\": \"选择工作簿\",\n        \"connect\": \"连接工作簿\"\n      },\n      \"google-drive\": {\n        \"name\": \"Google Drive\",\n        \"description\": \"上传文件到 Google Drive\",\n        \"actions\": {\n          \"upload\": \"上传文件\"\n        }\n      },\n      \"google-sheets\": {\n        \"name\": \"Google sheets\",\n        \"description\": \"读取或更新 Google sheets 数据\",\n        \"previewData\": \"预览数据\",\n        \"firstRow\": \"使用第一行作为主键\",\n        \"keysAsFirstRow\": \"使用主键作为第一行\",\n        \"insertData\": \"插入数据\",\n        \"valueInputOption\": \"输入选项值\",\n        \"insertDataOption\": \"插入数据选项\",\n        \"rangeToSearch\": \"开始搜索的范围\",\n        \"dataFrom\": {\n          \"label\": \"数据来自\",\n          \"options\": {\n            \"data-columns\": \"表格\",\n            \"custom\": \"自定义\"\n          }\n        },\n        \"refKey\": {\n          \"label\": \"参考键 (可选项)\",\n          \"placeholder\": \"键名称\"\n        },\n        \"spreadsheetId\": {\n          \"label\": \"电子表格 Id\",\n          \"link\": \"查看如何获取电子表格 Id\"\n        },\n        \"range\": {\n          \"label\": \"范围\",\n          \"link\": \"点击查看更多示例\"\n        },\n        \"select\": {\n          \"get\": \"获取电子表格单元格值\",\n          \"getRange\": \"获取电子表格范围\",\n          \"update\": \"更新电子表格单元格值\",\n          \"append\": \"追加电子表格单元格值\",\n          \"clear\": \"清除电子表格单元格值\",\n          \"create\": \"创建电子表格\",\n          \"add-sheet\": \"添加工作簿\"\n        }\n      },\n      \"active-tab\": {\n        \"name\": \"活动标签页\",\n        \"description\": \"将您所在的当前标签页设置为活动标签页\"\n      },\n      \"proxy\": {\n        \"name\": \"Proxy\",\n        \"description\": \"设置浏览器的代理\",\n        \"clear\": \"清除所有代理\",\n        \"bypass\": {\n          \"label\": \"免过滤清单\",\n          \"note\": \"使用英文逗号 (,) 分隔 URL\"\n        }\n      },\n      \"new-window\": {\n        \"name\": \"新建窗口\",\n        \"description\": \"创建一个新窗口\",\n        \"top\": \"上\",\n        \"left\": \"左\",\n        \"height\": \"高度\",\n        \"width\": \"宽度\",\n        \"note\": \"注意：使用 0 禁用\",\n        \"position\": \"窗口位置\",\n        \"size\": \"窗口尺寸\",\n        \"windowState\": {\n          \"placeholder\": \"窗口状态\",\n          \"options\": {\n            \"normal\": \"常规\",\n            \"minimized\": \"最小化\",\n            \"maximized\": \"最大化\",\n            \"fullscreen\": \"全屏\"\n          }\n        },\n        \"incognito\": {\n          \"text\": \"设置为无痕窗口\",\n          \"note\": \"您必须为此扩展启用“在无痕模式下启用”才能使用该选项\"\n        }\n      },\n      \"go-back\": {\n        \"name\": \"返回\",\n        \"description\": \"返回到上一页\"\n      },\n      \"forward-page\": {\n        \"name\": \"前进\",\n        \"description\": \"前进到下一页\"\n      },\n      \"close-tab\": {\n        \"name\": \"关闭 标签页/窗口\",\n        \"description\": \"\",\n        \"url\": \"匹配模式\",\n        \"activeTab\": \"关闭活动标签页\",\n        \"allWindows\": \"关闭所有窗口\"\n      },\n      \"event-click\": {\n        \"name\": \"点击元素\",\n        \"description\": \"\"\n      },\n      \"delay\": {\n        \"name\": \"延迟\",\n        \"description\": \"在执行下一个模块之前添加延迟\",\n        \"input\": {\n          \"title\": \"延迟毫秒\",\n          \"placeholder\": \"(毫秒)\"\n        }\n      },\n      \"parameter-prompt\": {\n        \"name\": \"参数提示\"\n      },\n      \"get-text\": {\n        \"name\": \"获取文本\",\n        \"description\": \"从元素中获取文本\",\n        \"checkbox\": \"插入到表格\",\n        \"includeTags\": \"包含 HTML 标记\",\n        \"prefixText\": {\n          \"placeholder\": \"文本前缀\",\n          \"title\": \"为文本添加前缀\"\n        },\n        \"suffixText\": {\n          \"placeholder\": \"文本后缀\",\n          \"title\": \"为文本添加后缀\"\n        }\n      },\n      \"export-data\": {\n        \"name\": \"导出数据\",\n        \"description\": \"导出工作流数据\",\n        \"exportAs\": \"导出为\",\n        \"refKey\": \"参考键\",\n        \"bomHeader\": \"添加 UTF-8 BOM\",\n        \"dataToExport\": {\n          \"placeholder\": \"要导出的数据\",\n          \"options\": {\n            \"data-columns\": \"表格\",\n            \"google-sheets\": \"Google sheets\",\n            \"variable\": \"变量\"\n          }\n        }\n      },\n      \"element-scroll\": {\n        \"name\": \"滚动元素\",\n        \"description\": \"\",\n        \"scrollY\": \"垂直滚动\",\n        \"scrollX\": \"水平滚动\",\n        \"intoView\": \"滚动查看\",\n        \"smooth\": \"平滑滚动\",\n        \"incScrollX\": \"增加水平滚动\",\n        \"incScrollY\": \"递增垂直滚动\"\n      },\n      \"switch-tab\": {\n        \"name\": \"切换标签页\",\n        \"description\": \"在标签页之间切换\",\n        \"matchPattern\": \"匹配模式\",\n        \"url\": \"新建标签页的 URL\",\n        \"createIfNoMatch\": \"如果没有匹配则创建\"\n      },\n      \"new-tab\": {\n        \"name\": \"新建标签页\",\n        \"description\": \"\",\n        \"url\": \"新建标签页 URL\",\n        \"tab-zoom\": \"标签页缩放\",\n        \"customUserAgent\": \"使用自定义 User-Agent\",\n        \"activeTab\": \"设为活动标签页\",\n        \"tabToGroup\": \"标签页分组\",\n        \"waitTabLoaded\": \"等待标签页加载完毕\",\n        \"updatePrevTab\": {\n          \"title\": \"使用之前打开的新标签页而不是新建标签页\",\n          \"text\": \"更新之前打开的标签页\"\n        }\n      },\n      \"link\": {\n        \"name\": \"链接\",\n        \"description\": \"打开链接元素\",\n        \"openInNewTab\": \"在新标签页打开\"\n      },\n      \"attribute-value\": {\n        \"name\": \"属性值\",\n        \"description\": \"获取元素属性的值\",\n        \"forms\": {\n          \"name\": \"属性名称\",\n          \"checkbox\": \"插入到表格\",\n          \"column\": \"选择列\",\n          \"value\": \"属性值\",\n          \"action\": {\n            \"get\": \"获取属性值\",\n            \"set\": \"设置属性值\"\n          },\n          \"extraRow\": {\n            \"checkbox\": \"添加额外的行\",\n            \"placeholder\": \"值\",\n            \"title\": \"额外行的值\"\n          }\n        }\n      },\n      \"forms\": {\n        \"name\": \"表单\",\n        \"description\": \"\",\n        \"selected\": \"已选择\",\n        \"type\": \"表单类型\",\n        \"getValue\": \"获取表单值\",\n        \"text-field\": {\n          \"name\": \"文本域\",\n          \"value\": \"值\",\n          \"clearValue\": \"清除表单值\",\n          \"delay\": {\n            \"placeholder\": \"延迟\",\n            \"label\": \"输入延迟（毫秒）（0 为禁用）\"\n          }\n        },\n        \"select\": {\n          \"name\": \"下拉列表\"\n        },\n        \"radio\": {\n          \"name\": \"单选框\"\n        },\n        \"checkbox\": {\n          \"name\": \"复选框\"\n        }\n      },\n      \"repeat-task\": {\n        \"name\": \"重复任务\",\n        \"description\": \"\",\n        \"times\": \"次数\",\n        \"repeatFrom\": \"重复自\"\n      },\n      \"javascript-code\": {\n        \"name\": \"JavaScript 代码\",\n        \"description\": \"在网页中执行您的 javascript 代码\",\n        \"availabeFuncs\": \"可用函数:\",\n        \"removeAfterExec\": \"模块执行后移除\",\n        \"everyNewTab\": \"每次新建标签都执行\",\n        \"context\": {\n          \"name\": \"执行上下文\",\n          \"items\": {\n            \"website\": \"当前标签页\",\n            \"background\": \"背景\"\n          }\n        },\n        \"modal\": {\n          \"tabs\": {\n            \"code\": \"JavaScript 代码\",\n            \"preloadScript\": \"预加载脚本\"\n          }\n        },\n        \"timeout\": {\n          \"placeholder\": \"超时（毫秒）\",\n          \"title\": \"Javascript 代码执行超时\"\n        }\n      },\n      \"trigger-event\": {\n        \"name\": \"触发器事件\",\n        \"description\": \"\",\n        \"selectEvent\": \"选择事件\"\n      },\n      \"conditions\": {\n        \"name\": \"条件\",\n        \"add\": \"添加条件\",\n        \"retryConditions\": \"如果所有条件都不满足，则重试\",\n        \"description\": \"条件模块\",\n        \"refresh\": \"刷新条件连接\",\n        \"fallbackTitle\": \"当所有比较不满足要求时执行\",\n        \"equals\": \"等于\",\n        \"gt\": \"大于\",\n        \"gte\": \"大于或等于\",\n        \"lt\": \"小于\",\n        \"lte\": \"小于或等于\",\n        \"ne\": \"不等于\",\n        \"contains\": \"包含\"\n      },\n      \"element-exists\": {\n        \"name\": \"元素存在\",\n        \"description\": \"检查元素是否存在\",\n        \"selector\": \"元素选择器\",\n        \"fallbackTitle\": \"当元素不存在时执行\",\n        \"throwError\": \"如果不存在抛出一个错误\",\n        \"tryFor\": {\n          \"title\": \"检查元素是否存在的尝试次数\",\n          \"label\": \"尝试\"\n        },\n        \"timeout\": {\n          \"label\": \"超时（毫秒）\",\n          \"title\": \"每次尝试超时\"\n        }\n      },\n      \"webhook\": {\n        \"name\": \"HTTP 请求\",\n        \"description\": \"发出 HTTP 请求\",\n        \"contentType\": \"内容类型\",\n        \"method\": \"请求方法\",\n        \"url\": \"请求 URL\",\n        \"fallback\": \"在发出 HTTP 请求失败或错误时执行\",\n        \"buttons\": {\n          \"header\": \"添加 header\"\n        },\n        \"timeout\": {\n          \"placeholder\": \"超时\",\n          \"title\": \"Http 请求执行超时(ms)\"\n        },\n        \"tabs\": {\n          \"headers\": \"Headers\",\n          \"body\": \"Body\",\n          \"response\": \"响应\"\n        }\n      },\n      \"while-loop\": {\n        \"name\": \"While 循环\",\n        \"description\": \"在满足条件时执行模块\",\n        \"editCondition\": \"编辑条件\",\n        \"fallback\": \"当条件为 false 时执行\"\n      },\n      \"loop-elements\": {\n        \"name\": \"循环元素\",\n        \"description\": \"遍历元素\",\n        \"loadMore\": \"载入更多元素\",\n        \"scrollToBottom\": \"滚动到底部\",\n        \"scrollToTop\": \"滚动到顶部\",\n        \"actions\": {\n          \"none\": \"无\",\n          \"click-element\": \"点击一个元素\",\n          \"scroll\": \"向下滚动\",\n          \"click-link\": \"点击一个链接\",\n          \"scroll-up\": \"向上滚动\"\n        }\n      },\n      \"loop-data\": {\n        \"name\": \"循环数据\",\n        \"description\": \"遍历表格或您的自定义数据\",\n        \"loopId\": \"循环 id\",\n        \"refKey\": \"参考键\",\n        \"startIndex\": \"从索引开始\",\n        \"resumeLastWorkflow\": \"恢复上次工作流\",\n        \"reverse\": \"反转循环顺序\",\n        \"modal\": {\n          \"fileTooLarge\": \"文件太大，无法编辑\",\n          \"maxFile\": \"最大文件尺寸为 1MB\",\n          \"options\": {\n            \"firstRow\": \"使用第一行作为主键\"\n          }\n        },\n        \"buttons\": {\n          \"clear\": \"清除数据\",\n          \"insert\": \"插入数据\",\n          \"import\": \"导入文件\"\n        },\n        \"maxLoop\": {\n          \"title\": \"要循环的最大数据数\",\n          \"label\": \"要循环的最大数据（0 禁用）\"\n        },\n        \"loopThrough\": {\n          \"placeholder\": \"依次通过\",\n          \"fromNumber\": \"从数字\",\n          \"toNumber\": \"到数字\",\n          \"options\": {\n            \"numbers\": \"数字\",\n            \"variable\": \"变量\",\n            \"data-columns\": \"表格\",\n            \"table\": \"表格\",\n            \"custom-data\": \"自定义数据\",\n            \"google-sheets\": \"Google sheets\",\n            \"elements\": \"元素\"\n          }\n        }\n      },\n      \"loop-breakpoint\": {\n        \"name\": \"循环断点\",\n        \"description\": \"告诉循环数据块必须在哪里停止\"\n      },\n      \"take-screenshot\": {\n        \"name\": \"截屏\",\n        \"fullPage\": \"截取整页截图\",\n        \"description\": \"截取当前活动标签页的屏幕截图\",\n        \"imageQuality\": \"图像质量\",\n        \"saveToColumn\": \"将屏幕截图插入表格\",\n        \"saveToComputer\": \"将屏幕截图保存到计算机\",\n        \"types\": {\n          \"title\": \"截图\",\n          \"page\": \"页面\",\n          \"fullpage\": \"完整页面\",\n          \"element\": \"一个元素\"\n        }\n      },\n      \"switch-to\": {\n        \"name\": \"切换框架\",\n        \"description\": \"在主窗口和 iframe 之间切换\",\n        \"iframeSelector\": \"iframe 元素选择器\",\n        \"windowTypes\": {\n          \"main\": \"主窗口\",\n          \"iframe\": \"Iframe\"\n        }\n      },\n      \"debugMode\": {\n        \"title\": \"调试模式\",\n        \"description\": \"使用 Chrome DevTools 协议执行当前模块\"\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "src/locales/zh/common.json",
    "content": "{\n  \"common\": {\n    \"dashboard\": \"主面板\",\n    \"workflow\": \"工作流 | 工作流\",\n    \"collection\": \"集合 | 集合\",\n    \"log\": \"日志 | 日志\",\n    \"block\": \"单元 | 单元\",\n    \"schedule\": \"计划\",\n    \"folder\": \"文件夹 | 文件夹\",\n    \"new\": \"新建\",\n    \"docs\": \"文档\",\n    \"search\": \"搜索\",\n    \"example\": \"示例 | 示例\",\n    \"import\": \"导入\",\n    \"export\": \"导出\",\n    \"rename\": \"重命名\",\n    \"execute\": \"执行\",\n    \"delete\": \"删除\",\n    \"cancel\": \"取消\",\n    \"settings\": \"设置\",\n    \"options\": \"选项\",\n    \"confirm\": \"确认\",\n    \"name\": \"名称\",\n    \"all\": \"全部\",\n    \"add\": \"添加\",\n    \"save\": \"保存\",\n    \"data\": \"数据\",\n    \"stop\": \"停止\",\n    \"sheet\": \"工作簿\",\n    \"pause\": \"暂停\",\n    \"resume\": \"恢复\",\n    \"action\": \"操作 | 操作\",\n    \"packages\": \"包\",\n    \"storage\": \"存储\",\n    \"editor\": \"编辑器\",\n    \"running\": \"运行\",\n    \"globalData\": \"全局数据\",\n    \"fileName\": \"文件名\",\n    \"description\": \"描述\",\n    \"disable\": \"禁用\",\n    \"disabled\": \"已禁用\",\n    \"enable\": \"启用\",\n    \"fallback\": \"反馈\",\n    \"update\": \"更新\",\n    \"feature\": \"特点\",\n    \"duplicate\": \"副本\",\n    \"password\": \"密码\",\n    \"category\": \"分类\",\n    \"optional\": \"可选\",\n    \"0disable\": \"0 到禁用\",\n    \"millisecond\": \"毫秒 | 毫秒\"\n  },\n  \"message\": {\n    \"noBlock\": \"没有模块\",\n    \"noData\": \"没有数据可以展示\",\n    \"noTriggerBlock\": \"没有找到触发器模块\",\n    \"useDynamicData\": \"了解如何添加动态数据\",\n    \"delete\": \"确定要删除\\\"{name}\\\"?\",\n    \"empty\": \"哎呀……你好像没有任何项目\",\n    \"maxSizeExceeded\": \"文件大小超出了允许的最大值\",\n    \"notSaved\": \"你真的要退出吗？ 你有未保存的更改！\",\n    \"somethingWrong\": \"出现错误\",\n    \"limitExceeded\": \"您已超出限制\"\n  },\n  \"sort\": {\n    \"sortBy\": \"排序方式\",\n    \"name\": \"名称\",\n    \"createdAt\": \"创建日期\",\n    \"updatedAt\": \"上次更新\",\n    \"mostUsed\": \"最常用\"\n  },\n  \"logStatus\": {\n    \"stopped\": \"停止\",\n    \"error\": \"错误\",\n    \"success\": \"成功\"\n  }\n}\n"
  },
  {
    "path": "src/locales/zh/newtab.json",
    "content": "{\n  \"home\": {\n    \"viewAll\": \"查看全部\",\n    \"communities\": \"社区\"\n  },\n  \"welcome\": {\n    \"title\": \"欢迎使用 Automa! 🎉\",\n    \"text\": \"从阅读文档或浏览 Automa 市场中的工作流开始.\",\n    \"marketplace\": \"市场\"\n  },\n  \"packages\": {\n    \"name\": \"包 | 包\",\n    \"add\": \"添加包\",\n    \"icon\": \"包图标\",\n    \"open\": \"打开包\",\n    \"new\": \"新建包\",\n    \"import\": \"导入包\",\n    \"set\": \"设置为包\",\n    \"settings\": {\n      \"asBlock\": \"把包设为模块\"\n    },\n    \"categories\": {\n      \"my\": \"我的包\",\n      \"installed\": \"已安装的包\"\n    }\n  },\n  \"scheduledWorkflow\": {\n    \"title\": \"计划的工作流\",\n    \"nextRun\": \"下次运行\",\n    \"active\": \"激活\",\n    \"refresh\": \"刷新\",\n    \"schedule\": {\n      \"title\": \"计划\",\n      \"types\": {\n        \"everyDay\": \"每天\",\n        \"general\": \"每 {time}\",\n        \"interval\": \"每 {time} 分\"\n      }\n    }\n  },\n  \"storage\": {\n    \"title\": \"存储\",\n    \"table\": {\n      \"add\": \"添加表格\",\n      \"edit\": \"编辑表格\",\n      \"createdAt\": \"创建于\",\n      \"modifiedAt\": \"修改于\",\n      \"rowsCount\": \"行数\",\n      \"delete\": \"删除表格\"\n    }\n  },\n  \"credential\": {\n    \"title\": \"凭据 | 凭据\",\n    \"add\": \"添加凭据\",\n    \"use\": {\n      \"title\": \"已用凭据\",\n      \"description\": \"此工作流使用了这些凭据\"\n    }\n  },\n  \"workflowPermissions\": {\n    \"title\": \"工作流权限\",\n    \"description\": \"此工作流需要这些权限才能正常运行\",\n    \"contextMenus\": {\n      \"title\": \"上下文菜单\",\n      \"description\": \"通过上下文菜单执行工作流\"\n    },\n    \"clipboard\": {\n      \"title\": \"剪贴板\",\n      \"description\": \"用于访问剪贴板数据\"\n    },\n    \"notifications\": {\n      \"title\": \"通知\",\n      \"description\": \"用于显示通知\"\n    },\n    \"downloads\": {\n      \"title\": \"下载\",\n      \"description\": \"保存页面资产并重命名下载的文件\"\n    },\n    \"cookies\": {\n      \"title\": \"Cookies\",\n      \"description\": \"读取,设置,或移除 cookies\"\n    }\n  },\n  \"updateMessage\": {\n    \"text1\": \"Automa 已更新为 v{version},\",\n    \"text2\": \"看看有什么新东西.\"\n  },\n  \"workflows\": {\n    \"folder\": {\n      \"new\": \"新建文件夹\",\n      \"name\": \"文件夹名\",\n      \"delete\": \"删除文件夹\",\n      \"rename\": \"重命名文件夹\"\n    }\n  },\n  \"auth\": {\n    \"title\": \"授权验证\",\n    \"signIn\": \"登录\",\n    \"username\": \"您需要先设置您的用户名\",\n    \"clickHere\": \"点击这里\",\n    \"text\": \"您需要先登录才能执行此操作\"\n  },\n  \"running\": {\n    \"start\": \"开始于 {date}\",\n    \"message\": \"仅显示最后 5 条日志\"\n  },\n  \"settings\": {\n    \"theme\": \"主题\",\n    \"shortcuts\": {\n      \"duplicate\": \"快捷方式已被 \\\"{name}\\\" 占用\"\n    },\n    \"editor\": {\n      \"title\": \"标题\",\n      \"curvature\": {\n        \"title\": \"编辑器线条曲率\",\n        \"line\": \"线条\",\n        \"reroute\": \"重绘路线\",\n        \"rerouteFirstLast\": \"重绘首尾点\"\n      },\n      \"arrow\": {\n        \"title\": \"箭头线\",\n        \"description\": \"添加箭头到线的尾端\"\n      },\n      \"snapGrid\": {\n        \"title\": \"对齐网格\",\n        \"description\": \"移动模块时对齐网格\"\n      },\n      \"saveWhenExecute\": {\n        \"title\": \"执行工作流程时自动保存\",\n        \"description\": \"执行工作流程时将自动保存更改的工作流程\"\n      }\n    },\n    \"deleteLog\": {\n      \"title\": \"自动删除工作流日志\",\n      \"after\": \"删除间隔\",\n      \"deleteAfter\": {\n        \"never\": \"从不\",\n        \"days\": \"{day} 天\"\n      }\n    },\n    \"language\": {\n      \"label\": \"语言\",\n      \"helpTranslate\": \"找不到您的语言？ 帮助翻译.\",\n      \"reloadPage\": \"重新加载页面生效\"\n    },\n    \"menu\": {\n      \"backup\": \"备份工作流\",\n      \"editor\": \"编辑器\",\n      \"general\": \"常规\",\n      \"shortcuts\": \"快捷键\",\n      \"about\": \"关于\"\n    },\n    \"backupWorkflows\": {\n      \"title\": \"本地备份\",\n      \"invalidPassword\": \"无效密码\",\n      \"workflowsAdded\": \"{count} 个工作流已被添加\",\n      \"name\": \"备份工作流\",\n      \"needSignin\": \"首先你需要登录账户\",\n      \"backup\": {\n        \"button\": \"备份\",\n        \"settings\": \"备份设置\",\n        \"encrypt\": \"用密码加密\",\n        \"schedule\": \"本地备份计划\"\n      },\n      \"restore\": {\n        \"title\": \"恢复工作流\",\n        \"button\": \"恢复\",\n        \"update\": \"如果工作流存在则更新\"\n      },\n      \"cloud\": {\n        \"buttons\": {\n          \"local\": \"本地\",\n          \"cloud\": \"云端\"\n        },\n        \"location\": \"位置\",\n        \"delete\": \"删除备份\",\n        \"title\": \"云端备份\",\n        \"sync\": \"同步\",\n        \"lastSync\": \"上次同步\",\n        \"lastBackup\": \"上次备份\",\n        \"select\": \"选择工作流\",\n        \"storedWorkflows\": \"存储在云中的工作流\",\n        \"selected\": \"已选择\",\n        \"selectText\": \"选择要备份的工作流\",\n        \"selectAll\": \"选择全部\",\n        \"deselectAll\": \"取消全选\",\n        \"needSelectWorkflow\": \"您需要选择要备份的工作流\"\n      }\n    }\n  },\n  \"workflow\": {\n    \"events\": {\n      \"title\": \"工作流事件\",\n      \"add-action\": \"添加操作\",\n      \"description\": \"事件发生时执行操作。\",\n      \"event\": \"事件 | 事件\",\n      \"action\": \"操作\",\n      \"actions\": {\n        \"js-code\": {\n          \"title\": \"执行 JS 代码\"\n        },\n        \"http-request\": {\n          \"title\": \"HTTP Request\"\n        }\n      },\n      \"types\": {\n        \"finish:success\": {\n          \"name\": \"完成 (成功)\",\n          \"description\": \"工作流执行已成功完成\"\n        },\n        \"finish:failed\": {\n          \"name\": \"完成 (失败)\",\n          \"description\": \"工作流执行已完成，但出现错误。\"\n        }\n      }\n    },\n      \"previewMode\": {\n      \"title\": \"预览模式\",\n      \"description\": \"正处于预览模式，你所做的修改不会被保存下来\"\n    },\n    \"pinWorkflow\": {\n      \"pin\": \"固定工作流\",\n      \"unpin\": \"取消固定工作流\",\n      \"pinned\": \"已固定的工作流\"\n    },\n    \"parameters\": {\n      \"add\": \"添加参数\",\n      \"preferInTab\": \"在标签页中输入参数\"\n    },\n    \"my\": \"我的工作流\",\n    \"testing\": {\n      \"title\": \"测试模式\",\n      \"nextBlock\": \"下一模块\",\n      \"startRun\": \"开始运行于\",\n      \"disabled\": \"首先保存更改\"\n    },\n    \"import\": \"导入工作流\",\n    \"new\": \"新建工作流\",\n    \"delete\": \"删除工作流\",\n    \"browse\": \"浏览工作流\",\n    \"name\": \"工作流名称\",\n    \"rename\": \"重命名工作流\",\n    \"backupCloud\": \"备份工作流到云端\",\n    \"add\": \"添加工作流\",\n    \"clickToEnable\": \"单击启用\",\n    \"toggleSidebar\": \"切换侧栏\",\n    \"cantEdit\": \"无法编辑共享的工作流\",\n    \"undo\": \"撤销\",\n    \"redo\": \"重做\",\n    \"autoAlign\": {\n      \"title\": \"自动对齐\"\n    },\n    \"blocksFolder\": {\n      \"title\": \"模块文件夹\",\n      \"add\": \"添加模块到文件夹\",\n      \"save\": \"保存到文件夹\"\n    },\n    \"searchBlocks\": {\n      \"title\": \"在编辑器中搜索模块\"\n    },\n    \"conditionBuilder\": {\n      \"title\": \"条件生成器\",\n      \"add\": \"附加条件\",\n      \"and\": \"AND\",\n      \"or\": \"OR\",\n      \"topAwait\": \"支持 top-level await 和\\\"automaRefData\\\"函数\"\n    },\n    \"host\": {\n      \"title\": \"主机工作流程\",\n      \"set\": \"设为主机工作流\",\n      \"id\": \"主机 Id\",\n      \"add\": \"添加托管工作流\",\n      \"sync\": {\n        \"title\": \"同步\",\n        \"description\": \"与主机工作流同步\"\n      },\n      \"messages\": {\n        \"hostExist\": \"您已添加此主机\",\n        \"notFound\": \"找不到托管工作流 \\\"{id}\\\" id\",\n        \"successAdded\": \"托管工作流 \\\"{id}\\\" id 已添加成功\"\n      }\n    },\n    \"type\": {\n      \"local\": \"本地\",\n      \"shared\": \"共享\",\n      \"host\": \"主机\"\n    },\n    \"unpublish\": {\n      \"title\": \"取消发布工作流\",\n      \"button\": \"取消发布\",\n      \"body\": \"您确定要取消发布 \\\"{name}\\\" 工作流吗?\"\n    },\n    \"share\": {\n      \"url\": \"共享 URL\",\n      \"publish\": \"发布\",\n      \"sharedAs\": \"共享为 \\\"{name}\\\"\",\n      \"title\": \"共享工作流\",\n      \"download\": \"添加工作流到本地\",\n      \"edit\": \"编辑详细描述\",\n      \"fetchLocal\": \"获取本地工作流\",\n      \"update\": \"更新\",\n      \"unpublish\": \"取消发布\",\n      \"linkCopied\": \"链接已复制到剪贴板\"\n    },\n    \"variables\": {\n      \"title\": \"变量 | 变量\",\n      \"name\": \"变量名称\",\n      \"assign\": \"分配给变量\"\n    },\n    \"protect\": {\n      \"title\": \"保护工作流\",\n      \"remove\": \"移除保护\",\n      \"button\": \"保护\",\n      \"note\": \"注意：您必须记住此密码，以后编辑和删除工作流时需要此密码。\"\n    },\n    \"locked\": {\n      \"title\": \"此工作流已被保护\",\n      \"body\": \"输入密码解锁\",\n      \"unlock\": \"解锁\",\n      \"messages\": {\n        \"incorrect-password\": \"密码错误\"\n      }\n    },\n    \"state\": {\n      \"executeBy\": \"执行于: \\\"{name}\\\"\"\n    },\n    \"table\": {\n      \"title\": \"表格\",\n      \"placeholder\": \"搜索或添加列\",\n      \"select\": \"选择列\",\n      \"column\": {\n        \"name\": \"列名称\",\n        \"type\": \"数据类型\"\n      }\n    },\n    \"sidebar\": {\n      \"workflowIcon\": \"工作流图标\"\n    },\n    \"editor\": {\n      \"zoomIn\": \"放大\",\n      \"zoomOut\": \"缩小\",\n      \"resetZoom\": \"重置缩放\",\n      \"duplicate\": \"副本\",\n      \"copy\": \"复制\",\n      \"paste\": \"粘贴\",\n      \"group\": \"分组模块\",\n      \"ungroup\": \"未分组模块\"\n    },\n    \"settings\": {\n      \"saveLog\": \"保存工作流日志\",\n      \"executedBlockOnWeb\": \"在网页上显示已执行的模块\",\n      \"notification\": {\n        \"title\": \"工作流通知\",\n        \"description\": \"执行后显示工作流状态（成功或失败）\",\n        \"noPermission\": \"Automa 需要 \\\"notifications\\\" 权限才能正常工作\"\n      },\n      \"publicId\": {\n        \"title\": \"工作流公开 Id\",\n        \"description\": \"用于JS自定义事件使用此公共ID执行工作流\"\n      },\n      \"aipower\": {\n        \"title\": \"AI Power 令牌\",\n        \"description\": \"用于执行调用 AI Power 的工作流\"\n      },\n      \"defaultColumn\": {\n        \"title\": \"插入信息到默认列\",\n        \"description\": \"如果模块中没有选择列，则将数据插入默认列\",\n        \"name\": \"默认列名称\"\n      },\n      \"autocomplete\": {\n        \"title\": \"自动完成\",\n        \"description\": \"在输入块中启用自动完成（如果造成 Automa 不稳定请禁用）\"\n      },\n      \"clearCache\": {\n        \"title\": \"清理缓存\",\n        \"description\": \"清除工作流的缓存（状态和循环索引）\",\n        \"info\": \"成功清除工作流缓存\",\n        \"btn\": \"清理\"\n      },\n      \"reuseLastState\": {\n        \"title\": \"重用上一个工作流状态\",\n        \"description\": \"使用上次执行的工作流状态数据（表、变量和全局数据）\"\n      },\n      \"debugMode\": {\n        \"title\": \"调试模式\",\n        \"description\": \"使用 Chrome DevTools 协议执行工作流\"\n      },\n      \"restartWorkflow\": {\n        \"for\": \"重新启动\",\n        \"times\": \"次数\",\n        \"description\": \"工作流可以重启的最大次数\"\n      },\n      \"onError\": {\n        \"title\": \"工作流出错时\",\n        \"description\": \"设置当工作流发生错误时要执行的操作\",\n        \"items\": {\n          \"keepRunning\": \"继续运行\",\n          \"stopWorkflow\": \"停止工作流\",\n          \"restartWorkflow\": \"重启工作流\"\n        }\n      },\n      \"timeout\": {\n        \"title\": \"工作流超时(毫秒)\"\n      },\n      \"blockDelay\": {\n        \"title\": \"模块延迟(毫秒)\",\n        \"description\": \"在执行每个块之前添加延迟\"\n      },\n      \"tabLoadTimeout\": {\n        \"title\": \"标签页载入超时\",\n        \"description\": \"加载标签的最大时间，以毫秒为单位，通过0来禁用超时。\"\n      }\n    }\n  },\n  \"collection\": {\n    \"description\": \"按顺序执行您的工作流\",\n    \"new\": \"新建集合\",\n    \"delete\": \"删除集合\",\n    \"add\": \"添加集合\",\n    \"rename\": \"重命名集合\",\n    \"flow\": \"流\",\n    \"dragDropText\": \"拖放工作流或模块到此处\",\n    \"options\": {\n      \"atOnce\": {\n        \"title\": \"一次执行集合中的所有工作流\",\n        \"description\": \"使用此选项时不会执行模块\"\n      }\n    },\n    \"globalData\": {\n      \"note\": \"这将覆盖工作流的全局数据\"\n    }\n  },\n  \"log\": {\n    \"flowId\": \"流程 Id\",\n    \"goBack\": \"返回到 \\\"{name}\\\" 日志\",\n    \"goWorkflow\": \"转到工作流\",\n    \"startedDate\": \"启动日期\",\n    \"duration\": \"期间\",\n    \"selectAll\": \"选择全部\",\n    \"deselectAll\": \"取消全选\",\n    \"deleteSelected\": \"删除已选日志\",\n    \"clearLogs\": {\n      \"title\": \"清除日志\",\n      \"description\": \"您确定清除所有日志吗？\"\n    },\n    \"types\": {\n      \"stop\": \"工作流已停止\",\n      \"finish\": \"完成\"\n    },\n    \"messages\": {\n      \"url-empty\": \"URL 为空\",\n      \"invalid-url\": \"URL 无效\",\n      \"conditions-empty\": \"条件为空\",\n      \"invalid-proxy-host\": \"无效的代理主机\",\n      \"workflow-disabled\": \"工作流已禁用\",\n      \"selector-empty\": \"元素选择器为空\",\n      \"invalid-body\": \"内容正文不是有效的 JSON\",\n      \"invalid-active-tab\": \"\\\"{url}\\\" 是无效 URL\",\n      \"empty-spreadsheet-id\": \"电子表格 ID 为空\",\n      \"invalid-loop-data\": \"要循环的数据无效\",\n      \"empty-workflow\": \"您必须先选择工作流\",\n      \"active-tab-removed\": \"工作流活动标签页已删除\",\n      \"empty-spreadsheet-range\": \"电子表格范围为空\",\n      \"stop-timeout\": \"工作流程因超时而停止\",\n      \"no-file-access\": \"Automa 无权访问该文件\",\n      \"no-workflow\": \"找不到含 \\\"{workflowId}\\\" ID 的工作流\",\n      \"no-match-tab\": \"找不到含 \\\"{pattern}\\\" 参数的标签页\",\n      \"no-clipboard-acces\": \"无权访问剪贴板\",\n      \"browser-not-supported\": \"{browser} 浏览器不支持此功能\",\n      \"element-not-found\": \"找不到含 \\\"{selector}\\\" 选择器的元素.\",\n      \"no-permission\": \"没有执行此操作的 \\\"{permission}\\\" 权限\",\n      \"not-iframe\": \"含 \\\"{selector}\\\" 选择器的元素并不是一个 Iframe 元素\",\n      \"iframe-not-found\": \"找不到含 \\\"{selector}\\\" 选择器的 Iframe 元素.\",\n      \"workflow-infinite-loop\": \"无法执行工作流以防止无限循环\",\n      \"not-debug-mode\": \"工作流必须在调试模式下运行，此模块才能正常工作\",\n      \"no-iframe-id\": \"找不到含 \\\"{selector}\\\" 选择器的 iframe 元素 ID \",\n      \"no-tab\": \"无法连接到标签页，请在使用 \\\"{name}\\\" 模块之前使用 \\\"新建标签页\\\" 或 \\\"活动标签页\\\" 模块。\"\n    },\n    \"description\": {\n      \"text\": \"在{date}用了{duration}执行{status}\",\n      \"status\": {\n        \"success\": \"成功\",\n        \"error\": \"失败\",\n        \"stopped\": \"停止\"\n      }\n    },\n    \"delete\": {\n      \"title\": \"删除日志\",\n      \"description\": \"您确定要删除所有选定的日志吗？\"\n    },\n    \"exportData\": {\n      \"title\": \"导出数据\",\n      \"types\": {\n        \"json\": \"JSON\",\n        \"csv\": \"CSV\",\n        \"plain-text\": \"Plain text\"\n      }\n    },\n    \"filter\": {\n      \"title\": \"筛选\",\n      \"byStatus\": \"按状态\",\n      \"byDate\": {\n        \"title\": \"按日期\",\n        \"items\": {\n          \"lastDay\": \"最近1天\",\n          \"last7Days\": \"最近7天\",\n          \"last30Days\": \"最近30天\"\n        }\n      }\n    }\n  },\n  \"components\": {\n    \"pagination\": {\n      \"text1\": \"显示\",\n      \"text2\": \"条 共 {count} 条\",\n      \"nextPage\": \"下一页\",\n      \"currentPage\": \"当前页\",\n      \"prevPage\": \"上一页\",\n      \"of\": \"共 {page} 页\"\n    }\n  }\n}\n"
  },
  {
    "path": "src/locales/zh/popup.json",
    "content": "{\n  \"recording\": {\n    \"stop\": \"停止录制\",\n    \"title\": \"录制中\"\n  },\n  \"home\": {\n    \"record\": {\n      \"title\": \"录制工作流\",\n      \"button\": \"录制\",\n      \"name\": \"工作流名称\",\n      \"selectBlock\": \"选择一个启动模块\",\n      \"anotherBlock\": \"无法从此模块启动\",\n      \"tabs\": {\n        \"new\": \"新建模块\",\n        \"existing\": \"已存在工作流\"\n      }\n    },\n    \"elementSelector\": {\n      \"name\": \"元素选择器\",\n      \"noAccess\": \"您没有权限访问此站点\"\n    },\n    \"workflow\": {\n      \"new\": \"新建工作流\",\n      \"rename\": \"重命名工作流\",\n      \"delete\": \"删除工作流\",\n      \"type\": {\n        \"host\": \"主机\",\n        \"local\": \"本地\"\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "src/locales/zh-TW/blocks.json",
    "content": "{\n  \"collection\": {\n  \"blocks\": {\n  \"export-result\": {\n  \"name\": \"匯出結果\",\n  \"description\": \"將集合結果匯出為 JSON\"\n  }\n  }\n  },\n  \"workflow\": {\n    \"blocks\": {\n      \"base\": {\n        \"title\": \"區塊\",\n        \"moveToGroup\": \"將區塊移動至區塊群組\",\n        \"selector\": \"元素選擇器\",\n        \"selectorOptions\": \"選擇器選項\",\n        \"timeout\": \"超時時間（毫秒）\",\n        \"noPermission\": \"Automa 沒有足夠的權限執行此操作\",\n        \"grantPermission\": \"授予權限\",\n        \"action\": \"操作\",\n        \"element\": {\n          \"select\": \"選擇一個元素\",\n          \"verify\": \"驗證選擇器\"\n        },\n        \"settings\": {\n          \"title\": \"區塊設定\",\n          \"blockTimeout\": {\n            \"title\": \"區塊執行超時時間（毫秒）\",\n            \"description\": \"區塊的最大執行時間（0 表示停用）\"\n          },\n          \"line\": {\n            \"title\": \"線條\",\n            \"label\": \"標籤\",\n            \"animated\": \"動畫效果\",\n            \"select\": \"選擇線條\",\n            \"to\": \"連接到 {name} 區塊的線條\",\n            \"lineColor\": \"顏色\"\n          }\n        },\n\"toggle\": {\n          \"enable\": \"啟用區塊\",\n          \"disable\": \"停用區塊\"\n        },\n        \"onError\": {\n          \"info\": \"這些規則將在區塊發生錯誤時應用\",\n          \"button\": \"發生錯誤時\",\n          \"title\": \"當錯誤發生時\",\n          \"retry\": \"重試操作\",\n          \"fallbackTitle\": \"當區塊發生錯誤時將執行\",\n          \"times\": {\n            \"name\": \"次數\",\n            \"description\": \"重試操作的次數\"\n          },\n          \"interval\": {\n            \"name\": \"間隔\",\n            \"description\": \"每次重試之間的等待時間\",\n            \"second\": \"秒\"\n          },\n          \"toDo\": {\n            \"error\": \"拋出錯誤\",\n            \"continue\": \"繼續流程\",\n            \"fallback\": \"執行備用方案\",\n            \"restart\": \"重新啟動流程\"\n          },\n          \"insertData\": {\n            \"name\": \"插入資料\"\n          }\n        },\n        \"table\": {\n          \"checkbox\": \"插入到表格\",\n          \"select\": \"選擇欄位\",\n          \"extraRow\": {\n            \"checkbox\": \"新增額外行\",\n            \"placeholder\": \"值\",\n            \"title\": \"額外行的值\"\n          }\n        },\n        \"findElement\": {\n          \"placeholder\": \"透過以下方式尋找元素\",\n          \"options\": {\n            \"cssSelector\": \"CSS 選擇器\",\n            \"xpath\": \"XPath\"\n          }\n        },\n        \"markElement\": {\n          \"title\": \"如果元素之前已被選擇過，則不會再次被選擇\",\n          \"text\": \"標記元素\"\n        },\n        \"multiple\": {\n          \"title\": \"選擇多個元素\",\n          \"text\": \"多選\"\n        },\n        \"waitSelector\": {\n          \"title\": \"等待選擇器\",\n          \"timeout\": \"選擇器超時時間（毫秒）\"\n        },\n        \"downloads\": {\n          \"onConflict\": {\n            \"uniquify\": \"唯一化\",\n            \"overwrite\": \"覆蓋\",\n            \"prompt\": \"提示\"\n          }\n        }\n      },\n      \"wait-connections\": {\n        \"name\": \"等待連線\",\n        \"description\": \"等待所有連線完成後再繼續到下一個區塊\",\n        \"specificFlow\": \"僅繼續特定流程\",\n        \"selectFlow\": \"選擇流程\"\n      },\n      \"cookie\": {\n        \"name\": \"Cookie\",\n        \"description\": \"取得、設定或移除 cookies\",\n        \"types\": {\n          \"get\": \"取得 cookies\",\n          \"set\": \"設定 cookie\",\n          \"remove\": \"移除 cookies\",\n          \"getAll\": \"取得所有 cookies\"\n        },\n        \"useJson\": \"使用 JSON 格式\"\n      },\n      \"note\": {\n        \"name\": \"備註\"\n      },\n      \"slice-variable\": {\n        \"name\": \"分割變數\",\n        \"description\": \"擷取變數值的一部分\",\n        \"start\": \"起始索引\",\n        \"end\": \"結束索引\"\n      },\n      \"workflow-state\": {\n        \"name\": \"流程狀態\",\n        \"description\": \"管理流程狀態\",\n        \"actions\": {\n          \"stop\": \"停止流程\"\n        }\n      },\n      \"regex-variable\": {\n        \"name\": \"正則表達式變數\",\n        \"description\": \"將變數值與正則表達式進行匹配\"\n      },\n      \"data-mapping\": {\n        \"source\": \"來源\",\n        \"destination\": \"目的地\",\n        \"name\": \"資料映射\",\n        \"edit\": \"編輯資料映射\",\n        \"dataSource\": \"資料來源\",\n        \"description\": \"映射變數或表格的資料\",\n        \"addSource\": \"新增來源\",\n        \"addDestination\": \"新增目的地\"\n      },\n      \"sort-data\": {\n        \"name\": \"排序資料\",\n        \"description\": \"對資料項目進行排序\",\n        \"property\": \"依項目屬性排序\",\n        \"addProperty\": \"新增屬性\"\n      },\n      \"increase-variable\": {\n        \"name\": \"增加變數\",\n        \"description\": \"將變數值增加特定數量\",\n        \"increase\": \"增加量\"\n      },\n      \"notification\": {\n        \"name\": \"通知\",\n        \"description\": \"顯示通知\",\n        \"title\": \"標題\",\n        \"message\": \"訊息\",\n        \"imageUrl\": \"圖片 URL（選填）\",\n        \"iconUrl\": \"圖示 URL（選填）\"\n      },\n      \"delete-data\": {\n        \"name\": \"刪除資料\",\n        \"description\": \"刪除表格或變數資料\",\n        \"from\": \"資料來源\",\n        \"allColumns\": \"[所有欄位]\"\n      },\n      \"log-data\": {\n        \"name\": \"取得日誌資料\",\n        \"description\": \"取得流程的最新日誌資料\",\n        \"data\": \"日誌資料\"\n      },\n      \"tab-url\": {\n        \"name\": \"取得分頁 URL\",\n        \"description\": \"取得分頁的 URL\",\n        \"select\": \"選擇分頁\",\n        \"types\": {\n          \"active-tab\": \"目前分頁\",\n          \"all\": \"所有分頁\"\n        },\n        \"query\": {\n          \"title\": \"查詢\",\n          \"matchPatterns\": \"@:workflow.blocks.switch-tab.matchPattern（選填）\",\n          \"tabTitle\": \"分頁標題（選填）\"\n        }\n      },\n      \"reload-tab\": {\n        \"name\": \"重新載入分頁\",\n        \"description\": \"重新載入目前的分頁\"\n      },\n      \"press-key\": {\n        \"name\": \"按下按鍵\",\n        \"description\": \"按下單一按鍵或組合鍵\",\n        \"target\": \"目標元素（選填）\",\n        \"key\": \"按鍵\",\n        \"detect\": \"偵測按鍵\",\n        \"actions\": {\n          \"press-key\": \"按下單一按鍵\",\n          \"multiple-keys\": \"按下多個按鍵\"\n        },\n        \"press-time\": \"按下時間（毫秒）\"\n      },\n      \"save-assets\": {\n        \"name\": \"儲存資源\",\n        \"description\": \"從元素或 URL 儲存資源（圖片、影片、音訊或檔案）\",\n        \"filename\": \"檔案名稱（選填）\",\n        \"saveDownloadIds\": \"儲存項目的下載 ID\",\n        \"contentTypes\": {\n          \"title\": \"類型\",\n          \"element\": \"媒體元素（圖片、音訊或影片）\",\n          \"url\": \"URL\"\n        }\n      },\n      \"handle-dialog\": {\n        \"name\": \"處理對話框\",\n        \"description\": \"接受或關閉由 JavaScript 觸發的對話框（警告、確認、提示或離開前確認）\",\n        \"accept\": \"接受對話框\",\n        \"promptText\": {\n          \"label\": \"提示文字（選填）\",\n          \"description\": \"在接受之前輸入到提示對話框中的文字\"\n        }\n      },\n      \"handle-download\": {\n        \"name\": \"處理下載\",\n        \"description\": \"處理下載的檔案\",\n        \"timeout\": \"超時時間（毫秒）\",\n        \"noPermission\": \"沒有權限存取下載項目\",\n        \"onConflict\": \"衝突處理\",\n        \"waitFile\": \"等待檔案下載完成\",\n        \"downloadId\": \"檔案下載 ID（選填）\",\n        \"filePath\": \"檔案路徑\"\n      },\n      \"insert-data\": {\n        \"name\": \"插入資料\",\n        \"description\": \"將資料插入表格或變數\"\n      },\n      \"clipboard\": {\n        \"name\": \"剪貼簿\",\n        \"description\": \"從剪貼簿取得複製的文字\",\n        \"data\": \"剪貼簿資料\",\n        \"noPermission\": \"沒有權限存取剪貼簿\",\n        \"grantPermission\": \"授予權限\",\n        \"copySelection\": \"複製頁面上選取的文字\",\n        \"types\": {\n          \"get\": \"取得剪貼簿資料\",\n          \"insert\": \"插入文字到剪貼簿\"\n        }\n      },\n      \"hover-element\": {\n        \"name\": \"懸停元素\",\n        \"description\": \"將滑鼠懸停在元素上\"\n      },\n      \"create-element\": {\n        \"name\": \"建立元素\",\n        \"description\": \"建立一個元素並插入到頁面中\",\n        \"edit\": \"編輯元素\",\n        \"wrap\": \"將元素包覆在內\",\n        \"insertEl\": {\n          \"title\": \"插入元素\",\n          \"items\": {\n            \"before\": \"作為第一個子元素\",\n            \"after\": \"作為最後一個子元素\",\n            \"next-sibling\": \"作為下一個兄弟元素\",\n            \"prev-sibling\": \"作為前一個兄弟元素\",\n            \"replace\": \"取代目標元素\"\n          }\n        }\n      },\n      \"upload-file\": {\n        \"name\": \"上傳檔案\",\n        \"description\": \"將檔案上傳至 <input type=\\\"file\\\"> 元素\",\n        \"filePath\": \"URL 或檔案路徑\",\n        \"addFile\": \"新增檔案\",\n        \"onlyURL\": \"Firefox 瀏覽器僅支援從 URL 上傳檔案\",\n        \"requirement\": \"使用此區塊前請閱讀需求\",\n        \"noFileAccess\": \"Automa 沒有存取檔案的權限\"\n      },\n      \"browser-event\": {\n        \"name\": \"瀏覽器事件\",\n        \"description\": \"當指定事件觸發時執行下一個區塊\",\n        \"events\": \"事件\",\n        \"timeout\": \"超時時間（毫秒）\",\n        \"activeTabLoaded\": \"目前分頁\",\n        \"setAsActiveTab\": \"設為目前分頁\"\n      },\n      \"blocks-group-2\": {\n        \"name\": \"@:workflow.blocks.blocks-group.name 2\",\n        \"description\": \"@:workflow.blocks.blocks-group.description\"\n      },\n      \"blocks-group\": {\n        \"name\": \"區塊群組\",\n        \"groupName\": \"群組名稱\",\n        \"description\": \"群組化區塊\",\n        \"dropText\": \"將區塊拖放到此處\",\n        \"cantAdd\": \"無法將「{blockName}」區塊加入群組\"\n      },\n      \"trigger\": {\n        \"name\": \"觸發器\",\n        \"description\": \"工作流程開始執行的區塊\",\n        \"addTime\": \"新增時間\",\n        \"selectDay\": \"選擇日期\",\n        \"timeExist\": \"您已經在 {day} 的 {time} 新增了觸發器\",\n        \"fixedDelay\": \"固定延遲\",\n        \"contextMenus\": {\n          \"noPermission\": \"此觸發器需要「contextMenus」權限才能運作\",\n          \"grantPermission\": \"授予權限\",\n          \"appearIn\": \"將出現在\",\n          \"contextName\": \"上下文選單中的工作流程名稱\"\n        },\n        \"days\": [\n          \"星期日\",\n          \"星期一\",\n          \"星期二\",\n          \"星期三\",\n          \"星期四\",\n          \"星期五\",\n          \"星期六\"\n        ],\n        \"useRegex\": \"使用正則表達式\",\n        \"shortcut\": {\n          \"tooltip\": \"記錄快捷鍵\",\n          \"stopRecord\": \"停止錄製\",\n          \"checkboxTitle\": \"即使在輸入元素中也能執行快捷鍵\",\n          \"checkbox\": \"在輸入時啟用\",\n          \"note\": \"注意：快捷鍵僅在網頁上時有效\"\n        },\n        \"forms\": {\n          \"triggerWorkflow\": \"觸發工作流程\",\n          \"interval\": \"間隔時間（分鐘）\",\n          \"delay\": \"延遲時間（分鐘）\",\n          \"date\": \"日期\",\n          \"time\": \"時間\",\n          \"url\": \"URL 或正則表達式\",\n          \"shortcut\": \"快捷鍵\",\n          \"cron-expression\": \"Cron 表達式\"\n        },\n        \"element-change\": {\n          \"target\": \"要觀察的目標元素\",\n          \"optionsInfo\": \"哪些元素變更會觸發工作流程\",\n          \"targetWebsite\": \"目標元素所在網站的 Match Pattern（點擊查看更多 Match Pattern 範例）\",\n          \"baseEl\": {\n            \"title\": \"基礎元素（選填）\",\n            \"description\": \"當此元素變更時，Automa 會重新開始觀察目標元素\"\n          },\n          \"subtree\": {\n            \"title\": \"包含子樹\",\n            \"description\": \"將監控範圍擴展至目標元素的整個子樹\"\n          },\n          \"childList\": {\n            \"title\": \"子元素列表\",\n            \"description\": \"監控新增或移除子元素\"\n          },\n          \"attributes\": {\n            \"title\": \"屬性\",\n            \"description\": \"監控目標元素屬性值的變更\"\n          },\n          \"attributeFilter\": {\n            \"title\": \"屬性過濾器\",\n            \"separate\": \"使用逗號 (,) 分隔屬性名稱\",\n            \"description\": \"僅監控特定屬性（留空則監控所有屬性）\"\n          },\n          \"characterData\": {\n            \"title\": \"文字資料\",\n            \"description\": \"監控目標元素內文字資料的變更\"\n          }\n        },\n        \"items\": {\n          \"manual\": \"手動\",\n          \"interval\": \"間隔\",\n          \"cron-job\": \"Cron 任務\",\n          \"date\": \"特定日期\",\n          \"context-menu\": \"上下文選單\",\n          \"element-change\": \"元素變更時\",\n          \"specific-day\": \"特定日期\",\n          \"visit-web\": \"造訪網站時\",\n          \"on-startup\": \"瀏覽器啟動時\",\n          \"keyboard-shortcut\": \"鍵盤快捷鍵\"\n        }\n      },\n      \"execute-workflow\": {\n        \"name\": \"執行工作流程\",\n        \"overwriteNote\": \"這將覆蓋所選工作流程的全域資料\",\n        \"select\": \"選擇工作流程\",\n        \"executeId\": \"執行 ID（選填）\",\n        \"description\": \"\",\n        \"insertAllVars\": \"使用所有目前工作流程的變數\",\n        \"insertVars\": \"插入目前工作流程的變數\",\n        \"useCommas\": \"使用逗號分隔變數名稱\",\n        \"insertAllGlobalData\": \"使用所有目前工作流程的全域資料\"\n      },\n      \"google-sheets-drive\": {\n        \"name\": \"@:workflow.blocks.google-sheets.name (GDrive)\",\n        \"description\": \"@:workflow.blocks.google-sheets.description\",\n        \"connected\": \"已連接的表格\",\n        \"select\": \"選擇表格\",\n        \"connect\": \"連接表格\"\n      },\n      \"google-drive\": {\n        \"name\": \"Google Drive\",\n        \"description\": \"將檔案上傳至 Google Drive\",\n        \"actions\": {\n          \"upload\": \"上傳檔案\"\n        }\n      },\n      \"google-sheets\": {\n        \"name\": \"Google Sheets\",\n        \"description\": \"讀取或更新 Google Sheets 資料\",\n        \"previewData\": \"預覽資料\",\n        \"firstRow\": \"使用第一行作為鍵值\",\n        \"keysAsFirstRow\": \"使用鍵值作為第一行\",\n        \"insertData\": \"插入資料\",\n        \"valueInputOption\": \"值輸入選項\",\n        \"insertDataOption\": \"插入資料選項\",\n        \"rangeToSearch\": \"開始搜尋的範圍\",\n        \"dataFrom\": {\n          \"label\": \"資料來源\",\n          \"options\": {\n            \"data-columns\": \"表格\",\n            \"custom\": \"自訂\"\n          }\n        },\n        \"refKey\": {\n          \"label\": \"參考鍵值（選填）\",\n          \"placeholder\": \"鍵值名稱\"\n        },\n        \"spreadsheetId\": {\n          \"label\": \"試算表 ID\",\n          \"link\": \"查看如何取得試算表 ID\"\n        },\n        \"range\": {\n          \"label\": \"範圍\",\n          \"link\": \"點擊查看更多範例\"\n        },\n        \"select\": {\n          \"get\": \"取得試算表儲存格值\",\n          \"getRange\": \"取得試算表範圍\",\n          \"update\": \"更新試算表儲存格值\",\n          \"append\": \"附加試算表儲存格值\",\n          \"clear\": \"清除試算表儲存格值\",\n          \"create\": \"建立試算表\",\n          \"add-sheet\": \"新增工作表\"\n        }\n      },\n      \"active-tab\": {\n        \"name\": \"目前分頁\",\n        \"description\": \"將您所在的分頁設為目前分頁\"\n      },\n      \"proxy\": {\n        \"name\": \"代理\",\n        \"description\": \"設定瀏覽器的代理\",\n        \"clear\": \"清除所有代理\",\n        \"bypass\": {\n          \"label\": \"繞過清單\",\n          \"note\": \"使用逗號 (,) 分隔 URL\"\n        }\n      },\n      \"new-window\": {\n        \"name\": \"新視窗\",\n        \"description\": \"建立新視窗\",\n        \"top\": \"頂部\",\n        \"left\": \"左側\",\n        \"height\": \"高度\",\n        \"width\": \"寬度\",\n        \"note\": \"注意：使用 0 來停用\",\n        \"position\": \"視窗位置\",\n        \"size\": \"視窗大小\",\n        \"windowState\": {\n          \"placeholder\": \"視窗狀態\",\n          \"options\": {\n            \"normal\": \"正常\",\n            \"minimized\": \"最小化\",\n            \"maximized\": \"最大化\",\n            \"fullscreen\": \"全螢幕\"\n          }\n        },\n        \"incognito\": {\n          \"text\": \"設為無痕視窗\",\n          \"note\": \"您必須先為此擴充功能啟用「在無痕模式中允許」\"\n        }\n      },\n      \"go-back\": {\n        \"name\": \"返回\",\n        \"description\": \"返回上一頁\"\n      },\n      \"forward-page\": {\n        \"name\": \"前進\",\n        \"description\": \"前進到下一頁\"\n      },\n      \"close-tab\": {\n        \"name\": \"關閉分頁/視窗\",\n        \"description\": \"\",\n        \"url\": \"Match Patterns\",\n        \"activeTab\": \"關閉目前分頁\",\n        \"allWindows\": \"關閉所有視窗\"\n      },\n      \"event-click\": {\n        \"name\": \"點擊元素\",\n        \"description\": \"\"\n      },\n      \"delay\": {\n        \"name\": \"延遲\",\n        \"description\": \"在執行下一個區塊前加入延遲\",\n        \"input\": {\n          \"title\": \"延遲時間（毫秒）\",\n          \"placeholder\": \"（毫秒）\"\n        }\n      },\n      \"parameter-prompt\": {\n        \"name\": \"參數提示\"\n      },\n      \"get-text\": {\n        \"name\": \"取得文字\",\n        \"description\": \"從元素中取得文字\",\n        \"checkbox\": \"插入到表格\",\n        \"includeTags\": \"包含 HTML 標籤\",\n        \"prefixText\": {\n          \"placeholder\": \"文字前綴\",\n          \"title\": \"為文字加入前綴\"\n        },\n        \"suffixText\": {\n          \"placeholder\": \"文字後綴\",\n          \"title\": \"為文字加入後綴\"\n        }\n      },\n      \"export-data\": {\n        \"name\": \"匯出資料\",\n        \"description\": \"匯出工作流程資料\",\n        \"exportAs\": \"匯出為\",\n        \"refKey\": \"參考鍵值\",\n        \"bomHeader\": \"加入 UTF-8 BOM\",\n        \"dataToExport\": {\n          \"placeholder\": \"要匯出的資料\",\n          \"options\": {\n            \"data-columns\": \"表格\",\n            \"google-sheets\": \"Google Sheets\",\n            \"variable\": \"變數\"\n          }\n        }\n      },\n      \"element-scroll\": {\n        \"name\": \"滾動元素\",\n        \"description\": \"\",\n        \"scrollY\": \"垂直滾動\",\n        \"scrollX\": \"水平滾動\",\n        \"intoView\": \"滾動至可見範圍\",\n        \"smooth\": \"平滑滾動\",\n        \"incScrollX\": \"水平滾動增量\",\n        \"incScrollY\": \"垂直滾動增量\"\n      },\n      \"switch-tab\": {\n        \"name\": \"切換分頁\",\n        \"description\": \"在分頁之間切換\",\n        \"matchPattern\": \"Match Patterns\",\n        \"url\": \"新分頁 URL\",\n        \"createIfNoMatch\": \"若無匹配則建立\"\n      },\n      \"new-tab\": {\n        \"name\": \"新分頁\",\n        \"description\": \"\",\n        \"url\": \"新分頁 URL\",\n        \"tab-zoom\": \"分頁縮放\",\n        \"customUserAgent\": \"使用自訂 User-Agent\",\n        \"activeTab\": \"設為目前分頁\",\n        \"tabToGroup\": \"將分頁加入群組\",\n        \"waitTabLoaded\": \"等待分頁載入完成\",\n        \"updatePrevTab\": {\n          \"title\": \"使用先前開啟的新分頁而非建立新分頁\",\n          \"text\": \"更新先前開啟的分頁\"\n        }\n      },\n      \"link\": {\n        \"name\": \"連結\",\n        \"description\": \"開啟連結元素\",\n        \"openInNewTab\": \"在新分頁中開啟\"\n      },\n      \"attribute-value\": {\n        \"name\": \"屬性值\",\n        \"description\": \"取得元素的屬性值\",\n        \"forms\": {\n          \"name\": \"屬性名稱\",\n          \"checkbox\": \"插入到表格\",\n          \"column\": \"選擇欄位\",\n          \"value\": \"屬性值\",\n          \"action\": {\n            \"get\": \"取得屬性值\",\n            \"set\": \"設定屬性值\"\n          },\n          \"extraRow\": {\n            \"checkbox\": \"新增額外行\",\n            \"placeholder\": \"值\",\n            \"title\": \"額外行的值\"\n          }\n        }\n      },\n      \"forms\": {\n        \"name\": \"表單\",\n        \"description\": \"\",\n        \"selected\": \"已選取\",\n        \"type\": \"表單類型\",\n        \"getValue\": \"取得表單值\",\n        \"text-field\": {\n          \"name\": \"文字欄位\",\n          \"value\": \"值\",\n          \"clearValue\": \"清除表單值\",\n          \"delay\": {\n            \"placeholder\": \"延遲\",\n            \"label\": \"輸入延遲（毫秒）（0 表示停用）\"\n          }\n        },\n        \"select\": {\n          \"name\": \"選取\"\n        },\n        \"radio\": {\n          \"name\": \"單選按鈕\"\n        },\n        \"checkbox\": {\n          \"name\": \"核取方塊\"\n        }\n      },\n      \"repeat-task\": {\n        \"name\": \"重複任務\",\n        \"description\": \"\",\n        \"times\": \"次\",\n        \"repeatFrom\": \"從此處重複\"\n      },\n      \"javascript-code\": {\n        \"name\": \"JavaScript 程式碼\",\n        \"description\": \"在網頁中執行您的 JavaScript 程式碼\",\n        \"availabeFuncs\": \"可用函式：\",\n        \"removeAfterExec\": \"區塊執行後移除\",\n        \"everyNewTab\": \"在每個新分頁中執行\",\n        \"context\": {\n          \"name\": \"執行環境\",\n          \"items\": {\n            \"website\": \"目前分頁\",\n            \"background\": \"背景\"\n          }\n        },\n        \"modal\": {\n          \"tabs\": {\n            \"code\": \"JavaScript 程式碼\",\n            \"preloadScript\": \"預載腳本\"\n          }\n        },\n        \"timeout\": {\n          \"placeholder\": \"逾時（毫秒）\",\n          \"title\": \"JavaScript 程式碼執行逾時\"\n        }\n      },\n      \"trigger-event\": {\n        \"name\": \"觸發事件\",\n        \"description\": \"\",\n        \"selectEvent\": \"選取事件\"\n      },\n      \"conditions\": {\n        \"name\": \"條件\",\n        \"add\": \"新增路徑\",\n        \"retryConditions\": \"若無條件符合則重試\",\n        \"description\": \"條件區塊\",\n        \"refresh\": \"重新整理條件連接\",\n        \"fallbackTitle\": \"當所有比較都不符合要求時執行\",\n        \"equals\": \"等於\",\n        \"gt\": \"大於\",\n        \"gte\": \"大於或等於\",\n        \"lt\": \"小於\",\n        \"lte\": \"小於或等於\",\n        \"ne\": \"不等於\",\n        \"contains\": \"包含\"\n      },\n      \"element-exists\": {\n        \"name\": \"元素存在\",\n        \"description\": \"檢查元素是否存在\",\n        \"selector\": \"元素選擇器\",\n        \"fallbackTitle\": \"當元素不存在時執行\",\n        \"throwError\": \"若不存在則拋出錯誤\",\n        \"tryFor\": {\n          \"title\": \"嘗試檢查元素存在的次數\",\n          \"label\": \"嘗試次數\"\n        },\n        \"timeout\": {\n          \"label\": \"逾時（毫秒）\",\n          \"title\": \"每次嘗試的逾時時間\"\n        }\n      },\n      \"webhook\": {\n        \"name\": \"HTTP 請求\",\n        \"description\": \"發送 HTTP 請求\",\n        \"contentType\": \"內容類型\",\n        \"method\": \"請求方法\",\n        \"url\": \"請求 URL\",\n        \"fallback\": \"當 HTTP 請求失敗時執行\",\n        \"buttons\": {\n          \"header\": \"新增標頭\"\n        },\n        \"timeout\": {\n          \"placeholder\": \"逾時\",\n          \"title\": \"HTTP 請求執行逾時（毫秒）\"\n        },\n        \"tabs\": {\n          \"headers\": \"標頭\",\n          \"body\": \"主體\",\n          \"response\": \"回應\"\n        }\n      },\n      \"while-loop\": {\n        \"name\": \"While 迴圈\",\n        \"description\": \"當條件符合時執行區塊\",\n        \"editCondition\": \"編輯條件\",\n        \"fallback\": \"當條件為假時執行\"\n      },\n      \"loop-elements\": {\n        \"name\": \"迴圈元素\",\n        \"description\": \"遍歷元素\",\n        \"loadMore\": \"載入更多元素\",\n        \"scrollToBottom\": \"滾動至底部\",\n        \"scrollToTop\": \"滾動至頂部\",\n        \"actions\": {\n          \"none\": \"無\",\n          \"click-element\": \"點擊元素\",\n          \"scroll\": \"向下滾動\",\n          \"click-link\": \"點擊連結\",\n          \"scroll-up\": \"向上滾動\"\n        }\n      },\n      \"loop-data\": {\n        \"name\": \"迴圈資料\",\n        \"description\": \"遍歷表格或自訂資料\",\n        \"loopId\": \"迴圈 ID\",\n        \"refKey\": \"參考鍵值\",\n        \"startIndex\": \"從索引開始\",\n        \"resumeLastWorkflow\": \"恢復上一個工作流程\",\n        \"reverse\": \"反轉迴圈順序\",\n        \"modal\": {\n          \"fileTooLarge\": \"檔案過大無法編輯\",\n          \"maxFile\": \"最大檔案大小為 1MB\",\n          \"options\": {\n            \"firstRow\": \"使用第一行作為鍵值\"\n          }\n        },\n        \"buttons\": {\n          \"clear\": \"清除資料\",\n          \"insert\": \"插入資料\",\n          \"import\": \"匯入檔案\"\n        },\n        \"maxLoop\": {\n          \"title\": \"最大迴圈資料數量\",\n          \"label\": \"最大迴圈資料數量（0 表示停用）\"\n        },\n        \"loopThrough\": {\n          \"placeholder\": \"遍歷\",\n          \"fromNumber\": \"從數字\",\n          \"toNumber\": \"到數字\",\n          \"options\": {\n            \"numbers\": \"數字\",\n            \"variable\": \"變數\",\n            \"data-columns\": \"表格\",\n            \"table\": \"表格\",\n            \"custom-data\": \"自訂資料\",\n            \"google-sheets\": \"Google Sheets\",\n            \"elements\": \"元素\"\n          }\n        }\n      },\n      \"loop-breakpoint\": {\n        \"name\": \"迴圈中斷點\",\n        \"description\": \"標示 Loop Data 區塊必須停止的位置\"\n      },\n      \"take-screenshot\": {\n        \"name\": \"擷取螢幕截圖\",\n        \"fullPage\": \"擷取完整頁面截圖\",\n        \"description\": \"擷取目前分頁的螢幕截圖\",\n        \"imageQuality\": \"圖片品質\",\n        \"saveToColumn\": \"將截圖插入表格\",\n        \"saveToComputer\": \"將截圖儲存至電腦\",\n        \"types\": {\n          \"title\": \"擷取以下內容的截圖\",\n          \"page\": \"頁面\",\n          \"fullpage\": \"完整頁面\",\n          \"element\": \"元素\"\n        }\n      },\n      \"switch-to\": {\n        \"name\": \"切換框架\",\n        \"description\": \"在主視窗與 iframe 之間切換\",\n        \"iframeSelector\": \"元素選擇器\",\n        \"windowTypes\": {\n          \"main\": \"主視窗\",\n          \"iframe\": \"Iframe\"\n        }\n      },\n      \"debugMode\": {\n        \"title\": \"除錯模式\",\n        \"description\": \"使用 Chrome DevTools Protocol 執行區塊\"\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "src/locales/zh-TW/common.json",
    "content": "{\n  \"common\": {\n    \"dashboard\": \"儀表板\",\n    \"workflow\": \"工作流程 | 工作流程\",\n    \"collection\": \"集合 | 集合\",\n    \"log\": \"日誌 | 日誌\",\n    \"block\": \"區塊 | 區塊\",\n    \"schedule\": \"排程\",\n    \"folder\": \"資料夾 | 資料夾\",\n    \"new\": \"新增\",\n    \"docs\": \"文件\",\n    \"search\": \"搜尋\",\n    \"example\": \"範例 | 範例\",\n    \"import\": \"匯入\",\n    \"export\": \"匯出\",\n    \"rename\": \"重新命名\",\n    \"execute\": \"執行\",\n    \"delete\": \"刪除\",\n    \"cancel\": \"取消\",\n    \"settings\": \"設定\",\n    \"options\": \"選項\",\n    \"confirm\": \"確認\",\n    \"name\": \"名稱\",\n    \"all\": \"全部\",\n    \"add\": \"新增\",\n    \"save\": \"儲存\",\n    \"data\": \"資料\",\n    \"stop\": \"停止\",\n    \"sheet\": \"工作表\",\n    \"pause\": \"暫停\",\n    \"resume\": \"繼續\",\n    \"action\": \"動作 | 動作\",\n    \"packages\": \"套件\",\n    \"storage\": \"儲存空間\",\n    \"editor\": \"編輯器\",\n    \"running\": \"執行中\",\n    \"globalData\": \"全域資料\",\n    \"fileName\": \"檔案名稱\",\n    \"description\": \"描述\",\n    \"disable\": \"停用\",\n    \"disabled\": \"已停用\",\n    \"enable\": \"啟用\",\n    \"fallback\": \"備用\",\n    \"update\": \"更新\",\n    \"feature\": \"功能\",\n    \"duplicate\": \"複製\",\n    \"password\": \"密碼\",\n    \"category\": \"分類\",\n    \"optional\": \"選填\",\n    \"0disable\": \"0 表示停用\",\n    \"millisecond\": \"毫秒 | 毫秒\"\n  },\n  \"message\": {\n    \"noBlock\": \"無區塊\",\n    \"noData\": \"無資料可顯示\",\n    \"noTriggerBlock\": \"找不到觸發區塊\",\n    \"useDynamicData\": \"了解如何加入動態資料\",\n    \"delete\": \"您確定要刪除 \\\"{name}\\\" 嗎？\",\n    \"empty\": \"哎呀... 看起來您沒有任何項目\",\n    \"maxSizeExceeded\": \"檔案大小已超過允許的最大值\",\n    \"notSaved\": \"您確定要離開嗎？您有未儲存的變更！\",\n    \"somethingWrong\": \"出了點問題\",\n    \"limitExceeded\": \"您已超過限制\"\n  },\n  \"sort\": {\n    \"sortBy\": \"排序依據\",\n    \"name\": \"名稱\",\n    \"createdAt\": \"建立日期\",\n    \"updatedAt\": \"最後更新\",\n    \"mostUsed\": \"最常使用\"\n  },\n  \"logStatus\": {\n    \"stopped\": \"已停止\",\n    \"error\": \"錯誤\",\n    \"success\": \"成功\"\n  }\n}\n"
  },
  {
    "path": "src/locales/zh-TW/newtab.json",
    "content": "{\n  \"home\": {\n    \"viewAll\": \"查看全部\",\n    \"communities\": \"社群\"\n  },\n  \"welcome\": {\n    \"title\": \"歡迎使用 Automa！🎉\",\n    \"text\": \"開始使用前，請閱讀文件或瀏覽 Automa 市集中的工作流程。\",\n    \"marketplace\": \"市集\"\n  },\n  \"packages\": {\n    \"name\": \"套件 | 套件\",\n    \"add\": \"新增套件\",\n    \"icon\": \"套件圖示\",\n    \"open\": \"開啟套件\",\n    \"new\": \"新增套件\",\n    \"import\": \"匯入套件\",\n    \"set\": \"設為套件\",\n    \"settings\": {\n      \"asBlock\": \"將套件設為區塊\"\n    },\n    \"categories\": {\n      \"my\": \"我的套件\",\n      \"installed\": \"已安裝套件\"\n    }\n  },\n  \"scheduledWorkflow\": {\n    \"title\": \"排程工作流程\",\n    \"nextRun\": \"下次執行\",\n    \"active\": \"啟用中\",\n    \"refresh\": \"重新整理\",\n    \"schedule\": {\n      \"title\": \"排程\",\n      \"types\": {\n        \"everyDay\": \"每天\",\n        \"general\": \"每 {time}\",\n        \"interval\": \"每 {time} 分鐘\"\n      }\n    }\n  },\n    \"storage\": {\n      \"title\": \"儲存空間\",\n      \"table\": {\n        \"add\": \"新增表格\",\n        \"edit\": \"編輯表格\",\n        \"createdAt\": \"建立於\",\n        \"modifiedAt\": \"修改於\",\n        \"rowsCount\": \"列數\",\n        \"delete\": \"刪除表格\"\n      }\n    },\n    \"credential\": {\n      \"title\": \"憑證 | 憑證\",\n      \"add\": \"新增憑證\",\n      \"use\": {\n        \"title\": \"已使用憑證\",\n        \"description\": \"此工作流程使用這些憑證\"\n      }\n    },\n    \"workflowPermissions\": {\n      \"title\": \"工作流程權限\",\n      \"description\": \"此工作流程需要這些權限才能正常執行\",\n      \"contextMenus\": {\n        \"title\": \"右鍵選單\",\n        \"description\": \"透過右鍵選單執行工作流程\"\n      },\n      \"clipboardRead\": {\n        \"title\": \"剪貼簿\",\n        \"description\": \"用於存取剪貼簿資料\"\n      },\n      \"notifications\": {\n        \"title\": \"通知\",\n        \"description\": \"用於顯示通知\"\n      },\n      \"downloads\": {\n        \"title\": \"下載\",\n        \"description\": \"儲存頁面資源並重新命名下載的檔案\"\n      },\n      \"cookies\": {\n        \"title\": \"Cookie\",\n        \"description\": \"讀取、設定或移除 Cookie\"\n      }\n    },\n    \"updateMessage\": {\n      \"text1\": \"Automa 已更新至 v{version},\",\n      \"text2\": \"查看新功能。\"\n    },\n    \"workflows\": {\n      \"folder\": {\n        \"new\": \"新增資料夾\",\n        \"name\": \"資料夾名稱\",\n        \"delete\": \"刪除資料夾\",\n        \"rename\": \"重新命名資料夾\"\n      }\n    },\n      \"auth\": {\n        \"title\": \"驗證\",\n        \"signIn\": \"登入\",\n        \"username\": \"您需要先設定您的使用者名稱\",\n        \"clickHere\": \"點擊這裡\",\n        \"text\": \"您需要先登入才能執行此操作\"\n      },\n      \"running\": {\n        \"start\": \"於 {date} 開始\",\n        \"message\": \"這僅顯示最近 5 筆日誌\"\n      },\n      \"settings\": {\n        \"theme\": \"主題\",\n        \"shortcuts\": {\n          \"duplicate\": \"快捷鍵已被 \\\"{name}\\\" 使用\"\n        },\n        \"editor\": {\n          \"title\": \"標題\",\n          \"curvature\": {\n            \"title\": \"線條曲度\",\n            \"line\": \"線條\",\n            \"reroute\": \"重新導向\",\n            \"rerouteFirstLast\": \"重新導向第一個和最後一個點\"\n          },\n          \"arrow\": {\n            \"title\": \"線條箭頭\",\n            \"description\": \"在線條末端添加箭頭\"\n          },\n          \"snapGrid\": {\n            \"title\": \"貼齊格線\",\n            \"description\": \"移動區塊時貼齊格線\"\n          },\n          \"saveWhenExecute\": {\n            \"title\": \"執行工作流程時自動儲存\",\n            \"description\": \"執行工作流程時將儲存工作流程變更\"\n          }\n        },\n        \"deleteLog\": {\n          \"title\": \"自動刪除工作流程日誌\",\n          \"after\": \"刪除於\",\n          \"deleteAfter\": {\n            \"never\": \"永不\",\n            \"days\": \"{day} 天\"\n          }\n        },\n          \"language\": {\n            \"label\": \"語言\",\n            \"helpTranslate\": \"找不到您的語言？協助翻譯。\",\n            \"reloadPage\": \"重新載入頁面以使變更生效\"\n          },\n          \"menu\": {\n            \"backup\": \"備份工作流程\",\n            \"editor\": \"編輯器\",\n            \"general\": \"一般\",\n            \"shortcuts\": \"快捷鍵\",\n            \"about\": \"關於\"\n          },\n          \"backupWorkflows\": {\n            \"title\": \"本機備份\",\n            \"invalidPassword\": \"密碼無效\",\n            \"workflowsAdded\": \"已新增 {count} 個工作流程\",\n            \"name\": \"備份工作流程\",\n            \"needSignin\": \"您需要先登入\",\n            \"backup\": {\n              \"button\": \"備份\",\n              \"settings\": \"備份設定\",\n              \"encrypt\": \"使用密碼加密\",\n              \"schedule\": \"排程本機備份\"\n            },\n            \"restore\": {\n              \"title\": \"還原工作流程\",\n              \"button\": \"還原\",\n              \"update\": \"如果工作流程存在則更新\"\n            },\n            \"cloud\": {\n              \"buttons\": {\n                \"local\": \"本機\",\n                \"cloud\": \"雲端\"\n              },\n              \"location\": \"位置\",\n              \"delete\": \"刪除備份\",\n              \"title\": \"雲端備份\",\n              \"sync\": \"同步\",\n              \"lastSync\": \"上次同步\",\n              \"lastBackup\": \"上次備份\",\n              \"select\": \"選擇工作流程\",\n              \"storedWorkflows\": \"儲存在雲端的工作流程\",\n              \"selected\": \"已選擇\",\n              \"selectText\": \"選擇您要備份的工作流程\",\n              \"selectAll\": \"全選\",\n              \"deselectAll\": \"取消全選\",\n              \"needSelectWorkflow\": \"您需要選擇您要備份的工作流程\"\n            }\n          }\n        },\n          \"workflow\": {\n            \"events\": {\n              \"title\": \"工作流程事件\",\n              \"add-action\": \"新增動作\",\n              \"description\": \"當事件發生時執行動作。\",\n              \"event\": \"事件 | 事件\",\n              \"action\": \"動作\",\n              \"actions\": {\n                \"js-code\": {\n                  \"title\": \"執行 JS 程式碼\"\n                },\n                \"http-request\": {\n                  \"title\": \"HTTP 請求\"\n                }\n              },\n              \"types\": {\n                \"finish:success\": {\n                  \"name\": \"完成 (成功)\",\n                  \"description\": \"工作流程執行完成且成功\"\n                },\n                \"finish:failed\": {\n                  \"name\": \"完成 (失敗)\",\n                  \"description\": \"工作流程執行完成但發生錯誤\"\n                }\n              }\n            },\n            \"previewMode\": {\n              \"title\": \"預覽模式\",\n              \"description\": \"您目前處於預覽模式，您所做的變更將不會被儲存\"\n            },\n            \"pinWorkflow\": {\n              \"pin\": \"釘選工作流程\",\n              \"unpin\": \"取消釘選工作流程\",\n              \"pinned\": \"已釘選的工作流程\"\n            },\n            \"parameters\": {\n              \"add\": \"新增參數\",\n              \"preferInTab\": \"優先在分頁中顯示輸入參數\"\n            },\n            \"my\": \"我的工作流程\",\n            \"testing\": {\n              \"title\": \"測試模式\",\n              \"nextBlock\": \"下一個區塊\",\n              \"startRun\": \"開始執行於\",\n              \"disabled\": \"請先儲存變更\"\n            },\n            \"import\": \"匯入工作流程\",\n            \"new\": \"新增工作流程\",\n            \"delete\": \"刪除工作流程\",\n            \"browse\": \"瀏覽工作流程\",\n            \"name\": \"工作流程名稱\",\n            \"rename\": \"重新命名工作流程\",\n            \"backupCloud\": \"將工作流程備份到雲端\",\n            \"add\": \"新增工作流程\",\n            \"clickToEnable\": \"點擊以啟用\",\n            \"toggleSidebar\": \"切換側邊欄\",\n            \"cantEdit\": \"無法編輯共享的工作流程\",\n            \"undo\": \"復原\",\n            \"redo\": \"重做\",\n            \"autoAlign\": {\n              \"title\": \"自動對齊\"\n            },\n            \"blocksFolder\": {\n              \"title\": \"區塊資料夾\",\n              \"add\": \"將區塊新增至資料夾\",\n              \"save\": \"儲存至資料夾\"\n            },\n            \"searchBlocks\": {\n              \"title\": \"在編輯器中搜尋區塊\"\n            },\n            \"conditionBuilder\": {\n              \"title\": \"條件建構器\",\n              \"add\": \"新增條件\",\n              \"and\": \"AND\",\n              \"or\": \"OR\",\n              \"topAwait\": \"支援頂層 await 和 \\\"automaRefData\\\" 函數\"\n            },\n            \"host\": {\n              \"title\": \"託管工作流程\",\n              \"set\": \"設定為託管工作流程\",\n              \"id\": \"主機 ID\",\n              \"add\": \"新增託管工作流程\",\n              \"sync\": {\n                \"title\": \"同步\",\n                \"description\": \"與託管工作流程同步\"\n              },\n              \"messages\": {\n                \"hostExist\": \"您已經新增過此主機\",\n                \"notFound\": \"找不到 ID 為 \\\"{id}\\\" 的託管工作流程\",\n                \"successAdded\": \"託管工作流程 \\\"{id}\\\" 已新增成功\"\n              }\n            },\n              \"type\": {\n                \"local\": \"本機\",\n                \"shared\": \"共享\",\n                \"host\": \"主機\"\n              },\n              \"unpublish\": {\n                \"title\": \"取消發布工作流程\",\n                \"button\": \"取消發布\",\n                \"body\": \"您確定要取消發布工作流程 \\\"{name}\\\" 嗎？\"\n              },\n              \"share\": {\n                \"url\": \"分享網址\",\n                \"publish\": \"發布\",\n                \"sharedAs\": \"共享為 \\\"{name}\\\"\",\n                \"title\": \"分享工作流程\",\n                \"download\": \"將工作流程儲存到本機\",\n                \"edit\": \"編輯描述\",\n                \"fetchLocal\": \"提取本機工作流程\",\n                \"update\": \"更新\",\n                \"unpublish\": \"取消發布\",\n                \"linkCopied\": \"連結已複製到剪貼簿\"\n              },\n              \"variables\": {\n                \"title\": \"變數 | 變數們\",\n                \"name\": \"變數名稱\",\n                \"assign\": \"賦予給變數\"\n              },\n              \"protect\": {\n                \"title\": \"保護工作流程\",\n                \"remove\": \"移除保護\",\n                \"button\": \"保護\",\n                \"note\": \"注意：稍後編輯或刪除工作流程時，將需要此密碼。\"\n              },\n              \"locked\": {\n                \"title\": \"此工作流程已受保護\",\n                \"body\": \"輸入密碼以解鎖\",\n                \"unlock\": \"解鎖\",\n                \"messages\": {\n                  \"incorrect-password\": \"密碼不正確\"\n                }\n              },\n              \"state\": {\n                \"executeBy\": \"由 \\\"{name}\\\" 執行\"\n              },\n              \"table\": {\n                \"title\": \"表格 | 表格們\",\n                \"placeholder\": \"搜尋或新增欄位\",\n                \"select\": \"選擇欄位\",\n                \"column\": {\n                  \"name\": \"欄位名稱\",\n                  \"type\": \"資料類型\"\n                }\n              },\n              \"sidebar\": {\n                \"workflowIcon\": \"工作流程圖示\"\n              },\n              \"editor\": {\n                \"zoomIn\": \"放大\",\n                \"zoomOut\": \"縮小\",\n                \"resetZoom\": \"重設縮放\",\n                \"duplicate\": \"複製\",\n                \"copy\": \"複製\",\n                \"paste\": \"貼上\",\n                \"group\": \"群組區塊\",\n                \"ungroup\": \"取消群組區塊\"\n              },\n              \"settings\": {\n                \"saveLog\": \"儲存工作流程日誌\",\n                \"executedBlockOnWeb\": \"在網頁上顯示已執行的區塊\",\n                \"notification\": {\n                  \"title\": \"工作流程通知\",\n                  \"description\": \"在工作流程執行後顯示工作流程狀態 (成功或失敗)\",\n                  \"noPermission\": \"此選項需要 \\\"notifications\\\" 權限才能運作\"\n                },\n                \"publicId\": {\n                  \"title\": \"工作流程公開 ID\",\n                  \"description\": \"設定公開 ID 以透過 JavaScript 自訂事件執行工作流程\"\n                },\n                \"defaultColumn\": {\n                  \"title\": \"插入到預設欄位\",\n                  \"description\": \"如果區塊中未選擇欄位，則將資料插入到預設欄位\",\n                  \"name\": \"預設欄位名稱\"\n                },\n                \"autocomplete\": {\n                  \"title\": \"自動完成\",\n                  \"description\": \"在輸入區塊中啟用自動完成 (如果它使 Automa 不穩定，請停用)\"\n                },\n                \"clearCache\": {\n                  \"title\": \"清除快取\",\n                  \"description\": \"清除工作流程的快取 (狀態和迴圈索引)\",\n                  \"info\": \"已成功清除工作流程快取\",\n                  \"btn\": \"清除\"\n                },\n                \"reuseLastState\": {\n                  \"title\": \"重複使用上次工作流程的狀態\",\n                  \"description\": \"使用上次執行的工作流程的狀態資料 (表格、變數和全域資料)\"\n                },\n                \"debugMode\": {\n                  \"title\": \"偵錯模式\",\n                  \"description\": \"使用 Chrome DevTools Protocol 執行工作流程\"\n                },\n                \"restartWorkflow\": {\n                  \"for\": \"重新啟動\",\n                  \"times\": \"次\",\n                  \"description\": \"工作流程將重新啟動的最大次數\"\n                },\n                \"onError\": {\n                  \"title\": \"在工作流程發生錯誤時\",\n                  \"description\": \"設定工作流程發生錯誤時要採取的動作\",\n                  \"items\": {\n                    \"keepRunning\": \"繼續執行\",\n                    \"stopWorkflow\": \"停止工作流程\",\n                    \"restartWorkflow\": \"重新啟動工作流程\"\n                  }\n                },\n                  \"timeout\": {\n                    \"title\": \"工作流程逾時 (毫秒)\"\n                  },\n                  \"blockDelay\": {\n                    \"title\": \"區塊延遲 (毫秒)\",\n                    \"description\": \"在執行每個區塊之前新增延遲\"\n                  },\n                  \"tabLoadTimeout\": {\n                    \"title\": \"分頁載入逾時\",\n                    \"description\": \"分頁載入的最大時間 (毫秒)，輸入 0 以停用逾時\"\n                  }\n                }\n              },\n              \"collection\": {\n                \"description\": \"依序執行您的工作流程\",\n                \"new\": \"新增集合\",\n                \"delete\": \"刪除集合\",\n                \"add\": \"新增集合\",\n                \"rename\": \"重新命名集合\",\n                \"flow\": \"流程\",\n                \"dragDropText\": \"將工作流程或區塊拖放到此處\",\n                \"options\": {\n                  \"atOnce\": {\n                    \"title\": \"一次執行集合中的所有工作流程\",\n                    \"description\": \"使用此選項時，區塊將不會執行\"\n                  }\n                },\n                \"globalData\": {\n                  \"note\": \"這將覆寫工作流程的全域資料\"\n                }\n              },\n              \"log\": {\n                \"flowId\": \"流程 ID\",\n                \"goBack\": \"返回 \\\"{name}\\\" 的日誌\",\n                \"goWorkflow\": \"前往工作流程\",\n                \"startedDate\": \"開始日期\",\n                \"duration\": \"持續時間\",\n                \"selectAll\": \"全選\",\n                \"deselectAll\": \"取消全選\",\n                \"deleteSelected\": \"刪除選取的日誌\",\n                \"clearLogs\": {\n                  \"title\": \"清除日誌\",\n                  \"description\": \"您確定要清除所有日誌嗎？\"\n                },\n                \"types\": {\n                  \"stop\": \"工作流程已停止\",\n                  \"finish\": \"完成\"\n                },\n                \"messages\": {\n                  \"url-empty\": \"網址為空\",\n                  \"invalid-url\": \"網址無效\",\n                  \"conditions-empty\": \"條件為空\",\n                  \"invalid-proxy-host\": \"Proxy 主機無效\",\n                  \"workflow-disabled\": \"工作流程已停用\",\n                  \"selector-empty\": \"元素選擇器為空\",\n                  \"invalid-body\": \"內容主體不是有效的 JSON\",\n                  \"invalid-active-tab\": \"\\\"{url}\\\" 是無效的網址\",\n                  \"empty-spreadsheet-id\": \"試算表 ID 為空\",\n                  \"invalid-loop-data\": \"要迴圈的資料無效\",\n                  \"empty-workflow\": \"您必須先選擇一個工作流程\",\n                  \"active-tab-removed\": \"工作流程的活動分頁已移除\",\n                  \"empty-spreadsheet-range\": \"試算表範圍為空\",\n                  \"stop-timeout\": \"工作流程因逾時而停止\",\n                  \"no-file-access\": \"Automa 無法存取該檔案\",\n                  \"no-workflow\": \"找不到 ID 為 \\\"{workflowId}\\\" 的工作流程\",\n                  \"no-match-tab\": \"找不到符合模式 \\\"{pattern}\\\" 的分頁\",\n                  \"no-clipboard-acces\": \"沒有權限存取剪貼簿\",\n                  \"browser-not-supported\": \"此功能在 {browser} 瀏覽器中不受支援\",\n                  \"element-not-found\": \"找不到具有選擇器 \\\"{selector}\\\" 的元素\",\n                  \"no-permission\": \"沒有 \\\"{permission}\\\" 權限執行此動作\",\n                  \"not-iframe\": \"具有 \\\"{selector}\\\" 選擇器的元素不是 iframe 元素\",\n                  \"iframe-not-found\": \"找不到具有選擇器 \\\"{selector}\\\" 的 iframe 元素\",\n                  \"workflow-infinite-loop\": \"無法執行工作流程以防止無限迴圈\",\n                  \"not-debug-mode\": \"工作流程必須在偵錯模式下執行，此區塊才能正常運作\",\n                  \"no-iframe-id\": \"找不到具有選擇器 \\\"{selector}\\\" 的 iframe 元素的 Frame ID\",\n                  \"no-tab\": \"無法連線到分頁，在使用 \\\"{name}\\\" 區塊之前，請先使用 \\\"新增分頁\\\" 或 \\\"活動分頁\\\" 區塊\"\n                },\n                \"description\": {\n                  \"text\": \"{status} 於 {date}，歷時 {duration}\",\n                  \"status\": {\n                    \"success\": \"成功\",\n                    \"error\": \"失敗\",\n                    \"stopped\": \"已停止\"\n                  }\n                },\n                \"delete\": {\n                  \"title\": \"刪除日誌\",\n                  \"description\": \"您確定要刪除所有選取的日誌嗎？\"\n                },\n                \"exportData\": {\n                  \"title\": \"匯出資料\",\n                  \"types\": {\n                    \"json\": \"JSON\",\n                    \"csv\": \"CSV\",\n                    \"plain-text\": \"純文字\"\n                  }\n                },\n                \"filter\": {\n                  \"title\": \"篩選\",\n                  \"byStatus\": \"依狀態\",\n                  \"byDate\": {\n                    \"title\": \"依日期\",\n                    \"items\": {\n                      \"lastDay\": \"最近一天\",\n                      \"last7Days\": \"最近七天\",\n                      \"last30Days\": \"最近三十天\"\n                    }\n                  }\n                }\n              },\n              \"components\": {\n                \"pagination\": {\n                  \"text1\": \"顯示\",\n                  \"text2\": \"個項目，共 {count} 個\",\n                  \"nextPage\": \"下一頁\",\n                  \"currentPage\": \"目前頁面\",\n                  \"prevPage\": \"上一頁\",\n                  \"of\": \"共 {page} 頁\"\n                }\n              }\n            }\n"
  },
  {
    "path": "src/locales/zh-TW/popup.json",
    "content": "{\n  \"recording\": {\n    \"stop\": \"停止錄製\",\n    \"title\": \"錄製中\"\n  },\n  \"home\": {\n    \"record\": {\n      \"title\": \"錄製工作流程\",\n      \"button\": \"錄製\",\n      \"name\": \"工作流程名稱\",\n      \"selectBlock\": \"選擇一個區塊開始\",\n      \"anotherBlock\": \"無法從此區塊開始\",\n      \"tabs\": {\n        \"new\": \"新增工作流程\",\n        \"existing\": \"現有工作流程\"\n      }\n    },\n    \"elementSelector\": {\n      \"name\": \"元素選擇器\",\n      \"noAccess\": \"無法存取此網站\"\n    },\n    \"workflow\": {\n      \"new\": \"新增工作流程\",\n      \"rename\": \"重新命名工作流程\",\n      \"delete\": \"刪除工作流程\",\n      \"type\": {\n        \"host\": \"主機\",\n        \"local\": \"本機\"\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "src/manifest.chrome.dev.json",
    "content": "{\n  \"manifest_version\": 3,\n  \"name\": \"Automa-Dev\",\n  \"minimum_chrome_version\": \"116\",\n  \"action\": {\n    \"default_popup\": \"popup.html\",\n    \"default_icon\": \"icon-dev-128.png\"\n  },\n  \"background\": {\n    \"service_worker\": \"background.bundle.js\",\n    \"type\": \"module\"\n  },\n  \"icons\": {\n    \"128\": \"icon-dev-128.png\"\n  },\n  \"commands\": {\n    \"open-dashboard\": {\n      \"suggested_key\": {\n        \"default\": \"Alt+A\",\n        \"mac\": \"Alt+A\"\n      },\n      \"description\": \"Open the dashboard\"\n    },\n    \"element-picker\": {\n      \"suggested_key\": {\n        \"default\": \"Alt+P\",\n        \"mac\": \"Alt+P\"\n      },\n      \"description\": \"Open element picker\"\n    }\n  },\n  \"host_permissions\": [\"<all_urls>\"],\n  \"content_scripts\": [\n    {\n      \"matches\": [\"<all_urls>\"],\n      \"js\": [\"contentScript.bundle.js\"],\n      \"run_at\": \"document_start\",\n      \"match_about_blank\": true,\n      \"all_frames\": true\n    },\n    {\n      \"matches\": [\n        \"http://localhost/*\",\n        \"*://*.automa.site/*\",\n        \"*://automa.vercel.app/*\"\n      ],\n      \"js\": [\"webService.bundle.js\"],\n      \"run_at\": \"document_start\",\n      \"all_frames\": false\n    }\n  ],\n  \"optional_permissions\": [\n    \"cookies\",\n    \"downloads\",\n    \"contextMenus\",\n    \"clipboardRead\",\n    \"notifications\"\n  ],\n  \"permissions\": [\n    \"tabs\",\n    \"proxy\",\n    \"alarms\",\n    \"storage\",\n    \"debugger\",\n    \"activeTab\",\n    \"offscreen\",\n    \"webNavigation\",\n    \"unlimitedStorage\",\n    \"scripting\"\n  ],\n  \"web_accessible_resources\": [\n    {\n      \"resources\": [\n        \"/locales/*\",\n        \"/icon-dev-128.png\",\n        \"/elementSelector.css\",\n        \"elementSelector.bundle.js\",\n        \"/Inter-roman-latin.var.woff2\"\n      ],\n      \"matches\": [\n        \"*://*/*\",\n        \"file://*/*\"\n      ]\n    }\n  ],\n  \"sandbox\": {\n    \"pages\": [\"/sandbox.html\"]\n  }\n}\n"
  },
  {
    "path": "src/manifest.chrome.json",
    "content": "{\n  \"manifest_version\": 3,\n  \"name\": \"Automa\",\n  \"minimum_chrome_version\": \"116\",\n  \"action\": {\n    \"default_popup\": \"popup.html\",\n    \"default_icon\": \"icon-128.png\"\n  },\n  \"background\": {\n    \"service_worker\": \"background.bundle.js\",\n    \"type\": \"module\"\n  },\n  \"icons\": {\n    \"128\": \"icon-128.png\"\n  },\n  \"commands\": {\n    \"open-dashboard\": {\n      \"suggested_key\": {\n        \"default\": \"Alt+A\",\n        \"mac\": \"Alt+A\"\n      },\n      \"description\": \"Open the dashboard\"\n    },\n    \"element-picker\": {\n      \"suggested_key\": {\n        \"default\": \"Alt+P\",\n        \"mac\": \"Alt+P\"\n      },\n      \"description\": \"Open element picker\"\n    }\n  },\n  \"host_permissions\": [\"<all_urls>\"],\n  \"content_scripts\": [\n    {\n      \"matches\": [\"<all_urls>\"],\n      \"js\": [\"contentScript.bundle.js\"],\n      \"run_at\": \"document_start\",\n      \"match_about_blank\": true,\n      \"all_frames\": true\n    },\n    {\n      \"matches\": [\n        \"http://localhost/*\",\n        \"*://*.automa.site/*\",\n        \"*://automa.vercel.app/*\"\n      ],\n      \"js\": [\"webService.bundle.js\"],\n      \"run_at\": \"document_start\",\n      \"all_frames\": false\n    }\n  ],\n  \"optional_permissions\": [\n    \"cookies\",\n    \"downloads\",\n    \"contextMenus\",\n    \"clipboardRead\",\n    \"notifications\"\n  ],\n  \"permissions\": [\n    \"tabs\",\n    \"proxy\",\n    \"alarms\",\n    \"storage\",\n    \"debugger\",\n    \"activeTab\",\n    \"offscreen\",\n    \"webNavigation\",\n    \"unlimitedStorage\",\n    \"scripting\"\n  ],\n  \"web_accessible_resources\": [\n    {\n      \"resources\": [\n        \"/locales/*\",\n        \"/icon-128.png\",\n        \"/elementSelector.css\",\n        \"elementSelector.bundle.js\",\n        \"/Inter-roman-latin.var.woff2\"\n      ],\n      \"matches\": [\n        \"*://*/*\",\n        \"file://*/*\"\n      ]\n    }\n  ],\n  \"sandbox\": {\n    \"pages\": [\"/sandbox.html\"]\n  }\n}\n"
  },
  {
    "path": "src/manifest.firefox.json",
    "content": "{\n  \"manifest_version\": 2,\n  \"name\": \"Automa\",\n  \"browser_specific_settings\": {\n    \"gecko\": {\n      \"strict_min_version\": \"91.1.0\"\n    }\n  },\n  \"background\": {\n    \"scripts\": [\"background.bundle.js\"],\n    \"persistent\": true\n  },\n  \"browser_action\": {\n    \"default_popup\": \"popup.html\",\n    \"default_icon\": \"icon-128.png\"\n  },\n  \"icons\": {\n    \"128\": \"icon-128.png\"\n  },\n  \"commands\": {\n    \"open-dashboard\": {\n      \"suggested_key\": {\n        \"default\": \"Alt+A\",\n        \"mac\": \"Alt+A\"\n      },\n      \"description\": \"Open the dashboard\"\n    },\n    \"element-picker\": {\n      \"suggested_key\": {\n        \"default\": \"Alt+P\",\n        \"mac\": \"Alt+P\"\n      },\n      \"description\": \"Open element picker\"\n    }\n  },\n  \"content_scripts\": [\n    {\n      \"matches\": [\"<all_urls>\"],\n      \"js\": [\"contentScript.bundle.js\"],\n      \"run_at\": \"document_start\",\n      \"all_frames\": true\n    },\n    {\n      \"matches\": [\"*://*.automa.site/*\", \"*://automa.vercel.app/*\"],\n      \"js\": [\"webService.bundle.js\"],\n      \"run_at\": \"document_start\",\n      \"all_frames\": false\n    }\n  ],\n  \"optional_permissions\": [\"clipboardRead\", \"clipboardWrite\", \"downloads\", \"notifications\", \"cookies\"],\n  \"permissions\": [\n    \"tabs\",\n    \"proxy\",\n    \"menus\",\n    \"alarms\",\n    \"storage\",\n    \"webNavigation\",\n    \"unlimitedStorage\",\n    \"<all_urls>\"\n  ],\n  \"web_accessible_resources\": [\n    \"/elementSelector.css\",\n    \"/icon-128.png\",\n    \"/Inter-roman-latin.var.woff2\",\n    \"/locales/*\",\n    \"elementSelector.bundle.js\"\n  ],\n  \"content_security_policy\": \"script-src 'self' 'unsafe-inline' https:; object-src 'self'\"\n}\n"
  },
  {
    "path": "src/newtab/App.vue",
    "content": "<template>\n  <template v-if=\"retrieved\">\n    <app-sidebar v-if=\"$route.name !== 'recording'\" />\n    <main :class=\"{ 'pl-16': $route.name !== 'recording' }\">\n      <router-view />\n    </main>\n    <app-logs />\n    <ui-dialog>\n      <template #auth>\n        <div class=\"text-center\">\n          <p class=\"text-xl font-semibold\">Oops!! 😬</p>\n          <p class=\"mt-2 text-gray-600 dark:text-gray-200\">\n            {{ t('auth.text') }}\n          </p>\n          <ui-button\n            tag=\"a\"\n            href=\"https://extension.automa.site/auth\"\n            class=\"mt-6 block w-full\"\n            variant=\"accent\"\n          >\n            {{ t('auth.signIn') }}\n          </ui-button>\n        </div>\n      </template>\n    </ui-dialog>\n    <div\n      v-if=\"isUpdated\"\n      class=\"fixed bottom-8 left-1/2 z-50 max-w-xl -translate-x-1/2 text-white dark:text-gray-900\"\n    >\n      <div class=\"flex items-center rounded-lg bg-accent p-4 shadow-2xl\">\n        <v-remixicon name=\"riInformationLine\" class=\"mr-3\" />\n        <p>\n          {{ t('updateMessage.text1', { version: currentVersion }) }}\n        </p>\n        <a\n          :href=\"`https://github.com/AutomaApp/automa/releases/latest`\"\n          target=\"_blank\"\n          rel=\"noopener\"\n          class=\"ml-1 underline\"\n        >\n          {{ t('updateMessage.text2') }}\n        </a>\n        <div class=\"flex-1\" />\n        <button\n          class=\"ml-6 text-gray-200 dark:text-gray-600\"\n          @click=\"isUpdated = false\"\n        >\n          <v-remixicon size=\"20\" name=\"riCloseLine\" />\n        </button>\n      </div>\n      <!-- <div class=\"mt-4 flex items-center rounded-lg bg-accent p-4 shadow-2xl\">\n        <v-remixicon name=\"riInformationLine\" class=\"mr-3 shrink-0\" />\n        <p>\n          Export your Automa workflows as a standalone extension using\n          <a\n            href=\"https://docs.extension.automa.site/extension-builder/\"\n            target=\"_blank\"\n            class=\"underline\"\n            >Automa Chrome Extension Builder</a\n          >\n        </p>\n      </div> -->\n    </div>\n    <shared-permissions-modal\n      v-model=\"permissionState.showModal\"\n      :permissions=\"permissionState.items\"\n    />\n  </template>\n  <div v-else class=\"py-8 text-center\">\n    <ui-spinner color=\"text-accent\" size=\"28\" />\n  </div>\n</template>\n<script setup>\nimport iconChrome from '@/assets/svg/logo.svg';\nimport iconFirefox from '@/assets/svg/logoFirefox.svg';\nimport AppLogs from '@/components/newtab/app/AppLogs.vue';\nimport AppSidebar from '@/components/newtab/app/AppSidebar.vue';\nimport SharedPermissionsModal from '@/components/newtab/shared/SharedPermissionsModal.vue';\nimport { useTheme } from '@/composable/theme';\nimport dbLogs from '@/db/logs';\nimport dayjs from '@/lib/dayjs';\nimport emitter from '@/lib/mitt';\nimport { loadLocaleMessages, setI18nLanguage } from '@/lib/vueI18n';\nimport { useFolderStore } from '@/stores/folder';\nimport { useHostedWorkflowStore } from '@/stores/hostedWorkflow';\nimport { useStore } from '@/stores/main';\nimport { usePackageStore } from '@/stores/package';\nimport { useSharedWorkflowStore } from '@/stores/sharedWorkflow';\nimport { useTeamWorkflowStore } from '@/stores/teamWorkflow';\nimport { useUserStore } from '@/stores/user';\nimport { useWorkflowStore } from '@/stores/workflow';\nimport { getUserWorkflows } from '@/utils/api';\nimport dataMigration from '@/utils/dataMigration';\nimport { MessageListener } from '@/utils/message';\nimport { getWorkflowPermissions } from '@/utils/workflowData';\nimport automa from '@business';\nimport { useHead } from '@vueuse/head';\nimport { compare } from 'compare-versions';\nimport { reactive, ref, watch } from 'vue';\nimport { useI18n } from 'vue-i18n';\nimport { useRoute, useRouter } from 'vue-router';\nimport browser from 'webextension-polyfill';\n\nconst iconElement = document.createElement('link');\niconElement.rel = 'icon';\niconElement.href =\n  window.location.protocol === 'moz-extension' ? iconFirefox : iconChrome;\ndocument.head.appendChild(iconElement);\n\nwindow.fromBackground = window.location.href.includes('?fromBackground=true');\n\nconst { t } = useI18n();\nconst route = useRoute();\nconst store = useStore();\nconst theme = useTheme();\nconst router = useRouter();\nconst userStore = useUserStore();\nconst folderStore = useFolderStore();\nconst packageStore = usePackageStore();\nconst workflowStore = useWorkflowStore();\nconst teamWorkflowStore = useTeamWorkflowStore();\nconst sharedWorkflowStore = useSharedWorkflowStore();\nconst hostedWorkflowStore = useHostedWorkflowStore();\n\ntheme.init();\n\nconst retrieved = ref(false);\nconst isUpdated = ref(false);\nconst permissionState = reactive({\n  permissions: [],\n  showModal: false,\n});\n\nconst currentVersion = browser.runtime.getManifest().version;\nconst prevVersion = localStorage.getItem('ext-version') || '0.0.0';\n\nasync function fetchUserData() {\n  try {\n    if (!userStore.user) return;\n\n    const { backup, hosted } = await getUserWorkflows();\n    userStore.hostedWorkflows = hosted || {};\n\n    if (backup && backup.length > 0) {\n      const { lastBackup } = browser.storage.local.get('lastBackup');\n      if (!lastBackup) {\n        const backupIds = backup.map(({ id }) => id);\n\n        userStore.backupIds = backupIds;\n        await browser.storage.local.set({\n          backupIds,\n          lastBackup: new Date().toISOString(),\n        });\n      }\n\n      await workflowStore.insertOrUpdate(backup, { checkUpdateDate: true });\n    }\n\n    userStore.retrieved = true;\n  } catch (error) {\n    console.error(error);\n  }\n}\n/* eslint-disable-next-line */\nfunction autoDeleteLogs() {\n  const deleteAfter = store.settings.deleteLogAfter;\n  if (deleteAfter === 'never') return;\n\n  const lastCheck =\n    +localStorage.getItem('checkDeleteLogs') || Date.now() - 8.64e7;\n  const dayDiff = dayjs().diff(dayjs(lastCheck), 'day');\n\n  if (dayDiff < 1) return;\n\n  const aDayInMs = 8.64e7;\n  const maxLogAge = Date.now() - aDayInMs * deleteAfter;\n\n  dbLogs.items\n    .where('endedAt')\n    .below(maxLogAge)\n    .toArray()\n    .then((values) => {\n      const ids = values.map(({ id }) => id);\n\n      dbLogs.items.bulkDelete(ids);\n      dbLogs.ctxData.where('logId').anyOf(ids).delete();\n      dbLogs.logsData.where('logId').anyOf(ids).delete();\n      dbLogs.histories.where('logId').anyOf(ids).delete();\n\n      localStorage.setItem('checkDeleteLogs', Date.now());\n    });\n}\nasync function syncHostedWorkflows() {\n  const hostIds = [];\n  const userHosted = userStore.getHostedWorkflows;\n  const hostedWorkflows = hostedWorkflowStore.workflows;\n\n  Object.keys(hostedWorkflows).forEach((hostId) => {\n    const isItsOwn = userHosted.find((item) => item.hostId === hostId);\n    if (isItsOwn) return;\n\n    hostIds.push({ hostId, updatedAt: hostedWorkflows[hostId].updatedAt });\n  });\n\n  if (hostIds.length === 0) return;\n\n  await hostedWorkflowStore.fetchWorkflows(hostIds);\n}\nfunction stopRecording() {\n  if (!window.stopRecording) return;\n\n  window.stopRecording();\n}\n\nconst messageEvents = {\n  'refresh-packages': function () {\n    packageStore.loadData(true);\n  },\n  'open-logs': function (data) {\n    emitter.emit('ui:logs', {\n      show: true,\n      logId: data.logId,\n    });\n  },\n  'workflow:added': function (data) {\n    if (data.source === 'team') {\n      teamWorkflowStore.loadData().then(() => {\n        router.push(\n          `/teams/${data.teamId}/workflows/${data.workflowId}?permission=true`\n        );\n      });\n    } else if (data.workflowData) {\n      workflowStore\n        .insert(data.workflowData, { duplicateId: true })\n        .then(async () => {\n          try {\n            const permissions = await getWorkflowPermissions(data.workflowData);\n            if (permissions.length === 0) return;\n\n            permissionState.items = permissions;\n            permissionState.showModal = true;\n          } catch (error) {\n            console.error(error);\n          }\n        })\n        .catch((error) => {\n          console.error(error);\n        });\n    }\n  },\n  'recording:stop': stopRecording,\n  'background--recording:stop': stopRecording,\n};\n\nbrowser.runtime.onMessage.addListener(({ type, data }) => {\n  if (!type || !messageEvents[type]) return;\n\n  messageEvents[type](data);\n});\n\nbrowser.storage.local.onChanged.addListener(({ workflowStates }) => {\n  if (!workflowStates) return;\n  const states = Object.values(workflowStates.newValue);\n  workflowStore.states = states;\n});\n\nuseHead(() => {\n  const runningWorkflows = workflowStore.popupStates.length;\n\n  return {\n    title: 'Dashboard',\n    titleTemplate:\n      runningWorkflows > 0\n        ? `%s (${runningWorkflows} Workflows Running) - Automa`\n        : '%s - Automa',\n  };\n});\n\n/* eslint-disable-next-line */\nwindow.onbeforeunload = () => {\n  const runningWorkflows = workflowStore.popupStates.length;\n  if (window.isDataChanged || runningWorkflows > 0) {\n    return t('message.notSaved');\n  }\n};\nwindow.addEventListener('message', ({ data }) => {\n  if (data?.type !== 'automa-fetch') return;\n\n  const sendResponse = (result) => {\n    const sandbox = document.getElementById('sandbox');\n    sandbox.contentWindow.postMessage(\n      {\n        type: 'fetchResponse',\n        data: result,\n        id: data.data.id,\n      },\n      '*'\n    );\n  };\n\n  MessageListener.sendMessage('fetch', data.data, 'background')\n    .then((result) => {\n      sendResponse({ isError: false, result });\n    })\n    .catch((error) => {\n      sendResponse({ isError: true, result: error.message });\n    });\n});\n\nwatch(\n  () => workflowStore.popupStates,\n  () => {\n    if (\n      !window.fromBackground ||\n      workflowStore.popupStates.length !== 0 ||\n      route.name !== 'workflows'\n    )\n      return;\n\n    window.close();\n  }\n);\n\n(async () => {\n  try {\n    const { workflowStates } = await browser.storage.local.get(\n      'workflowStates'\n    );\n    workflowStore.states = Object.values(workflowStates || {});\n\n    const tabs = await browser.tabs.query({\n      url: browser.runtime.getURL('/newtab.html'),\n    });\n\n    const currentWindow = await browser.windows.getCurrent();\n    if (currentWindow.type !== 'popup') {\n      await browser.tabs.remove([tabs[0].id]);\n      return;\n    }\n\n    if (tabs.length > 1) {\n      const firstTab = tabs.shift();\n      await browser.windows.update(firstTab.windowId, { focused: true });\n      await browser.tabs.update(firstTab.id, { active: true });\n\n      await browser.tabs.remove(tabs.map((tab) => tab.id));\n      return;\n    }\n\n    const { isFirstTime } = await browser.storage.local.get('isFirstTime');\n    isUpdated.value = !isFirstTime && compare(currentVersion, prevVersion, '>');\n\n    await Promise.allSettled([\n      folderStore.load(),\n      store.loadSettings(),\n      workflowStore.loadData(),\n      teamWorkflowStore.loadData(),\n      hostedWorkflowStore.loadData(),\n      packageStore.loadData(),\n    ]);\n\n    await loadLocaleMessages(store.settings.locale, 'newtab');\n    await setI18nLanguage(store.settings.locale);\n\n    await dataMigration();\n    await userStore.loadUser({ useCache: false, ttl: 2 });\n\n    await automa('app');\n\n    retrieved.value = true;\n\n    await Promise.allSettled([\n      sharedWorkflowStore.fetchWorkflows(),\n      fetchUserData(),\n      syncHostedWorkflows(),\n    ]);\n\n    const { isRecording } = await browser.storage.local.get('isRecording');\n    if (isRecording) {\n      router.push('/recording');\n\n      await (browser.action || browser.browserAction).setBadgeBackgroundColor({\n        color: '#ef4444',\n      });\n      await (browser.action || browser.browserAction).setBadgeText({\n        text: 'rec',\n      });\n    }\n\n    autoDeleteLogs();\n  } catch (error) {\n    retrieved.value = true;\n    console.error(error);\n  }\n\n  localStorage.setItem('ext-version', currentVersion);\n})();\n</script>\n<style>\nhtml,\nbody {\n  @apply bg-gray-50 dark:bg-gray-900 text-black dark:text-gray-100;\n}\n\nbody {\n  min-height: 100vh;\n}\n\n#app {\n  height: 100%;\n}\n\nh1,\nh2,\nh3 {\n  @apply dark:text-white;\n}\n</style>\n"
  },
  {
    "path": "src/newtab/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <title>Automa</title>\n  </head>\n\n  <body>\n    <div id=\"app\"></div>\n    <iframe src=\"/sandbox.html\" id=\"sandbox\" style=\"display: none;\"></iframe>\n  </body>\n</html>\n"
  },
  {
    "path": "src/newtab/index.js",
    "content": "import { createApp } from 'vue';\nimport { createHead } from '@vueuse/head';\nimport App from './App.vue';\nimport router from './router';\nimport pinia from '../lib/pinia';\nimport compsUi from '../lib/compsUi';\nimport vueI18n from '../lib/vueI18n';\nimport vRemixicon, { icons } from '../lib/vRemixicon';\nimport vueToastification from '../lib/vue-toastification';\nimport '../assets/css/tailwind.css';\nimport '../assets/css/fonts.css';\nimport '../assets/css/style.css';\nimport '../assets/css/flow.css';\n\nconst head = createHead();\n\ncreateApp(App)\n  .use(head)\n  .use(router)\n  .use(compsUi)\n  .use(pinia)\n  .use(vueI18n)\n  .use(vueToastification)\n  .use(vRemixicon, icons)\n  .mount('#app');\n\nif (module.hot) module.hot.accept();\n"
  },
  {
    "path": "src/newtab/pages/Packages.vue",
    "content": "<template>\n  <div class=\"container py-8 pb-4\">\n    <h1 class=\"text-2xl font-semibold\">\n      {{ $t('common.packages') }}\n    </h1>\n    <div class=\"mt-8 flex items-start\">\n      <div class=\"mr-8 hidden w-60 lg:block\">\n        <div class=\"flex items-center\">\n          <ui-button\n            class=\"w-full rounded-r-none border-r\"\n            variant=\"accent\"\n            @click=\"addState.show = true\"\n          >\n            <p>{{ t('packages.new') }}</p>\n          </ui-button>\n          <hr />\n          <ui-popover>\n            <template #trigger>\n              <ui-button icon variant=\"accent\" class=\"rounded-l-none\">\n                <v-remixicon name=\"riArrowDropDownLine\" />\n              </ui-button>\n            </template>\n            <ui-list>\n              <ui-list-item\n                v-close-popover\n                class=\"cursor-pointer\"\n                @click=\"importPackage\"\n              >\n                {{ t('packages.import') }}\n              </ui-list-item>\n            </ui-list>\n          </ui-popover>\n        </div>\n        <ui-list class=\"mt-4 space-y-1 text-gray-600 dark:text-gray-200\">\n          <ui-list-item\n            v-for=\"cat in categories\"\n            :key=\"cat.id\"\n            :active=\"cat.id === state.activeCat\"\n            class=\"cursor-pointer\"\n            color=\"bg-box-transparent text-black dark:text-gray-100\"\n            @click=\"state.activeCat = cat.id\"\n          >\n            {{ cat.name }}\n          </ui-list-item>\n        </ui-list>\n      </div>\n      <div class=\"flex-1\">\n        <div class=\"flex flex-wrap items-center\">\n          <div class=\"flex w-full items-center md:w-auto\">\n            <ui-input\n              v-model=\"state.query\"\n              :placeholder=\"t('common.search')\"\n              class=\"flex-1\"\n              prepend-icon=\"riSearch2Line\"\n            />\n            <ui-button\n              variant=\"accent\"\n              class=\"ml-4 lg:hidden\"\n              @click=\"addState.show = true\"\n            >\n              <v-remixicon name=\"riAddLine\" class=\"mr-2 -ml-1\" />\n              <span>{{ t('common.packages') }}</span>\n            </ui-button>\n          </div>\n          <div class=\"grow\" />\n          <div class=\"workflow-sort mt-4 flex items-center lg:mt-0\">\n            <ui-button\n              icon\n              class=\"rounded-r-none border-r border-gray-300 dark:border-gray-700\"\n              @click=\"\n                sortState.order = sortState.order === 'asc' ? 'desc' : 'asc'\n              \"\n            >\n              <v-remixicon\n                :name=\"sortState.order === 'asc' ? 'riSortAsc' : 'riSortDesc'\"\n              />\n            </ui-button>\n            <ui-select v-model=\"sortState.by\" :placeholder=\"t('sort.sortBy')\">\n              <option v-for=\"sort in sorts\" :key=\"sort\" :value=\"sort\">\n                {{ t(`sort.${sort}`) }}\n              </option>\n            </ui-select>\n          </div>\n        </div>\n        <div\n          class=\"mt-8 grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4\"\n        >\n          <ui-card\n            v-for=\"pkg in packages\"\n            :key=\"pkg.id\"\n            class=\"group flex flex-col hover:ring-2 hover:ring-accent dark:hover:ring-gray-200\"\n          >\n            <div class=\"flex items-center\">\n              <ui-img\n                v-if=\"pkg.icon?.startsWith('http')\"\n                :src=\"pkg.icon\"\n                class=\"overflow-hidden rounded-lg\"\n                style=\"height: 40px; width: 40px\"\n                alt=\"Can not display\"\n              />\n              <span v-else class=\"bg-box-transparent rounded-lg p-2\">\n                <v-remixicon :name=\"pkg.icon || 'mdiPackageVariantClosed'\" />\n              </span>\n              <div class=\"grow\" />\n              <ui-popover>\n                <template #trigger>\n                  <v-remixicon\n                    name=\"riMoreLine\"\n                    class=\"cursor-pointer text-gray-600 dark:text-gray-200\"\n                  />\n                </template>\n                <ui-list class=\"space-y-1\" style=\"min-width: 180px\">\n                  <ui-list-item\n                    v-if=\"pkg.isExternal\"\n                    v-close-popover\n                    :href=\"`https://extension.automa.site/packages/${pkg.id}`\"\n                    tag=\"a\"\n                    target=\"_blank\"\n                    class=\"cursor-pointer\"\n                  >\n                    <v-remixicon name=\"riExternalLinkLine\" class=\"mr-2 -ml-1\" />\n                    <span>Open package page</span>\n                  </ui-list-item>\n                  <template v-else>\n                    <ui-list-item\n                      v-close-popover\n                      class=\"cursor-pointer\"\n                      @click=\"duplicatePackage(pkg)\"\n                    >\n                      <v-remixicon name=\"riFileCopyLine\" class=\"mr-2 -ml-1\" />\n                      <span>{{ t('common.duplicate') }}</span>\n                    </ui-list-item>\n                    <ui-list-item\n                      v-close-popover\n                      class=\"cursor-pointer\"\n                      @click=\"exportPackage(pkg)\"\n                    >\n                      <v-remixicon name=\"riDownloadLine\" class=\"mr-2 -ml-1\" />\n                      <span>{{ t('common.export') }}</span>\n                    </ui-list-item>\n                  </template>\n                  <ui-list-item\n                    v-close-popover\n                    class=\"cursor-pointer text-red-500 dark:text-red-400\"\n                    @click=\"deletePackage(pkg)\"\n                  >\n                    <v-remixicon name=\"riDeleteBin7Line\" class=\"mr-2 -ml-1\" />\n                    <span>{{ t('common.delete') }}</span>\n                  </ui-list-item>\n                </ui-list>\n              </ui-popover>\n            </div>\n            <router-link\n              :to=\"`/packages/${pkg.id}`\"\n              class=\"mt-4 flex-1 cursor-pointer\"\n            >\n              <p class=\"text-overflow font-semibold\">\n                {{ pkg.name }}\n              </p>\n              <p\n                class=\"line-clamp leading-tight text-gray-600 dark:text-gray-200\"\n              >\n                {{ pkg.description }}\n              </p>\n            </router-link>\n            <div\n              class=\"mt-2 flex items-center text-gray-600 dark:text-gray-200\"\n            >\n              <p>{{ dayjs(pkg.createdAt).fromNow() }}</p>\n              <p v-if=\"pkg.author\" class=\"text-overflow ml-4 flex-1 text-right\">\n                By {{ pkg.author }}\n              </p>\n            </div>\n          </ui-card>\n        </div>\n      </div>\n    </div>\n    <ui-modal\n      v-model=\"addState.show\"\n      :title=\"t('packages.add')\"\n      @close=\"clearNewPackage\"\n    >\n      <ui-input\n        v-model=\"addState.name\"\n        :placeholder=\"t('common.name')\"\n        autofocus\n        class=\"w-full\"\n        @keyup.enter=\"addPackage\"\n      />\n      <ui-textarea\n        v-model=\"addState.description\"\n        :placeholder=\"t('common.description')\"\n        style=\"min-height: 200px\"\n        class=\"mt-2 w-full\"\n      />\n      <div class=\"mt-6 flex space-x-4\">\n        <ui-button class=\"flex-1\" @click=\"clearNewPackage\">\n          {{ t('common.cancel') }}\n        </ui-button>\n        <ui-button class=\"flex-1\" variant=\"accent\" @click=\"addPackage\">\n          {{ t('packages.add') }}\n        </ui-button>\n      </div>\n    </ui-modal>\n  </div>\n</template>\n<script setup>\nimport { useDialog } from '@/composable/dialog';\nimport dayjs from '@/lib/dayjs';\nimport { usePackageStore } from '@/stores/package';\nimport dataExporter from '@/utils/dataExporter';\nimport { arraySorter, openFilePicker, parseJSON } from '@/utils/helper';\nimport { computed, reactive } from 'vue';\nimport { useI18n } from 'vue-i18n';\n\nconst { t } = useI18n();\nconst dialog = useDialog();\nconst packageStore = usePackageStore();\n\nconst sorts = ['name', 'createdAt'];\nconst categories = [\n  { id: 'all', name: t('common.all') },\n  { id: 'user-pkgs', name: t('packages.categories.my') },\n  { id: 'installed-pkgs', name: t('packages.categories.installed') },\n];\n\nconst state = reactive({\n  query: '',\n  activeCat: 'all',\n});\nconst sortState = reactive({\n  order: 'desc',\n  by: 'createdAt',\n});\nconst addState = reactive({\n  show: false,\n  name: '',\n  description: '',\n});\n\nconst packages = computed(() => {\n  const filtered = packageStore.packages.filter((item) => {\n    let isInCategory = true;\n    const query = item.name\n      .toLocaleLowerCase()\n      .includes(state.query.toLocaleLowerCase());\n\n    if (state.activeCat !== 'all') {\n      isInCategory =\n        state.activeCat === 'user-pkgs' ? !item.isExternal : item.isExternal;\n    }\n\n    return isInCategory && query;\n  });\n\n  return arraySorter({\n    data: filtered,\n    key: sortState.by,\n    order: sortState.order,\n  });\n});\n\nfunction duplicatePackage(pkg) {\n  const copyPkg = JSON.parse(JSON.stringify(pkg));\n  delete copyPkg.id;\n\n  copyPkg.name += ' - copy';\n\n  packageStore.insert(copyPkg);\n}\nfunction importPackage() {\n  openFilePicker(['application/json']).then(([file]) => {\n    if (file.type !== 'application/json') return;\n\n    const fileReader = new FileReader();\n    fileReader.onload = () => {\n      const pkgJson = parseJSON(fileReader.result, null);\n      if (!pkgJson) return;\n      if (!pkgJson.name || !pkgJson.data) return;\n\n      packageStore.insert(pkgJson);\n    };\n    fileReader.readAsText(file);\n  });\n}\nfunction exportPackage(pkg) {\n  const copyPkg = JSON.parse(JSON.stringify(pkg));\n  delete copyPkg.id;\n\n  const blobUrl = dataExporter(\n    copyPkg,\n    { type: 'json', name: `${pkg.name}.automa-pkg` },\n    true\n  );\n  URL.revokeObjectURL(blobUrl);\n}\nfunction deletePackage({ id, name }) {\n  dialog.confirm({\n    title: 'Delete package',\n    body: `Are you sure want to delete \"${name}\" package?`,\n    okVariant: 'danger',\n    okText: 'Delete',\n    onConfirm: () => {\n      packageStore.delete(id);\n    },\n  });\n}\nfunction clearNewPackage() {\n  Object.assign(addState, {\n    name: '',\n    show: false,\n    description: '',\n  });\n}\nasync function addPackage() {\n  try {\n    await packageStore.insert({\n      name: addState.name.trim() || 'Unnamed',\n      description: addState.description,\n    });\n\n    clearNewPackage();\n  } catch (error) {\n    console.error(error);\n  }\n}\n</script>\n"
  },
  {
    "path": "src/newtab/pages/Recording.vue",
    "content": "<template>\n  <div class=\"mx-auto w-full max-w-xl p-5\">\n    <div class=\"flex items-center\">\n      <button\n        v-tooltip=\"t('recording.stop')\"\n        class=\"relative flex h-12 w-12 items-center justify-center rounded-full bg-red-400 focus:ring-0\"\n        @click=\"stopRecording\"\n      >\n        <span\n          class=\"absolute animate-ping rounded-full bg-red-400\"\n          style=\"height: 80%; width: 80%; animation-duration: 1.3s\"\n        ></span>\n        <ui-spinner v-if=\"state.isGenerating\" color=\"text-white\" />\n        <v-remixicon v-else name=\"riStopLine\" class=\"relative z-10\" />\n      </button>\n      <div class=\"ml-4 flex-1 overflow-hidden\">\n        <p class=\"text-sm\">{{ t('recording.title') }}</p>\n        <p class=\"text-overflow text-xl font-semibold leading-tight\">\n          {{ state.name }}\n        </p>\n      </div>\n    </div>\n    <p class=\"mt-6 mb-2 font-semibold\">Flows</p>\n    <ui-list class=\"space-y-1\">\n      <ui-list-item\n        v-for=\"(item, index) in state.flows\"\n        :key=\"index\"\n        class=\"group\"\n        small\n      >\n        <v-remixicon :name=\"tasks[item.id].icon\" />\n        <div class=\"mx-2 flex-1 overflow-hidden\">\n          <p class=\"leading-tight\">\n            {{ t(`workflow.blocks.${item.id}.name`) }}\n          </p>\n          <p\n            :title=\"item.data.description || item.description\"\n            class=\"text-overflow text-sm leading-tight text-gray-600 dark:text-gray-300\"\n          >\n            {{ item.data.description || item.description }}\n          </p>\n        </div>\n        <v-remixicon\n          name=\"riDeleteBin7Line\"\n          class=\"invisible cursor-pointer group-hover:visible\"\n          @click=\"removeBlock(index)\"\n        />\n      </ui-list-item>\n    </ui-list>\n  </div>\n</template>\n<script setup>\nimport { onMounted, reactive, toRaw, onBeforeUnmount } from 'vue';\nimport { useI18n } from 'vue-i18n';\nimport { useRouter } from 'vue-router';\nimport { nanoid } from 'nanoid';\nimport defu from 'defu';\nimport browser from 'webextension-polyfill';\nimport { tasks } from '@/utils/shared';\nimport { useWorkflowStore } from '@/stores/workflow';\nimport RecordWorkflowUtils from '@/newtab/utils/RecordWorkflowUtils';\n\nconst browserEvents = {\n  onTabCreated: (event) => RecordWorkflowUtils.onTabCreated(event),\n  onTabsActivated: (event) => RecordWorkflowUtils.onTabsActivated(event),\n  onCommitted: (event) => RecordWorkflowUtils.onWebNavigationCommited(event),\n  onWebNavigationCompleted: (event) =>\n    RecordWorkflowUtils.onWebNavigationCompleted(event),\n};\n\nconst { t } = useI18n();\nconst router = useRouter();\nconst workflowStore = useWorkflowStore();\n\nconst state = reactive({\n  name: '',\n  flows: [],\n  activeTab: {},\n  isGenerating: false,\n});\n\nfunction generateDrawflow(startBlock, startBlockData) {\n  let nextNodeId = nanoid();\n  const triggerId = startBlock?.id || nanoid();\n  let prevNodeId = startBlock?.id || triggerId;\n\n  const nodes = [];\n  const edges = [];\n\n  const addEdge = (data = {}) => {\n    edges.push({\n      ...data,\n      id: nanoid(),\n      class: `source-${data.sourceHandle} targte-${data.targetHandle}`,\n    });\n  };\n  addEdge({\n    source: prevNodeId,\n    target: nextNodeId,\n    targetHandle: `${nextNodeId}-input-1`,\n    sourceHandle: startBlock?.output || `${prevNodeId}-output-1`,\n  });\n\n  if (!startBlock) {\n    nodes.push({\n      position: {\n        x: 50,\n        y: 300,\n      },\n      id: triggerId,\n      label: 'trigger',\n      type: 'BlockBasic',\n      data: tasks.trigger.data,\n    });\n  }\n\n  const position = {\n    y: startBlockData ? startBlockData.position.y + 120 : 300,\n    x: startBlockData ? startBlockData.position.x + 280 : 320,\n  };\n  const groups = {};\n\n  state.flows.forEach((block, index) => {\n    if (block.groupId) {\n      if (!groups[block.groupId]) groups[block.groupId] = [];\n\n      groups[block.groupId].push({\n        id: block.id,\n        itemId: nanoid(),\n        data: defu(block.data, tasks[block.id].data),\n      });\n\n      const nextNodeInGroup = state.flows[index + 1]?.groupId;\n      if (nextNodeInGroup) return;\n\n      block.id = 'blocks-group';\n      block.data = { blocks: groups[block.groupId] };\n\n      delete groups[block.groupId];\n    }\n\n    const node = {\n      id: nextNodeId,\n      label: block.id,\n      type: tasks[block.id].component,\n      data: defu(block.data, tasks[block.id].data),\n      position: JSON.parse(JSON.stringify(position)),\n    };\n\n    prevNodeId = nextNodeId;\n    nextNodeId = nanoid();\n\n    if (index !== state.flows.length - 1) {\n      addEdge({\n        target: nextNodeId,\n        source: prevNodeId,\n        targetHandle: `${nextNodeId}-input-1`,\n        sourceHandle: `${prevNodeId}-output-1`,\n      });\n    }\n\n    const inNewRow = (index + 1) % 5 === 0;\n\n    position.x = inNewRow ? 50 : position.x + 280;\n    position.y = inNewRow ? position.y + 150 : position.y;\n\n    nodes.push(node);\n  });\n\n  return {\n    edges,\n    nodes,\n  };\n}\nasync function stopRecording() {\n  if (state.isGenerating) return;\n\n  try {\n    state.isGenerating = true;\n\n    if (state.flows.length !== 0) {\n      if (state.workflowId) {\n        const workflow = workflowStore.getById(state.workflowId);\n        const startBlock = workflow.drawflow.nodes.find(\n          (node) => node.id === state.connectFrom.id\n        );\n        const updatedDrawflow = generateDrawflow(state.connectFrom, startBlock);\n\n        const drawflow = {\n          ...workflow.drawflow,\n          nodes: [...workflow.drawflow.nodes, ...updatedDrawflow.nodes],\n          edges: [...workflow.drawflow.edges, ...updatedDrawflow.edges],\n        };\n\n        await workflowStore.update({\n          id: state.workflowId,\n          data: { drawflow },\n        });\n      } else {\n        const drawflow = generateDrawflow();\n\n        await workflowStore.insert({\n          drawflow,\n          name: state.name,\n          description: state.description ?? '',\n        });\n      }\n    }\n\n    await browser.storage.local.remove(['isRecording', 'recording']);\n    await (browser.action || browser.browserAction).setBadgeText({ text: '' });\n\n    const tabs = (await browser.tabs.query({})).filter((tab) =>\n      tab.url.startsWith('http')\n    );\n    Promise.allSettled(\n      tabs.map(({ id }) =>\n        browser.tabs.sendMessage(id, { type: 'recording:stop' })\n      )\n    );\n\n    state.isGenerating = false;\n\n    if (state.workflowId) {\n      router.replace(\n        `/workflows/${state.workflowId}?blockId=${state.connectFrom.id}`\n      );\n    } else {\n      router.replace('/');\n    }\n  } catch (error) {\n    state.isGenerating = false;\n    console.error(error);\n  }\n}\nfunction removeBlock(index) {\n  state.flows.splice(index, 1);\n\n  browser.storage.local.set({ recording: toRaw(state) });\n}\nfunction onStorageChanged({ recording }) {\n  if (!recording) return;\n\n  Object.assign(state, recording.newValue);\n}\n\nonMounted(async () => {\n  const { recording, isRecording } = await browser.storage.local.get([\n    'recording',\n    'isRecording',\n  ]);\n\n  if (!isRecording && !recording) return;\n\n  window.stopRecording = stopRecording;\n\n  browser.storage.onChanged.addListener(onStorageChanged);\n  browser.tabs.onCreated.addListener(browserEvents.onTabCreated);\n  browser.tabs.onActivated.addListener(browserEvents.onTabsActivated);\n  browser.webNavigation.onCommitted.addListener(browserEvents.onCommitted);\n  browser.webNavigation.onCompleted.addListener(\n    browserEvents.onWebNavigationCompleted\n  );\n\n  Object.assign(state, recording);\n});\nonBeforeUnmount(() => {\n  window.stopRecording = null;\n  browser.storage.local.onChanged.removeListener(onStorageChanged);\n  browser.storage.onChanged.removeListener(onStorageChanged);\n  browser.tabs.onCreated.removeListener(browserEvents.onTabCreated);\n  browser.tabs.onActivated.removeListener(browserEvents.onTabsActivated);\n  browser.webNavigation.onCommitted.removeListener(browserEvents.onCommitted);\n  browser.webNavigation.onCompleted.removeListener(\n    browserEvents.onWebNavigationCompleted\n  );\n});\n</script>\n"
  },
  {
    "path": "src/newtab/pages/ScheduledWorkflow.vue",
    "content": "<template>\n  <div class=\"container pt-8 pb-4\">\n    <h1 class=\"mb-12 text-2xl font-semibold capitalize\">\n      {{ t('scheduledWorkflow.title', 2) }}\n    </h1>\n    <div class=\"flex items-center\">\n      <ui-input\n        v-model=\"state.query\"\n        prepend-icon=\"riSearch2Line\"\n        :placeholder=\"t('common.search')\"\n      />\n      <div class=\"grow\" />\n      <ui-button\n        class=\"ml-4\"\n        style=\"min-width: 210px\"\n        @click=\"scheduleState.showModal = true\"\n      >\n        <v-remixicon name=\"riAddLine\" class=\"-ml-1 mr-2\" />\n        Schedule workflow\n      </ui-button>\n    </div>\n    <div class=\"scroll w-full overflow-x-auto\">\n      <ui-table\n        :headers=\"tableHeaders\"\n        :items=\"triggers\"\n        item-key=\"id\"\n        class=\"mt-8 w-full\"\n      >\n        <template #item-name=\"{ item }\">\n          <router-link\n            v-if=\"item.path\"\n            :to=\"item.path\"\n            class=\"block h-full w-full\"\n            style=\"min-height: 20px\"\n          >\n            {{ item.name }}\n          </router-link>\n          <span v-else>\n            {{ item.name }}\n          </span>\n        </template>\n        <template #item-schedule=\"{ item }\">\n          <p v-tooltip=\"{ content: item.scheduleDetail, allowHTML: true }\">\n            {{ item.schedule }}\n          </p>\n        </template>\n        <template #item-active=\"{ item }\">\n          <v-remixicon\n            v-if=\"item.active\"\n            class=\"inline-block text-green-500 dark:text-green-400\"\n            name=\"riCheckLine\"\n          />\n          <span v-else></span>\n        </template>\n        <template #item-action=\"{ item }\">\n          <button\n            v-tooltip=\"t('scheduledWorkflow.refresh')\"\n            class=\"rounded-md text-gray-600 dark:text-gray-300\"\n            @click=\"refreshSchedule(item.id)\"\n          >\n            <v-remixicon name=\"riRefreshLine\" />\n          </button>\n        </template>\n      </ui-table>\n    </div>\n    <ui-modal\n      v-model=\"scheduleState.showModal\"\n      title=\"Workflow Triggers\"\n      persist\n      content-class=\"max-w-2xl\"\n    >\n      <template #header-append>\n        <div>\n          <ui-button @click=\"clearAddWorkflowSchedule\">\n            {{ t('common.cancel') }}\n          </ui-button>\n          <ui-button\n            class=\"ml-4\"\n            variant=\"accent\"\n            @click=\"updateWorkflowTrigger\"\n          >\n            {{ t('common.save') }}\n          </ui-button>\n        </div>\n      </template>\n      <ui-autocomplete\n        v-if=\"!scheduleState.selectedWorkflow.id\"\n        :model-value=\"scheduleState.selectedWorkflow.query\"\n        :items=\"workflowStore.getWorkflows\"\n        block\n        class=\"mt-2\"\n        item-key=\"id\"\n        item-label=\"name\"\n        @selected=\"onSelectedWorkflow\"\n      >\n        <ui-input\n          v-model=\"scheduleState.selectedWorkflow.query\"\n          class=\"w-full\"\n          autocomplete=\"off\"\n          placeholder=\"Search workflow\"\n        />\n      </ui-autocomplete>\n      <template v-else>\n        <p class=\"font-semibold\">\n          {{ scheduleState.selectedWorkflow.name }}\n        </p>\n        <shared-workflow-triggers\n          :key=\"scheduleState.selectedWorkflow.id\"\n          v-model:triggers=\"scheduleState.selectedWorkflow.triggers\"\n          :exclude=\"[\n            'context-menu',\n            'on-startup',\n            'visit-web',\n            'keyboard-shortcut',\n          ]\"\n          class=\"mt-4\"\n        />\n      </template>\n    </ui-modal>\n  </div>\n</template>\n<script setup>\nimport { onMounted, reactive, computed } from 'vue';\nimport { useI18n } from 'vue-i18n';\nimport { nanoid } from 'nanoid';\nimport dayjs from 'dayjs';\nimport cloneDeep from 'lodash.clonedeep';\nimport browser from 'webextension-polyfill';\nimport { useUserStore } from '@/stores/user';\nimport { useWorkflowStore } from '@/stores/workflow';\nimport { useTeamWorkflowStore } from '@/stores/teamWorkflow';\nimport { useHostedWorkflowStore } from '@/stores/hostedWorkflow';\nimport { readableCron } from '@/lib/cronstrue';\nimport { findTriggerBlock, objectHasKey } from '@/utils/helper';\nimport {\n  registerWorkflowTrigger,\n  workflowTriggersMap,\n} from '@/utils/workflowTrigger';\nimport SharedWorkflowTriggers from '@/components/newtab/shared/SharedWorkflowTriggers.vue';\n\nconst { t } = useI18n();\nconst userStore = useUserStore();\nconst workflowStore = useWorkflowStore();\nconst teamWorkflowStore = useTeamWorkflowStore();\nconst hostedWorkflowStore = useHostedWorkflowStore();\n\nconst triggersData = {};\nconst state = reactive({\n  query: '',\n  triggers: [],\n  activeTrigger: 'scheduled',\n});\nconst scheduleState = reactive({\n  query: '',\n  showModal: false,\n  selectedWorkflow: {\n    id: '',\n    name: '',\n    triggers: [],\n  },\n});\n\nlet rowId = 0;\nconst scheduledTypes = ['interval', 'date', 'specific-day', 'cron-job'];\nconst tableHeaders = [\n  {\n    value: 'name',\n    filterable: true,\n    text: t('common.name'),\n    attrs: {\n      class: 'w-3/12',\n      style: 'min-width: 200px',\n    },\n  },\n  {\n    value: 'schedule',\n    text: t('scheduledWorkflow.schedule.title'),\n    attrs: {\n      class: 'w-4/12',\n      style: 'min-width: 200px',\n    },\n  },\n  {\n    value: 'nextRun',\n    text: t('scheduledWorkflow.nextRun'),\n  },\n  {\n    value: 'location',\n    text: 'Location',\n  },\n  {\n    value: 'active',\n    align: 'center',\n    text: t('scheduledWorkflow.active'),\n  },\n  {\n    value: 'action',\n    text: '',\n    sortable: false,\n    align: 'right',\n  },\n];\n\nconst triggers = computed(() =>\n  state.triggers.filter(({ name }) =>\n    name.toLocaleLowerCase().includes(state.query.toLocaleLowerCase())\n  )\n);\n\nfunction scheduleText(data) {\n  const text = {\n    schedule: '',\n    scheduleDetail: '',\n  };\n\n  switch (data.type) {\n    case 'specific-day': {\n      let rows = '';\n\n      const days = data.days.map((item) => {\n        const day = t(`workflow.blocks.trigger.days.${item.id}`);\n        rows += `<tr><td>${day}</td> <td>${item.times.join(', ')}</td></tr>`;\n\n        return day;\n      });\n\n      text.scheduleDetail = `<table><tbody>${rows}</tbody></table>`;\n      text.schedule =\n        data.days.length >= 6\n          ? t('scheduledWorkflow.schedule.types.everyDay')\n          : t('scheduledWorkflow.schedule.types.general', {\n              time: days.join(', '),\n            });\n      break;\n    }\n    case 'interval':\n      text.schedule = t('scheduledWorkflow.schedule.types.interval', {\n        time: data.interval,\n      });\n      break;\n    case 'date':\n      text.schedule = dayjs(`${data.date}, ${data.time}`).format(\n        'DD MMM YYYY, hh:mm:ss A'\n      );\n      break;\n    case 'cron-job':\n      text.schedule = readableCron(data.expression);\n      break;\n    default:\n  }\n\n  return text;\n}\nasync function getTriggersData(triggerData, { id, name }) {\n  try {\n    const alarms = await browser.alarms.getAll();\n    const getTrigger = async (trigger) => {\n      try {\n        if (!trigger || !scheduledTypes.includes(trigger.type)) return null;\n\n        rowId += 1;\n        const triggerObj = {\n          name,\n          id: rowId,\n          nextRun: '-',\n          schedule: '',\n          active: false,\n          type: trigger.type,\n          workflowId: id,\n          triggerId: trigger.id || null,\n        };\n\n        const alarm = alarms.find((alarmItem) => {\n          if (trigger.id) return alarmItem.name.includes(trigger.id);\n\n          return alarmItem.name.includes(id);\n        });\n        if (alarm) {\n          triggerObj.active = true;\n          triggerObj.nextRun = dayjs(alarm.scheduledTime).format(\n            'DD MMM YYYY, hh:mm:ss A'\n          );\n        }\n\n        triggersData[rowId] = {\n          ...trigger,\n          workflow: { id, name },\n        };\n        Object.assign(triggerObj, scheduleText(trigger));\n\n        return triggerObj;\n      } catch (error) {\n        return null;\n      }\n    };\n\n    if (triggerData?.triggers) {\n      const result = await Promise.all(\n        triggerData.triggers.map((trigger) => {\n          const triggerItemData = { ...trigger };\n          Object.assign(triggerItemData, triggerItemData.data);\n\n          delete triggerItemData.data;\n\n          return getTrigger(triggerItemData);\n        })\n      );\n\n      return result.reduce((acc, item) => {\n        if (item) {\n          acc.push(item);\n        }\n\n        return acc;\n      }, []);\n    }\n    const result = await getTrigger(triggerData);\n    if (!result) return [];\n\n    return [result];\n  } catch (error) {\n    console.error(error);\n    return [];\n  }\n}\nasync function refreshSchedule(id) {\n  try {\n    const triggerData = triggersData[id] ? cloneDeep(triggersData[id]) : null;\n    if (!triggerData) return;\n\n    const handler = workflowTriggersMap[triggerData.type];\n    if (!handler) return;\n\n    if (triggerData.id) {\n      triggerData.workflow.id = `trigger:${triggerData.workflow.id}:${triggerData.id}`;\n    }\n\n    await registerWorkflowTrigger(triggerData.workflow.id, {\n      data: triggerData,\n    });\n\n    const [triggerObj] = await getTriggersData(\n      triggerData,\n      triggerData.workflow\n    );\n    if (!triggerObj) return;\n\n    const triggerIndex = state.triggers.findIndex(\n      (trigger) => trigger.id === id\n    );\n    if (triggerIndex === -1) return;\n\n    Object.assign(state.triggers[triggerIndex], triggerObj);\n  } catch (error) {\n    console.error(error);\n  }\n}\nasync function getWorkflowTrigger(workflow, { location, path }) {\n  if (workflow.isDisabled) return;\n\n  let { trigger } = workflow;\n\n  if (!trigger) {\n    const drawflow =\n      typeof workflow.drawflow === 'string'\n        ? JSON.parse(workflow.drawflow)\n        : workflow.drawflow;\n    trigger = findTriggerBlock(drawflow)?.data;\n  }\n\n  const triggersList = await getTriggersData(trigger, workflow);\n  if (triggersList.length !== 0) {\n    triggersList.forEach((triggerData) => {\n      triggerData.path = path;\n      triggerData.location = location;\n      state.triggers.push(triggerData);\n    });\n  }\n}\nfunction iterateWorkflows({ workflows, path, location }) {\n  const promises = workflows.map(async (workflow) => {\n    const workflowPath = path(workflow);\n\n    await getWorkflowTrigger(workflow, { path: workflowPath, location });\n  });\n\n  return Promise.allSettled(promises);\n}\nfunction onSelectedWorkflow({ item }) {\n  if (!item.drawflow?.nodes) return;\n\n  const triggerBlock = findTriggerBlock(item.drawflow);\n  if (!triggerBlock) return;\n\n  let { triggersList } = triggerBlock.data;\n  if (!triggersList) {\n    triggersList = [\n      {\n        data: { ...triggerBlock.data },\n        type: triggerBlock.data.type,\n        id: nanoid(5),\n      },\n    ];\n  }\n\n  scheduleState.selectedWorkflow.id = item.id;\n  scheduleState.selectedWorkflow.name = item.name;\n  scheduleState.selectedWorkflow.triggers = [...triggersList];\n}\nfunction clearAddWorkflowSchedule() {\n  Object.assign(scheduleState, {\n    query: '',\n    showModal: false,\n    selectedWorkflow: {\n      id: '',\n      name: '',\n      triggers: [],\n    },\n  });\n}\nasync function updateWorkflowTrigger() {\n  try {\n    const {\n      triggers: workflowTriggers,\n      id,\n      name,\n    } = scheduleState.selectedWorkflow;\n    const workflowData = workflowStore.getById(id);\n    if (!workflowData || !workflowData?.drawflow?.nodes) return;\n\n    const triggerBlockIndex = workflowData.drawflow.nodes.findIndex(\n      (node) => node.label === 'trigger'\n    );\n    if (triggerBlockIndex === -1) return;\n\n    const copyNodes = [...workflowData.drawflow.nodes];\n    copyNodes[triggerBlockIndex].data.triggers = cloneDeep(workflowTriggers);\n    await workflowStore.update({\n      id,\n      data: {\n        trigger: { triggers: workflowTriggers },\n        drawflow: {\n          ...workflowData.drawflow,\n          nodes: copyNodes,\n        },\n      },\n    });\n\n    state.triggers = state.triggers.filter((trigger) => {\n      const isNotMatch =\n        scheduleState.selectedWorkflow.id !== trigger.workflowId;\n      if (!isNotMatch) {\n        delete triggersData[trigger.id];\n      }\n\n      return isNotMatch;\n    });\n\n    await registerWorkflowTrigger(id, {\n      data: { triggers: workflowTriggers },\n    });\n\n    const triggersList = await getTriggersData(\n      { triggers: workflowTriggers },\n      { id, name }\n    );\n    if (triggersList.length !== 0) {\n      triggersList.forEach((triggerData) => {\n        triggerData.location = 'Local';\n        triggerData.path = `/workflows/${id}`;\n        state.triggers.push(triggerData);\n      });\n    }\n\n    clearAddWorkflowSchedule();\n  } catch (error) {\n    console.error(error);\n  }\n}\n\nonMounted(async () => {\n  try {\n    await iterateWorkflows({\n      location: 'Local',\n      path: ({ id }) => `/workflows/${id}`,\n      workflows: workflowStore.getWorkflows,\n    });\n    await iterateWorkflows({\n      location: 'Hosted',\n      workflows: hostedWorkflowStore.toArray,\n      path: ({ id }) => `/workflows/${id}/hosted`,\n    });\n\n    const teamsObj = {};\n    if (userStore.user?.teams) {\n      userStore.user.teams.forEach((team) => {\n        teamsObj[team.id] = team.name;\n      });\n    }\n\n    Object.keys(teamWorkflowStore?.workflows || {}).forEach((teamId) => {\n      const teamExist = objectHasKey(teamsObj);\n      const teamName = teamsObj[teamId] ?? '(unknown)';\n\n      iterateWorkflows({\n        location: `Team: ${teamName.slice(0, 24)}`,\n        workflows: teamWorkflowStore.getByTeam(teamId),\n        path: ({ id }) =>\n          teamExist ? null : `/teams/${teamId}/workflows/${id}`,\n      });\n    });\n  } catch (error) {\n    console.error(error);\n  }\n});\n</script>\n"
  },
  {
    "path": "src/newtab/pages/Settings.vue",
    "content": "<template>\n  <div class=\"container pt-8 pb-4\">\n    <h1 class=\"mb-10 text-2xl font-semibold\">{{ t('common.settings') }}</h1>\n    <div class=\"flex items-start\">\n      <ui-list class=\"sticky top-8 mr-12 hidden w-64 space-y-2 md:block\">\n        <router-link\n          v-for=\"menu in menus\"\n          :key=\"menu.id\"\n          v-slot=\"{ href, navigate, isExactActive }\"\n          custom\n          :to=\"menu.path\"\n        >\n          <ui-list-item\n            :href=\"href\"\n            :class=\"[\n              isExactActive\n                ? 'bg-box-transparent'\n                : 'text-gray-600 dark:text-gray-200',\n            ]\"\n            tag=\"a\"\n            @click=\"navigate\"\n          >\n            <v-remixicon :name=\"menu.icon\" class=\"mr-2 -ml-1\" />\n            {{ t(`settings.menu.${menu.id}`) }}\n          </ui-list-item>\n        </router-link>\n      </ui-list>\n      <div class=\"settings-content flex-1\">\n        <ui-select\n          :model-value=\"$route.path\"\n          class=\"mb-4 w-full md:hidden\"\n          @change=\"onSelectChanged\"\n        >\n          <option v-for=\"menu in menus\" :key=\"menu.id\" :value=\"menu.path\">\n            {{ t(`settings.menu.${menu.id}`) }}\n          </option>\n        </ui-select>\n        <router-view />\n      </div>\n    </div>\n  </div>\n</template>\n<script setup>\nimport { useI18n } from 'vue-i18n';\nimport { useRouter } from 'vue-router';\n\nconst { t } = useI18n();\nconst router = useRouter();\n\nconst menus = [\n  { id: 'general', path: '/settings', icon: 'riSettings3Line' },\n  { id: 'profile', path: '/profile', icon: 'riUser3Line' },\n  { id: 'backup', path: '/backup', icon: 'riDatabase2Line' },\n  { id: 'editor', path: '/editor', icon: 'riMindMap' },\n  { id: 'shortcuts', path: '/shortcuts', icon: 'riKeyboardLine' },\n  { id: 'about', path: '/about', icon: 'riInformationLine' },\n];\n\nfunction onSelectChanged(value) {\n  router.push(value);\n}\n</script>\n"
  },
  {
    "path": "src/newtab/pages/Storage.vue",
    "content": "<template>\n  <div class=\"container py-8 pb-4\">\n    <div class=\"flex items-center\">\n      <h1 class=\"text-2xl font-semibold\">\n        {{ t('common.storage') }}\n      </h1>\n      <a\n        href=\"https://docs.extension.automa.site/reference/storage.html\"\n        title=\"Docs\"\n        class=\"ml-2 text-gray-600 dark:text-gray-200\"\n        target=\"_blank\"\n      >\n        <v-remixicon name=\"riInformationLine\" size=\"20\" />\n      </a>\n    </div>\n    <ui-tabs v-model=\"state.activeTab\" class=\"mt-5\" @change=\"onTabChange\">\n      <ui-tab value=\"tables\">\n        {{ t('workflow.table.title', 2) }}\n      </ui-tab>\n      <ui-tab value=\"variables\">\n        {{ t('workflow.variables.title', 2) }}\n      </ui-tab>\n      <ui-tab value=\"credentials\">\n        {{ t('credential.title', 2) }}\n      </ui-tab>\n    </ui-tabs>\n    <ui-tab-panels v-model=\"state.activeTab\">\n      <ui-tab-panel value=\"tables\">\n        <storage-tables />\n      </ui-tab-panel>\n      <ui-tab-panel value=\"variables\">\n        <storage-variables />\n      </ui-tab-panel>\n      <ui-tab-panel value=\"credentials\">\n        <storage-credentials />\n      </ui-tab-panel>\n    </ui-tab-panels>\n  </div>\n</template>\n<script setup>\nimport StorageCredentials from '@/components/newtab/storage/StorageCredentials.vue';\nimport StorageTables from '@/components/newtab/storage/StorageTables.vue';\nimport StorageVariables from '@/components/newtab/storage/StorageVariables.vue';\nimport { reactive } from 'vue';\nimport { useI18n } from 'vue-i18n';\nimport { useRoute, useRouter } from 'vue-router';\n\nconst tabs = ['tables', 'variables', 'credentials'];\n\nconst { t } = useI18n();\nconst route = useRoute();\nconst router = useRouter();\n\nconst { tab } = route.query;\n\nconst state = reactive({\n  activeTab: tabs.includes(tab) ? tab : 'tables',\n});\n\nfunction onTabChange(value) {\n  router.replace({ query: { tab: value } });\n}\n</script>\n"
  },
  {
    "path": "src/newtab/pages/Welcome.vue",
    "content": "<template>\n  <div class=\"mx-auto max-w-xl py-16\">\n    <h1 class=\"mb-8 text-3xl font-semibold\">\n      {{ t('welcome.title') }}\n    </h1>\n    <p>\n      Get started by reading the documentation or browsing workflows in the\n      Automa Marketplace. <br />\n      To learn how to use Automa, watch the tutorials on our YouTube Channel.\n    </p>\n    <div class=\"mt-8 flex items-center space-x-2\">\n      <a\n        v-for=\"link in links\"\n        :key=\"link.name\"\n        :href=\"link.url\"\n        target=\"_blank\"\n        rel=\"noopener\"\n        class=\"hoverable w-40 flex-1 rounded-lg border-2 p-4 transition\"\n      >\n        <v-remixicon size=\"28\" :name=\"link.icon\" />\n        <p class=\"mt-2\">\n          {{ link.name }}\n        </p>\n      </a>\n    </div>\n    <div class=\"mt-8\">\n      <p>{{ t('home.communities') }}</p>\n      <div class=\"mt-2 flex items-center space-x-2\">\n        <a\n          v-for=\"link in communities\"\n          :key=\"link.name\"\n          :href=\"link.url\"\n          target=\"_blank\"\n          rel=\"noopener\"\n          class=\"hoverable w-40 rounded-lg border-2 p-4 transition\"\n        >\n          <v-remixicon size=\"28\" :name=\"link.icon\" />\n          <p class=\"mt-2\">\n            {{ link.name }}\n          </p>\n        </a>\n      </div>\n    </div>\n  </div>\n</template>\n<script setup>\nimport { useI18n } from 'vue-i18n';\n\nconst { t } = useI18n();\n\nconst communities = [\n  {\n    name: 'GitHub',\n    icon: 'riGithubFill',\n    url: 'https://github.com/AutomaApp/automa',\n  },\n  {\n    name: 'Twitter',\n    icon: 'riTwitterLine',\n    url: 'https://twitter.com/AutomaApp',\n  },\n  {\n    name: 'Discord',\n    icon: 'riDiscordLine',\n    url: 'https://discord.gg/C6khwwTE84',\n  },\n];\nconst links = [\n  {\n    name: t('common.docs'),\n    icon: 'riBook3Line',\n    url: 'https://docs.extension.automa.site',\n  },\n  {\n    name: t('welcome.marketplace'),\n    icon: 'riCompass3Line',\n    url: 'https://extension.automa.site/workflows',\n  },\n  {\n    name: 'YouTube',\n    icon: 'riYoutubeLine',\n    url: 'https://youtube.com/channel/UCL3qU64hW0fsIj2vOayOQUQ',\n  },\n  {\n    name: 'Blog',\n    icon: 'riArticleLine',\n    url: 'https://blog.automa.site',\n  },\n];\n</script>\n"
  },
  {
    "path": "src/newtab/pages/Workflows.vue",
    "content": "<template>\n  <div class=\"flex flex-col\">\n    <div class=\"flex h-10 items-center border-b\">\n      <draggable\n        v-model=\"state.tabs\"\n        item-key=\"id\"\n        class=\"scroll scroll-xs flex h-full items-center overflow-auto text-sm text-gray-600 dark:text-gray-300\"\n      >\n        <template #item=\"{ element: tab, index }\">\n          <button\n            :value=\"tab.id\"\n            :class=\"[\n              state.activeTab === tab.id\n                ? 'border-accent dark:border-accent'\n                : 'border-transparent dark:border-transparent',\n              {\n                'bg-box-transparent text-black dark:text-gray-100':\n                  state.activeTab === tab.id,\n              },\n            ]\"\n            class=\"hoverable flex h-full cursor-default items-center border-b-2 px-4 focus:ring-0\"\n            @click=\"state.activeTab = tab.id\"\n          >\n            <p\n              :title=\"tab.name\"\n              class=\"text-overflow mr-2 max-w-[170px] flex-1\"\n            >\n              {{ tab.name }}\n            </p>\n            <span\n              class=\"hoverable rounded-full p-0.5 text-gray-600 dark:text-gray-300\"\n              title=\"Close tab\"\n              @click.stop=\"closeTab(index, tab)\"\n            >\n              <v-remixicon name=\"riCloseLine\" size=\"20\" />\n            </span>\n          </button>\n        </template>\n      </draggable>\n      <button class=\"h-full px-2\" @click=\"addTab()\">\n        <v-remixicon name=\"riAddLine\" />\n      </button>\n    </div>\n    <div class=\"flex-1\">\n      <router-view v-slot=\"{ Component }\">\n        <keep-alive>\n          <component :is=\"Component\" :key=\"$route.fullPath\"></component>\n        </keep-alive>\n      </router-view>\n    </div>\n  </div>\n</template>\n<script setup>\nimport { parseJSON } from '@/utils/helper';\nimport { nanoid } from 'nanoid/non-secure';\nimport { onMounted, reactive, watch } from 'vue';\nimport { useRoute, useRouter } from 'vue-router';\nimport Draggable from 'vuedraggable';\n\nlet tabTitleTimeout = null;\n\nconst route = useRoute();\nconst router = useRouter();\n\nconst state = reactive({\n  tabs: [],\n  activeTab: '',\n  tabChanging: false,\n});\n\nfunction addTab(detail = {}) {\n  const workflowsTab = state.tabs.find(\n    (tab) => tab.path === '/' || tab.path === '/workflows'\n  );\n\n  if (workflowsTab) {\n    state.activeTab = workflowsTab.id;\n    return;\n  }\n\n  const tabId = nanoid();\n\n  state.tabs.push({\n    id: tabId,\n    path: '/',\n    name: 'Workflows',\n    ...detail,\n  });\n  state.activeTab = tabId;\n}\nfunction closeTab(index, tab) {\n  if (state.tabs.length === 1) {\n    state.tabs[0] = {\n      path: '/',\n      id: nanoid(),\n      name: 'Workflows',\n    };\n  } else {\n    state.tabs.splice(index, 1);\n  }\n\n  if (tab.id === state.activeTab) {\n    state.activeTab = state.tabs[0].id;\n  }\n}\nfunction getTabTitle() {\n  if (route.name === 'workflows') return 'Workflows';\n\n  return `${document.title}`.replace(' - Automa', '');\n}\n\nwatch(\n  () => state.activeTab,\n  (id) => {\n    const tab = state.tabs.find((item) => item.id === id);\n    if (!tab) return;\n\n    state.tabChanging = true;\n\n    localStorage.setItem('activeTab', state.activeTab);\n    router.replace(tab.path);\n\n    setTimeout(() => {\n      state.tabChanging = false;\n    }, 1000);\n  }\n);\nwatch(\n  () => route.path,\n  () => {\n    if (state.tabChanging) return;\n\n    const index = state.tabs.findIndex((tab) => tab.id === state.activeTab);\n    if (index === -1) return;\n\n    const duplicateTab = state.tabs.find(\n      (tab) => tab.path === route.path && tab.id !== state.activeTab\n    );\n    if (duplicateTab) {\n      state.activeTab = duplicateTab.id;\n      state.tabs.splice(index, 1);\n      return;\n    }\n\n    clearTimeout(tabTitleTimeout);\n\n    tabTitleTimeout = setTimeout(() => {\n      Object.assign(state.tabs[index], {\n        path: route.path,\n        name: getTabTitle(),\n      });\n    }, 1000);\n  }\n);\nwatch(\n  () => state.tabs,\n  () => {\n    localStorage.setItem('tabs', JSON.stringify(state.tabs));\n  },\n  { deep: true }\n);\n\nonMounted(() => {\n  const tabs = parseJSON(localStorage.getItem('tabs'), null);\n  if (tabs) {\n    state.tabs = tabs;\n\n    const activeTab = localStorage.getItem('activeTab');\n    state.activeTab = activeTab || tabs[0].id;\n  }\n\n  if (state.tabs.length !== 0) {\n    if (/\\/workflows\\/.+/.test(route.path)) {\n      const routeTab = state.tabs.find((tab) => tab.path === route.path);\n      if (routeTab) {\n        if (routeTab.id !== state.activeTab) {\n          state.activeTab = routeTab.id;\n        }\n      } else {\n        const index = state.tabs.findIndex((tab) => tab.id === state.activeTab);\n        if (index !== -1) {\n          Object.assign(state.tabs[index], {\n            path: route.path,\n            name: getTabTitle(),\n          });\n\n          setTimeout(() => {\n            Object.assign(state.tabs[index], {\n              name: getTabTitle(),\n            });\n          }, 1000);\n        }\n      }\n    }\n    return;\n  }\n\n  addTab({\n    path: route.path,\n    name: getTabTitle(),\n  });\n});\n</script>\n"
  },
  {
    "path": "src/newtab/pages/logs/[id].vue",
    "content": "<template>\n  <p>Hello :)</p>\n</template>\n<script setup>\nimport { onMounted } from 'vue';\nimport { useRoute, useRouter } from 'vue-router';\nimport emitter from '@/lib/mitt';\n\nconst route = useRoute();\nconst router = useRouter();\n\nonMounted(() => {\n  emitter.emit('ui:logs', {\n    show: true,\n    logId: route.params.id,\n  });\n\n  router.replace('/');\n});\n</script>\n"
  },
  {
    "path": "src/newtab/pages/settings/SettingsAbout.vue",
    "content": "<template>\n  <div class=\"max-w-lg\">\n    <div class=\"bg-box-transparent mb-2 inline-block rounded-full p-3\">\n      <img src=\"@/assets/svg/logo.svg\" class=\"w-14\" />\n    </div>\n    <p class=\"text-2xl font-semibold\">Automa</p>\n    <p class=\"mb-2 mt-1\">Version: {{ extensionVersion }}</p>\n    <p class=\"text-gray-600 dark:text-gray-200\">\n      Automa is a chrome extension for browser automation. From auto-fill forms,\n      doing a repetitive task, taking a screenshot, to scraping data of the\n      website, it's up to you what you want to do with this extension.\n    </p>\n    <div class=\"mt-4 space-x-2\">\n      <a\n        v-for=\"link in links\"\n        :key=\"link.name\"\n        v-tooltip.group=\"link.name\"\n        :href=\"link.url\"\n        target=\"_blank\"\n        class=\"hoverable inline-block rounded-lg p-2 transition\"\n      >\n        <v-remixicon :name=\"link.icon\" />\n      </a>\n    </div>\n    <div class=\"my-8 border-b dark:border-gray-700\"></div>\n    <h2 class=\"text-xl font-semibold\">Contributors</h2>\n    <p class=\"mt-1 text-gray-600 dark:text-gray-200\">\n      Thanks to everyone who has submitted issues, made suggestions, and\n      generally helped make this a better project.\n    </p>\n    <div class=\"mt-4 mb-12 grid grid-cols-7 gap-2\">\n      <a\n        v-for=\"contributor in store.contributors\"\n        :key=\"contributor.username\"\n        v-tooltip.group=\"contributor.username\"\n        :href=\"contributor.url\"\n        target=\"_blank\"\n        rel=\"noopener\"\n      >\n        <img\n          :src=\"contributor.avatar\"\n          :alt=\"`${contributor.username} avatar`\"\n          class=\"w-16 rounded-lg\"\n        />\n      </a>\n    </div>\n  </div>\n</template>\n<script setup>\n/* eslint-disable camelcase */\nimport { useGroupTooltip } from '@/composable/groupTooltip';\nimport { useStore } from '@/stores/main';\nimport { communities } from '@/utils/shared';\nimport { onMounted } from 'vue';\nimport browser from 'webextension-polyfill';\n\nuseGroupTooltip();\nconst store = useStore();\n\nconst extensionVersion = browser.runtime.getManifest().version;\nconst links = [\n  ...communities,\n  {\n    name: 'Website',\n    icon: 'riGlobalLine',\n    url: 'https://extension.automa.site',\n  },\n  {\n    name: 'Documentation',\n    icon: 'riBook3Line',\n    url: 'https://docs.extension.automa.site',\n  },\n  {\n    name: 'Blog',\n    icon: 'riArticleLine',\n    url: 'https://blog.automa.site',\n  },\n];\n\nonMounted(async () => {\n  if (store.contributors) return;\n\n  try {\n    const response = await fetch(\n      'https://api.github.com/repositories/412741449/contributors'\n    );\n    const contributors = (await response.json()).reduce(\n      (acc, { type, avatar_url, login, html_url }) => {\n        if (type !== 'Bot') {\n          acc.push({\n            username: login,\n            url: html_url,\n            avatar: avatar_url,\n          });\n        }\n\n        return acc;\n      },\n      []\n    );\n\n    store.contributors = contributors;\n  } catch (error) {\n    console.error(error);\n  }\n});\n</script>\n"
  },
  {
    "path": "src/newtab/pages/settings/SettingsBackup.vue",
    "content": "<template>\n  <div class=\"max-w-xl\">\n    <ui-card class=\"mb-12\">\n      <h2 class=\"mb-2 font-semibold\">\n        {{ t('settings.backupWorkflows.cloud.title') }}\n      </h2>\n      <template v-if=\"userStore.user\">\n        <div\n          class=\"flex items-center rounded-lg border p-4 dark:border-gray-700\"\n        >\n          <span class=\"bg-box-transparent inline-block rounded-full p-2\">\n            <v-remixicon name=\"riUploadLine\" />\n          </span>\n          <div class=\"ml-4 flex-1 leading-tight\">\n            <p class=\"text-sm text-gray-600 dark:text-gray-200\">\n              {{ t('settings.backupWorkflows.cloud.lastBackup') }}\n            </p>\n            <p>{{ formatDate(state.lastBackup) }}</p>\n          </div>\n          <ui-button\n            :loading=\"backupState.loading\"\n            @click=\"backupState.modal = true\"\n          >\n            {{ t('settings.backupWorkflows.backup.button') }}\n          </ui-button>\n        </div>\n        <div\n          class=\"mt-2 flex items-center rounded-lg border p-4 dark:border-gray-700\"\n        >\n          <span class=\"bg-box-transparent inline-block rounded-full p-2\">\n            <v-remixicon name=\"riDownloadLine\" />\n          </span>\n          <p class=\"ml-4 flex-1\">\n            {{ t('settings.backupWorkflows.cloud.sync') }}\n          </p>\n          <ui-button\n            :loading=\"state.loadingSync\"\n            class=\"ml-2\"\n            @click=\"syncBackupWorkflows\"\n          >\n            {{ t('settings.backupWorkflows.cloud.sync') }}\n          </ui-button>\n        </div>\n      </template>\n      <div v-else class=\"py-4 text-center\">\n        <p>\n          {{ t('settings.backupWorkflows.needSignin') }}\n        </p>\n        <ui-button\n          tag=\"a\"\n          href=\"https://extension.automa.site/auth\"\n          target=\"_blank\"\n          class=\"mt-4 inline-block w-44\"\n        >\n          {{ t('auth.signIn') }}\n        </ui-button>\n      </div>\n      <!-- <p v-if=\"false\">\n        Upgrade to the\n        <a\n          href=\"https://extension.automa.site/pricing\"\n          target=\"_blank\"\n          class=\"text-yellow-500 underline dark:text-yellow-300\"\n        >\n          pro plan\n        </a>\n        to start back up your workflows to the cloud\n      </p> -->\n    </ui-card>\n    <h2 class=\"mb-2 font-semibold\">\n      {{ t('settings.backupWorkflows.title') }}\n    </h2>\n    <div class=\"flex space-x-4\">\n      <div class=\"w-6/12 rounded-lg border p-4 dark:border-gray-700\">\n        <div class=\"text-center\">\n          <span class=\"bg-box-transparent inline-block rounded-full p-4\">\n            <v-remixicon name=\"riDownloadLine\" size=\"36\" />\n          </span>\n        </div>\n        <ui-checkbox v-model=\"state.encrypt\" class=\"mt-6 mb-4\">\n          {{ t('settings.backupWorkflows.backup.encrypt') }}\n        </ui-checkbox>\n        <div class=\"flex items-center gap-2\">\n          <ui-popover @close=\"registerScheduleBackup\">\n            <template #trigger>\n              <ui-button\n                v-tooltip=\"t('settings.backupWorkflows.backup.settings')\"\n                icon\n                :class=\"{ 'text-primary': localBackupSchedule.schedule }\"\n              >\n                <v-remixicon name=\"riSettings3Line\" />\n              </ui-button>\n            </template>\n            <div class=\"w-64\">\n              <p class=\"mb-2 font-semibold\">\n                {{ t('settings.backupWorkflows.backup.settings') }}\n              </p>\n              <p>Also backup</p>\n              <div class=\"flex mt-1 flex-col gap-2\">\n                <ui-checkbox\n                  v-for=\"item in BACKUP_ITEMS_INCLUDES\"\n                  :key=\"item.id\"\n                  :model-value=\"\n                    localBackupSchedule.includedItems.includes(item.id)\n                  \"\n                  @change=\"\n                    $event\n                      ? localBackupSchedule.includedItems.push(item.id)\n                      : localBackupSchedule.includedItems.splice(\n                          localBackupSchedule.includedItems.indexOf(item.id),\n                          1\n                        )\n                  \"\n                >\n                  {{ item.name }}\n                </ui-checkbox>\n              </div>\n              <p class=\"mt-4\">\n                {{ t('settings.backupWorkflows.backup.schedule') }}\n              </p>\n              <template v-if=\"!downloadPermission.has.downloads\">\n                <p class=\"text-gray-600 dark:text-gray-300 mt-1\">\n                  Automa requires the \"Downloads\" permission for the schedule\n                  backup to work\n                </p>\n                <ui-button\n                  class=\"mt-2 w-full\"\n                  @click=\"downloadPermission.request()\"\n                >\n                  Allow \"Downloads\" permission\n                </ui-button>\n              </template>\n              <template v-else>\n                <ui-select\n                  v-model=\"localBackupSchedule.schedule\"\n                  class=\"w-full mt-2\"\n                >\n                  <option value=\"\">Never</option>\n                  <option\n                    v-for=\"(value, key) in BACKUP_SCHEDULES\"\n                    :key=\"key\"\n                    :value=\"key\"\n                  >\n                    {{ value }}\n                  </option>\n                  <option value=\"custom\">Custom</option>\n                </ui-select>\n                <template v-if=\"localBackupSchedule.schedule === 'custom'\">\n                  <ui-input\n                    v-model=\"localBackupSchedule.customSchedule\"\n                    label=\"Cron Expression\"\n                    class=\"w-full mt-2\"\n                    placeholder=\"0 8 * * *\"\n                  />\n                  <p className=\"text-sm text-gray-600 dark:text-gray-300\">\n                    {{ getBackupScheduleCron() }}\n                  </p>\n                </template>\n                <ui-input\n                  v-if=\"localBackupSchedule.schedule !== ''\"\n                  v-model=\"localBackupSchedule.folderName\"\n                  label=\"Folder name\"\n                  class=\"w-full mt-2\"\n                  placeholder=\"backup-folder\"\n                />\n                <p\n                  v-if=\"localBackupSchedule.lastBackup\"\n                  class=\"text-gray-600 dark:text-gray-300 text-sm mt-4\"\n                >\n                  Last backup:\n                  {{ dayjs(localBackupSchedule.lastBackup).fromNow() }}\n                </p>\n              </template>\n            </div>\n          </ui-popover>\n          <ui-button class=\"flex-1\" @click=\"backupWorkflows\">\n            {{ t('settings.backupWorkflows.backup.button') }}\n          </ui-button>\n        </div>\n      </div>\n      <div class=\"w-6/12 rounded-lg border p-4 dark:border-gray-700\">\n        <div class=\"text-center\">\n          <span class=\"bg-box-transparent inline-block rounded-full p-4\">\n            <v-remixicon name=\"riUploadLine\" size=\"36\" />\n          </span>\n        </div>\n        <ui-checkbox v-model=\"state.updateIfExists\" class=\"mt-6 mb-4\">\n          {{ t('settings.backupWorkflows.restore.update') }}\n        </ui-checkbox>\n        <ui-button class=\"w-full\" @click=\"restoreWorkflows\">\n          {{ t('settings.backupWorkflows.restore.button') }}\n        </ui-button>\n      </div>\n    </div>\n  </div>\n  <ui-modal\n    v-model=\"backupState.modal\"\n    :title=\"t('settings.backupWorkflows.cloud.title')\"\n    content-class=\"max-w-5xl\"\n  >\n    <settings-cloud-backup\n      v-model:ids=\"backupState.ids\"\n      @close=\"backupState.modal = false\"\n    />\n  </ui-modal>\n</template>\n<script setup>\nimport SettingsCloudBackup from '@/components/newtab/settings/SettingsCloudBackup.vue';\nimport { useDialog } from '@/composable/dialog';\nimport { useHasPermissions } from '@/composable/hasPermissions';\nimport dbStorage from '@/db/storage';\nimport { readableCron } from '@/lib/cronstrue';\nimport { useUserStore } from '@/stores/user';\nimport { useWorkflowStore } from '@/stores/workflow';\nimport { getUserWorkflows } from '@/utils/api';\nimport { fileSaver, openFilePicker, parseJSON } from '@/utils/helper';\nimport cronParser from 'cron-parser';\nimport AES from 'crypto-js/aes';\nimport encUtf8 from 'crypto-js/enc-utf8';\nimport hmacSHA256 from 'crypto-js/hmac-sha256';\nimport dayjs from 'dayjs';\nimport { onMounted, reactive, toRaw } from 'vue';\nimport { useI18n } from 'vue-i18n';\nimport { useToast } from 'vue-toastification';\nimport browser from 'webextension-polyfill';\n\nconst BACKUP_SCHEDULES = {\n  '0 8 * * *': 'Every day',\n  '0 8 * * 0': 'Every week',\n};\nconst BACKUP_ITEMS_INCLUDES = [\n  { id: 'storage:table', name: 'Storage tables' },\n  { id: 'storage:variables', name: 'Storage variables' },\n];\n\nconst { t } = useI18n();\nconst toast = useToast();\nconst dialog = useDialog();\nconst userStore = useUserStore();\nconst workflowStore = useWorkflowStore();\nconst downloadPermission = useHasPermissions(['downloads']);\n\nconst state = reactive({\n  lastSync: null,\n  encrypt: false,\n  lastBackup: null,\n  loadingSync: false,\n  updateIfExists: false,\n});\nconst backupState = reactive({\n  ids: [],\n  modal: false,\n  loading: false,\n});\nconst localBackupSchedule = reactive({\n  schedule: '',\n  lastBackup: null,\n  includedItems: [],\n  customSchedule: '',\n  folderName: 'automa-backup',\n});\n\nasync function registerScheduleBackup() {\n  try {\n    if (!localBackupSchedule.schedule.trim()) {\n      await browser.alarms.clear('schedule-local-backup');\n    } else {\n      const expression =\n        localBackupSchedule.schedule === 'custom'\n          ? localBackupSchedule.customSchedule\n          : localBackupSchedule.schedule;\n      const parsedExpression = cronParser.parseExpression(expression).next();\n      if (!parsedExpression) return;\n\n      await browser.alarms.create('schedule-local-backup', {\n        when: parsedExpression.getTime(),\n      });\n    }\n\n    browser.storage.local.set({\n      localBackupSettings: toRaw(localBackupSchedule),\n    });\n  } catch (error) {\n    console.error(error);\n  }\n}\nfunction getBackupScheduleCron() {\n  try {\n    const expression = localBackupSchedule.customSchedule;\n\n    return `${readableCron(expression)}`;\n  } catch (error) {\n    return error.message;\n  }\n}\nfunction formatDate(date) {\n  if (!date) return 'null';\n\n  return dayjs(date).format('DD MMMM YYYY, hh:mm A');\n}\nasync function syncBackupWorkflows() {\n  try {\n    state.loadingSync = true;\n    const { backup, hosted } = await getUserWorkflows(false);\n    const backupIds = backup.map(({ id }) => id);\n\n    userStore.backupIds = backupIds;\n    userStore.hostedWorkflows = hosted;\n\n    await browser.storage.local.set({\n      backupIds,\n      lastBackup: new Date().toISOString(),\n    });\n\n    await workflowStore.insertOrUpdate(backup, { checkUpdateDate: true });\n\n    state.loadingSync = false;\n  } catch (error) {\n    console.error(error);\n    toast.error(t('message.somethingWrong'));\n    state.loadingSync = false;\n  }\n}\nasync function backupWorkflows() {\n  try {\n    const workflows = workflowStore.getWorkflows.reduce((acc, workflow) => {\n      if (workflow.isProtected) return acc;\n\n      delete workflow.$id;\n      delete workflow.createdAt;\n      delete workflow.data;\n      delete workflow.isDisabled;\n      delete workflow.isProtected;\n\n      acc.push(workflow);\n\n      return acc;\n    }, []);\n\n    const payload = {\n      isProtected: state.encrypt,\n      workflows: JSON.stringify(workflows),\n    };\n\n    if (localBackupSchedule.includedItems.includes('storage:table')) {\n      const tables = await dbStorage.tablesItems.toArray();\n      payload.storageTables = JSON.stringify(tables);\n    }\n    if (localBackupSchedule.includedItems.includes('storage:variables')) {\n      const variables = await dbStorage.variables.toArray();\n      payload.storageVariables = JSON.stringify(variables);\n    }\n\n    const downloadFile = (data) => {\n      const fileName = `automa-${dayjs().format('DD-MM-YYYY')}.json`;\n      const blob = new Blob([JSON.stringify(data)], {\n        type: 'application/json',\n      });\n      const objectUrl = URL.createObjectURL(blob);\n\n      fileSaver(fileName, objectUrl);\n\n      URL.revokeObjectURL(objectUrl);\n    };\n\n    if (state.encrypt) {\n      dialog.prompt({\n        placeholder: t('common.password'),\n        title: t('settings.backupWorkflows.title'),\n        okText: t('settings.backupWorkflows.backup.button'),\n        inputType: 'password',\n        onConfirm: (password) => {\n          const encryptedWorkflows = AES.encrypt(\n            payload.workflows,\n            password\n          ).toString();\n          const hmac = hmacSHA256(encryptedWorkflows, password).toString();\n\n          payload.workflows = hmac + encryptedWorkflows;\n\n          downloadFile(payload);\n        },\n      });\n    } else {\n      downloadFile(payload);\n    }\n  } catch (error) {\n    console.error(error);\n  }\n}\nasync function restoreWorkflows() {\n  try {\n    const [file] = await openFilePicker('application/json');\n    const reader = new FileReader();\n    const insertWorkflows = (workflows) => {\n      const newWorkflows = workflows.map((workflow) => {\n        if (!state.updateIfExists) {\n          workflow.createdAt = Date.now();\n          delete workflow.id;\n        }\n\n        return workflow;\n      });\n      const showMessage = (event) => {\n        toast(\n          t('settings.backupWorkflows.workflowsAdded', {\n            count: Object.values(event).length,\n          })\n        );\n      };\n\n      if (state.updateIfExists) {\n        return workflowStore\n          .insertOrUpdate(newWorkflows, { duplicateId: true })\n          .then(showMessage);\n      }\n\n      return workflowStore.insert(newWorkflows).then(showMessage);\n    };\n\n    reader.onload = ({ target }) => {\n      let payload = parseJSON(target.result, null);\n      if (!payload)\n        payload = parseJSON(window.decodeURIComponent(target.result), null);\n\n      if (!payload) return;\n\n      const storageTables = parseJSON(payload.storageTables, null);\n      if (Array.isArray(storageTables)) {\n        dbStorage.tablesItems.bulkPut(storageTables);\n      }\n\n      const storageVariables = parseJSON(payload.storageVariables, null);\n      if (Array.isArray(storageVariables)) {\n        dbStorage.variables.bulkPut(storageVariables);\n      }\n\n      if (payload.isProtected) {\n        dialog.prompt({\n          placeholder: t('common.password'),\n          title: t('settings.backupWorkflows.restore.title'),\n          okText: t('settings.backupWorkflows.restore.button'),\n          inputType: 'password',\n          onConfirm: (password) => {\n            const hmac = payload.workflows.substring(0, 64);\n            const encryptedWorkflows = payload.workflows.substring(64);\n            const decryptedHmac = hmacSHA256(\n              encryptedWorkflows,\n              password\n            ).toString();\n\n            if (hmac !== decryptedHmac) {\n              toast.error(t('settings.backupWorkflows.invalidPassword'));\n\n              return;\n            }\n\n            const decryptedWorkflows = AES.decrypt(\n              encryptedWorkflows,\n              password\n            ).toString(encUtf8);\n            payload.workflows = parseJSON(decryptedWorkflows, []);\n\n            insertWorkflows(payload.workflows);\n          },\n        });\n      } else {\n        payload.workflows = parseJSON(payload.workflows, []);\n        insertWorkflows(payload.workflows);\n      }\n    };\n\n    reader.readAsText(file);\n  } catch (error) {\n    console.error(error);\n    toast.error(error.message);\n  }\n}\n\nonMounted(async () => {\n  const { lastBackup, lastSync, localBackupSettings } =\n    await browser.storage.local.get([\n      'lastSync',\n      'lastBackup',\n      'localBackupSettings',\n    ]);\n\n  Object.assign(localBackupSchedule, localBackupSettings || {});\n\n  state.lastSync = lastSync;\n  state.lastBackup = lastBackup;\n});\n</script>\n"
  },
  {
    "path": "src/newtab/pages/settings/SettingsEditor.vue",
    "content": "<template>\n  <div class=\"max-w-2xl\">\n    <p class=\"font-semibold\">Zoom</p>\n    <div class=\"mt-1 flex items-center space-x-4\">\n      <ui-input\n        v-model.number=\"settings.minZoom\"\n        type=\"number\"\n        label=\"Min zoom\"\n      />\n      <ui-input\n        v-model.number=\"settings.maxZoom\"\n        type=\"number\"\n        label=\"Max zoom\"\n      />\n    </div>\n    <ui-list class=\"mt-8 space-y-2\">\n      <ui-list-item small>\n        <ui-switch v-model=\"settings.arrow\" />\n        <div class=\"ml-4 flex-1\">\n          <p class=\"leading-tight\">\n            {{ t('settings.editor.arrow.title') }}\n          </p>\n          <p class=\"text-sm leading-tight text-gray-600 dark:text-gray-200\">\n            {{ t('settings.editor.arrow.description') }}\n          </p>\n        </div>\n      </ui-list-item>\n      <ui-list-item small>\n        <ui-switch v-model=\"settings.snapToGrid\" />\n        <div class=\"ml-4 flex-1\">\n          <p class=\"leading-tight\">\n            {{ t('settings.editor.snapGrid.title') }}\n          </p>\n          <p class=\"text-sm leading-tight text-gray-600 dark:text-gray-200\">\n            {{ t('settings.editor.snapGrid.description') }}\n          </p>\n        </div>\n      </ui-list-item>\n      <transition-expand>\n        <div\n          v-if=\"settings.snapToGrid\"\n          class=\"ml-2 space-x-2 pl-16\"\n          style=\"margin-top: 0\"\n        >\n          <ui-input\n            v-model.number=\"settings.snapGrid[0]\"\n            type=\"number\"\n            label=\"X Axis\"\n          />\n          <ui-input\n            v-model.number=\"settings.snapGrid[1]\"\n            type=\"number\"\n            label=\"Y Axis\"\n          />\n        </div>\n      </transition-expand>\n      <ui-list-item small>\n        <ui-switch v-model=\"settings.saveWhenExecute\" />\n        <div class=\"ml-4 flex-1\">\n          <p class=\"leading-tight\">\n            {{ t('settings.editor.saveWhenExecute.title') }}\n          </p>\n          <p class=\"text-sm leading-tight text-gray-600 dark:text-gray-200\">\n            {{ t('settings.editor.saveWhenExecute.description') }}\n          </p>\n        </div>\n      </ui-list-item>\n    </ui-list>\n  </div>\n</template>\n<script setup>\nimport { reactive, onMounted, watch } from 'vue';\nimport { useI18n } from 'vue-i18n';\nimport cloneDeep from 'lodash.clonedeep';\nimport { useStore } from '@/stores/main';\n\nconst { t } = useI18n();\nconst store = useStore();\n\nconst settings = reactive({});\n\nwatch(settings, () => {\n  store.updateSettings({ editor: settings });\n});\n\nonMounted(() => {\n  Object.assign(settings, cloneDeep(store.settings.editor));\n});\n</script>\n"
  },
  {
    "path": "src/newtab/pages/settings/SettingsIndex.vue",
    "content": "<template>\n  <div class=\"mb-12\">\n    <p class=\"mb-1 font-semibold\">{{ t('settings.theme') }}</p>\n    <div class=\"flex items-center space-x-4\">\n      <div\n        v-for=\"item in theme.themes\"\n        :key=\"item.id\"\n        class=\"cursor-pointer\"\n        role=\"button\"\n        @click=\"theme.set(item.id)\"\n      >\n        <div\n          :class=\"{ 'ring ring-accent': item.id === theme.activeTheme.value }\"\n          class=\"rounded-lg p-0.5\"\n        >\n          <img\n            :src=\"require(`@/assets/images/theme-${item.id}.png`)\"\n            width=\"140\"\n            class=\"rounded-lg\"\n          />\n        </div>\n        <span class=\"ml-1 text-sm text-gray-600 dark:text-gray-200\">\n          {{ item.name }}\n        </span>\n      </div>\n    </div>\n  </div>\n  <div class=\"flex items-center\">\n    <div id=\"languages\">\n      <p class=\"mb-1 font-semibold\">{{ t('settings.language.label') }}</p>\n      <ui-select\n        :model-value=\"settings.locale\"\n        class=\"w-80\"\n        @change=\"updateLanguage\"\n      >\n        <option\n          v-for=\"locale in supportLocales\"\n          :key=\"locale.id\"\n          :value=\"locale.id\"\n        >\n          {{ locale.name }}\n        </option>\n      </ui-select>\n      <a\n        class=\"ml-1 block text-gray-600 dark:text-gray-200\"\n        href=\"https://github.com/AutomaApp/automa/wiki/Help-Translate\"\n        target=\"_blank\"\n        rel=\"noopener\"\n      >\n        {{ t('settings.language.helpTranslate') }}\n      </a>\n    </div>\n    <p v-if=\"isLangChange\" class=\"ml-4 inline-block\">\n      {{ t('settings.language.reloadPage') }}\n    </p>\n  </div>\n  <div id=\"delete-logs\" class=\"mt-12\">\n    <p class=\"mb-1 font-semibold\">Workflow Logs</p>\n    <div class=\"flex items-center\">\n      <ui-select\n        :model-value=\"settings.deleteLogAfter\"\n        :label=\"t('settings.deleteLog.title')\"\n        placeholder=\"Delete after\"\n        class=\"w-80\"\n        @change=\"\n          updateSetting(\n            'deleteLogAfter',\n            $event === 'never' ? 'never' : +$event\n          )\n        \"\n      >\n        <option v-for=\"day in deleteLogDays\" :key=\"day\" :value=\"day\">\n          <template v-if=\"typeof day === 'string'\">\n            {{ t('settings.deleteLog.deleteAfter.never') }}\n          </template>\n          <template v-else>\n            {{ t('settings.deleteLog.deleteAfter.days', { day }) }}\n          </template>\n        </option>\n      </ui-select>\n      <ui-input\n        :model-value=\"settings.logsLimit\"\n        class=\"ml-4\"\n        type=\"number\"\n        label=\"Logs limit\"\n        min=\"10\"\n        @change=\"updateSetting('logsLimit', +$event <= 0 ? 1000 : +$event)\"\n      />\n    </div>\n  </div>\n</template>\n<script setup>\nimport { computed, ref } from 'vue';\nimport { useI18n } from 'vue-i18n';\nimport { useStore } from '@/stores/main';\nimport { useTheme } from '@/composable/theme';\nimport { supportLocales } from '@/utils/shared';\n\nconst deleteLogDays = ['never', 7, 14, 30, 60, 120];\n\nconst { t } = useI18n();\nconst store = useStore();\nconst theme = useTheme();\n\nconst isLangChange = ref(false);\nconst settings = computed(() => store.settings);\n\nfunction updateSetting(path, value) {\n  store.updateSettings({ [path]: value });\n}\nfunction updateLanguage(value) {\n  isLangChange.value = true;\n\n  updateSetting('locale', value);\n}\n</script>\n"
  },
  {
    "path": "src/newtab/pages/settings/SettingsProfile.vue",
    "content": "<template>\n  <div class=\"max-w-xl\">\n    <div v-if=\"!userStore.retrieved\" class=\"loading-state\">\n      <ui-spinner color=\"text-accent\" size=\"32\" />\n      <p>{{ t('settings.profile.loading') }}</p>\n    </div>\n\n    <ui-card v-else-if=\"!userStore.user\" class=\"not-signed-in\">\n      <div class=\"text-center\">\n        <h3 class=\"mb-2 text-xl font-semibold\">\n          {{ t('settings.profile.notSignedIn') }}\n        </h3>\n        <p class=\"mb-6 text-gray-600 dark:text-gray-300\">\n          {{ t('settings.profile.signInDesc') }}\n        </p>\n        <!-- <ui-button tag=\"button\" variant=\"accent\" class=\"w-64\">\n          {{ t('settings.profile.signIn') }}\n        </ui-button> -->\n      </div>\n    </ui-card>\n\n    <ui-card v-else class=\"profile-card\">\n      <div class=\"profile-header\">\n        <div class=\"avatar\">\n          <img\n            v-if=\"userAvatar\"\n            :src=\"userAvatar\"\n            :alt=\"`${displayName}'s avatar`\"\n            @error=\"avatarError = true\"\n          />\n          <div v-else class=\"default-avatar\">\n            {{ userInitials }}\n          </div>\n        </div>\n\n        <div class=\"user-info\">\n          <h3 class=\"username\">{{ displayName }}</h3>\n          <p v-if=\"userEmail\" class=\"email\">{{ userEmail }}</p>\n\n          <div v-if=\"userTeams.length > 0\" class=\"teams-badge\">\n            <v-remixicon name=\"riTeamLine\" size=\"16\" class=\"mr-1\" />\n            <span>{{\n              t('settings.profile.teamsCount', { count: userTeams.length })\n            }}</span>\n          </div>\n        </div>\n      </div>\n\n      <div class=\"warning-message\">\n        <v-remixicon\n          name=\"riAlertLine\"\n          size=\"20\"\n          class=\"mr-2 text-yellow-500\"\n        />\n        <p>{{ t('settings.profile.warningMessage') }}</p>\n      </div>\n\n      <div class=\"actions\">\n        <ui-button\n          variant=\"danger\"\n          class=\"w-full sign-out-button\"\n          :loading=\"state.loading\"\n          :disabled=\"state.loading\"\n          @click=\"handleSignOut\"\n        >\n          <v-remixicon\n            :name=\"state.loading ? 'riLoader4Line' : 'riLogoutCircleRLine'\"\n            class=\"mr-2\"\n            :class=\"{ 'animate-spin': state.loading }\"\n          />\n          {{\n            state.loading\n              ? t('settings.profile.signingOut')\n              : t('settings.profile.signOut')\n          }}\n        </ui-button>\n\n        <p class=\"text-sm text-gray-500 dark:text-gray-400 mt-2 text-center\">\n          {{ t('settings.profile.signOutNote') }}\n        </p>\n      </div>\n    </ui-card>\n  </div>\n</template>\n\n<script setup>\nimport { useDialog } from '@/composable/dialog';\nimport { useUserStore } from '@/stores/user';\nimport { computed, reactive } from 'vue';\nimport { useI18n } from 'vue-i18n';\nimport { useToast } from 'vue-toastification';\n\nconst { t } = useI18n();\nconst userStore = useUserStore();\nconst dialog = useDialog();\nconst toast = useToast();\n\nconst state = reactive({\n  loading: false,\n  avatarError: false,\n  isSigningOut: false,\n});\n\nconst displayName = computed(() => {\n  return userStore.user?.username || 'User';\n});\n\nconst userEmail = computed(() => {\n  return userStore.user?.email || '';\n});\n\nconst userAvatar = computed(() => {\n  if (state.avatarError) return null;\n\n  return userStore.user?.avatar_url || null;\n});\n\nconst userInitials = computed(() => {\n  if (!userStore.user) return 'U';\n\n  const username = userStore.user?.username || 'User';\n  return username.charAt(0).toUpperCase();\n});\n\nconst userTeams = computed(() => {\n  return userStore.user?.teams || [];\n});\n\nasync function handleSignOut() {\n  if (state.loading || state.isSigningOut) {\n    return;\n  }\n\n  const confirmed = await dialog.confirm({\n    title: t('settings.profile.signOutConfirmTitle'),\n    body: t('settings.profile.signOutConfirmMessage'),\n    okVariant: 'danger',\n    okText: t('common.yes'),\n    cancelText: t('common.no'),\n  });\n\n  if (!confirmed) {\n    return;\n  }\n\n  try {\n    state.loading = true;\n    state.isSigningOut = true;\n\n    await userStore.signOut();\n\n    sessionStorage.removeItem('user-profile');\n    sessionStorage.removeItem('shared-workflows');\n    sessionStorage.removeItem('user-workflows');\n    sessionStorage.removeItem('backup-workflows');\n\n    toast.success(t('settings.profile.signedOut'));\n  } catch (error) {\n    console.error('Sign out error:', error);\n    toast.error(t('message.somethingWrong'));\n  } finally {\n    state.loading = false;\n    state.isSigningOut = false;\n  }\n}\n</script>\n\n<style scoped>\n.loading-state {\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  justify-content: center;\n  padding: 4rem;\n}\n\n.not-signed-in {\n  padding: 3rem;\n  text-align: center;\n}\n\n.profile-card {\n  padding: 2rem;\n}\n\n.profile-header {\n  display: flex;\n  align-items: center;\n  margin-bottom: 1.5rem;\n}\n\n.avatar {\n  width: 80px;\n  height: 80px;\n  border-radius: 50%;\n  overflow: hidden;\n  flex-shrink: 0;\n  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);\n}\n\n.avatar img {\n  width: 100%;\n  height: 100%;\n  object-fit: cover;\n}\n\n.default-avatar {\n  width: 100%;\n  height: 100%;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  color: white;\n  font-size: 2rem;\n  font-weight: 600;\n}\n\n.user-info {\n  margin-left: 1.5rem;\n  flex: 1;\n}\n\n.username {\n  font-size: 1.5rem;\n  font-weight: 600;\n  margin-bottom: 0.25rem;\n}\n\n.email {\n  color: rgb(107 114 128);\n  font-size: 0.875rem;\n}\n\n.teams-badge {\n  display: inline-flex;\n  align-items: center;\n  margin-top: 0.75rem;\n  padding: 0.375rem 0.75rem;\n  background-color: rgb(243 244 246);\n  border-radius: 9999px;\n  font-size: 0.75rem;\n  color: rgb(75 85 99);\n}\n\n.dark .teams-badge {\n  background-color: rgb(55 65 81);\n  color: rgb(156 163 175);\n}\n\n.warning-message {\n  display: flex;\n  align-items: flex-start;\n  padding: 1rem;\n  margin-bottom: 1.5rem;\n  background-color: rgb(254 252 231);\n  border: 1px solid rgb(253 230 138);\n  border-radius: 0.5rem;\n  color: rgb(120 113 108);\n}\n\n.dark .warning-message {\n  background-color: rgb(113 63 18);\n  border-color: rgb(161 98 7);\n  color: rgb(253 230 138);\n}\n\n.actions {\n  border-top: 1px solid rgb(229 231 235);\n  padding-top: 1.5rem;\n}\n\n.dark .actions {\n  border-top-color: rgb(55 65 81);\n}\n\n.sign-out-button {\n  min-height: 48px;\n  font-weight: 600;\n}\n\n@keyframes spin {\n  from {\n    transform: rotate(0deg);\n  }\n  to {\n    transform: rotate(360deg);\n  }\n}\n\n.animate-spin {\n  animation: spin 1s linear infinite;\n}\n</style>\n"
  },
  {
    "path": "src/newtab/pages/settings/SettingsShortcuts.vue",
    "content": "<template>\n  <p v-if=\"recording.isChanged\" class=\"mb-4 text-gray-600 dark:text-gray-200\">\n    {{ t('settings.language.reloadPage') }}\n  </p>\n  <div class=\"mb-8 rounded-lg border border-gray-200 p-4 dark:border-gray-800\">\n    <p class=\"mb-2 font-semibold capitalize\">Automa</p>\n    <ui-list>\n      <ui-list-item class=\"group\">\n        <p class=\"flex-1\">Shortcut</p>\n        <template v-if=\"recording.id === 'automa:shortcut'\">\n          <kbd v-for=\"key in recording.keys\" :key=\"key\">\n            {{ getReadableShortcut(key) }}\n          </kbd>\n          <button\n            v-tooltip=\"t('common.cancel')\"\n            class=\"mr-2 ml-4\"\n            @click=\"cleanUp\"\n          >\n            <v-remixicon name=\"riCloseLine\" />\n          </button>\n          <button\n            v-tooltip=\"t('workflow.blocks.trigger.shortcut.stopRecord')\"\n            @click=\"stopRecording\"\n          >\n            <v-remixicon name=\"riStopLine\" />\n          </button>\n        </template>\n        <template v-else>\n          <button\n            v-tooltip=\"'Remove shortcut'\"\n            class=\"invisible mr-4 group-hover:visible\"\n            @click=\"removeShortcut('automa:shortcut')\"\n          >\n            <v-remixicon name=\"riDeleteBin7Line\" />\n          </button>\n          <button\n            v-tooltip=\"t('workflow.blocks.trigger.shortcut.tooltip')\"\n            class=\"invisible group-hover:visible\"\n            @click=\"startRecording({ id: 'automa:shortcut' })\"\n          >\n            <v-remixicon name=\"riRecordCircleLine\" />\n          </button>\n          <kbd v-for=\"key in automaShortcut.split('+')\" :key=\"key\">\n            {{ key }}\n          </kbd>\n        </template>\n      </ui-list-item>\n    </ui-list>\n  </div>\n  <div\n    v-for=\"(items, category) in shortcutsCats\"\n    :key=\"category\"\n    class=\"mb-8 rounded-lg border border-gray-200 p-4 dark:border-gray-800\"\n  >\n    <p class=\"mb-2 font-semibold capitalize\">{{ category }}</p>\n    <ui-list class=\"space-y-1 text-gray-600 dark:text-gray-200\">\n      <ui-list-item\n        v-for=\"shortcut in items\"\n        :key=\"shortcut.id\"\n        class=\"group h-12\"\n      >\n        <p class=\"mr-4 flex-1 capitalize\">\n          {{ shortcut.name }}\n        </p>\n        <template v-if=\"recording.id === shortcut.id\">\n          <kbd v-for=\"key in recording.keys\" :key=\"key\">\n            {{ getReadableShortcut(key) }}\n          </kbd>\n          <button\n            v-tooltip=\"t('common.cancel')\"\n            class=\"mr-2 ml-4\"\n            @click=\"cleanUp\"\n          >\n            <v-remixicon name=\"riCloseLine\" />\n          </button>\n          <button\n            v-tooltip=\"t('workflow.blocks.trigger.shortcut.stopRecord')\"\n            @click=\"stopRecording\"\n          >\n            <v-remixicon name=\"riStopLine\" />\n          </button>\n        </template>\n        <template v-else>\n          <button\n            v-tooltip=\"t('workflow.blocks.trigger.shortcut.tooltip')\"\n            class=\"invisible group-hover:visible\"\n            @click=\"startRecording(shortcut)\"\n          >\n            <v-remixicon name=\"riRecordCircleLine\" />\n          </button>\n          <kbd v-for=\"key in shortcut.keys\" :key=\"key\">\n            {{ key }}\n          </kbd>\n        </template>\n      </ui-list-item>\n    </ui-list>\n  </div>\n</template>\n<script setup>\nimport { ref, reactive, computed, onBeforeUnmount, onMounted } from 'vue';\nimport { useI18n } from 'vue-i18n';\nimport { useToast } from 'vue-toastification';\nimport browser from 'webextension-polyfill';\nimport { mapShortcuts, getReadableShortcut } from '@/composable/shortcut';\nimport { recordShortcut } from '@/utils/recordKeys';\n\nconst { t } = useI18n();\nconst toast = useToast();\n\nconst shortcuts = ref(mapShortcuts);\nconst automaShortcut = ref(getReadableShortcut('mod+shift+e'));\nconst recording = reactive({\n  id: '',\n  keys: [],\n  isChanged: false,\n});\n\nconst shortcutsCats = computed(() => {\n  const arr = Object.values(shortcuts.value);\n  const result = {};\n\n  arr.forEach((item) => {\n    const [category, shortcutName] = item.id.split(':');\n    const readableKey = getReadableShortcut(item.combo);\n    const name = shortcutName.replace('-', ' ');\n\n    (result[category] = result[category] || []).push({\n      ...item,\n      name,\n      keys: readableKey.split('+'),\n    });\n  });\n\n  return result;\n});\n\nfunction keydownListener(event) {\n  event.preventDefault();\n  event.stopPropagation();\n\n  if (!recording.id) {\n    document.removeEventListener('keydown', keydownListener, true);\n    return;\n  }\n\n  recordShortcut(event, (keys) => {\n    recording.keys = keys;\n  });\n}\nfunction cleanUp() {\n  recording.id = '';\n  recording.keys = [];\n\n  document.removeEventListener('keydown', keydownListener, true);\n}\nfunction startRecording({ id }) {\n  if (!recording.id) {\n    document.addEventListener('keydown', keydownListener, true);\n  }\n\n  recording.keys = [];\n  recording.id = id;\n}\nfunction removeShortcut(shortcutId) {\n  if (shortcutId !== 'automa:shortcut') return;\n\n  browser.storage.local.set({ automaShortcut: [] });\n  automaShortcut.value = '';\n}\nfunction stopRecording() {\n  if (recording.keys.length === 0) return;\n\n  const newCombo = recording.keys.join('+');\n\n  if (recording.id.startsWith('automa')) {\n    browser.storage.local.set({ automaShortcut: newCombo });\n    automaShortcut.value = getReadableShortcut(newCombo);\n    cleanUp();\n\n    return;\n  }\n\n  const isDuplicate = Object.keys(shortcuts.value).find((key) => {\n    return shortcuts.value[key].combo === newCombo && key !== recording.id;\n  });\n\n  if (isDuplicate) {\n    toast.error(t('settings.shortcuts.duplicate', { name: isDuplicate }));\n\n    return;\n  }\n\n  shortcuts.value[recording.id].combo = newCombo;\n  cleanUp();\n\n  recording.isChanged = true;\n\n  localStorage.setItem('shortcuts', JSON.stringify(shortcuts.value));\n}\n\nonMounted(() => {\n  browser.storage.local.get('automaShortcut').then((storage) => {\n    if (!storage.automaShortcut) return;\n\n    automaShortcut.value = getReadableShortcut(storage.automaShortcut);\n  });\n});\nonBeforeUnmount(() => {\n  document.removeEventListener('keydown', keydownListener, true);\n});\n</script>\n<style scoped>\nkbd {\n  min-width: 30px;\n  text-align: center;\n  text-transform: uppercase;\n  @apply p-1 px-2 rounded-lg border text-sm shadow ml-1;\n}\n</style>\n"
  },
  {
    "path": "src/newtab/pages/storage/Tables.vue",
    "content": "<template>\n  <div v-if=\"tableDetail && tableData\" class=\"container py-8 pb-4\">\n    <div class=\"mb-12 flex items-center\">\n      <h1 class=\"text-3xl font-semibold\">\n        {{ tableDetail.name }}\n      </h1>\n      <div class=\"grow\"></div>\n      <ui-button\n        v-tooltip.group=\"'Clear data'\"\n        icon\n        class=\"ml-2\"\n        @click=\"clearData\"\n      >\n        <v-remixicon name=\"riFileShredLine\" />\n      </ui-button>\n      <ui-button\n        v-tooltip=\"'Delete table'\"\n        icon\n        class=\"ml-4 text-red-400 dark:text-red-300\"\n        @click=\"deleteTable\"\n      >\n        <v-remixicon name=\"riDeleteBin7Line\" />\n      </ui-button>\n    </div>\n    <div class=\"mb-4 flex flex-wrap items-center\">\n      <ui-input\n        v-model=\"state.query\"\n        :placeholder=\"t('common.search')\"\n        prepend-icon=\"riSearch2Line\"\n        class=\"mb-4 w-full md:mb-0 md:w-auto\"\n      />\n      <div class=\"grow\" />\n      <ui-button class=\"md:ml-4\" @click=\"editTable\">\n        <v-remixicon name=\"riPencilLine\" class=\"mr-2 -ml-1\" />\n        <span>Edit table</span>\n      </ui-button>\n      <ui-popover trigger-width class=\"ml-4\">\n        <template #trigger>\n          <ui-button variant=\"accent\">\n            <span>{{ t('log.exportData.title') }}</span>\n            <v-remixicon name=\"riArrowDropDownLine\" class=\"ml-2 -mr-1\" />\n          </ui-button>\n        </template>\n        <ui-list class=\"space-y-1\">\n          <ui-list-item\n            v-for=\"type in dataExportTypes\"\n            :key=\"type.id\"\n            v-close-popover\n            class=\"cursor-pointer\"\n            @click=\"exportData(type.id)\"\n          >\n            {{ t(`log.exportData.types.${type.id}`) }}\n          </ui-list-item>\n        </ui-list>\n      </ui-popover>\n    </div>\n    <div class=\"scroll w-full overflow-x-auto\">\n      <ui-table\n        with-pagination\n        :headers=\"table.header\"\n        :items=\"table.body\"\n        :search=\"state.query\"\n        item-key=\"id\"\n        class=\"w-full\"\n      >\n        <template #item-action=\"{ item }\">\n          <v-remixicon\n            title=\"Delete row\"\n            class=\"cursor-pointer\"\n            name=\"riDeleteBin7Line\"\n            @click=\"deleteRow(item)\"\n          />\n        </template>\n      </ui-table>\n    </div>\n    <storage-edit-table\n      v-model=\"editState.show\"\n      :title=\"t('storage.table.edit')\"\n      :name=\"editState.name\"\n      :columns=\"editState.columns\"\n      @save=\"saveEditedTable\"\n    />\n  </div>\n</template>\n<script setup>\nimport { watch, shallowRef, shallowReactive, toRaw, triggerRef } from 'vue';\nimport { useRoute, useRouter } from 'vue-router';\nimport { useI18n } from 'vue-i18n';\nimport { useWorkflowStore } from '@/stores/workflow';\nimport { useLiveQuery } from '@/composable/liveQuery';\nimport { useDialog } from '@/composable/dialog';\nimport { objectHasKey } from '@/utils/helper';\nimport { dataExportTypes } from '@/utils/shared';\nimport StorageEditTable from '@/components/newtab/storage/StorageEditTable.vue';\nimport dbStorage from '@/db/storage';\nimport dataExporter from '@/utils/dataExporter';\n\nconst { t } = useI18n();\nconst route = useRoute();\nconst dialog = useDialog();\nconst router = useRouter();\nconst workflowStore = useWorkflowStore();\n\nconst tableId = +route.params.id;\n\nconst tableDetail = useLiveQuery(() =>\n  dbStorage.tablesItems.where('id').equals(tableId).first()\n);\nconst tableData = useLiveQuery(() =>\n  dbStorage.tablesData.where('tableId').equals(tableId).first()\n);\n\nconst table = shallowRef({\n  body: [],\n  header: [],\n});\nconst state = shallowReactive({\n  query: '',\n});\nconst editState = shallowReactive({\n  name: '',\n  columns: [],\n  show: false,\n});\n\nfunction editTable() {\n  editState.name = tableDetail.value.name;\n  editState.columns = tableDetail.value.columns;\n  editState.show = true;\n}\nfunction additionalHeaders(headers) {\n  headers.unshift({ value: '$$id', text: '', sortable: false });\n  headers.push({\n    value: 'action',\n    text: '',\n    sortable: false,\n    align: 'right',\n    attrs: {\n      width: '100px',\n    },\n  });\n\n  return headers;\n}\nfunction exportData(type) {\n  dataExporter(\n    tableData.value.items,\n    { name: tableDetail.value.name, type },\n    true\n  );\n}\nasync function saveEditedTable({ columns, name, changes }) {\n  const columnsChanges = Object.values(changes);\n\n  try {\n    await dbStorage.tablesItems.update(tableId, {\n      name,\n      columns,\n    });\n\n    const headers = [];\n    const newTableData = [];\n    const newColumnsIndex = {};\n    const { columnsIndex } = tableData.value;\n\n    columns.forEach(({ name: columnName, id, type }) => {\n      const index = columnsIndex[id]?.index || 0;\n\n      newColumnsIndex[id] = {\n        type,\n        index,\n        name: columnName,\n      };\n      headers.push({\n        text: columnName,\n        value: columnName,\n        filterable: ['string', 'any'].includes(type),\n      });\n    });\n\n    if (columnsIndex.column) {\n      newColumnsIndex.column = toRaw(columnsIndex.column);\n    }\n\n    table.value.header = additionalHeaders(headers);\n    table.value.body = table.value.body.map((item, index) => {\n      columnsChanges.forEach(\n        ({ type, oldValue, newValue, name: columnName }) => {\n          if (type === 'rename' && objectHasKey(item, oldValue)) {\n            item[newValue] = item[oldValue];\n\n            delete item[oldValue];\n          } else if (type === 'delete') {\n            delete item[columnName];\n          }\n        }\n      );\n\n      delete item.$$id;\n      newTableData.push({ ...item });\n      item.$$id = index + 1;\n\n      return item;\n    });\n\n    await dbStorage.tablesData.where('tableId').equals(tableId).modify({\n      items: newTableData,\n      columnsIndex: newColumnsIndex,\n    });\n\n    editState.show = false;\n  } catch (error) {\n    console.error(error);\n  }\n}\nfunction deleteRow(item) {\n  const rowIndex = table.value.body.findIndex(({ $$id }) => $$id === item.$$id);\n  if (rowIndex === -1) return;\n\n  const cache = {};\n  const { columnsIndex } = tableData.value;\n  const columns = Object.values(tableDetail.value.columns);\n\n  Object.keys(item).forEach((key) => {\n    if (key === '$$id') return;\n\n    const column =\n      cache[key] || columns.find((currColumn) => currColumn.name === key);\n    if (!column) return;\n\n    const columnIndex = columnsIndex[column.id];\n    if (columnIndex && columnIndex.index >= item.$$id - 1) {\n      columnIndex.index -= 1;\n    }\n\n    cache[key] = column;\n  });\n\n  table.value.body.splice(rowIndex, 1);\n  tableData.value.items.splice(rowIndex, 1);\n\n  dbStorage.tablesItems.update(tableId, {\n    modifiedAt: Date.now(),\n    rowsCount: tableDetail.value.rowsCount - 1,\n  });\n  dbStorage.tablesData\n    .where('tableId')\n    .equals(tableId)\n    .modify({\n      items: toRaw(tableData.value.items),\n      columnsIndex: toRaw(columnsIndex),\n    })\n    .then(() => {\n      triggerRef(table);\n    });\n}\nfunction clearData() {\n  dialog.confirm({\n    title: 'Clear data',\n    okVariant: 'danger',\n    body: 'Are you sure want to clear the table data?',\n    onConfirm: async () => {\n      await dbStorage.tablesItems.update(tableId, {\n        rowsCount: 0,\n        modifiedAt: Date.now(),\n      });\n\n      const columnsIndex = tableDetail.value.columns.reduce(\n        (acc, column) => {\n          acc[column.id] = {\n            index: 0,\n            type: column.type,\n            name: column.name,\n          };\n\n          return acc;\n        },\n        { column: { index: 0, type: 'any', name: 'column' } }\n      );\n      await dbStorage.tablesData.where('tableId').equals(tableId).modify({\n        items: [],\n        columnsIndex,\n      });\n    },\n  });\n}\nfunction deleteTable() {\n  dialog.confirm({\n    title: t('storage.table.delete'),\n    okVariant: 'danger',\n    body: t('message.delete', { name: tableDetail.value.name }),\n    onConfirm: async () => {\n      try {\n        await dbStorage.tablesItems.where('id').equals(tableId).delete();\n        await dbStorage.tablesData.where('tableId').equals(tableId).delete();\n\n        await workflowStore.update({\n          id: (workflow) => workflow.connectedTable === tableId,\n          data: { connectedTable: null },\n        });\n\n        router.replace('/storage');\n      } catch (error) {\n        console.error(error);\n      }\n    },\n  });\n}\n\nwatch(tableData, () => {\n  if (!tableDetail.value || !tableData.value) return;\n\n  const dataTable = { header: [], body: [] };\n  const headers = tableDetail.value.columns.map(({ name, type }) => ({\n    text: name,\n    value: name,\n    filterable: ['string', 'any'].includes(type),\n  }));\n\n  dataTable.body = tableData.value.items.map((item, index) => ({\n    ...item,\n    $$id: index + 1,\n  }));\n  dataTable.header = additionalHeaders(headers);\n\n  table.value = dataTable;\n});\n</script>\n"
  },
  {
    "path": "src/newtab/pages/workflows/Host.vue",
    "content": "<template>\n  <div v-if=\"workflow\" class=\"relative h-screen\">\n    <div class=\"absolute top-0 left-0 z-10 flex w-full items-center p-4\">\n      <ui-card\n        padding=\"px-2\"\n        class=\"flex items-center overflow-hidden\"\n        style=\"min-width: 150px; height: 48px\"\n      >\n        <span class=\"inline-block\">\n          <ui-img\n            v-if=\"workflow.icon.startsWith('http')\"\n            :src=\"workflow.icon\"\n            class=\"h-8 w-8\"\n          />\n          <v-remixicon v-else :name=\"workflow.icon\" size=\"26\" />\n        </span>\n        <div class=\"ml-2 max-w-sm\">\n          <p\n            :class=\"{ 'text-lg': !workflow.description }\"\n            class=\"text-overflow font-semibold leading-tight\"\n          >\n            {{ workflow.name }}\n          </p>\n          <p\n            :class=\"{ 'text-sm': workflow.description }\"\n            class=\"text-overflow leading-tight text-gray-600 dark:text-gray-200\"\n          >\n            {{ workflow.description }}\n          </p>\n        </div>\n      </ui-card>\n      <ui-tabs\n        model-value=\"'editor'\"\n        class=\"ml-4 h-full space-x-1 rounded-lg border-none bg-white px-2 dark:bg-gray-800\"\n        style=\"height: 48px\"\n      >\n        <ui-tab value=\"editor\">{{ t('common.editor') }}</ui-tab>\n        <ui-tab value=\"logs\" @click=\"openLogs\">\n          {{ t('common.log', 2) }}\n          <span\n            v-if=\"workflowStates.length > 0\"\n            class=\"ml-2 inline-block rounded-full bg-accent p-1 text-center text-xs text-white dark:text-black\"\n            style=\"min-width: 25px\"\n          >\n            {{ workflowStates.length }}\n          </span>\n        </ui-tab>\n      </ui-tabs>\n      <div class=\"grow\"></div>\n      <ui-card padding=\"p-1\">\n        <button\n          v-tooltip.group=\"state.triggerText\"\n          class=\"hoverable rounded-lg p-2\"\n        >\n          <v-remixicon name=\"riFlashlightLine\" />\n        </button>\n        <button\n          v-tooltip.group=\"\n            `${t('common.execute')} (${\n              shortcut['editor:execute-workflow'].readable\n            })`\n          \"\n          class=\"hoverable rounded-lg p-2\"\n          @click=\"executeCurrWorkflow\"\n        >\n          <v-remixicon name=\"riPlayLine\" />\n        </button>\n      </ui-card>\n      <ui-card padding=\"p-1 ml-4 flex items-center\">\n        <button\n          v-tooltip.group=\"t('common.delete')\"\n          class=\"hoverable mr-2 rounded-lg p-2\"\n          @click=\"deleteWorkflowHost\"\n        >\n          <v-remixicon name=\"riDeleteBin7Line\" />\n        </button>\n        <ui-button\n          v-tooltip.group=\"t('workflow.host.sync.description')\"\n          :loading=\"state.loadingSync\"\n          variant=\"accent\"\n          @click=\"syncWorkflow\"\n        >\n          {{ t('workflow.host.sync.title') }}\n        </ui-button>\n      </ui-card>\n    </div>\n    <ui-tab-panels\n      v-model=\"state.activeTab\"\n      :class=\"{ 'container pb-4 pt-24': state.activeTab !== 'editor' }\"\n      class=\"h-full\"\n    >\n      <ui-tab-panel class=\"h-full\" value=\"editor\">\n        <workflow-editor\n          v-if=\"state.retrieved\"\n          :id=\"route.params.id\"\n          :key=\"state.editorKey\"\n          :data=\"workflow.drawflow\"\n          :options=\"editorOptions\"\n          :disabled=\"true\"\n          class=\"h-full w-full\"\n          @init=\"onEditorInit\"\n        />\n      </ui-tab-panel>\n    </ui-tab-panels>\n  </div>\n</template>\n<script setup>\nimport { computed, reactive, onMounted, watch } from 'vue';\nimport { useI18n } from 'vue-i18n';\nimport { useRoute, useRouter } from 'vue-router';\nimport { useHead } from '@vueuse/head';\nimport { useDialog } from '@/composable/dialog';\nimport { useShortcut } from '@/composable/shortcut';\nimport { useGroupTooltip } from '@/composable/groupTooltip';\nimport { findTriggerBlock } from '@/utils/helper';\nimport convertWorkflowData from '@/utils/convertWorkflowData';\nimport { useWorkflowStore } from '@/stores/workflow';\nimport RendererWorkflowService from '@/service/renderer/RendererWorkflowService';\nimport { useHostedWorkflowStore } from '@/stores/hostedWorkflow';\nimport getTriggerText from '@/utils/triggerText';\nimport WorkflowEditor from '@/components/newtab/workflow/WorkflowEditor.vue';\nimport emitter from '@/lib/mitt';\n\nuseGroupTooltip();\n\nconst { t } = useI18n();\nconst route = useRoute();\nconst router = useRouter();\nconst dialog = useDialog();\nconst workflowStore = useWorkflowStore();\nconst hostedWorkflowStore = useHostedWorkflowStore();\n\nconst workflowId = route.params.id;\nconst editorOptions = {\n  disabled: true,\n  fitViewOnInit: true,\n  nodesDraggable: false,\n  edgesUpdatable: false,\n  nodesConnectable: false,\n  elementsSelectable: false,\n};\n\n/* eslint-disable-next-line */\nconst shortcut = useShortcut('editor:execute-workflow', executeCurrWorkflow);\n\nconst state = reactive({\n  editorKey: 0,\n  retrieved: false,\n  loadingSync: false,\n  activeTab: 'editor',\n  trigger: 'Trigger: Manually',\n});\n\nconst workflow = computed(() => hostedWorkflowStore.getById(workflowId));\nconst workflowStates = computed(() =>\n  workflowStore.getWorkflowStates(workflowId)\n);\n\nuseHead({\n  title: () =>\n    workflow.value?.name\n      ? `${workflow.value.name} workflow`\n      : 'Hosted workflow',\n});\n\nfunction openLogs() {\n  emitter.emit('ui:logs', {\n    workflowId,\n    show: true,\n  });\n}\nfunction syncWorkflow() {\n  state.loadingSync = true;\n  const hostId = {\n    hostId: workflow.value.hostId,\n    updatedAt: null,\n  };\n\n  hostedWorkflowStore\n    .fetchWorkflows([hostId])\n    .then(() => {\n      if (!workflow.value) {\n        router.replace('/workflows');\n      }\n      /* eslint-disable-next-line */\n      retrieveTriggerText();\n      state.loadingSync = false;\n    })\n    .catch((error) => {\n      console.error(error);\n      state.loadingSync = false;\n    });\n}\nasync function deleteWorkflowHost() {\n  dialog.confirm({\n    title: t('workflow.delete'),\n    okVariant: 'danger',\n    body: t('message.delete', { name: workflow.value.name }),\n    onConfirm: async () => {\n      try {\n        await hostedWorkflowStore.delete(workflowId);\n\n        router.replace('/workflows');\n      } catch (error) {\n        console.error(error);\n      }\n    },\n  });\n}\nfunction executeCurrWorkflow() {\n  const payload = {\n    ...workflow.value,\n    id: workflowId,\n  };\n\n  RendererWorkflowService.executeWorkflow(payload);\n}\nasync function retrieveTriggerText() {\n  const triggerBlock = findTriggerBlock(workflow.value.drawflow);\n  if (!triggerBlock) return;\n\n  state.triggerText = await getTriggerText(\n    triggerBlock.data,\n    t,\n    workflowId,\n    true\n  );\n}\nfunction onEditorInit(editor) {\n  editor.setInteractive(false);\n}\n\nwatch(workflow, () => {\n  state.editorKey += 1;\n});\n\nonMounted(() => {\n  const currentWorkflow = hostedWorkflowStore.workflows[workflowId];\n  if (!currentWorkflow) {\n    router.push('/workflows');\n    return;\n  }\n\n  const convertedData = convertWorkflowData(currentWorkflow);\n  hostedWorkflowStore.update({ id: workflowId, ...convertedData });\n\n  retrieveTriggerText();\n\n  state.retrieved = true;\n});\n</script>\n<style>\n.parent-drawflow.is-shared .drawflow-node * {\n  pointer-events: none;\n}\n.parent-drawflow.is-shared .drawflow-node .move-to-group,\n.parent-drawflow.is-shared .drawflow-node .menu {\n  display: none;\n}\n</style>\n"
  },
  {
    "path": "src/newtab/pages/workflows/Shared.vue",
    "content": "<template>\n  <div v-if=\"workflow\" class=\"relative h-screen\">\n    <div class=\"absolute top-0 left-0 z-10 flex w-full items-center p-4\">\n      <ui-card\n        padding=\"px-2\"\n        class=\"flex items-center overflow-hidden\"\n        style=\"min-width: 150px; height: 48px\"\n      >\n        <span class=\"inline-block\">\n          <ui-img\n            v-if=\"workflow.icon.startsWith('http')\"\n            :src=\"workflow.icon\"\n            class=\"h-8 w-8\"\n          />\n          <v-remixicon v-else :name=\"workflow.icon\" size=\"26\" />\n        </span>\n        <div class=\"ml-2 max-w-sm\">\n          <p\n            :class=\"{ 'text-lg': !workflow.description }\"\n            class=\"text-overflow font-semibold leading-tight\"\n          >\n            {{ workflow.name }}\n          </p>\n          <p\n            :class=\"{ 'text-sm': workflow.description }\"\n            class=\"text-overflow leading-tight text-gray-600 dark:text-gray-200\"\n          >\n            {{ workflow.description }}\n          </p>\n        </div>\n      </ui-card>\n      <ui-card padding=\"p-1 ml-4\">\n        <ui-input\n          v-tooltip=\"t('workflow.share.url')\"\n          prepend-icon=\"riLinkM\"\n          :model-value=\"`https://extension.automa.site/workflow/${workflow.id}`\"\n          readonly\n          @click=\"copyLink\"\n        />\n      </ui-card>\n      <div class=\"pointer-events-none grow\" />\n      <ui-card padding=\"p-1 ml-4\">\n        <router-link\n          v-if=\"state.hasLocalCopy\"\n          v-tooltip.group=\"'Go to local version'\"\n          :to=\"`/workflows/${workflowId}`\"\n          class=\"hoverable block rounded-lg p-2\"\n        >\n          <v-remixicon name=\"riComputerLine\" />\n        </router-link>\n      </ui-card>\n      <ui-card padding=\"p-1 ml-4\">\n        <button\n          v-if=\"state.hasLocalCopy\"\n          v-tooltip.group=\"t('workflow.share.fetchLocal')\"\n          class=\"hoverable rounded-lg p-2\"\n          @click=\"fetchLocalWorkflow\"\n        >\n          <v-remixicon name=\"riRefreshLine\" />\n        </button>\n        <button\n          v-else\n          v-tooltip.group=\"t('workflow.share.download')\"\n          class=\"hoverable rounded-lg p-2\"\n          @click=\"insertToLocal\"\n        >\n          <v-remixicon name=\"riDownloadLine\" />\n        </button>\n        <button\n          v-tooltip.group=\"t('workflow.share.edit')\"\n          class=\"hoverable rounded-lg p-2\"\n          @click=\"initEditWorkflow\"\n        >\n          <v-remixicon name=\"riFileEditLine\" />\n        </button>\n      </ui-card>\n      <ui-card padding=\"p-1 flex ml-4\">\n        <button\n          v-tooltip.group=\"t('workflow.share.unpublish')\"\n          class=\"hoverable relative mr-2 rounded-lg p-2\"\n          @click=\"unpublishSharedWorkflow\"\n        >\n          <ui-spinner\n            v-if=\"state.isUnpublishing\"\n            color=\"text-accent\"\n            class=\"absolute top-2 left-2\"\n          />\n          <v-remixicon\n            name=\"riLock2Line\"\n            :class=\"{ 'opacity-75': state.isUnpublishing }\"\n          />\n        </button>\n        <ui-button\n          :loading=\"state.isUpdating\"\n          :disabled=\"state.isUnpublishing\"\n          variant=\"accent\"\n          @click=\"saveUpdatedSharedWorkflow\"\n        >\n          <span\n            v-if=\"state.isChanged\"\n            class=\"absolute top-0 left-0 -ml-1 -mt-1 flex h-3 w-3\"\n          >\n            <span\n              class=\"absolute inline-flex h-full w-full animate-ping rounded-full bg-primary opacity-75\"\n            ></span>\n            <span\n              class=\"relative inline-flex h-3 w-3 rounded-full bg-blue-600\"\n            ></span>\n          </span>\n          {{ t('workflow.share.update') }}\n        </ui-button>\n      </ui-card>\n    </div>\n    <workflow-editor\n      v-if=\"state.retrieved\"\n      :id=\"route.params.id\"\n      :key=\"state.editorKey\"\n      :data=\"workflow.drawflow\"\n      :options=\"editorOptions\"\n      :disabled=\"true\"\n      class=\"h-full w-full\"\n      @init=\"onEditorInit\"\n    />\n  </div>\n  <ui-modal\n    v-model=\"editState.showModal\"\n    custom-content\n    @close=\"updateSharedWorkflow(editState.data)\"\n  >\n    <workflow-share\n      :workflow=\"workflow\"\n      is-update\n      @change=\"onEditWorkflowChange\"\n    >\n      <template #prepend>\n        <div class=\"mb-6 flex justify-between\">\n          <p>{{ t('workflow.share.edit') }}</p>\n          <v-remixicon\n            name=\"riCloseLine\"\n            class=\"cursor-pointer\"\n            @click=\"\n              editState.showModal = false;\n              updateSharedWorkflow(editState.data);\n            \"\n          />\n        </div>\n      </template>\n    </workflow-share>\n  </ui-modal>\n</template>\n<script setup>\nimport WorkflowEditor from '@/components/newtab/workflow/WorkflowEditor.vue';\nimport WorkflowShare from '@/components/newtab/workflow/WorkflowShare.vue';\nimport { useDialog } from '@/composable/dialog';\nimport { useGroupTooltip } from '@/composable/groupTooltip';\nimport { useSharedWorkflowStore } from '@/stores/sharedWorkflow';\nimport { useWorkflowStore } from '@/stores/workflow';\nimport { fetchApi } from '@/utils/api';\nimport convertWorkflowData from '@/utils/convertWorkflowData';\nimport { useHead } from '@vueuse/head';\nimport { computed, onMounted, reactive, shallowRef, watch } from 'vue';\nimport { useI18n } from 'vue-i18n';\nimport { useRoute, useRouter } from 'vue-router';\nimport { useToast } from 'vue-toastification';\nimport browser from 'webextension-polyfill';\n\nuseGroupTooltip();\n\nconst { t } = useI18n();\nconst toast = useToast();\nconst route = useRoute();\nconst router = useRouter();\nconst dialog = useDialog();\nconst workflowStore = useWorkflowStore();\nconst sharedWorkflowStore = useSharedWorkflowStore();\n\nconst workflowId = route.params.id;\nconst editorOptions = {\n  disabled: true,\n  fitViewOnInit: true,\n  nodesDraggable: false,\n  edgesUpdatable: false,\n  nodesConnectable: false,\n  elementsSelectable: false,\n};\n\nconst editState = reactive({\n  showModal: false,\n  data: {\n    name: '',\n    content: '',\n    category: '',\n    description: '',\n  },\n});\nconst state = reactive({\n  editorKey: 0,\n  retrieved: false,\n  isChanged: false,\n  isUpdating: false,\n  isUnpublishing: false,\n  trigger: 'Trigger: Manually',\n});\nconst editor = shallowRef(null);\n\nconst workflow = computed(() => sharedWorkflowStore.getById(route.params.id));\n\nuseHead({\n  title: () =>\n    workflow.value?.name\n      ? `${workflow.value.name} workflow`\n      : 'Shared workflow',\n});\n\nconst changingKeys = new Set();\n\nfunction updateSharedWorkflow(data = {}) {\n  Object.keys(data).forEach((key) => {\n    changingKeys.add(key);\n  });\n\n  sharedWorkflowStore.update({\n    data,\n    id: workflowId,\n  });\n\n  if (data.drawflow) {\n    editor.value.setNodes(data.drawflow.nodes);\n    editor.value.setEdges(data.drawflow.edges);\n    editor.value.fitView();\n  }\n\n  state.isChanged = true;\n}\nfunction initEditWorkflow() {\n  ['name', 'content', 'category', 'description'].forEach((key) => {\n    editState.data[key] = workflow.value[key];\n  });\n  editState.showModal = true;\n}\nfunction onEditWorkflowChange({ name, content, category, description }) {\n  editState.data.name = name;\n  editState.data.content = content;\n  editState.data.category = category;\n  editState.data.description = description;\n}\nfunction unpublishSharedWorkflow() {\n  dialog.confirm({\n    title: t('workflow.unpublish.title'),\n    body: t('workflow.unpublish.body', { name: workflow.value.name }),\n    okVariant: 'danger',\n    okText: t('workflow.unpublish.button'),\n    async onConfirm() {\n      try {\n        state.isUnpublishing = true;\n\n        const response = await fetchApi(`/me/workflows/shared/${workflowId}`, {\n          auth: true,\n          method: 'DELETE',\n        });\n\n        if (response.status !== 200) {\n          throw new Error(response.statusText);\n        }\n\n        sharedWorkflowStore.delete(workflowId);\n        sessionStorage.setItem(\n          'shared-workflows',\n          JSON.stringify(workflowStore.workflows)\n        );\n\n        router.push('/');\n\n        state.isUnpublishing = false;\n      } catch (error) {\n        console.error(error);\n        state.isUnpublishing = false;\n        toast.error(t('message.somethingWrong'));\n      }\n    },\n  });\n}\nasync function saveUpdatedSharedWorkflow() {\n  try {\n    state.isUpdating = true;\n\n    const payload = {};\n    changingKeys.forEach((key) => {\n      if (key === 'drawflow') {\n        const flow = workflow.value.drawflow;\n        payload.drawflow = typeof flow === 'string' ? JSON.parse(flow) : flow;\n      } else {\n        payload[key] = workflow.value[key];\n      }\n    });\n\n    const url = `/me/workflows/shared/${workflowId}`;\n    const response = await fetchApi(url, {\n      auth: true,\n      method: 'PUT',\n      body: JSON.stringify(payload),\n    });\n\n    if (response.status !== 200) {\n      toast.error(t('message.somethingWrong'));\n      throw new Error(response.statusText);\n    }\n\n    state.isChanged = false;\n    changingKeys.clear();\n    sessionStorage.setItem(\n      'shared-workflows',\n      JSON.stringify(sharedWorkflowStore.workflows)\n    );\n\n    state.isUpdating = false;\n  } catch (error) {\n    console.error(error);\n    state.isUpdating = false;\n  }\n}\nfunction fetchLocalWorkflow() {\n  const workflowData = {};\n  const keys = [\n    'drawflow',\n    'name',\n    'description',\n    'icon',\n    'globalData',\n    'dataColumns',\n    'table',\n    'settings',\n  ];\n  const localWorkflow = workflowStore.getById(workflowId);\n\n  keys.forEach((key) => {\n    workflowData[key] = localWorkflow[key];\n  });\n\n  const convertedData = convertWorkflowData(workflowData);\n  convertedData.version = browser.runtime.getManifest().version;\n\n  updateSharedWorkflow(convertedData);\n}\nfunction insertToLocal() {\n  const copy = {\n    ...workflow.value,\n    createdAt: Date.now(),\n    version: browser.runtime.getManifest().version,\n  };\n\n  workflowStore.insert(copy, { duplicateId: true }).then(() => {\n    state.hasLocalCopy = true;\n  });\n}\nfunction onEditorInit(instance) {\n  instance.setInteractive(false);\n  editor.value = instance;\n}\n\nfunction copyLink(e) {\n  e.target.select();\n  navigator.clipboard.writeText(\n    `https://extension.automa.site/workflow/${workflow.value.id}`\n  );\n  toast.success(t('workflow.share.linkCopied'));\n}\n\nwatch(workflow, () => {\n  state.editorKey += 1;\n});\n\nonMounted(() => {\n  if (!workflow.value) {\n    router.push('/workflows');\n    return;\n  }\n\n  const convertedData = convertWorkflowData(workflow.value);\n  sharedWorkflowStore.update({\n    id: workflowId,\n    data: {\n      drawflow: convertedData.drawflow ?? workflow.value.drawflow,\n    },\n  });\n\n  state.hasLocalCopy = workflowStore.getWorkflows.some(\n    ({ id }) => id === workflowId\n  );\n\n  state.retrieved = true;\n});\n</script>\n<style>\n.parent-drawflow.is-shared .drawflow-node * {\n  pointer-events: none;\n}\n.parent-drawflow.is-shared .drawflow-node .move-to-group,\n.parent-drawflow.is-shared .drawflow-node .menu {\n  display: none;\n}\n</style>\n"
  },
  {
    "path": "src/newtab/pages/workflows/[id].vue",
    "content": "<template>\n  <div v-if=\"workflow\" class=\"flex\" style=\"height: calc(100vh - 40px)\">\n    <div\n      v-if=\"state.showSidebar && haveEditAccess\"\n      :class=\"\n        editState.editing\n          ? 'absolute h-full md:relative z-50'\n          : 'hidden md:flex'\n      \"\n      class=\"sidebar w-80 flex-col border-l border-gray-100 bg-white py-6 dark:border-gray-700 dark:border-opacity-50 dark:bg-gray-800\"\n      :style=\"{\n        width: `${sidebarCss.width}px`,\n        padding: sidebarCss.padding,\n        position: 'relative',\n      }\"\n    >\n      <workflow-edit-block\n        v-if=\"editState.editing\"\n        :data=\"editState.blockData\"\n        :workflow=\"workflow\"\n        :editor=\"editor\"\n        @update=\"updateBlockData\"\n        @close=\"closeEditingCard\"\n      />\n      <workflow-details-card\n        v-else\n        :workflow=\"workflow\"\n        @update=\"updateWorkflow\"\n      />\n      <!-- drag-element -->\n      <div ref=\"sidebarRef\" class=\"custom-drag\" @mousedown=\"startDrag\"></div>\n    </div>\n    <div class=\"relative flex-1 overflow-auto\">\n      <div\n        class=\"pointer-events-none absolute left-0 top-0 z-10 flex w-full items-center p-4\"\n      >\n        <ui-card\n          v-if=\"!haveEditAccess\"\n          padding=\"px-2 mr-4\"\n          class=\"flex items-center overflow-hidden\"\n          style=\"min-width: 150px; height: 48px\"\n        >\n          <span class=\"inline-block\">\n            <ui-img\n              v-if=\"workflow.icon.startsWith('http')\"\n              :src=\"workflow.icon\"\n              class=\"h-8 w-8\"\n            />\n            <v-remixicon v-else :name=\"workflow.icon\" size=\"26\" />\n          </span>\n          <div class=\"ml-2 max-w-sm\">\n            <p\n              :class=\"{ 'text-lg': !workflow.description }\"\n              class=\"text-overflow font-semibold leading-tight\"\n            >\n              {{ workflow.name }}\n            </p>\n            <p\n              :class=\"{ 'text-sm': workflow.description }\"\n              class=\"text-overflow leading-tight text-gray-600 dark:text-gray-200\"\n            >\n              {{ workflow.description }}\n            </p>\n          </div>\n        </ui-card>\n        <ui-tabs\n          :model-value=\"isPackage ? state.activeTab : 'editor'\"\n          class=\"pointer-events-auto h-full space-x-1 rounded-lg border-none bg-white px-2 dark:bg-gray-800\"\n          @change=\"onTabChange\"\n        >\n          <button\n            v-if=\"haveEditAccess\"\n            v-tooltip=\"\n              `${t('workflow.toggleSidebar')} (${\n                shortcut['editor:toggle-sidebar'].readable\n              })`\n            \"\n            style=\"margin-right: 6px\"\n            @click=\"toggleSidebar\"\n          >\n            <v-remixicon\n              :name=\"state.showSidebar ? 'riSideBarFill' : 'riSideBarLine'\"\n            />\n          </button>\n          <ui-tab value=\"editor\">{{ t('common.editor') }}</ui-tab>\n          <template v-if=\"isPackage\">\n            <ui-tab value=\"package-details\"> Details </ui-tab>\n            <ui-tab value=\"package-settings\">\n              {{ t('common.settings') }}\n            </ui-tab>\n          </template>\n          <ui-tab v-else value=\"logs\" class=\"flex items-center\">\n            {{ t('common.log', 2) }}\n            <span\n              v-if=\"workflowStates.length > 0\"\n              class=\"ml-2 inline-block rounded-full bg-accent p-1 text-center text-xs text-white dark:text-black\"\n              style=\"min-width: 25px\"\n            >\n              {{ workflowStates.length }}\n            </span>\n          </ui-tab>\n        </ui-tabs>\n        <ui-card v-if=\"isTeamWorkflow\" padding=\"p-1 ml-4 pointer-events-auto\">\n          <ui-input\n            v-tooltip=\"'Workflow URL'\"\n            prepend-icon=\"riLinkM\"\n            :model-value=\"`https://extension.automa.site/teams/${teamId}/workflows/${workflow.id}`\"\n            readonly\n            @click=\"$event.target.select()\"\n          />\n        </ui-card>\n        <div class=\"pointer-events-none grow\" />\n        <editor-used-credentials v-if=\"editor\" :editor=\"editor\" />\n        <template v-if=\"isPackage\">\n          <ui-button\n            v-if=\"workflow.isExternal\"\n            v-tooltip=\"t('workflow.previewMode.description')\"\n            class=\"pointer-events-auto cursor-default\"\n          >\n            <v-remixicon name=\"riEyeLine\" class=\"mr-2 -ml-1\" />\n            <span>{{ t('workflow.previewMode.title') }}</span>\n          </ui-button>\n          <editor-pkg-actions\n            v-else\n            :editor=\"editor\"\n            :data=\"workflow\"\n            :is-data-changed=\"state.dataChanged\"\n            @update=\"onActionUpdated\"\n          />\n        </template>\n        <editor-local-actions\n          v-else\n          :editor=\"editor\"\n          :workflow=\"workflow\"\n          :is-data-changed=\"state.dataChanged\"\n          :is-team=\"isTeamWorkflow\"\n          :is-package=\"isPackage\"\n          :can-edit=\"haveEditAccess\"\n          @update=\"onActionUpdated\"\n          @permission=\"checkWorkflowPermission\"\n          @modal=\"(modalState.name = $event), (modalState.show = true)\"\n        />\n      </div>\n      <ui-tab-panels\n        v-model=\"state.activeTab\"\n        :class=\"{ 'overflow-hidden': !state.activeTab.startsWith('package') }\"\n        class=\"h-full w-full\"\n        @drop=\"onDropInEditor\"\n        @dragend=\"clearHighlightedElements\"\n        @dragover.prevent=\"onDragoverEditor\"\n      >\n        <template v-if=\"isPackage\">\n          <ui-tab-panel value=\"package-details\" class=\"container pt-24\">\n            <package-details :data=\"workflow\" @update=\"updateWorkflow\" />\n          </ui-tab-panel>\n          <ui-tab-panel value=\"package-settings\" class=\"container pt-24\">\n            <package-settings\n              :data=\"workflow\"\n              :editor=\"editor\"\n              @update=\"updateWorkflow\"\n              @goBlock=\"goToPkgBlock\"\n            />\n          </ui-tab-panel>\n        </template>\n        <ui-tab-panel cache value=\"editor\" class=\"w-full\" @keydown=\"onKeydown\">\n          <editor-debugging\n            v-if=\"workflow.testingMode && workflowStates.length > 0\"\n            :states=\"workflowStates\"\n            @goToBlock=\"goToBlock\"\n          />\n          <workflow-editor\n            v-if=\"state.workflowConverted\"\n            :id=\"route.params.id\"\n            :data=\"editorData\"\n            :disabled=\"isTeamWorkflow && !haveEditAccess\"\n            :class=\"{ 'animate-blocks': state.animateBlocks }\"\n            class=\"workflow-editor focus:outline-none\"\n            style=\"height: calc(100vh - 40px)\"\n            tabindex=\"0\"\n            @init=\"onEditorInit\"\n            @edit=\"initEditBlock\"\n            @update:node=\"state.dataChanged = true\"\n            @delete:node=\"state.dataChanged = true\"\n            @update:settings=\"onUpdateBlockSettings\"\n          >\n            <template\n              v-if=\"!isTeamWorkflow || haveEditAccess\"\n              #controls-prepend\n            >\n              <ui-card padding=\"p-0 ml-2 undo-redo\">\n                <button\n                  v-tooltip.group=\"\n                    `${t('workflow.undo')} (${getReadableShortcut('mod+z')})`\n                  \"\n                  :disabled=\"!commandManager.state.value.canUndo\"\n                  class=\"rounded-lg p-2 transition-colors\"\n                  @click=\"executeCommand('undo')\"\n                >\n                  <v-remixicon name=\"riArrowGoBackLine\" />\n                </button>\n                <button\n                  v-tooltip.group=\"\n                    `${t('workflow.redo')} (${getReadableShortcut(\n                      'mod+shift+z'\n                    )})`\n                  \"\n                  :disabled=\"!commandManager.state.value.canRedo\"\n                  class=\"rounded-lg p-2 transition-colors\"\n                  @click=\"executeCommand('redo')\"\n                >\n                  <v-remixicon name=\"riArrowGoForwardLine\" />\n                </button>\n              </ui-card>\n              <button\n                v-if=\"!isPackage && haveEditAccess\"\n                v-tooltip=\"t('packages.open')\"\n                class=\"control-button hoverable ml-2\"\n                @click=\"blockFolderModal.showList = !blockFolderModal.showList\"\n              >\n                <v-remixicon name=\"mdiPackageVariantClosed\" />\n              </button>\n              <button\n                v-tooltip=\"t('workflow.autoAlign.title')\"\n                class=\"control-button hoverable ml-2\"\n                @click=\"autoAlign\"\n              >\n                <v-remixicon name=\"riMagicLine\" />\n              </button>\n            </template>\n          </workflow-editor>\n          <editor-local-saved-blocks\n            v-if=\"blockFolderModal.showList\"\n            @close=\"blockFolderModal.showList = false\"\n          />\n          <editor-local-ctx-menu\n            v-if=\"editor\"\n            :editor=\"editor\"\n            :is-package=\"isPackage\"\n            :is-team=\"isTeamWorkflow\"\n            :package-io=\"workflow.settings?.asBlock\"\n            @group=\"groupBlocks\"\n            @ungroup=\"ungroupBlocks\"\n            @packageIo=\"addPackageIO\"\n            @recording=\"startRecording\"\n            @copy=\"copySelectedElements\"\n            @paste=\"pasteCopiedElements\"\n            @saveBlock=\"initBlockFolder\"\n            @duplicate=\"duplicateElements\"\n          />\n        </ui-tab-panel>\n      </ui-tab-panels>\n    </div>\n  </div>\n  <ui-modal\n    v-model=\"modalState.show\"\n    :content-class=\"activeWorkflowModal?.width || 'max-w-xl'\"\n    v-bind=\"activeWorkflowModal.attrs || {}\"\n  >\n    <template v-if=\"activeWorkflowModal.title\" #header>\n      {{ activeWorkflowModal.title }}\n      <a\n        v-if=\"activeWorkflowModal.docs\"\n        :title=\"t('common.docs')\"\n        :href=\"activeWorkflowModal.docs\"\n        target=\"_blank\"\n        class=\"inline-block align-middle\"\n      >\n        <v-remixicon name=\"riInformationLine\" size=\"20\" />\n      </a>\n    </template>\n    <component\n      :is=\"activeWorkflowModal.component\"\n      v-bind=\"{ workflow }\"\n      v-on=\"activeWorkflowModal?.events || {}\"\n      @update=\"updateWorkflow\"\n      @close=\"modalState.show = false\"\n    />\n  </ui-modal>\n  <shared-permissions-modal\n    v-model=\"permissionState.showModal\"\n    :permissions=\"permissionState.items\"\n    @granted=\"registerTrigger\"\n  />\n  <ui-modal v-model=\"blockFolderModal.showModal\" :title=\"t('packages.set')\">\n    <editor-add-package\n      :data=\"{\n        name: blockFolderModal.name,\n        description: blockFolderModal.description,\n        icon: blockFolderModal.icon,\n      }\"\n      @update=\"Object.assign(blockFolderModal, $event)\"\n      @cancel=\"clearBlockFolderModal\"\n      @add=\"saveBlockToFolder\"\n    />\n  </ui-modal>\n</template>\n<script setup>\nimport PackageDetails from '@/components/newtab/package/PackageDetails.vue';\nimport PackageSettings from '@/components/newtab/package/PackageSettings.vue';\nimport SharedPermissionsModal from '@/components/newtab/shared/SharedPermissionsModal.vue';\nimport EditorAddPackage from '@/components/newtab/workflow/editor/EditorAddPackage.vue';\nimport EditorDebugging from '@/components/newtab/workflow/editor/EditorDebugging.vue';\nimport EditorLocalActions from '@/components/newtab/workflow/editor/EditorLocalActions.vue';\nimport EditorLocalCtxMenu from '@/components/newtab/workflow/editor/EditorLocalCtxMenu.vue';\nimport EditorLocalSavedBlocks from '@/components/newtab/workflow/editor/EditorLocalSavedBlocks.vue';\nimport EditorPkgActions from '@/components/newtab/workflow/editor/EditorPkgActions.vue';\nimport EditorUsedCredentials from '@/components/newtab/workflow/editor/EditorUsedCredentials.vue';\nimport WorkflowDataTable from '@/components/newtab/workflow/WorkflowDataTable.vue';\nimport WorkflowDetailsCard from '@/components/newtab/workflow/WorkflowDetailsCard.vue';\nimport WorkflowEditBlock from '@/components/newtab/workflow/WorkflowEditBlock.vue';\nimport WorkflowEditor from '@/components/newtab/workflow/WorkflowEditor.vue';\nimport WorkflowGlobalData from '@/components/newtab/workflow/WorkflowGlobalData.vue';\nimport WorkflowSettings from '@/components/newtab/workflow/WorkflowSettings.vue';\nimport WorkflowShare from '@/components/newtab/workflow/WorkflowShare.vue';\nimport WorkflowShareTeam from '@/components/newtab/workflow/WorkflowShareTeam.vue';\nimport { useCommandManager } from '@/composable/commandManager';\nimport { useGroupTooltip } from '@/composable/groupTooltip';\nimport {\n  getReadableShortcut,\n  getShortcut,\n  useShortcut,\n} from '@/composable/shortcut';\nimport dbStorage from '@/db/storage';\nimport emitter from '@/lib/mitt';\nimport startRecordWorkflow from '@/newtab/utils/startRecordWorkflow';\nimport RendererWorkflowService from '@/service/renderer/RendererWorkflowService';\nimport { usePackageStore } from '@/stores/package';\nimport { useTeamWorkflowStore } from '@/stores/teamWorkflow';\nimport { useUserStore } from '@/stores/user';\nimport { useWorkflowStore } from '@/stores/workflow';\nimport { fetchApi } from '@/utils/api';\nimport convertWorkflowData from '@/utils/convertWorkflowData';\nimport DroppedNode from '@/utils/editor/DroppedNode';\nimport extractAutocopmleteData from '@/utils/editor/editorAutocomplete';\nimport EditorCommands from '@/utils/editor/EditorCommands';\nimport { getBlocks } from '@/utils/getSharedData';\nimport { debounce, getActiveTab, parseJSON, throttle } from '@/utils/helper';\nimport { excludeGroupBlocks } from '@/utils/shared';\nimport { getWorkflowPermissions } from '@/utils/workflowData';\nimport { registerWorkflowTrigger } from '@/utils/workflowTrigger';\nimport functions from '@/workflowEngine/templating/templatingFunctions';\nimport { useHead } from '@vueuse/head';\nimport dagre from 'dagre';\nimport defu from 'defu';\nimport cloneDeep from 'lodash.clonedeep';\nimport { customAlphabet } from 'nanoid';\nimport {\n  computed,\n  markRaw,\n  onBeforeUnmount,\n  onDeactivated,\n  onMounted,\n  provide,\n  reactive,\n  ref,\n  shallowRef,\n  watch,\n} from 'vue';\nimport { useI18n } from 'vue-i18n';\nimport { onBeforeRouteLeave, useRoute, useRouter } from 'vue-router';\nimport { useToast } from 'vue-toastification';\nimport browser from 'webextension-polyfill';\n\nconst blocks = getBlocks();\n\nlet editorCommands = null;\nconst executeCommandTimeout = null;\nconst nanoid = customAlphabet('1234567890abcdefghijklmnopqrstuvwxyz', 7);\n\nuseGroupTooltip();\n\nconst { t, te } = useI18n();\nconst toast = useToast();\nconst route = useRoute();\nconst router = useRouter();\nconst userStore = useUserStore();\nconst packageStore = usePackageStore();\nconst workflowStore = useWorkflowStore();\nconst commandManager = useCommandManager();\nconst teamWorkflowStore = useTeamWorkflowStore();\n\nconst { teamId, id: workflowId } = route.params;\nconst isTeamWorkflow = route.name === 'team-workflows';\nconst isPackage = route.name === 'packages-details';\nconst funcsAutocomplete = Object.keys(functions).reduce((acc, name) => {\n  acc[`$${name}`] = '';\n\n  return acc;\n}, {});\n\nconst editor = shallowRef(null);\nconst connectedTable = shallowRef(null);\n\nconst sidebarRef = ref(null);\nconst sidebarCss = reactive({\n  width: 360,\n  padding: '20px',\n  isDragging: false,\n  startX: 0,\n  startWidth: 0,\n});\nconst drag = (event) => {\n  if (sidebarCss.isDragging) {\n    const diffX = event.clientX - sidebarCss.startX;\n    sidebarCss.width = Math.max(360, sidebarCss.startWidth + diffX); // min-width : 360px,max-width: 30%\n  }\n};\nconst stopDrag = () => {\n  sidebarCss.isDragging = false;\n  document.removeEventListener('mousemove', drag);\n  document.removeEventListener('mouseup', stopDrag);\n};\n\nconst startDrag = (event) => {\n  sidebarCss.isDragging = true;\n  sidebarCss.startX = event.clientX;\n  sidebarCss.startWidth = sidebarCss.width;\n  document.addEventListener('mousemove', drag);\n  document.addEventListener('mouseup', stopDrag);\n};\n\nconst state = reactive({\n  showSidebar: true,\n  sidebarState: true,\n  dataChanged: false,\n  animateBlocks: false,\n  isExecuteCommand: false,\n  workflowConverted: false,\n  activeTab: route.query.tab || 'editor',\n});\nconst blockFolderModal = reactive({\n  name: '',\n  icon: '',\n  nodes: [],\n  description: '',\n  showList: false,\n  showModal: false,\n});\nconst permissionState = reactive({\n  permissions: [],\n  showModal: false,\n});\nconst modalState = reactive({\n  name: '',\n  show: false,\n});\nconst editState = reactive({\n  blockData: {},\n  editing: false,\n});\nconst autocompleteState = reactive({\n  blocks: {},\n  common: {},\n});\nconst workflowPayload = {\n  data: {},\n  isUpdating: false,\n};\n\nconst workflowModals = {\n  table: {\n    icon: 'riKey2Line',\n    width: 'max-w-2xl',\n    component: WorkflowDataTable,\n    title: t('workflow.table.title'),\n    docs: 'https://docs.extension.automa.site/workflow/table.html',\n    events: {\n      /* eslint-disable-next-line */\n      connect: fetchConnectedTable,\n      disconnect() {\n        connectedTable.value = null;\n      },\n    },\n  },\n  'workflow-share': {\n    icon: 'riShareLine',\n    component: WorkflowShare,\n    attrs: {\n      blur: true,\n      persist: true,\n      customContent: true,\n    },\n    events: {\n      close() {\n        modalState.show = false;\n        modalState.name = '';\n      },\n      publish() {\n        modalState.show = false;\n        modalState.name = '';\n      },\n    },\n  },\n  'workflow-share-team': {\n    icon: 'riShareLine',\n    component: WorkflowShareTeam,\n    attrs: {\n      blur: true,\n      persist: true,\n      customContent: true,\n    },\n    events: {\n      close() {\n        modalState.show = false;\n        modalState.name = '';\n      },\n      publish() {\n        modalState.show = false;\n        modalState.name = '';\n      },\n    },\n  },\n  'global-data': {\n    width: 'max-w-2xl',\n    icon: 'riDatabase2Line',\n    component: WorkflowGlobalData,\n    title: t('common.globalData'),\n    docs: 'https://docs.extension.automa.site/workflow/global-data.html',\n  },\n  settings: {\n    width: 'max-w-2xl',\n    icon: 'riSettings3Line',\n    component: WorkflowSettings,\n    title: t('common.settings'),\n    attrs: {\n      customContent: true,\n    },\n    events: {\n      close() {\n        modalState.show = false;\n        modalState.name = '';\n      },\n    },\n  },\n};\n\nconst autocompleteList = computed(() => {\n  const autocompleteData = {\n    loopData: {},\n    googleSheets: {},\n    table: {},\n    ...funcsAutocomplete,\n    globalData: autocompleteState.common.globalData,\n    variables: { ...autocompleteState.common.variables },\n  };\n\n  Object.values(autocompleteState.blocks).forEach((item) => {\n    if (item.loopData) Object.assign(autocompleteData.loopData, item.loopData);\n    if (item.variables)\n      Object.assign(autocompleteData.variables, item.variables);\n    if (item.googleSheets)\n      Object.assign(autocompleteData.googleSheets, item.googleSheets);\n  });\n\n  return autocompleteData;\n});\nconst haveEditAccess = computed(() => {\n  if (!isTeamWorkflow) return true;\n\n  return userStore.validateTeamAccess(teamId, ['edit', 'owner', 'create']);\n});\nconst workflow = computed(() => {\n  if (isTeamWorkflow) {\n    return teamWorkflowStore.getById(teamId, workflowId);\n  }\n  if (isPackage) {\n    return packageStore.getById(workflowId);\n  }\n\n  return workflowStore.getById(workflowId);\n});\nconst workflowStates = computed(() =>\n  workflowStore.getWorkflowStates(route.params.id)\n);\nconst activeWorkflowModal = computed(\n  () => workflowModals[modalState.name] || {}\n);\nconst workflowColumns = computed(() => {\n  if (connectedTable.value) {\n    return connectedTable.value.columns;\n  }\n\n  return workflow.value.table;\n});\nconst editorData = computed(() => {\n  if (isPackage) return workflow.value.data;\n\n  return workflow.value.drawflow;\n});\n\nconst updateBlockData = debounce((data) => {\n  console.log('🚀 ~ updateBlockData ~ data:', data);\n  if (!haveEditAccess.value) return;\n  const node = editor.value.getNode.value(editState.blockData.blockId);\n  const dataCopy = JSON.parse(JSON.stringify(data));\n\n  let autocompleteId = '';\n\n  if (editState.blockData.itemId) {\n    const itemIndex = node.data.blocks.findIndex(\n      ({ itemId }) => itemId === editState.blockData.itemId\n    );\n\n    if (itemIndex !== -1) {\n      node.data.blocks[itemIndex].data = dataCopy;\n      autocompleteId = editState.blockData.itemId;\n    }\n  } else {\n    node.data = dataCopy;\n    autocompleteId = editState.blockData.blockId;\n  }\n\n  if (autocompleteState.blocks[autocompleteId]) {\n    const { id, blockId } = editState.blockData;\n    Object.assign(\n      autocompleteState.blocks,\n      /* eslint-disable-next-line */\n      extractAutocopmleteData(id, { data, id: blockId })\n    );\n  }\n\n  editState.blockData.data = data;\n  state.dataChanged = true;\n}, 250);\nconst updateHostedWorkflow = throttle(async () => {\n  if (isTeamWorkflow) return;\n  if (!userStore.user || workflowPayload.isUpdating) return;\n\n  const isHosted = userStore.hostedWorkflows[route.params.id];\n  const isBackup = userStore.backupIds?.includes(route.params.id);\n  const workflowExist = workflowStore.getById(route.params.id);\n\n  if (\n    (!isBackup && !isHosted) ||\n    !workflowExist ||\n    Object.keys(workflowPayload.data).length === 0\n  )\n    return;\n\n  workflowPayload.isUpdating = true;\n\n  const delKeys = [\n    'id',\n    'pass',\n    'logs',\n    'trigger',\n    'createdAt',\n    'isDisabled',\n    'isProtected',\n  ];\n  delKeys.forEach((key) => {\n    delete workflowPayload.data[key];\n  });\n\n  try {\n    if (typeof workflowPayload.data.drawflow === 'string') {\n      workflowPayload.data.drawflow = parseJSON(\n        workflowPayload.data.drawflow,\n        workflowPayload.data.drawflow\n      );\n    }\n\n    const response = await fetchApi(`/me/workflows/${route.params.id}`, {\n      auth: true,\n      method: 'PUT',\n      keepalive: true,\n      body: JSON.stringify({\n        workflow: workflowPayload.data,\n      }),\n    });\n\n    if (!response.ok) throw new Error(response.message);\n    if (isBackup) {\n      const result = await response.json();\n\n      if (result.updatedAt) {\n        await browser.storage.local.set({ lastBackup: result.updatedAt });\n      }\n    }\n\n    workflowPayload.data = {};\n    workflowPayload.isUpdating = false;\n  } catch (error) {\n    console.error(error);\n    workflowPayload.isUpdating = false;\n  }\n}, 5000);\nconst onEdgesChange = debounce((changes) => {\n  changes.forEach(({ type, item }) => {\n    if (\n      type === 'add' &&\n      item.sourceHandle.includes('output') &&\n      item.targetHandle.includes('output')\n    ) {\n      editor.value.removeEdges([item.id]);\n\n      return;\n    }\n\n    if (state.dataChanged) return;\n    state.dataChanged = type !== 'select';\n  });\n}, 250);\n\nfunction onTabChange(tabVal) {\n  if (tabVal === 'logs') {\n    emitter.emit('ui:logs', {\n      workflowId,\n      show: true,\n    });\n    return;\n  }\n\n  state.activeTab = tabVal;\n}\nfunction onUpdateBlockSettings({ blockId, itemId, settings }) {\n  state.dataChanged = true;\n\n  if (!editState.editing) return;\n  if (itemId && itemId !== editState.blockData.itemId) return;\n  if (editState.blockData.blockId !== blockId) return;\n\n  editState.blockData.data = { ...editState.blockData.data, ...settings };\n}\nfunction closeEditingCard() {\n  editState.editing = false;\n  editState.blockData = {};\n\n  state.showSidebar = state.sidebarState;\n}\nasync function executeFromBlock(blockId) {\n  try {\n    if (!blockId) return;\n\n    const workflowOptions = { blockId };\n\n    let tab = await getActiveTab();\n    if (!tab) {\n      [tab] = await browser.tabs.query({ active: true, url: '*://*/*' });\n    }\n    if (tab) {\n      workflowOptions.tabId = tab.id;\n    }\n\n    RendererWorkflowService.executeWorkflow(workflow.value, workflowOptions);\n  } catch (error) {\n    console.error(error);\n  }\n}\nfunction startRecording({ nodeId, handleId }) {\n  if (state.dataChanged) {\n    alert('Make sure to save the workflow before starting recording');\n    return;\n  }\n\n  const options = {\n    workflowId,\n    name: workflow.value.name,\n    connectFrom: {\n      id: nodeId,\n      output: handleId,\n    },\n  };\n  startRecordWorkflow(options).then(() => {\n    state.dataChanged = false;\n    router.replace('/recording');\n  });\n}\nfunction goToBlock(blockId) {\n  if (!editor.value) return;\n\n  const block = editor.value.getNode.value(blockId);\n  if (!block) return;\n\n  editor.value.addSelectedNodes([block]);\n  setTimeout(() => {\n    const editorContainer = document.querySelector('.vue-flow');\n    const { height, width } = editorContainer.getBoundingClientRect();\n    const { x, y } = block.position;\n\n    editor.value.setTransform({\n      y: -(y - height / 2),\n      x: -(x - width / 2) - 200,\n      zoom: 1,\n    });\n  }, 200);\n}\nfunction goToPkgBlock(blockId) {\n  state.activeTab = 'editor';\n  goToBlock(blockId);\n}\nfunction addPackageIO({ type, handleId, nodeId }) {\n  const copyPkgIO = [...workflow.value[type]];\n  const itemExist = copyPkgIO.some(\n    (io) => io.blockId === nodeId && handleId === io.handleId\n  );\n  if (itemExist) {\n    toast.error(`You already add this as an ${type.slice(0, -1)}`);\n    return;\n  }\n\n  copyPkgIO.push({\n    handleId,\n    name: '',\n    id: nanoid(),\n    blockId: nodeId,\n  });\n\n  /* eslint-disable-next-line */\n  updateWorkflow({ [type]: copyPkgIO });\n}\nfunction initBlockFolder({ nodes }) {\n  Object.assign(blockFolderModal, {\n    nodes,\n    showModal: true,\n  });\n}\nfunction clearBlockFolderModal() {\n  Object.assign(blockFolderModal, {\n    name: '',\n    nodes: [],\n    asBlock: false,\n    description: '',\n    showModal: false,\n    icon: 'mdiPackageVariantClosed',\n  });\n}\nasync function saveBlockToFolder() {\n  try {\n    const seen = new Set();\n    const nodeList = [\n      ...editor.value.getSelectedNodes.value,\n      ...blockFolderModal.nodes,\n    ].reduce((acc, node) => {\n      if (seen.has(node.id)) return acc;\n\n      const { label, data, position, id, type } = node;\n      acc.push(cloneDeep({ label, data, position, id, type }));\n      seen.add(node.id);\n\n      return acc;\n    }, []);\n    const edges = editor.value.getSelectedEdges.value.map(\n      ({ source, target, targetHandle, sourceHandle, id }) =>\n        cloneDeep({ id, source, target, targetHandle, sourceHandle })\n    );\n\n    packageStore.insert({\n      data: { nodes: nodeList, edges },\n      name: blockFolderModal.name || 'unnamed',\n      description: blockFolderModal.description,\n      asBlock: blockFolderModal?.asBlock ?? false,\n      icon: blockFolderModal.icon || 'mdiPackageVariantClosed',\n    });\n\n    clearBlockFolderModal();\n  } catch (error) {\n    console.error(error);\n  }\n}\nfunction groupBlocks({ position }) {\n  const nodesToDelete = [];\n  const nodes = editor.value.getSelectedNodes.value;\n  const groupBlocksList = nodes.reduce((acc, node) => {\n    if (excludeGroupBlocks.includes(node.label)) return acc;\n\n    acc.push({\n      id: node.label,\n      itemId: node.id,\n      data: node.data,\n    });\n    nodesToDelete.push(node);\n\n    return acc;\n  }, []);\n\n  if (groupBlocksList.length === 0) return;\n\n  editor.value.removeNodes(nodesToDelete);\n\n  const { component, data } = blocks['blocks-group'];\n  editor.value.addNodes([\n    {\n      id: nanoid(),\n      type: component,\n      label: 'blocks-group',\n      data: { ...data, blocks: groupBlocksList },\n      position: editor.value.project({\n        x: position.clientX - 360,\n        y: position.clientY,\n      }),\n    },\n  ]);\n}\nfunction ungroupBlocks({ nodes }) {\n  const [node] = nodes;\n  if (!node || node.label !== 'blocks-group') return;\n\n  const edges = [];\n  const position = { ...node.position };\n  const copyBlocks = cloneDeep(node.data?.blocks || []);\n  const groupBlocksList = copyBlocks.map((item, index) => {\n    const nextNode = copyBlocks[index + 1];\n    if (nextNode) {\n      edges.push({\n        source: item.itemId,\n        target: nextNode.itemId,\n        sourceHandle: `${item.itemId}-output-1`,\n        targetHandle: `${nextNode.itemId}-input-1`,\n      });\n    }\n\n    item.label = item.id;\n    item.id = item.itemId;\n    item.position = { ...position };\n    item.type = blocks[item.label].component;\n\n    delete item.itemId;\n\n    position.x += 250;\n\n    return item;\n  });\n\n  editor.value.removeNodes(nodes);\n  editor.value.addNodes(groupBlocksList);\n  editor.value.addSelectedNodes(groupBlocksList);\n  editor.value.addEdges(edges);\n}\nasync function initAutocomplete() {\n  const autocompleteCache = sessionStorage.getItem(\n    `autocomplete:${workflowId}`\n  );\n  if (autocompleteCache) {\n    const objData = parseJSON(autocompleteCache, {});\n    autocompleteState.blocks = objData;\n  } else {\n    const autocompleteData = {};\n    editorData.value.nodes.forEach(({ label, id, data }) => {\n      Object.assign(\n        autocompleteData,\n        extractAutocopmleteData(label, { data, id })\n      );\n    });\n\n    autocompleteState.blocks = autocompleteData;\n  }\n\n  try {\n    const storageVars = await dbStorage.variables.toArray();\n    autocompleteState.common.globalData = parseJSON(\n      workflow.value.globalData,\n      {}\n    );\n    autocompleteState.common.variables = {};\n\n    storageVars.forEach((variable) => {\n      autocompleteState.common.variables[`$$${variable.name}`] = {};\n    });\n  } catch (error) {\n    console.error(error);\n  }\n}\nfunction registerTrigger() {\n  const triggerBlock = editorData.value.nodes.find(\n    (node) => node.label === 'trigger'\n  );\n  registerWorkflowTrigger(workflowId, triggerBlock);\n}\nfunction executeCommand(type) {\n  state.isExecuteCommand = true;\n\n  if (type === 'undo') {\n    commandManager.undo();\n  } else if (type === 'redo') {\n    commandManager.redo();\n  }\n\n  clearTimeout(executeCommandTimeout);\n  setTimeout(() => {\n    state.isExecuteCommand = false;\n  }, 500);\n}\nfunction onNodesChange(changes) {\n  const nodeChanges = { added: [], removed: [] };\n  changes.forEach(({ type, id, item }) => {\n    if (type === 'remove') {\n      if (editState.blockData.blockId === id) {\n        editState.editing = false;\n        editState.blockData = {};\n      }\n\n      state.dataChanged = true;\n      nodeChanges.removed.push(id);\n    } else if (type === 'add') {\n      if (isPackage) {\n        const excludeBlocks = ['block-package', 'trigger', 'execute-workflow'];\n        if (excludeBlocks.includes(item.label)) {\n          editor.value.removeNodes([item]);\n        }\n\n        return;\n      }\n\n      nodeChanges.added.push(item);\n    }\n  });\n\n  if (state.isExecuteCommand) return;\n\n  let command = null;\n\n  if (nodeChanges.added.length > 0) {\n    command = editorCommands.nodeAdded(nodeChanges.added);\n  } else if (nodeChanges.removed.length > 0) {\n    command = editorCommands.nodeRemoved(nodeChanges.removed);\n  }\n\n  if (command) {\n    commandManager.add(command);\n  }\n}\nfunction autoAlign() {\n  state.animateBlocks = true;\n\n  const graph = new dagre.graphlib.Graph();\n  graph.setGraph({\n    rankdir: 'LR',\n    ranksep: 100,\n    ranker: 'tight-tree',\n  });\n  graph._isMultigraph = true;\n  graph.setDefaultEdgeLabel(() => ({}));\n  editor.value.getNodes.value.forEach(\n    ({ id, label, dimensions, parentNode }) => {\n      if (label === 'blocks-group-2' || parentNode) return;\n\n      graph.setNode(id, {\n        label,\n        width: dimensions.width,\n        height: dimensions.height,\n      });\n    }\n  );\n  editor.value.getEdges.value.forEach(({ source, target, id }) => {\n    graph.setEdge(source, target, { id });\n  });\n\n  dagre.layout(graph);\n  const nodeChanges = [];\n  graph.nodes().forEach((nodeId) => {\n    const graphNode = graph.node(nodeId);\n    if (!graphNode) return;\n\n    const { x, y } = graphNode;\n\n    if (editorCommands.state.nodes[nodeId]) {\n      editorCommands.state.nodes[nodeId].position = { x, y };\n    }\n\n    nodeChanges.push({\n      id: nodeId,\n      type: 'position',\n      dragging: false,\n      position: { x, y },\n    });\n  });\n\n  editor.value.applyNodeChanges(nodeChanges);\n  editor.value.fitView();\n\n  setTimeout(() => {\n    state.dataChanged = true;\n    state.animateBlocks = false;\n  }, 500);\n}\nfunction toggleSidebar() {\n  state.showSidebar = !state.showSidebar;\n  localStorage.setItem('workflow:sidebar', state.showSidebar);\n}\nfunction initEditBlock(data) {\n  const { editComponent, data: blockDefData, name } = blocks[data.id];\n\n  if (!editComponent) return;\n\n  const blockData = defu(data.data, blockDefData);\n  const blockEditComponent =\n    typeof editComponent === 'string' ? editComponent : markRaw(editComponent);\n\n  editState.blockData = {\n    ...data,\n    editComponent: blockEditComponent,\n    name,\n    data: blockData,\n  };\n\n  if (data.id === 'wait-connections') {\n    const connections = editor.value.getEdges.value.reduce(\n      (acc, { target, sourceNode, source }) => {\n        if (target !== data.blockId) return acc;\n\n        const blockNameKey = `workflow.blocks.${sourceNode.label}.name`;\n        let blockName = te(blockNameKey)\n          ? t(blockNameKey)\n          : blocks[sourceNode.label].name;\n\n        const { description, name: groupName } = sourceNode.data;\n        if (description || groupName)\n          blockName += ` (${description || groupName})`;\n\n        acc.push({\n          id: source,\n          name: blockName,\n        });\n\n        return acc;\n      },\n      []\n    );\n\n    editState.blockData.connections = connections;\n  }\n\n  state.showSidebar = true;\n  editState.editing = true;\n}\nasync function updateWorkflow(data) {\n  try {\n    if (isPackage) {\n      if (workflow.value.isExternal) return;\n\n      delete data.drawflow;\n      await packageStore.update({\n        id: workflowId,\n        data,\n      });\n      return;\n    }\n\n    if (isTeamWorkflow) {\n      if (!haveEditAccess.value && !data.globalData) return;\n      await teamWorkflowStore.update({\n        data,\n        teamId,\n        id: workflowId,\n      });\n    } else {\n      await workflowStore.update({\n        data,\n        id: route.params.id,\n      });\n    }\n\n    workflowPayload.data = { ...workflowPayload.data, ...data };\n\n    if (!isTeamWorkflow) await updateHostedWorkflow();\n  } catch (error) {\n    console.error(error);\n  }\n}\nfunction onActionUpdated({ data, changedIndicator }) {\n  state.dataChanged = changedIndicator;\n\n  workflowPayload.data = { ...workflowPayload.data, ...data };\n  if (!isPackage) updateHostedWorkflow();\n}\nfunction onEditorInit(instance) {\n  editor.value = instance;\n\n  let nodeToConnect = null;\n\n  instance.onEdgesChange(onEdgesChange);\n  instance.onNodesChange(onNodesChange);\n  instance.onEdgeDoubleClick(({ edge }) => {\n    instance.removeEdges([edge]);\n  });\n  instance.onConnectStart(({ nodeId, handleId, handleType }) => {\n    if (handleType !== 'source') return;\n\n    nodeToConnect = { nodeId, handleId };\n  });\n  instance.onConnectEnd(({ target }) => {\n    if (!nodeToConnect) return;\n\n    if (target.hasAttribute('data-handleid')) {\n      const handleId = target.getAttribute('data-handleid');\n      if (handleId.includes('-output-')) return;\n    }\n\n    const isNotTargetHandle = !target.closest('.vue-flow__handle.target');\n    const targetNode = isNotTargetHandle && target.closest('.vue-flow__node');\n\n    if (targetNode && targetNode.dataset.id !== nodeToConnect.nodeId) {\n      const nodeId = targetNode.dataset.id;\n      const nodeData = editor.value.getNode.value(nodeId);\n\n      if (nodeData && nodeData.handleBounds.target.length >= 1) {\n        const targetHandle = nodeData.handleBounds.target.find(\n          (item) => item.id\n        );\n        if (!targetHandle) return;\n\n        editor.value.addEdges([\n          {\n            target: nodeId,\n            source: nodeToConnect.nodeId,\n            targetHandle: targetHandle.id,\n            sourceHandle: nodeToConnect.handleId,\n          },\n        ]);\n      }\n    }\n\n    nodeToConnect = null;\n  });\n  // instance.onEdgeUpdateEnd(({ edge }) => {\n  //   editorCommands.state.edges[edge.id] = edge;\n  // });\n\n  instance.onNodeDragStop(({ nodes }) => {\n    if (!editorCommands?.state?.nodes) return;\n\n    nodes.forEach((node) => {\n      editorCommands.state.nodes[node.id] = node;\n    });\n  });\n\n  const convertToObj = (array) =>\n    array.reduce((acc, item) => {\n      acc[item.id] = item;\n\n      return acc;\n    }, {});\n  setTimeout(() => {\n    const commandInitState = {\n      nodes: convertToObj(instance.getNodes.value),\n      edges: convertToObj(instance.getEdges.value),\n    };\n    editorCommands = new EditorCommands(instance, commandInitState);\n  }, 1000);\n\n  const { blockId } = route.query;\n  if (blockId) goToBlock(blockId);\n}\nfunction clearHighlightedElements() {\n  const elements = document.querySelectorAll(\n    '.dropable-area__node, .dropable-area__handle'\n  );\n  elements.forEach((element) => {\n    element.classList.remove('dropable-area__node');\n    element.classList.remove('dropable-area__handle');\n  });\n}\nfunction toggleHighlightElement({ target, elClass, classes }) {\n  const targetEl = target.closest(elClass);\n\n  if (targetEl) {\n    targetEl.classList.add(classes);\n  } else {\n    const elements = document.querySelectorAll(`.${classes}`);\n    elements.forEach((element) => {\n      element.classList.remove(classes);\n    });\n  }\n}\nfunction onDragoverEditor({ target }) {\n  toggleHighlightElement({\n    target,\n    elClass: '.vue-flow__handle.source',\n    classes: 'dropable-area__handle',\n  });\n\n  if (!target.closest('.vue-flow__handle')) {\n    toggleHighlightElement({\n      target,\n      elClass: '.vue-flow__node:not(.vue-flow__node-BlockGroup)',\n      classes: 'dropable-area__node',\n    });\n  }\n}\nfunction onDropInEditor({ dataTransfer, clientX, clientY, target }) {\n  const savedBlocks = parseJSON(dataTransfer.getData('savedBlocks'), null);\n\n  const editorRect = editor.value.viewportRef.value.getBoundingClientRect();\n  const position = editor.value.project({\n    y: clientY - editorRect.top,\n    x: clientX - editorRect.left,\n  });\n\n  if (savedBlocks && !isPackage) {\n    if (savedBlocks.settings.asBlock) {\n      editor.value.addNodes([\n        {\n          position,\n          id: nanoid(),\n          data: savedBlocks,\n          type: 'BlockPackage',\n          label: 'block-package',\n        },\n      ]);\n    } else {\n      const { nodes, edges } = savedBlocks.data;\n      /* eslint-disable-next-line */\n      const newElements = copyElements(nodes, edges, { clientX, clientY });\n\n      editor.value.addNodes(newElements.nodes);\n      editor.value.addEdges(newElements.edges);\n    }\n\n    state.dataChanged = true;\n    return;\n  }\n\n  const block = parseJSON(dataTransfer.getData('block'), null);\n  if (!block || block.fromBlockBasic) return;\n\n  if (block.id === 'trigger' && isPackage) return;\n\n  clearHighlightedElements();\n\n  const isTriggerExists =\n    block.id === 'trigger' &&\n    editor.value.getNodes.value.some((node) => node.label === 'trigger');\n  if (isTriggerExists) return;\n\n  const nodeEl = DroppedNode.isNode(target);\n  if (nodeEl) {\n    DroppedNode.replaceNode(editor.value, { block, target: nodeEl });\n    return;\n  }\n\n  const nodeId = nanoid();\n  const newNode = {\n    position,\n    label: block.id,\n    data: block.data,\n    type: block.component,\n    id: block.id === 'blocks-group-2' ? `group-${nodeId}` : nodeId,\n  };\n  editor.value.addNodes([newNode]);\n\n  const edgeEl = DroppedNode.isEdge(target);\n  const handleEl = DroppedNode.isHandle(target);\n\n  if (handleEl) {\n    DroppedNode.appendNode(editor.value, {\n      target: handleEl,\n      nodeId: newNode.id,\n    });\n  } else if (edgeEl) {\n    DroppedNode.insertBetweenNode(editor.value, {\n      target: edgeEl,\n      nodeId: newNode.id,\n      outputs: block.outputs,\n    });\n  }\n\n  if (block.fromGroup) {\n    setTimeout(() => {\n      const blockEl = document.querySelector(`[data-id=\"${newNode.id}\"]`);\n      blockEl?.setAttribute('group-item-id', block.itemId);\n    }, 200);\n  }\n\n  state.dataChanged = true;\n}\nfunction copyElements(nodes, edges, initialPos) {\n  const newIds = new Map();\n  let firstNodePos = null;\n\n  const newNodes = nodes.map(({ id, label, position, data, type }, index) => {\n    const newNodeId = nanoid();\n\n    const nodePos = {\n      z: position.z || 0,\n      y: position.y + 50,\n      x: position.x + 50,\n    };\n    newIds.set(id, newNodeId);\n\n    if (initialPos) {\n      if (index === 0) {\n        firstNodePos = {\n          x: nodePos.x,\n          y: nodePos.y,\n        };\n        initialPos = editor.value.project({\n          y: initialPos.clientY,\n          x: initialPos.clientX - 360,\n        });\n\n        Object.assign(nodePos, initialPos);\n      } else {\n        const xDistance = nodePos.x - firstNodePos.x;\n        const yDistance = nodePos.y - firstNodePos.y;\n\n        nodePos.x = initialPos.x + xDistance;\n        nodePos.y = initialPos.y + yDistance;\n      }\n    }\n\n    const copyNode = cloneDeep({\n      data,\n      label,\n      id: newNodeId,\n      selected: true,\n      position: nodePos,\n      type: type || blocks[label].component,\n    });\n    copyNode.data = reactive(copyNode.data);\n\n    return copyNode;\n  });\n  const newEdges = edges.reduce(\n    (acc, { target, targetHandle, source, sourceHandle }) => {\n      const targetId = newIds.get(target);\n      const sourceId = newIds.get(source);\n\n      if (!targetId || !sourceId) return acc;\n\n      const copyEdge = cloneDeep({\n        selected: true,\n        target: targetId,\n        source: sourceId,\n        id: `edge-${nanoid()}`,\n        targetHandle: targetHandle.replace(target, targetId),\n        sourceHandle: sourceHandle.replace(source, sourceId),\n      });\n      acc.push(copyEdge);\n\n      return acc;\n    },\n    []\n  );\n\n  return {\n    nodes: newNodes,\n    edges: newEdges,\n  };\n}\nfunction duplicateElements({ nodes, edges }) {\n  const selectedNodes = editor.value.getSelectedNodes.value;\n  const selectedEdges = editor.value.getSelectedEdges.value;\n\n  const { edges: newEdges, nodes: newNodes } = copyElements(\n    nodes || selectedNodes,\n    edges || selectedEdges\n  );\n\n  selectedNodes.forEach((node) => {\n    node.selected = false;\n  });\n  selectedEdges.forEach((edge) => {\n    edge.selected = false;\n  });\n\n  editor.value.addNodes(newNodes);\n  editor.value.addEdges(newEdges);\n\n  state.dataChanged = true;\n}\nfunction copySelectedElements(data = {}) {\n  const nodes = data.nodes || editor.value.getSelectedNodes.value;\n  const edges = data.edges || editor.value.getSelectedEdges.value;\n\n  const clipboardData = JSON.stringify({\n    name: 'automa-blocks',\n    data: { nodes, edges },\n  });\n  navigator.clipboard.writeText(clipboardData).catch((error) => {\n    console.error(error);\n  });\n}\nasync function pasteCopiedElements(position) {\n  editor.value.removeSelectedNodes(editor.value.getSelectedNodes.value);\n  editor.value.removeSelectedEdges(editor.value.getSelectedEdges.value);\n\n  const permission = await browser.permissions.request({\n    permissions: ['clipboardRead'],\n  });\n  if (!permission) {\n    toast.error('Automa require clipboard permission to paste blocks');\n    return;\n  }\n\n  try {\n    const copiedText = await navigator.clipboard.readText();\n    const workflowBlocks = parseJSON(copiedText);\n\n    if (workflowBlocks && workflowBlocks.name === 'automa-blocks') {\n      const { nodes, edges } = copyElements(\n        workflowBlocks.data.nodes,\n        workflowBlocks.data.edges,\n        position\n      );\n      editor.value.addNodes(nodes);\n      editor.value.addEdges(edges);\n\n      state.dataChanged = true;\n    }\n  } catch (error) {\n    console.error(error);\n  }\n}\nfunction undoRedoCommand(type, { target }) {\n  const els = ['INPUT', 'SELECT', 'TEXTAREA'];\n  if (els.includes(target.tagName) || target.isContentEditable) return;\n\n  executeCommand(type);\n}\nfunction onKeydown({ ctrlKey, metaKey, shiftKey, key, target, repeat }) {\n  if (repeat) return;\n\n  const els = ['INPUT', 'SELECT', 'TEXTAREA'];\n  if (\n    els.includes(target.tagName) ||\n    target.isContentEditable ||\n    !target.closest('.workflow-editor')\n  )\n    return;\n\n  if (isPackage && workflow.value.isExternal) return;\n\n  const command = (keyName) => (ctrlKey || metaKey) && keyName === key;\n  if (command('c')) {\n    copySelectedElements();\n  } else if (command('v')) {\n    pasteCopiedElements();\n  } else if (command('z')) {\n    undoRedoCommand(shiftKey ? 'redo' : 'undo', { target });\n  }\n}\nasync function fetchConnectedTable() {\n  const table = await dbStorage.tablesItems\n    .where('id')\n    .equals(workflow.value.connectedTable)\n    .first();\n  if (!table) return;\n\n  connectedTable.value = table;\n}\nfunction checkWorkflowPermission() {\n  getWorkflowPermissions(editorData.value).then((permissions) => {\n    if (permissions.length === 0) return;\n\n    permissionState.items = permissions;\n    permissionState.showModal = true;\n  });\n}\nfunction checkWorkflowUpdate() {\n  const updatedAt = encodeURIComponent(workflow.value.updatedAt);\n  fetchApi(\n    `/teams/${teamId}/workflows/${workflowId}/check-update?updatedAt=${updatedAt}`,\n    { auth: true }\n  )\n    .then((response) => response.json())\n    .then((result) => {\n      if (!result) return;\n\n      updateWorkflow(result).then(() => {\n        editor.value.setNodes(result.drawflow.nodes || []);\n        editor.value.setEdges(result.drawflow.edges || []);\n        editor.value.fitView();\n      });\n    })\n    .catch((error) => {\n      console.error(error);\n    });\n}\n/* eslint-disable consistent-return */\nfunction onBeforeLeave() {\n  // disselect node before leave\n  const selectedNodes = editor.value?.getSelectedNodes?.value;\n  selectedNodes?.forEach((node) => {\n    node.selected = false;\n  });\n\n  updateHostedWorkflow();\n\n  const dataNotChanged = !state.dataChanged || !haveEditAccess.value;\n  const isExternalPkg = isPackage && workflow.value.isExternal;\n  if (dataNotChanged || isExternalPkg) return;\n\n  const confirm = window.confirm(t('message.notSaved'));\n\n  if (!confirm) return false;\n}\n\nuseHead({\n  title: () =>\n    `${workflow.value?.name} ${isPackage ? 'package' : 'workflow'}` || 'Automa',\n});\nconst shortcut = useShortcut([\n  getShortcut('editor:toggle-sidebar', toggleSidebar),\n  getShortcut('editor:duplicate-block', duplicateElements),\n]);\n\nprovide('workflow-editor', editor);\nprovide('autocompleteData', autocompleteList);\nprovide('workflow', {\n  editState,\n  isPackage,\n  data: workflow,\n  columns: workflowColumns,\n});\nprovide('workflow-utils', {\n  executeFromBlock,\n});\n\nwatch(\n  () => state.activeTab,\n  (value) => {\n    router.replace({ ...route, query: { tab: value } });\n  }\n);\nwatch(\n  () => state.dataChanged,\n  (isDataChanged) => {\n    window.isDataChanged = isDataChanged && haveEditAccess.value;\n  }\n);\n\nonDeactivated(() => {\n  const selectedNodes = editor.value?.getSelectedNodes?.value;\n  selectedNodes?.forEach((node) => {\n    node.selected = false;\n  });\n});\nonBeforeRouteLeave(onBeforeLeave);\nonMounted(() => {\n  if (!workflow.value) {\n    router.replace(isPackage ? '/packages' : '/');\n    return null;\n  }\n\n  const sidebarState =\n    JSON.parse(localStorage.getItem('workflow:sidebar')) ?? true;\n  state.showSidebar = sidebarState;\n  state.sidebarState = sidebarState;\n\n  if (!isPackage) {\n    const convertedData = convertWorkflowData(workflow.value);\n    updateWorkflow({ drawflow: convertedData.drawflow }).then(() => {\n      state.workflowConverted = true;\n    });\n  } else {\n    state.workflowConverted = true;\n  }\n\n  if (route.query.permission || (isTeamWorkflow && !haveEditAccess.value))\n    checkWorkflowPermission();\n\n  if (isTeamWorkflow && !haveEditAccess.value && workflow.value.updatedAt) {\n    checkWorkflowUpdate();\n  }\n\n  if (workflow.value.connectedTable) {\n    fetchConnectedTable();\n  }\n\n  initAutocomplete();\n});\nonBeforeUnmount(() => {\n  if (isPackage && workflow.value.isExternal) return;\n  updateHostedWorkflow();\n});\n</script>\n<style>\n.vue-flow,\n.editor-tab {\n  width: 100%;\n  height: 100%;\n}\n.vue-flow__node {\n  @apply rounded-lg;\n}\n.dropable-area__node,\n.dropable-area__handle {\n  @apply ring-4;\n}\n.animate-blocks {\n  .vue-flow__transformationpane,\n  .vue-flow__node {\n    transition: transform 300ms ease;\n  }\n}\n.undo-redo {\n  button:not(:disabled):hover {\n    @apply bg-box-transparent;\n  }\n  button:disabled {\n    @apply text-gray-500 dark:text-gray-400;\n  }\n}\n.sidebar {\n  max-width: 30%;\n}\n\n.custom-drag {\n  position: absolute;\n  width: 8px;\n  height: 90%;\n  right: 0;\n  top: 50%;\n  transform: translateY(-50%);\n\n  border-radius: 4px;\n  opacity: 0;\n  cursor: col-resize;\n  transition: opacity 0.5s;\n  background-color: #436dec;\n}\n.custom-drag:hover {\n  cursor: col-resize;\n  opacity: 1;\n}\n</style>\n"
  },
  {
    "path": "src/newtab/pages/workflows/index.vue",
    "content": "<template>\n  <div class=\"container pt-8 pb-4\">\n    <h1 class=\"text-2xl font-semibold capitalize\">\n      {{ t('common.workflow', 2) }}\n    </h1>\n    <div class=\"mt-8 flex items-start\">\n      <div class=\"sticky top-8 hidden w-60 lg:block\">\n        <div class=\"flex w-full\">\n          <ui-button\n            :title=\"shortcut['action:new'].readable\"\n            variant=\"accent\"\n            class=\"flex-1 rounded-r-none border-r font-semibold\"\n            @click=\"addWorkflowModal.show = true\"\n          >\n            {{ t('workflow.new') }}\n          </ui-button>\n          <ui-popover>\n            <template #trigger>\n              <ui-button icon class=\"rounded-l-none\" variant=\"accent\">\n                <v-remixicon name=\"riArrowLeftSLine\" rotate=\"-90\" />\n              </ui-button>\n            </template>\n            <ui-list class=\"space-y-1\">\n              <ui-list-item\n                v-close-popover\n                class=\"cursor-pointer\"\n                @click=\"openImportDialog\"\n              >\n                {{ t('workflow.import') }}\n              </ui-list-item>\n              <ui-list-item\n                v-close-popover\n                class=\"cursor-pointer\"\n                @click=\"initRecordWorkflow\"\n              >\n                {{ t('home.record.title') }}\n              </ui-list-item>\n              <ui-list-item\n                v-close-popover\n                class=\"cursor-pointer\"\n                @click=\"addHostedWorkflow\"\n              >\n                {{ t('workflow.host.add') }}\n              </ui-list-item>\n            </ui-list>\n          </ui-popover>\n        </div>\n        <ui-list class=\"mt-6 space-y-2\">\n          <ui-list-item\n            tag=\"a\"\n            href=\"https://extension.automa.site/workflows\"\n            target=\"_blank\"\n          >\n            <v-remixicon name=\"riCompass3Line\" />\n            <span class=\"ml-4 capitalize\">\n              {{ t('workflow.browse') }}\n            </span>\n          </ui-list-item>\n          <ui-expand\n            v-if=\"state.teams.length > 0\"\n            append-icon\n            header-class=\"px-4 py-2 rounded-lg mb-1 hoverable w-full flex items-center\"\n          >\n            <template #header>\n              <v-remixicon name=\"riTeamLine\" />\n              <span class=\"ml-4 flex-1 text-left capitalize\">\n                Team Workflows\n              </span>\n            </template>\n            <ui-list class=\"space-y-1\">\n              <ui-list-item\n                v-for=\"team in state.teams\"\n                :key=\"team.id\"\n                :active=\"state.teamId === team.id || +state.teamId === team.id\"\n                :title=\"team.name\"\n                color=\"bg-box-transparent font-semibold\"\n                class=\"cursor-pointer pl-14\"\n                @click=\"updateActiveTab({ activeTab: 'team', teamId: team.id })\"\n              >\n                <span class=\"text-overflow\">\n                  {{ team.name }}\n                </span>\n              </ui-list-item>\n            </ui-list>\n          </ui-expand>\n          <ui-expand\n            :model-value=\"true\"\n            append-icon\n            header-class=\"px-4 py-2 rounded-lg hoverable w-full flex items-center\"\n          >\n            <template #header>\n              <v-remixicon name=\"riFlowChart\" />\n              <span class=\"ml-4 flex-1 text-left capitalize\">\n                {{ t('workflow.my') }}\n              </span>\n            </template>\n            <ui-list class=\"mt-1 space-y-1\">\n              <ui-list-item\n                tag=\"button\"\n                :active=\"state.activeTab === 'local'\"\n                color=\"bg-box-transparent font-semibold\"\n                class=\"pl-14\"\n                @click=\"updateActiveTab({ activeTab: 'local' })\"\n              >\n                <span class=\"capitalize\">\n                  {{ t('workflow.type.local') }}\n                </span>\n              </ui-list-item>\n              <ui-list-item\n                v-if=\"userStore.user\"\n                :active=\"state.activeTab === 'shared'\"\n                tag=\"button\"\n                color=\"bg-box-transparent font-semibold\"\n                class=\"pl-14\"\n                @click=\"updateActiveTab({ activeTab: 'shared' })\"\n              >\n                <span class=\"capitalize\">\n                  {{ t('workflow.type.shared') }}\n                </span>\n              </ui-list-item>\n              <ui-list-item\n                v-if=\"hostedWorkflows?.length > 0\"\n                :active=\"state.activeTab === 'host'\"\n                color=\"bg-box-transparent font-semibold\"\n                tag=\"button\"\n                class=\"pl-14\"\n                @click=\"updateActiveTab({ activeTab: 'host' })\"\n              >\n                <span class=\"capitalize\">\n                  {{ t('workflow.type.host') }}\n                </span>\n              </ui-list-item>\n            </ui-list>\n          </ui-expand>\n        </ui-list>\n        <workflows-folder\n          v-if=\"state.activeTab === 'local'\"\n          v-model=\"state.activeFolder\"\n        />\n      </div>\n      <div\n        class=\"workflows-list flex-1 lg:ml-8\"\n        style=\"min-height: calc(100vh - 8rem)\"\n        @dblclick=\"clearSelectedWorkflows\"\n      >\n        <div class=\"flex flex-wrap items-center\">\n          <div class=\"flex w-full items-center md:w-auto\">\n            <ui-input\n              id=\"search-input\"\n              v-model=\"state.query\"\n              class=\"flex-1 md:w-auto\"\n              :placeholder=\"`${t(`common.search`)}... (${\n                shortcut['action:search'].readable\n              })`\"\n              prepend-icon=\"riSearch2Line\"\n            />\n            <ui-popover>\n              <template #trigger>\n                <ui-button variant=\"accent\" class=\"ml-4 lg:hidden\">\n                  <v-remixicon name=\"riAddLine\" class=\"mr-2 -ml-1\" />\n                  <span>{{ t('common.workflow') }}</span>\n                </ui-button>\n              </template>\n              <ui-list class=\"space-y-1\">\n                <ui-list-item\n                  v-close-popover\n                  class=\"cursor-pointer\"\n                  @click=\"addWorkflowModal.show = true\"\n                >\n                  {{ t('workflow.new') }}\n                </ui-list-item>\n                <ui-list-item\n                  v-close-popover\n                  class=\"cursor-pointer\"\n                  @click=\"openImportDialog\"\n                >\n                  {{ t('workflow.import') }}\n                </ui-list-item>\n                <ui-list-item\n                  v-close-popover\n                  class=\"cursor-pointer\"\n                  @click=\"initRecordWorkflow\"\n                >\n                  {{ t('home.record.title') }}\n                </ui-list-item>\n                <ui-list-item\n                  v-close-popover\n                  class=\"cursor-pointer\"\n                  @click=\"addHostedWorkflow\"\n                >\n                  {{ t('workflow.host.add') }}\n                </ui-list-item>\n              </ui-list>\n            </ui-popover>\n          </div>\n          <div class=\"grow\"></div>\n          <div class=\"mt-4 flex w-full items-center md:mt-0 md:w-auto\">\n            <span\n              v-tooltip:bottom.group=\"t('workflow.backupCloud')\"\n              class=\"mr-4\"\n            >\n              <ui-button\n                tag=\"router-link\"\n                to=\"/backup\"\n                class=\"inline-block\"\n                icon\n              >\n                <v-remixicon name=\"riUploadCloud2Line\" />\n              </ui-button>\n            </span>\n            <div class=\"workflow-sort flex flex-1 items-center\">\n              <ui-button\n                icon\n                class=\"rounded-r-none border-r border-gray-300 dark:border-gray-700\"\n                @click=\"\n                  state.sortOrder = state.sortOrder === 'asc' ? 'desc' : 'asc'\n                \"\n              >\n                <v-remixicon\n                  :name=\"state.sortOrder === 'asc' ? 'riSortAsc' : 'riSortDesc'\"\n                />\n              </ui-button>\n              <ui-select\n                v-model=\"state.sortBy\"\n                :placeholder=\"t('sort.sortBy')\"\n                class=\"flex-1\"\n              >\n                <option v-for=\"sort in sorts\" :key=\"sort\" :value=\"sort\">\n                  {{ t(`sort.${sort}`) }}\n                </option>\n              </ui-select>\n            </div>\n            <ui-select\n              v-model=\"state.activeTab\"\n              class=\"ml-4 lg:hidden\"\n              :placeholder=\"t('common.workflow', 2)\"\n            >\n              <option value=\"local\">\n                {{ t('workflow.type.local') }}\n              </option>\n              <option v-if=\"userStore.user\" value=\"shared\">\n                {{ t('workflow.type.shared') }}\n              </option>\n              <option v-if=\"hostedWorkflows?.length > 0\" value=\"host\">\n                {{ t('workflow.type.host') }}\n              </option>\n            </ui-select>\n          </div>\n        </div>\n        <ui-tab-panels v-model=\"state.activeTab\" class=\"mt-6 flex-1\">\n          <ui-tab-panel value=\"team\" cache>\n            <workflows-user-team\n              :active=\"state.activeTab === 'team'\"\n              :team-id=\"state.teamId\"\n              :search=\"state.query\"\n              :sort=\"{ by: state.sortBy, order: state.sortOrder }\"\n            />\n          </ui-tab-panel>\n          <ui-tab-panel value=\"shared\">\n            <workflows-shared\n              :search=\"state.query\"\n              :sort=\"{ by: state.sortBy, order: state.sortOrder }\"\n            />\n          </ui-tab-panel>\n          <ui-tab-panel value=\"host\" class=\"workflows-container\">\n            <workflows-hosted\n              :search=\"state.query\"\n              :sort=\"{ by: state.sortBy, order: state.sortOrder }\"\n            />\n          </ui-tab-panel>\n          <ui-tab-panel value=\"local\">\n            <workflows-local\n              v-model:per-page=\"state.perPage\"\n              :search=\"state.query\"\n              :folder-id=\"state.activeFolder\"\n              :sort=\"{ by: state.sortBy, order: state.sortOrder }\"\n            />\n          </ui-tab-panel>\n        </ui-tab-panels>\n        <ui-card\n          v-if=\"workflowStore.isFirstTime\"\n          class=\"first-card relative mt-8 dark:text-gray-200\"\n        >\n          <v-remixicon\n            name=\"riCloseLine\"\n            class=\"absolute top-4 right-4 cursor-pointer\"\n            @click=\"workflowStore.isFirstTime = false\"\n          />\n          <p>Create your first workflow by recording your actions:</p>\n          <ol class=\"list-inside list-decimal\">\n            <li>Open your browser and go to your destination URL</li>\n            <li>\n              Click the \"Record workflow\" button, and do your simple repetitive\n              task\n            </li>\n            <li>\n              Need more help? Join\n              <a\n                href=\"https://discord.gg/C6khwwTE84\"\n                target=\"_blank\"\n                rel=\"noreferer\"\n                >the community</a\n              >, or email us at\n              <a href=\"mailto:support@automa.site\" target=\"_blank\"\n                >support@automa.site</a\n              >\n            </li>\n          </ol>\n          <p class=\"mt-4\">\n            Learn more about recording in\n            <a\n              href=\"https://docs.extension.automa.site/guide/quick-start.html#recording-actions\"\n              target=\"_blank\"\n              >the documentation</a\n            >\n          </p>\n        </ui-card>\n      </div>\n    </div>\n    <ui-modal v-model=\"addWorkflowModal.show\" title=\"Workflow\">\n      <ui-input\n        v-model=\"addWorkflowModal.name\"\n        :placeholder=\"t('common.name')\"\n        autofocus\n        class=\"mb-4 w-full\"\n        @keyup.enter=\"\n          addWorkflowModal.type === 'manual'\n            ? addWorkflow()\n            : startRecordWorkflow()\n        \"\n      />\n      <ui-textarea\n        v-model=\"addWorkflowModal.description\"\n        :placeholder=\"t('common.description')\"\n        height=\"165px\"\n        class=\"w-full dark:text-gray-200\"\n        max=\"300\"\n      />\n      <p class=\"mb-6 text-right text-gray-600 dark:text-gray-200\">\n        {{ addWorkflowModal.description.length }}/300\n      </p>\n      <div class=\"flex space-x-2\">\n        <ui-button class=\"w-full\" @click=\"clearAddWorkflowModal\">\n          {{ t('common.cancel') }}\n        </ui-button>\n        <ui-button\n          variant=\"accent\"\n          class=\"w-full\"\n          @click=\"\n            addWorkflowModal.type === 'manual'\n              ? addWorkflow()\n              : startRecordWorkflow()\n          \"\n        >\n          {{\n            addWorkflowModal.type === 'manual'\n              ? t('common.add')\n              : t('home.record.button')\n          }}\n        </ui-button>\n      </div>\n    </ui-modal>\n    <shared-permissions-modal\n      v-model=\"permissionState.showModal\"\n      :permissions=\"permissionState.items\"\n    />\n  </div>\n</template>\n<script setup>\nimport SharedPermissionsModal from '@/components/newtab/shared/SharedPermissionsModal.vue';\nimport WorkflowsFolder from '@/components/newtab/workflows/WorkflowsFolder.vue';\nimport WorkflowsHosted from '@/components/newtab/workflows/WorkflowsHosted.vue';\nimport WorkflowsLocal from '@/components/newtab/workflows/WorkflowsLocal.vue';\nimport WorkflowsShared from '@/components/newtab/workflows/WorkflowsShared.vue';\nimport WorkflowsUserTeam from '@/components/newtab/workflows/WorkflowsUserTeam.vue';\nimport { useDialog } from '@/composable/dialog';\nimport { useGroupTooltip } from '@/composable/groupTooltip';\nimport { useShortcut } from '@/composable/shortcut';\nimport recordWorkflow from '@/newtab/utils/startRecordWorkflow';\nimport { useHostedWorkflowStore } from '@/stores/hostedWorkflow';\nimport { useTeamWorkflowStore } from '@/stores/teamWorkflow';\nimport { useUserStore } from '@/stores/user';\nimport { useWorkflowStore } from '@/stores/workflow';\nimport { fetchApi } from '@/utils/api';\nimport { findTriggerBlock, isWhitespace } from '@/utils/helper';\nimport { getWorkflowPermissions, importWorkflow } from '@/utils/workflowData';\nimport { registerWorkflowTrigger } from '@/utils/workflowTrigger';\nimport { computed, onMounted, shallowReactive, watch } from 'vue';\nimport { useI18n } from 'vue-i18n';\nimport { useRouter } from 'vue-router';\nimport { useToast } from 'vue-toastification';\n\nuseGroupTooltip();\n\nconst { t } = useI18n();\nconst toast = useToast();\nconst dialog = useDialog();\nconst router = useRouter();\nconst userStore = useUserStore();\nconst workflowStore = useWorkflowStore();\nconst teamWorkflowStore = useTeamWorkflowStore();\nconst hostedWorkflowStore = useHostedWorkflowStore();\n\nconst sorts = ['name', 'createdAt', 'updatedAt', 'mostUsed'];\nconst { teamId, active: routeActive } = router.currentRoute.value.query;\nconst savedSorts = JSON.parse(localStorage.getItem('workflow-sorts') || '{}');\nconst validTeamId = userStore.user?.teams?.some(\n  ({ id }) => id === teamId || id === +teamId\n);\n\nconst state = shallowReactive({\n  teams: [],\n  query: '',\n  activeFolder: '',\n  activeTab: routeActive || 'local',\n  teamId: validTeamId ? teamId : '',\n  perPage: savedSorts.perPage || 18,\n  sortBy: savedSorts.sortBy || 'createdAt',\n  sortOrder: savedSorts.sortOrder || 'desc',\n});\nconst addWorkflowModal = shallowReactive({\n  name: '',\n  show: false,\n  type: 'manual',\n  description: '',\n});\nconst permissionState = shallowReactive({\n  items: [],\n  showModal: false,\n});\n\nconst hostedWorkflows = computed(() => hostedWorkflowStore.toArray);\n\nfunction clearAddWorkflowModal() {\n  Object.assign(addWorkflowModal, {\n    name: '',\n    show: false,\n    type: 'manual',\n    description: '',\n  });\n}\nfunction initRecordWorkflow() {\n  addWorkflowModal.show = true;\n  addWorkflowModal.type = 'recording';\n}\nfunction startRecordWorkflow() {\n  recordWorkflow({\n    name: addWorkflowModal.name,\n    description: addWorkflowModal.description,\n  }).then(() => {\n    router.push('/recording');\n  });\n}\nasync function updateActiveTab(data = {}) {\n  if (data.activeTab !== 'team') {\n    data.teamId = '';\n  }\n\n  const query = {\n    ...router.currentRoute.value.query,\n    active: data.activeTab,\n  };\n\n  if (data.teamId) {\n    query.teamId = data.teamId;\n  } else {\n    delete query.teamId;\n  }\n\n  await router.replace({\n    ...router.currentRoute.value,\n    query,\n  });\n\n  Object.assign(state, { ...data });\n}\nfunction addWorkflow() {\n  workflowStore\n    .insert({\n      name: addWorkflowModal.name,\n      folderId: state.activeFolder,\n      description: addWorkflowModal.description,\n    })\n    .then((workflows) => {\n      const workflowId = Object.keys(workflows)[0];\n      router.push(`/workflows/${workflowId}`);\n    })\n    .finally(clearAddWorkflowModal);\n}\nasync function checkWorkflowPermissions(workflows) {\n  let requiredPermissions = [];\n\n  for (const workflow of workflows) {\n    if (workflow.drawflow) {\n      const permissions = await getWorkflowPermissions(workflow.drawflow);\n      requiredPermissions.push(...permissions);\n    }\n  }\n\n  requiredPermissions = Array.from(new Set(requiredPermissions));\n  if (requiredPermissions.length === 0) return;\n\n  permissionState.items = requiredPermissions;\n  permissionState.showModal = true;\n}\nfunction addHostedWorkflow() {\n  dialog.prompt({\n    async: true,\n    inputType: 'url',\n    okText: t('common.add'),\n    title: t('workflow.host.add'),\n    label: t('workflow.host.id'),\n    placeholder: 'abcd123',\n    onConfirm: async (value) => {\n      if (isWhitespace(value)) return false;\n      const hostId = value.replace(/\\s/g, '');\n\n      try {\n        if (!userStore.user && hostedWorkflowStore.toArray.length >= 3)\n          throw new Error('rate-exceeded');\n\n        const isTheUserHost = userStore.getHostedWorkflows.some(\n          (host) => hostId === host.hostId\n        );\n        if (isTheUserHost) throw new Error('exist');\n\n        const response = await fetchApi('/workflows/hosted', {\n          auth: true,\n          method: 'POST',\n          body: JSON.stringify({ hostId }),\n        });\n        const result = await response.json();\n\n        if (!response.ok) {\n          const error = new Error(result.message);\n          error.data = result.data;\n\n          throw error;\n        }\n\n        if (result === null) throw new Error('not-found');\n\n        result.hostId = `${hostId}`;\n        result.createdAt = Date.now();\n\n        await checkWorkflowPermissions([result]);\n        await hostedWorkflowStore.insert(result, hostId);\n\n        const triggerBlock = findTriggerBlock(result.drawflow);\n        await registerWorkflowTrigger(hostId, triggerBlock);\n\n        toast.success(t('workflow.host.messages.successAdded', { id: hostId }));\n\n        return true;\n      } catch (error) {\n        console.error(error);\n        const messages = {\n          exists: t('workflow.host.messages.hostExist'),\n          'rate-exceeded': t('message.rateExceeded'),\n          'not-found': t('workflow.host.messages.notFound', { id: hostId }),\n        };\n        const errorMessage = messages[error.message] || error.message;\n\n        toast.error(errorMessage);\n\n        return false;\n      }\n    },\n  });\n}\nasync function openImportDialog() {\n  try {\n    const workflows = await importWorkflow({ multiple: true });\n    await checkWorkflowPermissions(Object.values(workflows));\n  } catch (error) {\n    console.error(error);\n  }\n}\n\nconst shortcut = useShortcut(['action:search', 'action:new'], ({ id }) => {\n  if (id === 'action:search') {\n    const searchInput = document.querySelector('#search-input input');\n    searchInput?.focus();\n  } else {\n    addWorkflowModal.show = true;\n  }\n});\n\nwatch(\n  () => [state.sortOrder, state.sortBy, state.perPage],\n  ([sortOrder, sortBy, perPage]) => {\n    localStorage.setItem(\n      'workflow-sorts',\n      JSON.stringify({ sortOrder, sortBy, perPage })\n    );\n  }\n);\nwatch(\n  () => router.currentRoute.value.query,\n  (query) => {\n    const newState = {};\n\n    if (query.active && query.active !== state.activeTab) {\n      newState.activeTab = query.active;\n    }\n\n    if (query.teamId !== undefined) {\n      newState.teamId = query.teamId || '';\n    }\n\n    if (Object.keys(newState).length > 0) {\n      Object.assign(state, newState);\n    }\n  },\n  { immediate: true }\n);\n\nonMounted(() => {\n  const teams = [];\n  let unknownInputted = false;\n  Object.keys(teamWorkflowStore.workflows).forEach((id) => {\n    const userTeam = userStore.user?.teams?.find(\n      (team) => team.id === id || team.id === +id\n    );\n\n    if (userTeam) {\n      teams.push({ name: userTeam.name, id: userTeam.id });\n    } else if (!unknownInputted && teamWorkflowStore.getByTeam(id).length > 0) {\n      unknownInputted = true;\n      teams.unshift({ name: '(unknown)', id: '(unknown)' });\n    }\n  });\n\n  state.teams = teams;\n});\n</script>\n<style>\n.workflow-sort select {\n  @apply rounded-l-none !important;\n}\n.workflows-container {\n  @apply grid gap-4 grid-cols-1 md:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4;\n}\n\n.first-card {\n  a {\n    @apply text-blue-400 underline;\n  }\n}\n</style>\n"
  },
  {
    "path": "src/newtab/router.js",
    "content": "import { createRouter, createWebHashHistory } from 'vue-router';\nimport Welcome from './pages/Welcome.vue';\nimport Packages from './pages/Packages.vue';\nimport Workflows from './pages/workflows/index.vue';\nimport WorkflowContainer from './pages/Workflows.vue';\nimport WorkflowHost from './pages/workflows/Host.vue';\nimport WorkflowDetails from './pages/workflows/[id].vue';\nimport WorkflowShared from './pages/workflows/Shared.vue';\nimport ScheduledWorkflow from './pages/ScheduledWorkflow.vue';\nimport Storage from './pages/Storage.vue';\nimport StorageTables from './pages/storage/Tables.vue';\nimport LogsDetails from './pages/logs/[id].vue';\nimport Recording from './pages/Recording.vue';\nimport Settings from './pages/Settings.vue';\nimport SettingsIndex from './pages/settings/SettingsIndex.vue';\nimport SettingsAbout from './pages/settings/SettingsAbout.vue';\nimport SettingsProfile from './pages/settings/SettingsProfile.vue';\nimport SettingsShortcuts from './pages/settings/SettingsShortcuts.vue';\nimport SettingsBackup from './pages/settings/SettingsBackup.vue';\nimport SettingsEditor from './pages/settings/SettingsEditor.vue';\n\nconst routes = [\n  {\n    name: 'home',\n    path: '/',\n    redirect: '/workflows',\n    component: Workflows,\n  },\n  {\n    name: 'welcome',\n    path: '/welcome',\n    component: Welcome,\n  },\n  {\n    name: 'packages',\n    path: '/packages',\n    component: Packages,\n  },\n  {\n    name: 'recording',\n    path: '/recording',\n    component: Recording,\n  },\n  {\n    name: 'packages-details',\n    path: '/packages/:id',\n    component: WorkflowDetails,\n  },\n  {\n    path: '/workflows',\n    component: WorkflowContainer,\n    children: [\n      {\n        path: '',\n        name: 'workflows',\n        component: Workflows,\n      },\n      {\n        path: ':id',\n        name: 'workflows-details',\n        component: WorkflowDetails,\n      },\n      {\n        name: 'team-workflows',\n        path: '/teams/:teamId/workflows/:id',\n        component: WorkflowDetails,\n      },\n      {\n        name: 'workflow-host',\n        path: '/workflows/:id/host',\n        component: WorkflowHost,\n      },\n      {\n        name: 'workflow-shared',\n        path: '/workflows/:id/shared',\n        component: WorkflowShared,\n      },\n    ],\n  },\n  {\n    name: 'schedule',\n    path: '/schedule',\n    component: ScheduledWorkflow,\n  },\n  {\n    name: 'storage',\n    path: '/storage',\n    component: Storage,\n  },\n  {\n    name: 'storage-tables',\n    path: '/storage/tables/:id',\n    component: StorageTables,\n  },\n  {\n    name: 'logs-details',\n    path: '/logs/:id?',\n    component: LogsDetails,\n  },\n  {\n    path: '/settings',\n    component: Settings,\n    children: [\n      { path: '', component: SettingsIndex },\n      { path: '/profile', component: SettingsProfile },\n      { path: '/about', component: SettingsAbout },\n      { path: '/backup', component: SettingsBackup },\n      { path: '/editor', component: SettingsEditor },\n      { path: '/shortcuts', component: SettingsShortcuts },\n    ],\n  },\n];\n\nexport default createRouter({\n  routes,\n  history: createWebHashHistory(),\n});\n"
  },
  {
    "path": "src/newtab/utils/RecordWorkflowUtils.js",
    "content": "import browser from 'webextension-polyfill';\n\nconst validateUrl = (str) => str?.startsWith('http');\nconst isMV2 = browser.runtime.getManifest().manifest_version === 2;\n\nclass RecordWorkflowUtils {\n  static async updateRecording(callback) {\n    const { isRecording, recording } = await browser.storage.local.get([\n      'isRecording',\n      'recording',\n    ]);\n\n    if (!isRecording || !recording) return;\n\n    callback(recording);\n\n    await browser.storage.local.set({ recording });\n  }\n\n  static onTabCreated(tab) {\n    this.updateRecording((recording) => {\n      const url = tab.url || tab.pendingUrl;\n      const lastFlow = recording.flows[recording.flows.length - 1];\n      const invalidPrevFlow =\n        lastFlow &&\n        lastFlow.id === 'new-tab' &&\n        !validateUrl(lastFlow.data.url);\n\n      if (!invalidPrevFlow) {\n        const validUrl = validateUrl(url) ? url : '';\n\n        recording.flows.push({\n          id: 'new-tab',\n          data: {\n            url: validUrl,\n            description: tab.title || validUrl,\n          },\n        });\n      }\n\n      recording.activeTab = {\n        url,\n        id: tab.id,\n      };\n\n      browser.storage.local.set({ recording });\n    });\n  }\n\n  static async onTabsActivated({ tabId }) {\n    const { url, id, title } = await browser.tabs.get(tabId);\n\n    if (!validateUrl(url)) return;\n\n    this.updateRecording((recording) => {\n      recording.activeTab = { id, url };\n      recording.flows.push({\n        id: 'switch-tab',\n        description: title,\n        data: {\n          url,\n          matchPattern: url,\n          createIfNoMatch: true,\n        },\n      });\n    });\n  }\n\n  static onWebNavigationCommited({ frameId, tabId, url, transitionType }) {\n    const allowedType = ['link', 'typed'];\n    if (frameId !== 0 || !allowedType.includes(transitionType)) return;\n\n    this.updateRecording((recording) => {\n      if (recording.activeTab.id && tabId !== recording.activeTab.id) return;\n\n      const lastFlow = recording.flows.at(-1) ?? {};\n      const isInvalidNewtabFlow =\n        lastFlow &&\n        lastFlow.id === 'new-tab' &&\n        !validateUrl(lastFlow.data.url);\n\n      if (isInvalidNewtabFlow) {\n        lastFlow.data.url = url;\n        lastFlow.description = url;\n      } else if (validateUrl(url)) {\n        if (lastFlow?.id !== 'link' || !lastFlow.isClickLink) {\n          recording.flows.push({\n            id: 'new-tab',\n            description: url,\n            data: {\n              url,\n              updatePrevTab: recording.activeTab.id === tabId,\n            },\n          });\n        }\n\n        recording.activeTab.id = tabId;\n        recording.activeTab.url = url;\n      }\n    });\n  }\n\n  static async onWebNavigationCompleted({ tabId, url, frameId }) {\n    if (frameId > 0 || !url.startsWith('http')) return;\n\n    try {\n      const { isRecording } = await browser.storage.local.get('isRecording');\n      if (!isRecording) return;\n\n      if (isMV2) {\n        await browser.tabs.executeScript(tabId, {\n          allFrames: true,\n          runAt: 'document_start',\n          file: './recordWorkflow.bundle.js',\n        });\n      } else {\n        await browser.scripting.executeScript({\n          target: {\n            tabId,\n            allFrames: true,\n          },\n          files: ['recordWorkflow.bundle.js'],\n        });\n      }\n    } catch (error) {\n      console.error(error);\n    }\n  }\n}\n\nexport default RecordWorkflowUtils;\n"
  },
  {
    "path": "src/newtab/utils/blocksValidation.js",
    "content": "import browser from 'webextension-polyfill';\n\nconst checkPermissions = (permissions) =>\n  browser.permissions.contains({ permissions });\nconst isEmptyStr = (str) => !str.trim();\nconst isFirefox = BROWSER_TYPE === 'firefox';\nconst defaultOptions = {\n  once: false,\n};\n\nexport async function validateTrigger(data) {\n  const errors = [];\n  const checkValue = (value, { name, location }) => {\n    if (value && value.trim()) return;\n\n    errors.push(`\"${name}\" is empty in the ${location}`);\n  };\n  const triggersValidation = {\n    'cron-job': (triggerData) => {\n      checkValue(triggerData.expression, {\n        name: 'Expression',\n        location: 'Cron job trigger',\n      });\n    },\n    'context-menu': async (triggerData) => {\n      const permission = isFirefox ? 'menus' : 'contextMenus';\n      const hasPermission = await checkPermissions([permission]);\n\n      if (!hasPermission) {\n        errors.push(\n          \"Doesn't have permission for the Context menu trigger (ignore if you already grant the permissions)\"\n        );\n      } else {\n        checkValue(triggerData.contextMenuName, {\n          name: 'Context menu name',\n          location: 'Context menu trigger',\n        });\n      }\n    },\n    date: (triggerData) => {\n      checkValue(triggerData.date, {\n        name: 'Date',\n        location: 'On a specific date tigger',\n      });\n    },\n    'visit-web': (triggerData) => {\n      checkValue(triggerData.url, {\n        name: 'URL',\n        location: 'Visit web trigger',\n      });\n    },\n    'keyboard-shortcut': (triggerData) => {\n      checkValue(triggerData.shortcut, {\n        name: 'Shortcut',\n        location: 'Shortcut trigger',\n      });\n    },\n  };\n\n  if (data.triggers) {\n    for (const trigger of data.triggers) {\n      const validate = triggersValidation[trigger.type];\n      if (validate) await validate(trigger.data);\n    }\n  } else {\n    const validate = triggersValidation[data.type];\n    if (validate) await validate(data);\n  }\n\n  return errors;\n}\n\nexport async function validateExecuteWorkflow(data) {\n  if (isEmptyStr(data.workflowId)) return ['No workflow selected'];\n\n  return [];\n}\n\nexport async function validateNewTab(data) {\n  if (isEmptyStr(data.url)) return ['URL is empty'];\n\n  return [];\n}\n\nexport async function validateSwitchTab(data) {\n  const errors = [];\n  const validateItems = {\n    'match-patterns': () => {\n      if (isEmptyStr(data.matchPattern))\n        errors.push('The Match patterns is empty');\n    },\n    'tab-title': () => {\n      if (isEmptyStr(data.tabTitle)) errors.push('The Tab title is empty');\n    },\n  };\n\n  if (validateItems[data.findTabBy]) validateItems[data.findTabBy]();\n\n  return errors;\n}\n\nexport async function validateProxy(data) {\n  if (isEmptyStr(data.host)) return ['The Host is empty'];\n\n  return [];\n}\n\nexport async function validateCloseTab(data) {\n  if (data.closeType === 'tab' && !data.activeTab && isEmptyStr(data.url)) {\n    return ['The Match patterns is empty'];\n  }\n\n  return [];\n}\n\nexport async function validateTakeScreenshot(data) {\n  if (data.type === 'element' && isEmptyStr(data.selector)) {\n    return ['The CSS selector is empty'];\n  }\n\n  return [];\n}\n\nexport async function validateInteractionBasic(data) {\n  if (isEmptyStr(data.selector)) return ['The Selector is empty'];\n\n  return [];\n}\n\nexport async function validateExportData(data) {\n  const errors = [];\n\n  const hasPermission = await checkPermissions(['downloads']);\n  if (!hasPermission)\n    errors.push(\n      \"Don't have download permission (ignore if you already grant the permissions)\"\n    );\n\n  if (data.dataToExport === 'variable' && isEmptyStr(data.variableName)) {\n    errors.push('The Variable name is empty');\n  } else if (data.dataToExport === 'google-sheets' && isEmptyStr(data.refKey)) {\n    errors.push('The Reference key is empty');\n  }\n\n  return errors;\n}\n\nexport async function validateAttributeValue(data) {\n  const errors = [];\n\n  if (isEmptyStr(data.selector)) errors.push('The Selector is empty');\n  if (isEmptyStr(data.attributeName))\n    errors.push('The Attribute name is empty');\n\n  return errors;\n}\n\nexport async function validateGoogleSheets(data) {\n  const errors = [];\n\n  if (isEmptyStr(data.spreadsheetId))\n    errors.push('The Spreadsheet Id is empty');\n  if (isEmptyStr(data.range)) errors.push('The Range is empty');\n\n  return errors;\n}\n\nexport async function validateWebhook(data) {\n  if (isEmptyStr(data.url)) return ['The URL is empty'];\n\n  return [];\n}\n\nexport async function validateLoopData(data) {\n  const errors = [];\n  if (isEmptyStr(data.loopId)) errors.push('The Loop id is empty');\n\n  const loopThroughItems = {\n    'google-sheets': () => {\n      if (isEmptyStr(data.referenceKey))\n        errors.push('The Reference key is empty');\n    },\n    variable: () => {\n      if (isEmptyStr(data.variableName))\n        errors.push('The Variable name is empty');\n    },\n  };\n  const validateItem = loopThroughItems[data.loopThrough];\n  if (validateItem) validateItem();\n\n  return errors;\n}\n\nexport async function validateLoopElements(data) {\n  const errors = [];\n  if (isEmptyStr(data.loopId)) errors.push('The Loop id is empty');\n  if (isEmptyStr(data.selector)) errors.push('The Selector is empty');\n\n  if (\n    ['click-element', 'click-link'].includes(data.loadMoreAction) &&\n    isEmptyStr(data.actionElSelector)\n  ) {\n    errors.push('The Selector for loading more elements is empty');\n  }\n\n  return errors;\n}\n\nexport async function validateClipboard() {\n  const permissions = isFirefox\n    ? ['clipboardRead', 'clipboardWrite']\n    : ['clipboardRead'];\n  const hasPermission = await checkPermissions(permissions);\n\n  if (!hasPermission)\n    return [\n      \"Don't have permission to access the clipboard (ignore if you already grant the permissions)\",\n    ];\n\n  return [];\n}\n\nexport async function validateSwitchTo(data) {\n  if (data.windowType === 'iframe' && isEmptyStr(data.selector)) {\n    return ['The Selector for Iframe is empty'];\n  }\n\n  return [];\n}\n\nexport async function validateUploadFile(data) {\n  const errors = [];\n\n  if (isEmptyStr(data.selector)) errors.push('The Selector is empty');\n\n  const someInputsEmpty = data.filePaths.some((path) => isEmptyStr(path));\n  if (someInputsEmpty) errors.push('Some of the file paths is empty');\n\n  return errors;\n}\n\nexport async function validateSaveAssets(data) {\n  const errors = [];\n\n  const hasPermission = await checkPermissions(['downloads']);\n  if (!hasPermission)\n    errors.push(\n      \"Don't have download permission (ignore if you already grant the permissions)\"\n    );\n  else if (isEmptyStr(data.selector) && data.type === 'element')\n    errors.push('The Selector is empty');\n\n  return errors;\n}\n\nexport async function validatePressKey(data) {\n  const errors = [];\n\n  const isKeyEmpty =\n    !data.action || (data.action === 'press-key' && isEmptyStr(data.keys));\n  const isMultipleKeysEmpty =\n    data.action === 'multiple-keys' && isEmptyStr(data.keysToPress);\n  if (isKeyEmpty || isMultipleKeysEmpty)\n    errors.push('The Keys to press is empty');\n\n  return errors;\n}\n\nexport async function validateNotification() {\n  const hasPermission = await checkPermissions(['notifications']);\n  if (!hasPermission) return [\"Don't have notifications permissions\"];\n\n  return [];\n}\n\nexport async function validateCookie() {\n  const hasPermission = await checkPermissions(['cookies']);\n  if (!hasPermission) return [\"Don't have cookies permissions\"];\n\n  return [];\n}\n\nexport default {\n  trigger: {\n    ...defaultOptions,\n    func: validateTrigger,\n  },\n  'execute-workflow': {\n    ...defaultOptions,\n    func: validateExecuteWorkflow,\n  },\n  'new-tab': {\n    ...defaultOptions,\n    func: validateNewTab,\n  },\n  'switch-tab': {\n    ...defaultOptions,\n    func: validateSwitchTab,\n  },\n  proxy: {\n    ...defaultOptions,\n    func: validateProxy,\n  },\n  'close-tab': {\n    ...defaultOptions,\n    func: validateCloseTab,\n  },\n  'take-screenshot': {\n    ...defaultOptions,\n    func: validateTakeScreenshot,\n  },\n  'event-click': {\n    ...defaultOptions,\n    func: validateInteractionBasic,\n  },\n  'get-text': {\n    ...defaultOptions,\n    func: validateInteractionBasic,\n  },\n  'export-data': {\n    ...defaultOptions,\n    func: validateExportData,\n  },\n  'element-scroll': {\n    ...defaultOptions,\n    func: validateInteractionBasic,\n  },\n  link: {\n    ...defaultOptions,\n    func: validateInteractionBasic,\n  },\n  'attribute-value': {\n    ...defaultOptions,\n    func: validateAttributeValue,\n  },\n  forms: {\n    ...defaultOptions,\n    func: validateInteractionBasic,\n  },\n  'trigger-event': {\n    ...defaultOptions,\n    func: validateInteractionBasic,\n  },\n  'google-sheets': {\n    ...defaultOptions,\n    func: validateGoogleSheets,\n  },\n  'element-exists': {\n    ...defaultOptions,\n    func: validateInteractionBasic,\n  },\n  webhook: {\n    ...defaultOptions,\n    func: validateWebhook,\n  },\n  'loop-data': {\n    ...defaultOptions,\n    func: validateLoopData,\n  },\n  'loop-elements': {\n    ...defaultOptions,\n    func: validateLoopElements,\n  },\n  clipboard: {\n    ...defaultOptions,\n    once: true,\n    func: validateClipboard,\n  },\n  'switch-to': {\n    ...defaultOptions,\n    func: validateSwitchTo,\n  },\n  'upload-file': {\n    ...defaultOptions,\n    func: validateUploadFile,\n  },\n  'hover-element': {\n    ...defaultOptions,\n    func: validateInteractionBasic,\n  },\n  'save-assets': {\n    ...defaultOptions,\n    func: validateSaveAssets,\n  },\n  'press-key': {\n    ...defaultOptions,\n    func: validatePressKey,\n  },\n  notification: {\n    ...defaultOptions,\n    func: validateNotification,\n  },\n  'create-element': {\n    ...defaultOptions,\n    func: validateInteractionBasic,\n  },\n  cookie: {\n    ...defaultOptions,\n    func: validateCookie,\n  },\n};\n"
  },
  {
    "path": "src/newtab/utils/elementSelector.js",
    "content": "import browser from 'webextension-polyfill';\nimport { isXPath, sleep, getActiveTab } from '@/utils/helper';\n\nconst isMV2 = browser.runtime.getManifest().manifest_version === 2;\n\nasync function makeDashboardFocus() {\n  const [currentTab] = await browser.tabs.query({\n    active: true,\n    currentWindow: true,\n  });\n  await browser.windows.update(currentTab.windowId, {\n    focused: true,\n  });\n}\n\nexport async function initElementSelector(tab = null) {\n  let activeTab = tab;\n\n  if (!tab) {\n    activeTab = await getActiveTab();\n  }\n\n  const result = await browser.tabs.sendMessage(activeTab.id, {\n    type: 'automa-element-selector',\n  });\n\n  if (!result) {\n    if (isMV2) {\n      await browser.tabs.executeScript(activeTab.id, {\n        allFrames: true,\n        runAt: 'document_start',\n        file: './elementSelector.bundle.js',\n      });\n    } else {\n      await browser.scripting.executeScript({\n        target: {\n          allFrames: true,\n          tabId: activeTab.id,\n        },\n        files: ['./elementSelector.bundle.js'],\n      });\n    }\n  }\n\n  await browser.tabs.update(activeTab.id, { active: true });\n  await browser.windows.update(activeTab.windowId, { focused: true });\n}\n\nasync function verifySelector(data) {\n  try {\n    const activeTab = await getActiveTab();\n\n    if (!data.findBy) {\n      data.findBy = isXPath(data.selector) ? 'xpath' : 'cssSelector';\n    }\n\n    await browser.tabs.update(activeTab.id, { active: true });\n    await browser.windows.update(activeTab.windowId, { focused: true });\n\n    const result = await browser.tabs.sendMessage(\n      activeTab.id,\n      {\n        data,\n        isBlock: true,\n        label: 'verify-selector',\n      },\n      { frameId: 0 }\n    );\n\n    return result;\n  } catch (error) {\n    console.error(error);\n    await sleep(1000);\n\n    return { notFound: true };\n  } finally {\n    await makeDashboardFocus();\n  }\n}\n\nasync function selectElement(name) {\n  const tab = await getActiveTab();\n\n  await initElementSelector(tab);\n\n  const port = await browser.tabs.connect(tab.id, { name });\n  const getSelector = () => {\n    return new Promise((resolve, reject) => {\n      port.onDisconnect.addListener(() => {\n        reject(new Error('Port closed'));\n      });\n      port.onMessage.addListener(async (message) => {\n        try {\n          makeDashboardFocus();\n        } catch (error) {\n          console.error(error);\n        } finally {\n          resolve(message);\n        }\n      });\n    });\n  };\n\n  const selector = await getSelector();\n\n  return selector;\n}\n\nexport default {\n  selectElement,\n  verifySelector,\n};\n"
  },
  {
    "path": "src/newtab/utils/startRecordWorkflow.js",
    "content": "import browser from 'webextension-polyfill';\n\nconst isMV2 = browser.runtime.getManifest().manifest_version === 2;\n\nexport default async function (options = {}) {\n  try {\n    const flows = [];\n    const [activeTab] = await browser.tabs.query({\n      active: true,\n      url: '*://*/*',\n    });\n\n    if (activeTab && activeTab.url.startsWith('http')) {\n      flows.push({\n        id: 'new-tab',\n        description: activeTab.url,\n        data: { url: activeTab.url },\n      });\n\n      await browser.windows.update(activeTab.windowId, { focused: true });\n    }\n\n    await browser.storage.local.set({\n      isRecording: true,\n      recording: {\n        flows,\n        name: 'unnamed',\n        activeTab: {\n          id: activeTab?.id,\n          url: activeTab?.url,\n        },\n        ...options,\n      },\n    });\n\n    const action = browser.action || browser.browserAction;\n    await action.setBadgeBackgroundColor({ color: '#ef4444' });\n    await action.setBadgeText({ text: 'rec' });\n\n    const tabs = await browser.tabs.query({});\n    for (const tab of tabs) {\n      if (\n        tab.url.startsWith('http') &&\n        !tab.url.includes('chrome.google.com')\n      ) {\n        if (isMV2) {\n          await browser.tabs.executeScript(tab.id, {\n            allFrames: true,\n            runAt: 'document_start',\n            file: './recordWorkflow.bundle.js',\n          });\n        } else {\n          await browser.scripting.executeScript({\n            target: {\n              tabId: tab.id,\n              allFrames: true,\n            },\n            files: ['recordWorkflow.bundle.js'],\n          });\n        }\n      }\n    }\n  } catch (error) {\n    console.error(error);\n  }\n}\n"
  },
  {
    "path": "src/offscreen/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n  <meta charset=\"UTF-8\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n  <title>Offscreen</title>\n</head>\n<body>\n  <iframe src=\"/sandbox.html\" id=\"sandbox\" style=\"display: none;\"></iframe>\n</body>\n</html>\n"
  },
  {
    "path": "src/offscreen/index.js",
    "content": "import './message-listener';\n"
  },
  {
    "path": "src/offscreen/message-listener.js",
    "content": "import BrowserAPIEventHandler from '@/service/browser-api/BrowserAPIEventHandler';\nimport { MessageListener } from '@/utils/message';\nimport WorkflowManager from '@/workflowEngine/WorkflowManager';\nimport Browser from 'webextension-polyfill';\n\nconst messageListener = new MessageListener('offscreen');\nBrowser.runtime.onMessage.addListener(messageListener.listener);\n\nmessageListener.on('workflow:execute', ({ workflow, options }) => {\n  WorkflowManager.instance.execute(workflow, options);\n});\n\nmessageListener.on('workflow:stop', (stateId) => {\n  WorkflowManager.instance.stopExecution(stateId);\n});\n\nmessageListener.on('workflow:resume', ({ id, nextBlock }) => {\n  WorkflowManager.instance.resumeExecution(id, nextBlock);\n});\n\nmessageListener.on('workflow:update', ({ id, data }) => {\n  WorkflowManager.instance.updateExecution(id, data);\n});\n\nmessageListener.on(BrowserAPIEventHandler.RuntimeEvents.ON_EVENT, (event) =>\n  BrowserAPIEventHandler.instance.onBrowserEventListener(event)\n);\n"
  },
  {
    "path": "src/params/App.vue",
    "content": "<template>\n  <div\n    v-if=\"retrieved\"\n    class=\"mx-auto flex h-full w-full max-w-lg flex-col dark:text-gray-100\"\n  >\n    <nav class=\"mb-4 flex w-full items-center border-b p-4\">\n      <span class=\"bg-box-transparent rounded-full p-1 dark:bg-none\">\n        <img src=\"@/assets/svg/logo.svg\" class=\"w-10\" />\n      </span>\n      <p class=\"ml-4 text-lg font-semibold\">Automa</p>\n    </nav>\n    <div class=\"scroll flex-1 overflow-auto px-4 pb-4\">\n      <p class=\"my-4 text-gray-600 dark:text-gray-200\">\n        Input these workflows parameters before it runs.\n      </p>\n      <ui-expand\n        v-for=\"(workflow, index) in sortedWorkflows\"\n        :key=\"index\"\n        :model-value=\"true\"\n        append-icon\n        header-class=\"flex items-center text-left p-4 w-full rounded-lg\"\n        class=\"mb-4 rounded-lg bg-white dark:bg-gray-800\"\n      >\n        <template #header>\n          <ui-img\n            v-if=\"workflow.data.icon?.startsWith('http')\"\n            :src=\"workflow.data.icon\"\n            class=\"overflow-hidden rounded-lg\"\n            style=\"height: 40px; width: 40px\"\n            alt=\"Can not display\"\n          />\n          <span v-else class=\"bg-box-transparent rounded-lg p-2\">\n            <v-remixicon :name=\"workflow.data.icon\" />\n          </span>\n          <div class=\"ml-4 flex-1 overflow-hidden\">\n            <p class=\"text-overflow mr-4 leading-tight\">\n              {{ workflow.data.name }}\n            </p>\n            <p\n              class=\"text-overflow leading-tight text-gray-600 dark:text-gray-200\"\n            >\n              {{ workflow.data.description }}\n            </p>\n          </div>\n        </template>\n        <p v-if=\"workflow.type === 'block'\" class=\"px-4 pb-2\">\n          By Parameter Prompt block\n        </p>\n        <div class=\"px-4 pb-4\">\n          <ul class=\"space-y-4 divide-y\">\n            <li\n              v-for=\"(param, paramIdx) in workflow.params\"\n              :key=\"paramIdx\"\n              class=\"flex flex-col gap-3\"\n            >\n              <component\n                :is=\"paramsList[param.type].valueComp\"\n                v-if=\"paramsList[param.type]\"\n                v-model=\"param.value\"\n                :autofocus=\"paramIdx === 0\"\n                :label=\"param.name + (param.data?.required ? '*' : '')\"\n                :param-data=\"param\"\n                class=\"w-full\"\n                @execute=\"\n                  workflow.type === 'block'\n                    ? continueWorkflow(index, workflow)\n                    : runWorkflow(index, workflow)\n                \"\n              />\n              <ui-input\n                v-else\n                v-model=\"param.value\"\n                :type=\"param.inputType\"\n                :label=\"param.name + (param.data?.required ? '*' : '')\"\n                :placeholder=\"param.placeholder\"\n                class=\"w-full\"\n              />\n              <p\n                v-if=\"param.description\"\n                title=\"Description\"\n                class=\"ml-1 text-sm leading-tight\"\n              >\n                {{ param.description }}\n              </p>\n            </li>\n          </ul>\n          <div class=\"mt-6 flex items-center\">\n            <p>{{ dayjs(workflow.addedDate).fromNow() }}</p>\n            <div class=\"grow\" />\n            <template v-if=\"workflow.type === 'block'\">\n              <ui-button\n                class=\"mr-4\"\n                @click=\"cancelParamBlock(index, workflow, 'Canceled')\"\n              >\n                Cancel\n              </ui-button>\n              <ui-button\n                :disabled=\"!isValidParams(workflow.params)\"\n                variant=\"accent\"\n                @click=\"continueWorkflow(index, workflow)\"\n              >\n                Continue\n              </ui-button>\n            </template>\n            <template v-else>\n              <ui-button class=\"mr-4\" @click=\"deleteWorkflow(index)\">\n                Cancel\n              </ui-button>\n              <ui-button\n                :disabled=\"!isValidParams(workflow.params)\"\n                variant=\"accent\"\n                @click=\"runWorkflow(index, workflow)\"\n              >\n                <v-remixicon name=\"riPlayLine\" class=\"mr-2 -ml-1\" />\n                Run\n              </ui-button>\n            </template>\n          </div>\n        </div>\n      </ui-expand>\n    </div>\n  </div>\n</template>\n<script setup>\nimport ParameterCheckboxValue from '@/components/newtab/workflow/edit/Parameter/ParameterCheckboxValue.vue';\nimport ParameterInputValue from '@/components/newtab/workflow/edit/Parameter/ParameterInputValue.vue';\nimport ParameterJsonValue from '@/components/newtab/workflow/edit/Parameter/ParameterJsonValue.vue';\nimport { useTheme } from '@/composable/theme';\nimport dayjs from '@/lib/dayjs';\nimport { parseJSON } from '@/utils/helper';\nimport automa from '@business';\nimport workflowParameters from '@business/parameters';\nimport { computed, onMounted, ref } from 'vue';\nimport browser from 'webextension-polyfill';\n\nconst paramsList = {\n  string: {\n    id: 'string',\n    name: 'Input (string)',\n    valueComp: ParameterInputValue,\n  },\n  json: {\n    id: 'json',\n    name: 'Input (JSON)',\n    valueComp: ParameterJsonValue,\n  },\n  checkbox: {\n    id: 'checkbox',\n    name: 'Checkbox',\n    valueComp: ParameterCheckboxValue,\n    data: {\n      required: false,\n    },\n  },\n};\n\nconst theme = useTheme();\ntheme.init();\n\nconst retrieved = ref(false);\nconst workflows = ref([]);\n\nconst sortedWorkflows = computed(() =>\n  workflows.value.slice().sort((a, b) => b.addedDate - a.addedDate)\n);\n\nconst flattenTeamWorkflows = (items) => Object.values(Object.values(items)[0]);\n\nasync function findWorkflow(workflowId) {\n  if (!workflowId) return null;\n\n  if (workflowId.startsWith('hosted')) {\n    const { workflowHosts } = await browser.storage.local.get('workflowHosts');\n    if (!workflowHosts) return null;\n    const _hostId = workflowId.split(':')[1];\n    return workflowHosts[_hostId] || undefined;\n  }\n\n  if (workflowId.startsWith('team')) {\n    const { teamWorkflows } = await browser.storage.local.get('teamWorkflows');\n    if (!teamWorkflows) return null;\n\n    const teamWorkflowsArr = flattenTeamWorkflows(teamWorkflows);\n\n    return teamWorkflowsArr.find((item) => item.id === workflowId);\n  }\n\n  const { workflows: localWorkflows, workflowHosts } =\n    await browser.storage.local.get(['workflows', 'workflowHosts']);\n  let workflow = Array.isArray(localWorkflows)\n    ? localWorkflows.find(({ id }) => id === workflowId)\n    : localWorkflows[workflowId];\n\n  if (!workflow) {\n    workflow = Object.values(workflowHosts || {}).find(\n      ({ hostId }) => hostId === workflowId\n    );\n\n    if (workflow) workflow.id = workflow.hostId;\n  }\n\n  return workflow;\n}\nfunction deleteWorkflow(index) {\n  workflows.value.splice(index, 1);\n\n  if (workflows.value.length === 0) {\n    window.close();\n  }\n}\nasync function addWorkflow(workflowId) {\n  console.log('🚀 ~ addWorkflow ~ workflowId:', workflowId);\n  try {\n    const workflow =\n      typeof workflowId === 'string'\n        ? await findWorkflow(workflowId)\n        : workflowId;\n    console.log('🚀 ~ addWorkflow ~ workflow:', workflow);\n    const triggerBlock = workflow.drawflow.nodes.find(\n      (node) => node.label === 'trigger'\n    );\n    if (!triggerBlock) return;\n\n    const params = triggerBlock.data.parameters.map((param) => ({\n      ...param,\n      value: param.defaultValue,\n      inputType: param.type === 'string' ? 'text' : 'number',\n    }));\n\n    workflows.value.push({\n      params,\n      data: workflow,\n      addedDate: Date.now(),\n    });\n  } catch (error) {\n    console.error(error);\n  }\n}\nfunction getParamsValues(params) {\n  const getParamVal = {\n    string: (str) => str,\n    number: (num) => (Number.isNaN(+num) ? 0 : +num),\n    json: (value) => parseJSON(value, null),\n    default: (value) => value,\n  };\n\n  return params.reduce((acc, param) => {\n    const valueFunc =\n      getParamVal[param.type] ||\n      paramsList[param.type]?.getValue ||\n      getParamVal.default;\n    const value = valueFunc(param.value || param.defaultValue);\n    acc[param.name] = value;\n\n    return acc;\n  }, {});\n}\nfunction runWorkflow(index, { data, params }) {\n  /* eslint-disable-next-line */\n  const isParamsValid = isValidParams(params);\n  if (!isParamsValid) return;\n\n  const variables = getParamsValues(params);\n  let payload = {\n    name: 'background--workflow:execute',\n    data: {\n      ...data,\n      options: {\n        checkParams: false,\n        data: { variables },\n      },\n    },\n  };\n  const isFirefox = BROWSER_TYPE === 'firefox';\n  payload = isFirefox ? JSON.stringify(payload) : payload;\n\n  browser.runtime\n    .sendMessage(payload)\n    .then(() => {\n      deleteWorkflow(index);\n    })\n    .catch((error) => {\n      console.error(error);\n    });\n}\nfunction cancelParamBlock(index, { data }, message) {\n  browser.storage.local\n    .set({\n      [data.promptId]: {\n        message,\n        $isError: true,\n      },\n    })\n    .then(() => {\n      deleteWorkflow(index);\n    });\n}\nfunction continueWorkflow(index, { data, params }) {\n  /* eslint-disable-next-line */\n  const isParamsValid = isValidParams(params);\n  if (!isParamsValid) return;\n\n  const timeout = data.timeoutMs > 0 ? Date.now() > data.timeout : false;\n\n  browser.storage.local\n    .set({\n      [data.promptId]: timeout ? { $timeout: true } : getParamsValues(params),\n    })\n    .then(() => {\n      deleteWorkflow(index);\n    });\n}\nfunction isValidParams(params) {\n  const isValid = params.every((param) => {\n    if (!param.data?.required) return true;\n\n    return param.value;\n  });\n\n  return isValid;\n}\n\nlet checkTimeout = null;\n\nbrowser.runtime.onMessage.addListener(({ name, data }) => {\n  console.log('🚀 params html ~ name:', name, data);\n  if (name === 'workflow:params') {\n    console.log('🚀 从popup的事件监听中触发', name, data);\n    addWorkflow(data);\n  } else if (name === 'workflow:params-block') {\n    const params = [...data.params];\n    delete data.params;\n\n    workflows.value.push({\n      data,\n      params,\n      type: 'block',\n      addedDate: Date.now(),\n    });\n\n    if (!checkTimeout) {\n      checkTimeout = setInterval(() => {\n        workflows.value.forEach((workflow, index) => {\n          if (\n            workflow.type !== 'block' ||\n            Date.now() < workflow.data.timeout ||\n            workflow.data.timeoutMs <= 0\n          )\n            return;\n\n          cancelParamBlock(index, workflow, 'Timeout');\n        });\n      }, 1000);\n    }\n  }\n});\n\nonMounted(async () => {\n  try {\n    const query = new URLSearchParams(window.location.search);\n    const workflowId = query.get('workflowId');\n    if (workflowId) addWorkflow(workflowId);\n    await automa('content');\n\n    Object.assign(paramsList, workflowParameters());\n  } catch (error) {\n    // Do nothing\n  } finally {\n    retrieved.value = true;\n  }\n});\n</script>\n"
  },
  {
    "path": "src/params/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <title></title>\n  </head>\n\n  <body>\n    <div id=\"app\" class=\"scroll\"></div>\n  </body>\n</html>\n"
  },
  {
    "path": "src/params/index.js",
    "content": "import { createApp } from 'vue';\nimport App from './App.vue';\nimport compsUi from '../lib/compsUi';\nimport vRemixicon, { icons } from '../lib/vRemixicon';\nimport '../assets/css/tailwind.css';\nimport '../assets/css/fonts.css';\nimport '../assets/css/flow.css';\n\ncreateApp(App).use(compsUi).use(vRemixicon, icons).mount('#app');\n\nif (module.hot) module.hot.accept();\n"
  },
  {
    "path": "src/popup/App.vue",
    "content": "<template>\n  <template v-if=\"retrieved\">\n    <router-view />\n    <ui-dialog />\n  </template>\n</template>\n<script setup>\nimport { ref, onMounted } from 'vue';\nimport browser from 'webextension-polyfill';\nimport { useStore } from '@/stores/main';\nimport { sendMessage } from '@/utils/message';\nimport { useWorkflowStore } from '@/stores/workflow';\nimport { useHostedWorkflowStore } from '@/stores/hostedWorkflow';\nimport { loadLocaleMessages, setI18nLanguage } from '@/lib/vueI18n';\n\nconst store = useStore();\nconst workflowStore = useWorkflowStore();\nconst hostedWorkflowStore = useHostedWorkflowStore();\n\nconst retrieved = ref(false);\n\nbrowser.storage.local.get('isRecording').then(({ isRecording }) => {\n  if (!isRecording) return;\n\n  sendMessage('open:dashboard', '/recording', 'background').then(() => {\n    window.close();\n  });\n});\n\nonMounted(async () => {\n  try {\n    await store.loadSettings();\n    await loadLocaleMessages(store.settings.locale, 'popup');\n    await setI18nLanguage(store.settings.locale);\n\n    await workflowStore.loadData();\n    await hostedWorkflowStore.loadData();\n\n    retrieved.value = true;\n  } catch (error) {\n    console.error(error);\n    retrieved.value = true;\n  }\n});\n</script>\n<style>\nbody {\n  height: 500px;\n  width: 350px;\n  font-size: 16px;\n}\n</style>\n"
  },
  {
    "path": "src/popup/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <title></title>\n  </head>\n\n  <body>\n    <div id=\"app\" class=\"scroll\"></div>\n  </body>\n</html>\n"
  },
  {
    "path": "src/popup/index.js",
    "content": "import { createApp } from 'vue';\nimport App from './App.vue';\nimport router from './router';\nimport pinia from '../lib/pinia';\nimport compsUi from '../lib/compsUi';\nimport vueI18n from '../lib/vueI18n';\nimport vRemixicon, { icons } from '../lib/vRemixicon';\nimport '../assets/css/tailwind.css';\nimport '../assets/css/fonts.css';\nimport '../assets/css/flow.css';\n\ncreateApp(App)\n  .use(router)\n  .use(compsUi)\n  .use(vueI18n)\n  .use(pinia)\n  .use(vRemixicon, icons)\n  .mount('#app');\n\nif (module.hot) module.hot.accept();\n"
  },
  {
    "path": "src/popup/pages/Home.vue",
    "content": "<template>\n  <div\n    :class=\"[!showTab ? 'h-48' : 'h-56']\"\n    class=\"absolute top-0 left-0 w-full rounded-b-2xl bg-accent\"\n  ></div>\n  <div\n    :class=\"[!showTab ? 'mb-6' : 'mb-2']\"\n    class=\"dark relative z-10 px-5 pt-8 text-white placeholder:text-black\"\n  >\n    <div class=\"mb-4 flex items-center\">\n      <h1 class=\"text-xl font-semibold text-white\">Automa</h1>\n      <div class=\"grow\"></div>\n      <ui-button\n        v-tooltip.group=\"\n          'Start recording by opening the dashboard. Click to learn more'\n        \"\n        icon\n        class=\"mr-2\"\n        @click=\"openDocs\"\n      >\n        <v-remixicon name=\"riRecordCircleLine\" />\n      </ui-button>\n      <ui-button\n        v-tooltip.group=\"\n          t(`home.elementSelector.${state.haveAccess ? 'name' : 'noAccess'}`)\n        \"\n        icon\n        class=\"mr-2\"\n        @click=\"initElementSelector\"\n      >\n        <v-remixicon name=\"riFocus3Line\" />\n      </ui-button>\n      <ui-button\n        v-tooltip.group=\"t('common.dashboard')\"\n        icon\n        :title=\"t('common.dashboard')\"\n        @click=\"openDashboard('')\"\n      >\n        <v-remixicon name=\"riHome5Line\" />\n      </ui-button>\n    </div>\n    <div class=\"flex\">\n      <ui-input\n        v-model=\"state.query\"\n        :placeholder=\"`${t('common.search')}...`\"\n        autocomplete=\"off\"\n        prepend-icon=\"riSearch2Line\"\n        class=\"search-input w-full\"\n      />\n    </div>\n    <ui-tabs\n      v-if=\"showTab\"\n      v-model=\"state.activeTab\"\n      fill\n      class=\"mt-1\"\n      @change=\"onTabChange\"\n    >\n      <ui-tab value=\"local\">\n        {{ t(`home.workflow.type.local`) }}\n      </ui-tab>\n      <ui-tab v-if=\"hostedWorkflowStore.toArray.length > 0\" value=\"host\">\n        {{ t(`home.workflow.type.host`) }}\n      </ui-tab>\n      <ui-tab v-if=\"userStore.user?.teams?.length\" value=\"team\"> Teams </ui-tab>\n    </ui-tabs>\n  </div>\n  <home-team-workflows\n    v-if=\"state.retrieved\"\n    v-show=\"state.activeTab === 'team'\"\n    :search=\"state.query\"\n  />\n  <div\n    v-if=\"state.activeTab !== 'team'\"\n    class=\"relative z-20 space-y-2 px-5 pb-5\"\n  >\n    <ui-card v-if=\"workflowStore.getWorkflows.length === 0\" class=\"text-center\">\n      <img src=\"@/assets/svg/alien.svg\" />\n      <p class=\"font-semibold\">{{ t('message.empty') }}</p>\n      <ui-button\n        variant=\"accent\"\n        class=\"mt-6\"\n        @click=\"openDashboard('/workflows')\"\n      >\n        {{ t('home.workflow.new') }}\n      </ui-button>\n    </ui-card>\n    <div v-if=\"pinnedWorkflows.length > 0\" class=\"mt-1 mb-4 border-b pb-4\">\n      <div class=\"mb-1 flex items-center text-gray-300\">\n        <v-remixicon name=\"riPushpin2Line\" size=\"20\" class=\"mr-2\" />\n        <span>Pinned workflows</span>\n      </div>\n      <home-workflow-card\n        v-for=\"workflow in pinnedWorkflows\"\n        :key=\"workflow.id\"\n        :workflow=\"workflow\"\n        :tab=\"state.activeTab\"\n        :pinned=\"true\"\n        class=\"mb-2\"\n        @details=\"openWorkflowPage\"\n        @update=\"updateWorkflow(workflow.id, $event)\"\n        @execute=\"executeWorkflow\"\n        @rename=\"renameWorkflow\"\n        @delete=\"deleteWorkflow\"\n        @toggle-pin=\"togglePinWorkflow(workflow)\"\n      />\n    </div>\n    <div\n      :class=\"{ 'p-2 rounded-lg bg-white': pinnedWorkflows.length === 0 }\"\n      class=\"flex items-center\"\n    >\n      <ui-select v-model=\"state.activeFolder\" class=\"flex-1\">\n        <option value=\"\">Folder (all)</option>\n        <option\n          v-for=\"folder in folderStore.items\"\n          :key=\"folder.id\"\n          :value=\"folder.id\"\n        >\n          {{ folder.name }}\n        </option>\n      </ui-select>\n      <ui-popover class=\"ml-2\">\n        <template #trigger>\n          <ui-button>\n            <v-remixicon name=\"riSortDesc\" class=\"mr-2 -ml-1\" />\n            <span>Sort</span>\n          </ui-button>\n        </template>\n        <div class=\"w-48\">\n          <ui-select v-model=\"sortState.order\" block placeholder=\"Sort order\">\n            <option value=\"asc\">Ascending</option>\n            <option value=\"desc\">Descending</option>\n          </ui-select>\n          <ui-select\n            v-model=\"sortState.by\"\n            :placeholder=\"t('sort.sortBy')\"\n            block\n            class=\"mt-2 flex-1\"\n          >\n            <option v-for=\"sort in sorts\" :key=\"sort\" :value=\"sort\">\n              {{ t(`sort.${sort}`) }}\n            </option>\n          </ui-select>\n        </div>\n      </ui-popover>\n    </div>\n    <home-workflow-card\n      v-for=\"workflow in workflows\"\n      :key=\"workflow.id\"\n      :workflow=\"workflow\"\n      :tab=\"state.activeTab\"\n      :pinned=\"state.pinnedWorkflows.includes(workflow.id)\"\n      @details=\"openWorkflowPage\"\n      @update=\"updateWorkflow(workflow.id, $event)\"\n      @execute=\"executeWorkflow\"\n      @rename=\"renameWorkflow\"\n      @delete=\"deleteWorkflow\"\n      @toggle-pin=\"togglePinWorkflow(workflow)\"\n    />\n    <div\n      v-if=\"state.showSettingsPopup\"\n      class=\"fixed bottom-5 left-0 m-4 rounded-lg bg-accent p-4 text-white shadow-md dark:text-black z-10\"\n    >\n      <p class=\"text-sm leading-tight\">\n        If the workflow runs for less than 5 minutes, set it to run in the\n        background in the\n        <a\n          href=\"https://docs.extension.automa.site/workflow/settings.html#workflow-execution\"\n          class=\"font-semibold underline\"\n          target=\"_blank\"\n        >\n          workflow settings.\n        </a>\n      </p>\n      <v-remixicon\n        name=\"riCloseLine\"\n        class=\"absolute top-2 right-2 cursor-pointer text-gray-300 dark:text-gray-600\"\n        size=\"20\"\n        @click=\"closeSettingsPopup\"\n      />\n    </div>\n  </div>\n</template>\n<script setup>\nimport BackgroundUtils from '@/background/BackgroundUtils';\nimport HomeTeamWorkflows from '@/components/popup/home/HomeTeamWorkflows.vue';\nimport HomeWorkflowCard from '@/components/popup/home/HomeWorkflowCard.vue';\nimport { useDialog } from '@/composable/dialog';\nimport { useGroupTooltip } from '@/composable/groupTooltip';\nimport { initElementSelector as initElementSelectorFunc } from '@/newtab/utils/elementSelector';\nimport RendererWorkflowService from '@/service/renderer/RendererWorkflowService';\nimport { useFolderStore } from '@/stores/folder';\nimport { useHostedWorkflowStore } from '@/stores/hostedWorkflow';\nimport { useTeamWorkflowStore } from '@/stores/teamWorkflow';\nimport { useUserStore } from '@/stores/user';\nimport { useWorkflowStore } from '@/stores/workflow';\nimport { arraySorter, parseJSON } from '@/utils/helper';\nimport automa from '@business';\nimport { computed, onMounted, shallowReactive, watch } from 'vue';\nimport { useI18n } from 'vue-i18n';\nimport browser from 'webextension-polyfill';\n\nconst isMV2 = browser.runtime.getManifest().manifest_version === 2;\n\nconst { t } = useI18n();\nconst dialog = useDialog();\nconst userStore = useUserStore();\nconst folderStore = useFolderStore();\nconst workflowStore = useWorkflowStore();\nconst teamWorkflowStore = useTeamWorkflowStore();\nconst hostedWorkflowStore = useHostedWorkflowStore();\n\nuseGroupTooltip();\n\nconst sorts = ['name', 'createdAt', 'updatedAt', 'mostUsed'];\nconst savedSorts =\n  parseJSON(localStorage.getItem('popup-workflow-sort'), {}) || {};\n\nconst sortState = shallowReactive({\n  by: savedSorts.sortBy || 'createdAt',\n  order: savedSorts.sortOrder || 'desc',\n});\nconst state = shallowReactive({\n  query: '',\n  teams: [],\n  cardHeight: 255,\n  retrieved: false,\n  haveAccess: true,\n  activeTab: 'local',\n  pinnedWorkflows: [],\n  activeFolder: savedSorts.activeFolder,\n  showSettingsPopup: isMV2\n    ? false\n    : parseJSON(localStorage.getItem('settingsPopup'), true) ?? true,\n});\n\nconst pinnedWorkflows = computed(() => {\n  if (state.activeTab !== 'local') return [];\n\n  const list = [];\n  state.pinnedWorkflows.forEach((workflowId) => {\n    const workflow = workflowStore.getById(workflowId);\n    if (\n      !workflow ||\n      !workflow.name\n        .toLocaleLowerCase()\n        .includes(state.query.toLocaleLowerCase())\n    )\n      return;\n\n    list.push(workflow);\n  });\n\n  return list;\n});\nconst hostedWorkflows = computed(() => {\n  if (state.activeTab !== 'host') return [];\n\n  return hostedWorkflowStore.toArray.filter((workflow) =>\n    workflow.name.toLocaleLowerCase().includes(state.query.toLocaleLowerCase())\n  );\n});\nconst localWorkflows = computed(() => {\n  if (state.activeTab !== 'local') return [];\n\n  const filteredLocalWorkflows = workflowStore.getWorkflows.filter(\n    ({ name, folderId }) => {\n      const isInFolder = !state.activeFolder || state.activeFolder === folderId;\n      const nameMatch = name\n        .toLocaleLowerCase()\n        .includes(state.query.toLocaleLowerCase());\n\n      return isInFolder && nameMatch;\n    }\n  );\n\n  return arraySorter({\n    key: sortState.by,\n    order: sortState.order,\n    data: filteredLocalWorkflows,\n  });\n});\nconst workflows = computed(() =>\n  state.activeTab === 'local' ? localWorkflows.value : hostedWorkflows.value\n);\nconst showTab = computed(\n  () =>\n    hostedWorkflowStore.toArray.length > 0 || userStore.user?.teams?.length > 0\n);\n\nfunction openDocs() {\n  window.open(\n    'https://docs.extension.automa.site/guide/quick-start.html#recording-actions',\n    '_blank'\n  );\n}\nfunction closeSettingsPopup() {\n  state.showSettingsPopup = false;\n  localStorage.setItem('settingsPopup', false);\n}\nfunction togglePinWorkflow(workflow) {\n  const index = state.pinnedWorkflows.indexOf(workflow.id);\n  const copyData = [...state.pinnedWorkflows];\n\n  if (index === -1) {\n    copyData.push(workflow.id);\n  } else {\n    copyData.splice(index, 1);\n  }\n\n  state.pinnedWorkflows = copyData;\n  browser.storage.local.set({\n    pinnedWorkflows: copyData,\n  });\n}\nasync function executeWorkflow(workflow) {\n  try {\n    await RendererWorkflowService.executeWorkflow(workflow, workflow.options);\n    window.close();\n  } catch (error) {\n    console.error(error);\n  }\n}\nfunction updateWorkflow(id, data) {\n  return workflowStore.update({\n    id,\n    data,\n  });\n}\nfunction renameWorkflow({ id, name }) {\n  dialog.prompt({\n    title: t('home.workflow.rename'),\n    placeholder: t('common.name'),\n    okText: t('common.rename'),\n    inputValue: name,\n    onConfirm: (newName) => {\n      updateWorkflow(id, { name: newName });\n    },\n  });\n}\nfunction deleteWorkflow({ id, hostId, name }) {\n  dialog.confirm({\n    title: t('home.workflow.delete'),\n    okVariant: 'danger',\n    body: t('message.delete', { name }),\n    onConfirm: () => {\n      if (state.activeTab === 'local') {\n        workflowStore.delete(id);\n      } else {\n        hostedWorkflowStore.delete(hostId);\n      }\n    },\n  });\n}\nfunction openDashboard(url) {\n  BackgroundUtils.openDashboard(url);\n}\nasync function initElementSelector() {\n  const [tab] = await browser.tabs.query({\n    url: '*://*/*',\n    active: true,\n    currentWindow: true,\n  });\n  if (!tab) return;\n  initElementSelectorFunc(tab).then(() => {\n    window.close();\n  });\n}\nfunction openWorkflowPage({ id, hostId }) {\n  let url = `/workflows/${id}`;\n\n  if (state.activeTab === 'host') {\n    url = `/workflows/${hostId}/host`;\n  }\n\n  openDashboard(url);\n}\nfunction onTabChange(value) {\n  localStorage.setItem('popup-tab', value);\n}\n\nwatch(\n  () => [sortState.by, sortState.order, state.activeFolder],\n  ([sortBy, sortOrder, activeFolder]) => {\n    localStorage.setItem(\n      'popup-workflow-sort',\n      JSON.stringify({ sortOrder, sortBy, activeFolder })\n    );\n  }\n);\n\nonMounted(async () => {\n  const [tab] = await browser.tabs.query({ active: true, currentWindow: true });\n  state.haveAccess = /^(https?)/.test(tab.url);\n\n  const storage = await browser.storage.local.get('pinnedWorkflows');\n  state.pinnedWorkflows = storage.pinnedWorkflows || [];\n\n  await folderStore.load();\n  await userStore.loadUser({ storage: localStorage, ttl: 1000 * 60 * 5 });\n  await teamWorkflowStore.loadData();\n\n  let activeTab = localStorage.getItem('popup-tab') || 'local';\n\n  await automa('app');\n\n  if (activeTab === 'team' && !userStore.user?.teams) activeTab = 'local';\n  else if (activeTab === 'host' && hostedWorkflowStore.toArray.length < 1)\n    activeTab = 'local';\n\n  state.retrieved = true;\n  state.activeTab = activeTab;\n\n  if (state.activeFolder) {\n    const folderExist = folderStore.items.some(\n      (folder) => folder.id === state.activeFolder\n    );\n    if (!folderExist) state.activeFolder = '';\n  }\n});\n</script>\n<style>\n.recording-card {\n  transition: height 300ms cubic-bezier(0.4, 0, 0.2, 1) !important;\n}\n</style>\n"
  },
  {
    "path": "src/popup/router.js",
    "content": "import { createRouter, createWebHashHistory } from 'vue-router';\nimport Home from './pages/Home.vue';\n\nconst routes = [\n  {\n    path: '/',\n    name: 'home',\n    component: Home,\n  },\n];\n\nexport default createRouter({\n  routes,\n  history: createWebHashHistory(),\n});\n"
  },
  {
    "path": "src/sandbox/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n\t<meta charset=\"UTF-8\">\n\t<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n\t<title>Sandbox</title>\n</head>\n<body>\n</body>\n</html>"
  },
  {
    "path": "src/sandbox/index.js",
    "content": "import objectPath from 'object-path';\nimport handleConditionCode from './utils/handleConditionCode';\nimport handleJavascriptBlock from './utils/handleJavascriptBlock';\nimport handleBlockExpression from './utils/handleBlockExpression';\n\nwindow.$getNestedProperties = objectPath.get;\n\nfunction fetchResponse({ id, data }) {\n  window.dispatchEvent(\n    new CustomEvent(`automa-fetch-response-${id}`, {\n      detail: data,\n    })\n  );\n}\n\nconst eventHandlers = {\n  fetchResponse,\n  conditionCode: handleConditionCode,\n  blockExpression: handleBlockExpression,\n  javascriptBlock: handleJavascriptBlock,\n};\n\nwindow.addEventListener('message', ({ data }) => {\n  if (!data.id || !data.type || !eventHandlers[data.type]) return;\n\n  function sendResponse(payload) {\n    window.top.postMessage(\n      {\n        id: data.id,\n        type: 'sandbox',\n        result: payload,\n      },\n      '*'\n    );\n  }\n\n  eventHandlers[data.type](data, sendResponse);\n});\n"
  },
  {
    "path": "src/sandbox/utils/handleBlockExpression.js",
    "content": "import tmpl from '@/lib/tmpl';\nimport functions from '@/workflowEngine/templating/templatingFunctions';\n\nconst templatingFunctions = Object.keys(functions).reduce((acc, funcName) => {\n  acc[`$${funcName}`] = functions[funcName];\n\n  return acc;\n}, {});\n\nexport default function ({ str, data }, sendResponse) {\n  const value = tmpl.tmpl(str, { ...data, ...templatingFunctions });\n\n  sendResponse({\n    list: {},\n    value: value.slice(2),\n  });\n}\n"
  },
  {
    "path": "src/sandbox/utils/handleConditionCode.js",
    "content": "export default function (data) {\n  const propertyName = `automa${data.id}`;\n\n  const script = document.createElement('script');\n  script.textContent = `\n    (async () => {\n      function automaRefData(keyword, path = '') {\n        if (!keyword) return null;\n        if (!path) return ${propertyName}.refData[keyword];\n\n        return window.$getNestedProperties(${propertyName}.refData, keyword + '.' + path);\n      }\n\n      try {\n        ${data.data.code}\n      } catch (error) {\n        return {\n          $isError: true,\n          message: error.message,\n        }\n      }\n    })()\n      .then((result) => {\n        ${propertyName}.done(result);\n      });\n  `;\n\n  window[propertyName] = {\n    refData: data.refData,\n    done: (result) => {\n      script.remove();\n      delete window[propertyName];\n\n      window.top.postMessage(\n        {\n          result,\n          id: data.id,\n          type: 'sandbox',\n        },\n        '*'\n      );\n    },\n  };\n\n  (document.body || document.documentElement).appendChild(script);\n}\n"
  },
  {
    "path": "src/sandbox/utils/handleJavascriptBlock.js",
    "content": "import { nanoid } from 'nanoid/non-secure';\n\nexport default function (data) {\n  let timeout;\n  const instanceId = nanoid();\n  const scriptId = `script${data.id}`;\n  const propertyName = `automa${data.id}`;\n\n  const isScriptExists = document.querySelector(`#${scriptId}`);\n  if (isScriptExists) {\n    window.top.postMessage(\n      {\n        id: data.id,\n        type: 'sandbox',\n        result: {\n          columns: {},\n          variables: {},\n        },\n      },\n      '*'\n    );\n\n    return;\n  }\n\n  const preloadScripts = data.preloadScripts.map((item) => {\n    const scriptEl = document.createElement('script');\n    scriptEl.textContent = item.script;\n\n    (document.body || document.documentElement).appendChild(scriptEl);\n\n    return scriptEl;\n  });\n\n  if (!data.blockData.code.includes('automaNextBlock')) {\n    data.blockData.code += `\\n automaNextBlock()`;\n  }\n\n  const script = document.createElement('script');\n  script.id = scriptId;\n  script.textContent = `\n    (() => {\n      function automaRefData(keyword, path = '') {\n        if (!keyword) return null;\n        if (!path) return ${propertyName}.refData[keyword];\n\n        return window.$getNestedProperties(${propertyName}.refData, keyword + '.' + path);\n      }\n      function automaSetVariable(name, value) {\n        const variables = ${propertyName}.refData.variables;\n        if (!variables) ${propertyName}.refData.variables = {}\n\n        ${propertyName}.refData.variables[name] = value;\n      }\n      function automaNextBlock(data = {}, insert = true) {\n        ${propertyName}.nextBlock({ data, insert });\n      }\n      function automaResetTimeout() {\n        ${propertyName}.resetTimeout();\n      }\n      function automaFetch(type, resource) {\n        return ${propertyName}.fetch(type, resource);\n      }\n\n      try {\n        ${data.blockData.code}\n      } catch (error) {\n        console.error(error);\n        automaNextBlock({ $error: true, message: error.message });\n      }\n    })();\n  `;\n\n  function cleanUp() {\n    script.remove();\n    preloadScripts.forEach((preloadScript) => {\n      preloadScript.remove();\n    });\n\n    delete window[propertyName];\n  }\n\n  window[propertyName] = {\n    refData: data.refData,\n    nextBlock: (result) => {\n      cleanUp();\n      window.top.postMessage(\n        {\n          id: data.id,\n          type: 'sandbox',\n          result: {\n            variables: data?.refData?.variables,\n            columns: {\n              data: result?.data,\n              insert: result?.insert,\n            },\n          },\n        },\n        '*'\n      );\n    },\n    resetTimeout: () => {\n      clearTimeout(timeout);\n      timeout = setTimeout(cleanUp, data.blockData.timeout);\n    },\n    fetch: (type, resource) => {\n      return new Promise((resolve, reject) => {\n        const types = ['json', 'text'];\n        if (!type || !types.includes(type)) {\n          reject(new Error('The \"type\" must be \"text\" or \"json\"'));\n          return;\n        }\n\n        window.top.postMessage(\n          {\n            type: 'automa-fetch',\n            data: { id: instanceId, type, resource },\n          },\n          '*'\n        );\n\n        const eventName = `automa-fetch-response-${instanceId}`;\n\n        const eventListener = ({ detail }) => {\n          window.removeEventListener(eventName, eventListener);\n\n          if (detail.isError) {\n            reject(new Error(detail.result));\n          } else {\n            resolve(detail.result);\n          }\n        };\n\n        window.addEventListener(eventName, eventListener);\n      });\n    },\n  };\n\n  timeout = setTimeout(cleanUp, data.blockData.timeout);\n  (document.body || document.documentElement).appendChild(script);\n}\n"
  },
  {
    "path": "src/service/browser-api/BrowserAPIEventHandler.js",
    "content": "/* eslint-disable class-methods-use-this */\n\nimport { MessageListener } from '@/utils/message';\nimport { browserAPIMap } from './browser-api-map';\n\nconst BROWSER_API_EVENTS = {\n  ON_EVENT: 'browser-api:on-browser-event',\n  TOGGLE: 'browser-api:toggle-browser-event-listener',\n};\n\nfunction onBrowserAPIEvent(name, ...args) {\n  MessageListener.sendMessage(\n    BROWSER_API_EVENTS.ON_EVENT,\n    { name, args },\n    'offscreen'\n  );\n}\n\n/**\n * @typedef { 'chrome.debugger.onEvent' | 'browser.tabs.onRemoved' | 'browser.webNavigation.onCreatedNavigationTarget' | 'browser.windows.onRemoved' } BrowserAPIEventsName\n */\n\nclass BrowserAPIEventHandler {\n  /** @type {BrowserAPIEventHandler} */\n  static #_instance;\n\n  /**\n   * BrowserAPIEventHandler singleton\n   * @type {BrowserAPIEventHandler}\n   */\n  static get instance() {\n    if (!this.#_instance) this.#_instance = new BrowserAPIEventHandler();\n\n    return this.#_instance;\n  }\n\n  static RuntimeEvents = BROWSER_API_EVENTS;\n\n  /** @type {Record<string, ((...args: unknown[]) => void)[]>} */\n  #events;\n\n  /** @type {Record<string, { addListener: (...args: unknown[]) => void, removeListener: (...args: unknown[]) => void }>} */\n  #eventsHandler;\n\n  /** @type {Set<string>} */\n  #isEventAdded;\n\n  /** @type {Record<string, ((...args: unknown[]) => void)>} */\n  #browserEvents;\n\n  constructor() {\n    this.#events = {};\n    this.#browserEvents = {};\n    this.#eventsHandler = {};\n\n    this.#isEventAdded = new Set();\n  }\n\n  /**\n   * @param {BrowserAPIEventsName} name\n   * @param {*} browserAPI\n   */\n  createEventListener(name) {\n    if (this.#eventsHandler[name]) return this.#eventsHandler[name];\n\n    if (!this.#events[name]) this.#events[name] = [];\n\n    /**\n     * This callback is displayed as a global member.\n     * @callback eventListenerCallback\n     * @param {...*} args\n     */\n\n    /**\n     * @param {eventListenerCallback} callback\n     */\n    const addListener = (callback) => {\n      this.#events[name].push(callback);\n\n      if (this.#isEventAdded.has(name)) return;\n\n      MessageListener.sendMessage(\n        BROWSER_API_EVENTS.TOGGLE,\n        {\n          name,\n          type: 'add',\n        },\n        'background'\n      ).then(() => {\n        this.#isEventAdded.add(name);\n      });\n    };\n\n    /**\n     * @param {eventListenerCallback} callback\n     */\n    const removeListener = (callback) => {\n      const index = this.#events[name].indexOf(callback);\n      if (index === -1) return;\n\n      this.#events[name].splice(index, 1);\n\n      if (this.#events[name].length > 0) return;\n\n      MessageListener.sendMessage(\n        BROWSER_API_EVENTS.TOGGLE,\n        {\n          name,\n          type: 'remove',\n        },\n        'background'\n      );\n      delete this.#eventsHandler[name];\n    };\n\n    const hasListeners = () => {\n      return this.#events[name].length > 0;\n    };\n\n    /**\n     * @param {eventListenerCallback} callback\n     */\n    const hasListener = (callback) => {\n      return this.#events[name].includes(callback);\n    };\n\n    this.#eventsHandler[name] = {\n      addListener,\n      hasListener,\n      hasListeners,\n      removeListener,\n    };\n\n    return this.#eventsHandler[name];\n  }\n\n  /**\n   * @param {{ name: BrowserAPIEventsName, args: unknown[] }} event\n   */\n  onBrowserEventListener(event) {\n    if (!event.name || !this.#events[event.name]) return;\n\n    this.#events[event.name].forEach((listener) => listener(...event.args));\n  }\n\n  /**\n   * @param {{ name: BrowserAPIEventsName, type: 'add' | 'remove' }} data\n   */\n  onToggleBrowserEventListener({ name, type }) {\n    const isAddListener = type === 'add';\n\n    if (isAddListener && this.#browserEvents[name]) return;\n    if (!isAddListener && !this.#browserEvents[name]) return;\n\n    const browserEventAPI = browserAPIMap\n      .find((item) => item.path === name && item.isEvent)\n      ?.api();\n    if (!browserAPIMap) return;\n\n    const eventType = isAddListener ? 'addListener' : 'removeListener';\n    const listener = isAddListener\n      ? onBrowserAPIEvent.bind(null, name)\n      : this.#browserEvents[name];\n\n    if (isAddListener) this.#browserEvents[name] = listener;\n\n    browserEventAPI[eventType](listener);\n  }\n}\n\nexport default BrowserAPIEventHandler;\n"
  },
  {
    "path": "src/service/browser-api/BrowserAPIService.js",
    "content": "/* eslint-disable max-classes-per-file */\n/* eslint-disable prefer-rest-params */\nimport { MessageListener } from '@/utils/message';\nimport {\n  deserializeFunctions,\n  serializeFunctions,\n} from '@/utils/serialization';\nimport objectPath from 'object-path';\nimport Browser from 'webextension-polyfill';\nimport BrowserAPIEventHandler from './BrowserAPIEventHandler';\nimport { browserAPIMap } from './browser-api-map';\n\n/**\n * @typedef {Object} ScriptInjectTarget\n * @property {number} tabId\n * @property {number=} frameId\n * @property {boolean=} allFrames\n */\n\n// Maybe there's a better way?\nexport const IS_BROWSER_API_AVAILABLE = 'tabs' in Browser;\n\nfunction sendBrowserApiMessage(name, ...args) {\n  const serializedArgs = serializeFunctions(args);\n\n  return MessageListener.sendMessage(\n    'browser-api',\n    {\n      name,\n      args: serializedArgs,\n    },\n    'background'\n  );\n}\n\nclass BrowserContentScript {\n  /**\n   * Check if content script injected\n   * @param {ScriptInjectTarget} target\n   * @param {string=} messageId\n   */\n  static async isContentScriptInjected(target, messageId) {\n    if (!IS_BROWSER_API_AVAILABLE) {\n      return sendBrowserApiMessage(\n        'contentScript.isContentScriptInjected',\n        ...arguments\n      );\n    }\n\n    try {\n      // 发送测试消息到目标标签页\n      await Browser.tabs.sendMessage(\n        target.tabId,\n        { type: messageId || 'content-script-exists' },\n        {\n          frameId: target.allFrames ? undefined : target.frameId,\n        }\n      );\n\n      return true;\n    } catch (error) {\n      return false;\n    }\n  }\n\n  /**\n   * Inject content script into targeted tab\n   * @param {Object} script\n   * @param {ScriptInjectTarget} script.target\n   * @param {string} script.file\n   * @param {boolean=} script.injectImmediately\n   * @param {(boolean|{timeoutMs?: number, maxTry?: number, messageId?: string})=} script.waitUntilInjected\n   * @returns {Promise<boolean>}\n   */\n  static async inject({ file, target, injectImmediately, waitUntilInjected }) {\n    if (!IS_BROWSER_API_AVAILABLE) {\n      return sendBrowserApiMessage('contentScript.inject', ...arguments);\n    }\n\n    const frameId =\n      Object.hasOwn(target, 'frameId') && !target.allFrames\n        ? target.frameId\n        : undefined;\n\n    // MV2 or firefox\n    if (Browser.tabs.injectContentScript) {\n      await Browser.tabs.executeScript(target.tabId, {\n        file,\n        frameId,\n        allFrames: target.allFrames,\n      });\n    } else {\n      // MV3 chrome\n      await Browser.scripting.executeScript({\n        target: {\n          tabId: target.tabId,\n          allFrames: target.allFrames,\n          frameIds: typeof frameId === 'number' ? [frameId] : undefined,\n        },\n        files: [file],\n        injectImmediately,\n      });\n    }\n\n    if (!waitUntilInjected) return true;\n\n    const maxTryCount = waitUntilInjected.maxTry ?? 3;\n    const timeoutMs = waitUntilInjected.timeoutMs ?? 1000;\n\n    let tryCount = 0;\n\n    return new Promise((resolve) => {\n      const checkIfInjected = async () => {\n        try {\n          if (tryCount > maxTryCount) {\n            resolve(false);\n            return;\n          }\n\n          tryCount += 1;\n\n          const isInjected = await BrowserContentScript.isContentScriptInjected(\n            target,\n            waitUntilInjected.messageId\n          );\n          if (isInjected) {\n            resolve(true);\n            return;\n          }\n\n          setTimeout(() => checkIfInjected(), timeoutMs);\n        } catch (error) {\n          console.error(error);\n          setTimeout(() => checkIfInjected(), timeoutMs);\n        }\n      };\n      checkIfInjected();\n    });\n  }\n\n  /**\n   * Check if content script injected\n   * @param {ScriptInjectTarget} target\n   * @param {string=} messageId\n   */\n  static async isInjected({ tabId, allFrames, frameId }, messageId) {\n    if (!IS_BROWSER_API_AVAILABLE) {\n      return sendBrowserApiMessage('contentScript.isInjected', ...arguments);\n    }\n\n    try {\n      await Browser.tabs.sendMessage(\n        tabId,\n        { type: messageId || 'content-script-exists' },\n        { frameId: allFrames ? undefined : frameId }\n      );\n\n      return true;\n    } catch (error) {\n      return false;\n    }\n  }\n}\n\nclass BrowserAPIService {\n  /**\n   * Handle runtime message that send by BrowserAPIService when API is not available\n   * @param {{ name: string; args: any[] }} payload;\n   */\n  static runtimeMessageHandler({ args, name }) {\n    const deserializedArgs = deserializeFunctions(args);\n    const apiHandler = objectPath.get(this, name);\n    if (!apiHandler) throw new Error(`\"${name}\" is invalid method`);\n\n    return deserializedArgs ? apiHandler(...deserializedArgs) : apiHandler();\n  }\n\n  static runtime = Browser.runtime;\n\n  /** @type {typeof Browser.tabs} */\n  static tabs;\n\n  /** @type {typeof Browser.proxy} */\n  static proxy;\n\n  /** @type {typeof Browser.storage} */\n  static storage;\n\n  /** @type {typeof Browser.windows} */\n  static windows;\n\n  /** @type {typeof chrome.debugger} */\n  static debugger;\n\n  /** @type {typeof Browser.webNavigation} */\n  static webNavigation;\n\n  /** @type {typeof Browser.permissions} */\n  static permissions;\n\n  /** @type {typeof Browser.downloads} */\n  static downloads;\n\n  /** @type {typeof Browser.notifications} */\n  static notifications;\n\n  /** @type {typeof Browser.browserAction} */\n  static browserAction;\n\n  /** @type {typeof Browser.extension} */\n  static extension;\n\n  static contentScript = BrowserContentScript;\n}\n\n(() => {\n  browserAPIMap.forEach((item) => {\n    let value;\n    if (IS_BROWSER_API_AVAILABLE) {\n      value = item.api();\n    } else {\n      value = item.isEvent\n        ? BrowserAPIEventHandler.instance.createEventListener(item.path)\n        : (...args) => sendBrowserApiMessage(item.path, ...args);\n    }\n\n    objectPath.set(BrowserAPIService, item.path, value);\n  });\n})();\n\nexport default BrowserAPIService;\n"
  },
  {
    "path": "src/service/browser-api/browser-api-map.js",
    "content": "import Browser from 'webextension-polyfill';\n\n/** @type {{api: () => unknown, path: string; isEvent?: true}[]} */\nexport const browserAPIMap = [\n  { api: () => Browser.tabs.get, path: 'tabs.get' },\n  { api: () => chrome.tabs.group, path: 'tabs.group' },\n  { api: () => Browser.tabs.query, path: 'tabs.query' },\n  { api: () => Browser.tabs.update, path: 'tabs.update' },\n  { api: () => Browser.tabs.create, path: 'tabs.create' },\n  { api: () => Browser.tabs.remove, path: 'tabs.remove' },\n  { api: () => Browser.tabs.reload, path: 'tabs.reload' },\n  { api: () => Browser.tabs.goBack, path: 'tabs.goBack' },\n  { api: () => Browser.tabs.setZoom, path: 'tabs.setZoom' },\n  { api: () => Browser.tabs.goForward, path: 'tabs.goForward' },\n  { api: () => Browser.tabs.captureTab, path: 'tabs.captureTab' },\n  { api: () => Browser.tabs.captureVisibleTab, path: 'tabs.captureVisibleTab' },\n  { api: () => Browser.tabs.sendMessage, path: 'tabs.sendMessage' },\n  { isEvent: true, api: () => Browser.tabs.onRemoved, path: 'tabs.onRemoved' },\n  {\n    isEvent: true,\n    path: 'webNavigation.onCreatedNavigationTarget',\n    api: () => Browser.webNavigation.onCreatedNavigationTarget,\n  },\n  {\n    isEvent: true,\n    path: 'webNavigation.onErrorOccurred',\n    api: () => Browser.webNavigation.onErrorOccurred,\n  },\n  {\n    path: 'webNavigation.getAllFrames',\n    api: () => Browser.webNavigation.getAllFrames,\n  },\n  { api: () => Browser.windows.get, path: 'windows.get' },\n  { api: () => Browser.windows.update, path: 'windows.update' },\n  { api: () => Browser.windows.create, path: 'windows.create' },\n  { api: () => Browser.windows.getAll, path: 'windows.getAll' },\n  { api: () => Browser.windows.remove, path: 'windows.remove' },\n  { api: () => Browser.windows.getCurrent, path: 'windows.getCurrent' },\n  {\n    isEvent: true,\n    path: 'windows.onRemoved',\n    api: () => Browser.windows.onRemoved,\n  },\n  {\n    isEvent: true,\n    path: 'storage.onChanged',\n    api: () => Browser.storage.onChanged,\n  },\n  {\n    api:\n      () =>\n      (...args) =>\n        Browser.storage.local.get(...args),\n    path: 'storage.local.get',\n  },\n  {\n    api:\n      () =>\n      (...args) =>\n        Browser.storage.local.set(...args),\n    path: 'storage.local.set',\n  },\n  {\n    api:\n      () =>\n      (...args) =>\n        Browser.storage.local.remove(...args),\n    path: 'storage.local.remove',\n  },\n  { api: () => Browser.proxy.settings.clear, path: 'proxy.settings.clear' },\n  { api: () => Browser.proxy.settings.set, path: 'proxy.settings.set' },\n  {\n    isEvent: true,\n    path: 'debugger.onEvent',\n    api: () => chrome.debugger.onEvent,\n  },\n  { path: 'debugger.detach', api: () => chrome.debugger.detach },\n  { path: 'debugger.attach', api: () => chrome.debugger.attach },\n  { path: 'debugger.sendCommand', api: () => chrome.debugger.sendCommand },\n  { path: 'permissions.contains', api: () => Browser.permissions.contains },\n  { path: 'permissions.request', api: () => Browser.permissions.request },\n  { path: 'cookies.get', api: () => Browser.cookies?.get },\n  { path: 'cookies.getAll', api: () => Browser.cookies?.getAll },\n  { path: 'cookies.remove', api: () => Browser.cookies?.remove },\n  { path: 'cookies.set', api: () => Browser.cookies?.set },\n  { path: 'downloads.search', api: () => Browser.downloads?.search },\n  { path: 'downloads.download', api: () => Browser.downloads?.download },\n  {\n    isEvent: true,\n    path: 'downloads.onCreated',\n    api: () => Browser.downloads?.onCreated,\n  },\n  {\n    isEvent: true,\n    path: 'downloads.onDeterminingFilename',\n    api: () => chrome.downloads?.onDeterminingFilename,\n  },\n  {\n    isEvent: true,\n    path: 'downloads.onChanged',\n    api: () => Browser.downloads?.onChanged,\n  },\n  {\n    path: 'browserAction.setBadgeText',\n    api: () => (Browser.action || Browser.browserAction).setBadgeText,\n  },\n  {\n    path: 'notifications.create',\n    api: () => Browser.notifications?.create,\n  },\n  {\n    path: 'extension.isAllowedFileSchemeAccess',\n    api: () => Browser.extension.isAllowedFileSchemeAccess,\n  },\n];\n"
  },
  {
    "path": "src/service/renderer/RendererWorkflowService.js",
    "content": "import { MessageListener } from '@/utils/message';\nimport { toRaw } from 'vue';\n\nclass RendererWorkflowService {\n  static executeWorkflow(workflowData, options) {\n    /**\n     * Convert Vue-created proxy into plain object.\n     * It will throw error if there a proxy inside the object.\n     */\n    const clonedWorkflowData = {};\n    Object.keys(workflowData).forEach((key) => {\n      clonedWorkflowData[key] = toRaw(workflowData[key]);\n    });\n\n    return MessageListener.sendMessage(\n      'workflow:execute',\n      { ...workflowData, options },\n      'background'\n    );\n  }\n\n  static stopWorkflowExecution(executionId) {\n    return MessageListener.sendMessage(\n      'workflow:stop',\n      executionId,\n      'background'\n    );\n  }\n}\n\nexport default RendererWorkflowService;\n"
  },
  {
    "path": "src/stores/folder.js",
    "content": "import { defineStore } from 'pinia';\nimport { nanoid } from 'nanoid';\nimport browser from 'webextension-polyfill';\n\nexport const useFolderStore = defineStore('folder', {\n  storageMap: {\n    items: 'folders',\n  },\n  state: () => ({\n    items: [],\n    retrieved: false,\n  }),\n  actions: {\n    async addFolder(name) {\n      this.items.push({\n        name,\n        id: nanoid(),\n      });\n\n      await this.saveToStorage('items');\n\n      return this.items.at(-1);\n    },\n    async deleteFolder(id) {\n      const index = this.items.findIndex((folder) => folder.id === id);\n      if (index === -1) return null;\n\n      this.items.splice(index, 1);\n      await this.saveToStorage('items');\n\n      return index;\n    },\n    async updateFolder(id, data = {}) {\n      const index = this.items.findIndex((folder) => folder.id === id);\n      if (index === -1) return null;\n\n      Object.assign(this.items[index], data);\n      await this.saveToStorage('items');\n\n      return this.items[index];\n    },\n    load() {\n      return browser.storage.local.get('folders').then(({ folders }) => {\n        this.items = folders || [];\n        this.retrieved = true;\n        return folders;\n      });\n    },\n  },\n});\n"
  },
  {
    "path": "src/stores/hostedWorkflow.js",
    "content": "import { defineStore } from 'pinia';\nimport browser from 'webextension-polyfill';\nimport { fetchApi } from '@/utils/api';\nimport {\n  registerWorkflowTrigger,\n  cleanWorkflowTriggers,\n} from '@/utils/workflowTrigger';\nimport { findTriggerBlock } from '@/utils/helper';\n\nexport const useHostedWorkflowStore = defineStore('hosted-workflows', {\n  storageMap: {\n    workflows: 'workflowHosts',\n  },\n  state: () => ({\n    workflows: {},\n    retrieved: false,\n  }),\n  getters: {\n    getById: (state) => (id) => state.workflows[id],\n    toArray: (state) => Object.values(state.workflows),\n  },\n  actions: {\n    async loadData() {\n      const { workflowHosts } = await browser.storage.local.get(\n        'workflowHosts'\n      );\n      this.workflows = workflowHosts || {};\n      this.retrieved = true;\n    },\n    async insert(data, idKey = 'hostId') {\n      if (Array.isArray(data)) {\n        data.forEach((item) => {\n          this.workflows[idKey] = item;\n        });\n      } else {\n        this.workflows[idKey] = data;\n      }\n\n      await this.saveToStorage('workflows');\n\n      return data;\n    },\n    async delete(id) {\n      delete this.workflows[id];\n\n      await this.saveToStorage('workflows');\n      await cleanWorkflowTriggers(id);\n\n      return id;\n    },\n    async update({ id, data }) {\n      if (!this.workflows[id]) return null;\n\n      Object.assign(this.workflows[id], data);\n      await this.saveToStorage('workflows');\n\n      return this.workflows[id];\n    },\n    async fetchWorkflows(ids) {\n      if (!ids || ids.length === 0) return null;\n\n      const response = await fetchApi('/workflows/hosted', {\n        auth: true,\n        method: 'POST',\n        body: JSON.stringify({ hosts: ids }),\n      });\n      const result = await response.json();\n\n      if (!response.ok) throw new Error(result.message);\n\n      const dataToReturn = [];\n\n      result.forEach(({ hostId, status, data }) => {\n        if (status === 'deleted') {\n          delete this.workflows[hostId];\n          cleanWorkflowTriggers(hostId);\n          return;\n        }\n        if (status === 'updated') {\n          const triggerBlock = findTriggerBlock(data.drawflow);\n          registerWorkflowTrigger(hostId, triggerBlock);\n        }\n\n        data.hostId = hostId;\n        dataToReturn.push(data);\n        this.workflows[hostId] = data;\n      });\n\n      await this.saveToStorage('workflows');\n\n      return dataToReturn;\n    },\n  },\n});\n"
  },
  {
    "path": "src/stores/main.js",
    "content": "import { defineStore } from 'pinia';\nimport defu from 'defu';\nimport browser from 'webextension-polyfill';\nimport deepmerge from 'lodash.merge';\nimport { fetchGapi, fetchApi } from '@/utils/api';\n\nexport const useStore = defineStore('main', {\n  storageMap: {\n    tabs: 'tabs',\n    settings: 'settings',\n  },\n  state: () => ({\n    tabs: [],\n    copiedEls: {\n      edges: [],\n      nodes: [],\n    },\n    settings: {\n      locale: 'en',\n      deleteLogAfter: 30,\n      logsLimit: 1000,\n      editor: {\n        minZoom: 0.3,\n        maxZoom: 1.3,\n        arrow: true,\n        snapToGrid: false,\n        lineType: 'default',\n        saveWhenExecute: false,\n        snapGrid: { 0: 15, 1: 15 },\n      },\n    },\n    integrations: {\n      googleDrive: false,\n    },\n    integrationsRetrieved: {\n      googleDrive: false,\n    },\n    retrieved: true,\n    connectedSheets: [],\n    connectedSheetsRetrieved: false,\n  }),\n  actions: {\n    loadSettings() {\n      return browser.storage.local.get('settings').then(({ settings }) => {\n        this.settings = defu(settings || {}, this.settings);\n        this.retrieved = true;\n      });\n    },\n    async updateSettings(settings = {}) {\n      this.settings = deepmerge(this.settings, settings);\n      await this.saveToStorage('settings');\n    },\n    async checkGDriveIntegration(force = false, retryCount = 0) {\n      try {\n        if (this.integrationsRetrieved.googleDrive && !force) return;\n\n        const result = await fetchGapi(\n          `https://www.googleapis.com/oauth2/v1/tokeninfo`\n        );\n        if (!result) return;\n\n        const isIntegrated = result.scope.includes('auth/drive.file');\n        const { sessionToken } = await browser.storage.local.get(\n          'sessionToken'\n        );\n\n        if (!isIntegrated && sessionToken?.refresh && retryCount < 3) {\n          const response = await fetchApi(\n            `/me/refresh-session?token=${sessionToken.refresh}`,\n            { auth: true }\n          );\n          const refreshResult = await response.json();\n          if (!response.ok) throw new Error(refreshResult.message);\n\n          await browser.storage.local.set({\n            sessionToken: {\n              ...sessionToken,\n              access: refreshResult.token,\n            },\n          });\n          await this.checkGDriveIntegration(force, retryCount + 1);\n\n          return;\n        }\n\n        this.integrations.googleDrive = isIntegrated;\n      } catch (error) {\n        console.error(error);\n      }\n    },\n    async getConnectedSheets() {\n      try {\n        if (this.connectedSheetsRetrieved) return;\n\n        const result = await fetchGapi(\n          'https://www.googleapis.com/drive/v3/files'\n        );\n\n        this.integrations.googleDrive = true;\n        this.connectedSheets = result.files.filter(\n          (file) => file.mimeType === 'application/vnd.google-apps.spreadsheet'\n        );\n      } catch (error) {\n        if (\n          error.message === 'no-scope' ||\n          error.message.includes('insufficient authentication')\n        ) {\n          this.integrations.googleDrive = false;\n        }\n\n        console.error(error);\n      }\n    },\n  },\n});\n"
  },
  {
    "path": "src/stores/package.js",
    "content": "import { defineStore } from 'pinia';\nimport { nanoid } from 'nanoid';\nimport browser from 'webextension-polyfill';\nimport { fetchApi } from '@/utils/api';\n\nconst defaultPackage = {\n  id: '',\n  name: '',\n  icon: 'mdiPackageVariantClosed',\n  isExtenal: false,\n  content: null,\n  inputs: [],\n  outputs: [],\n  variable: [],\n  settings: {\n    asBlock: false,\n  },\n  data: {\n    edges: [],\n    nodes: [],\n  },\n};\n\nexport const usePackageStore = defineStore('packages', {\n  storageMap: {\n    packages: 'savedBlocks',\n  },\n  state: () => ({\n    packages: [],\n    sharedPkgs: [],\n    retrieved: false,\n    sharedRetrieved: false,\n  }),\n  getters: {\n    getById: (state) => (pkgId) => {\n      return state.packages.find((pkg) => pkg.id === pkgId);\n    },\n    isShared: (state) => (pkgId) => {\n      return state.sharedPkgs.some((pkg) => pkg.id === pkgId);\n    },\n  },\n  actions: {\n    async insert(data, newId = true) {\n      const packageData = {\n        ...defaultPackage,\n        ...data,\n        createdAt: Date.now(),\n      };\n      if (newId) packageData.id = nanoid();\n\n      this.packages.push(packageData);\n      await this.saveToStorage('packages');\n    },\n    async update({ id, data }) {\n      const index = this.packages.findIndex((pkg) => pkg.id === id);\n      if (index === -1) return null;\n\n      Object.assign(this.packages[index], data);\n      await this.saveToStorage('packages');\n\n      return this.packages[index];\n    },\n    async delete(id) {\n      const index = this.packages.findIndex((pkg) => pkg.id === id);\n      if (index === -1) return null;\n\n      const data = this.packages[index];\n      this.packages.splice(index, 1);\n\n      await this.saveToStorage('packages');\n\n      return data;\n    },\n    deleteShared(id) {\n      const index = this.sharedPkgs.findIndex((item) => item.id === id);\n      if (index !== -1) this.sharedPkgs.splice(index, 1);\n    },\n    insertShared(id) {\n      this.sharedPkgs.push({ id });\n    },\n    async loadData(force = false) {\n      if (this.retrieved && !force) return this.packages;\n\n      const { savedBlocks } = await browser.storage.local.get('savedBlocks');\n\n      this.packages = savedBlocks || [];\n      this.retrieved = true;\n\n      return this.packages;\n    },\n    async loadShared() {\n      try {\n        if (this.sharedRetrieved) return;\n\n        const response = await fetchApi('/me/packages', { auth: true });\n        const result = await response.json();\n\n        if (!response.ok) throw new Error(result.message);\n\n        this.sharedPkgs = result;\n        this.sharedRetrieved = true;\n      } catch (error) {\n        console.error(error.message);\n\n        throw error;\n      }\n    },\n  },\n});\n"
  },
  {
    "path": "src/stores/sharedWorkflow.js",
    "content": "import { defineStore } from 'pinia';\nimport { fetchApi, cacheApi } from '@/utils/api';\nimport { useUserStore } from './user';\n\nexport const useSharedWorkflowStore = defineStore('shared-workflows', {\n  state: () => ({\n    workflows: {},\n  }),\n  getters: {\n    toArray: (state) => Object.values(state.workflows),\n    getById: (state) => (id) => {\n      if (!state.workflows) return null;\n\n      return state.workflows[id] || null;\n    },\n  },\n  actions: {\n    insert(data) {\n      if (Array.isArray(data)) {\n        data.forEach((item) => {\n          this.workflows[item.id] = item;\n        });\n      } else {\n        this.workflows[data.id] = data;\n      }\n    },\n    update({ id, data }) {\n      if (!this.workflows[id]) return null;\n\n      Object.assign(this.workflows[id], data);\n\n      return this.workflows[id];\n    },\n    delete(id) {\n      delete this.workflows[id];\n    },\n    async fetchWorkflows(useCache = true) {\n      const userStore = useUserStore();\n      if (!userStore.user) return;\n\n      const workflows = await cacheApi(\n        'shared-workflows',\n        async () => {\n          try {\n            const response = await fetchApi('/me/workflows/shared?data=all', {\n              auth: true,\n            });\n\n            if (response.status !== 200) throw new Error(response.statusText);\n\n            const result = await response.json();\n            const sharedWorkflows = result.reduce((acc, item) => {\n              item.table = item.table || item.dataColumns || [];\n              item.createdAt = new Date(item.createdAt || Date.now()).getTime();\n\n              acc[item.id] = item;\n\n              return acc;\n            }, {});\n\n            return sharedWorkflows;\n          } catch (error) {\n            console.error(error);\n            return {};\n          }\n        },\n        useCache\n      );\n\n      this.workflows = workflows || {};\n    },\n  },\n});\n"
  },
  {
    "path": "src/stores/teamWorkflow.js",
    "content": "import { defineStore } from 'pinia';\nimport browser from 'webextension-polyfill';\nimport lodashDeepmerge from 'lodash.merge';\nimport { cleanWorkflowTriggers } from '@/utils/workflowTrigger';\n\nexport const useTeamWorkflowStore = defineStore('team-workflows', {\n  storageMap: {\n    workflows: 'teamWorkflows',\n  },\n  state: () => ({\n    workflows: {},\n    retrieved: false,\n  }),\n  getters: {\n    toArray: (state) => Object.values(state.workflows),\n    getByTeam: (state) => (teamId) => {\n      if (!state.workflows) return [];\n\n      return Object.values(state.workflows[teamId] || {});\n    },\n    getById: (state) => (teamId, id) => {\n      if (!state.workflows || !state.workflows[teamId]) return null;\n\n      return state.workflows[teamId][id] || null;\n    },\n  },\n  actions: {\n    async insert(teamId, data) {\n      if (!this.workflows[teamId]) this.workflows[teamId] = {};\n\n      if (Array.isArray(data)) {\n        data.forEach((item) => {\n          this.workflows[teamId][item.id] = item;\n        });\n      } else {\n        this.workflows[teamId][data.id] = data;\n      }\n\n      await this.saveToStorage('workflows');\n    },\n    async update({ teamId, id, data, deepmerge = false }) {\n      const isWorkflowExists = Boolean(this.workflows[teamId]?.[id]);\n      if (!isWorkflowExists) return null;\n\n      if (deepmerge) {\n        this.workflows[teamId][id] = lodashDeepmerge(\n          this.workflows[teamId][id],\n          data\n        );\n      } else {\n        Object.assign(this.workflows[teamId][id], data);\n      }\n\n      await this.saveToStorage('workflows');\n\n      return this.workflows[teamId][id];\n    },\n    async delete(teamId, id) {\n      if (!this.workflows[teamId]) return;\n\n      delete this.workflows[teamId][id];\n\n      await this.saveToStorage('workflows');\n      await cleanWorkflowTriggers(id);\n    },\n    async loadData() {\n      const { teamWorkflows } = await browser.storage.local.get(\n        'teamWorkflows'\n      );\n\n      this.workflows = teamWorkflows || {};\n      this.retrieved = true;\n    },\n  },\n});\n"
  },
  {
    "path": "src/stores/user.js",
    "content": "import { cacheApi, fetchApi } from '@/utils/api';\nimport { defineStore } from 'pinia';\nimport browser from 'webextension-polyfill';\n\nexport const useUserStore = defineStore('user', {\n  state: () => ({\n    user: null,\n    backupIds: [],\n    retrieved: false,\n    hostedWorkflows: {},\n    sharedPackages: [],\n  }),\n  getters: {\n    getHostedWorkflows: (state) => Object.values(state.hostedWorkflows),\n    validateTeamAccess:\n      (state) =>\n      (teamId, access = []) => {\n        const currentTeam = state.user?.teams?.find(\n          ({ id }) => teamId === id || +teamId === id\n        );\n        if (!currentTeam) return false;\n\n        return access.some((item) => currentTeam.access.includes(item));\n      },\n  },\n  actions: {\n    async loadUser(options = false) {\n      try {\n        const user = await cacheApi(\n          'user-profile',\n          async () => {\n            try {\n              const response = await fetchApi('/me', { auth: true });\n              const result = await response.json();\n\n              if (!response.ok) throw new Error(response.message);\n\n              return result;\n            } catch (error) {\n              console.error(error);\n              return null;\n            }\n          },\n          options\n        );\n\n        const username = localStorage.getItem('username');\n        if (!user || username !== user.username) {\n          sessionStorage.removeItem('shared-workflows');\n          sessionStorage.removeItem('user-workflows');\n          sessionStorage.removeItem('backup-workflows');\n\n          await browser.storage.local.remove([\n            'backupIds',\n            'lastSync',\n            'lastBackup',\n          ]);\n\n          if (!user) {\n            this.retrieved = true;\n            return;\n          }\n        }\n\n        localStorage.setItem('username', user?.username);\n\n        const { backupIds } = await browser.storage.local.get('backupIds');\n        this.backupIds = backupIds || [];\n\n        this.user = user;\n        this.retrieved = true;\n\n        if (user) {\n          await browser.storage.local.set({ user });\n        } else {\n          await browser.storage.local.remove('user');\n        }\n      } catch (error) {\n        this.retrieved = true;\n        console.error(error);\n      }\n    },\n    async signOut() {\n      try {\n        await browser.storage.local.remove([\n          'session',\n          'sessionToken',\n          'user',\n          'backupIds',\n        ]);\n\n        localStorage.removeItem('username');\n\n        this.user = null;\n        this.backupIds = [];\n        this.hostedWorkflows = {};\n        this.sharedPackages = [];\n        this.retrieved = false;\n      } catch (error) {\n        console.error('Sign out error:', error);\n        throw error;\n      }\n    },\n  },\n});\n"
  },
  {
    "path": "src/stores/workflow.js",
    "content": "import { fetchApi } from '@/utils/api';\nimport firstWorkflows from '@/utils/firstWorkflows';\nimport { tasks } from '@/utils/shared';\nimport {\n  cleanWorkflowTriggers,\n  registerWorkflowTrigger,\n} from '@/utils/workflowTrigger';\nimport dayjs from 'dayjs';\nimport defu from 'defu';\nimport deepmerge from 'lodash.merge';\nimport { nanoid } from 'nanoid';\nimport { defineStore } from 'pinia';\nimport browser from 'webextension-polyfill';\nimport { useUserStore } from './user';\n\nconst defaultWorkflow = (data = null, options = {}) => {\n  let workflowData = {\n    id: nanoid(),\n    name: '',\n    icon: 'riGlobalLine',\n    folderId: null,\n    content: null,\n    connectedTable: null,\n    drawflow: {\n      edges: [],\n      zoom: 1.3,\n      nodes: [\n        {\n          position: {\n            x: 100,\n            y: window.innerHeight / 2,\n          },\n          id: nanoid(),\n          label: 'trigger',\n          data: tasks.trigger.data,\n          type: tasks.trigger.component,\n        },\n      ],\n    },\n    table: [],\n    dataColumns: [],\n    description: '',\n    trigger: null,\n    createdAt: Date.now(),\n    updatedAt: Date.now(),\n    isDisabled: false,\n    settings: {\n      publicId: '',\n      aipowerToken: '',\n      blockDelay: 0,\n      saveLog: true,\n      debugMode: false,\n      restartTimes: 3,\n      notification: true,\n      execContext: 'popup',\n      reuseLastState: false,\n      inputAutocomplete: true,\n      onError: 'stop-workflow',\n      executedBlockOnWeb: false,\n      insertDefaultColumn: false,\n      defaultColumnName: 'column',\n    },\n    version: browser.runtime.getManifest().version,\n    globalData: '{\\n\\t\"key\": \"value\"\\n}',\n  };\n\n  if (data) {\n    if (options.duplicateId && data.id) {\n      delete workflowData.id;\n    }\n\n    if (data.drawflow?.nodes?.length > 0) {\n      workflowData.drawflow.nodes = [];\n    }\n\n    workflowData = defu(data, workflowData);\n  }\n\n  return workflowData;\n};\n\nfunction convertWorkflowsToObject(workflows) {\n  if (Array.isArray(workflows)) {\n    return workflows.reduce((acc, workflow) => {\n      acc[workflow.id] = workflow;\n\n      return acc;\n    }, {});\n  }\n\n  return workflows;\n}\n\nexport const useWorkflowStore = defineStore('workflow', {\n  storageMap: {\n    workflows: 'workflows',\n  },\n  state: () => ({\n    states: [],\n    workflows: {},\n    popupStates: [],\n    retrieved: false,\n    isFirstTime: false,\n  }),\n  getters: {\n    getAllStates: (state) => [...state.popupStates, ...state.states],\n    getById: (state) => (id) => state.workflows[id],\n    getWorkflows: (state) => Object.values(state.workflows),\n    getWorkflowStates: (state) => (id) =>\n      [...state.states, ...state.popupStates].filter(\n        ({ workflowId }) => workflowId === id\n      ),\n  },\n  actions: {\n    async loadData() {\n      const { workflows, isFirstTime } = await browser.storage.local.get([\n        'workflows',\n        'isFirstTime',\n      ]);\n\n      let localWorkflows = workflows || {};\n\n      if (isFirstTime) {\n        localWorkflows = firstWorkflows.map((workflow) =>\n          defaultWorkflow(workflow)\n        );\n        await browser.storage.local.set({\n          isFirstTime: false,\n          workflows: localWorkflows,\n        });\n      }\n\n      this.isFirstTime = isFirstTime;\n      this.workflows = convertWorkflowsToObject(localWorkflows);\n\n      this.retrieved = true;\n    },\n    updateStates(newStates) {\n      this.states = newStates;\n    },\n    async insert(data = {}, options = {}) {\n      const insertedWorkflows = {};\n\n      if (Array.isArray(data)) {\n        data.forEach((item) => {\n          if (!options.duplicateId) {\n            delete item.id;\n          }\n\n          const workflow = defaultWorkflow(item, options);\n          this.workflows[workflow.id] = workflow;\n          insertedWorkflows[workflow.id] = workflow;\n        });\n      } else {\n        if (!options.duplicateId) {\n          delete data.id;\n        }\n\n        const workflow = defaultWorkflow(data, options);\n        this.workflows[workflow.id] = workflow;\n        insertedWorkflows[workflow.id] = workflow;\n      }\n\n      await this.saveToStorage('workflows');\n\n      return insertedWorkflows;\n    },\n    async update({ id, data = {}, deep = false }) {\n      const isFunction = typeof id === 'function';\n      if (!isFunction && !this.workflows[id]) return null;\n\n      const updatedWorkflows = {};\n      const updateData = { ...data, updatedAt: Date.now() };\n\n      const workflowUpdater = (workflowId) => {\n        if (deep) {\n          this.workflows[workflowId] = deepmerge(\n            this.workflows[workflowId],\n            updateData\n          );\n        } else {\n          Object.assign(this.workflows[workflowId], updateData);\n        }\n\n        this.workflows[workflowId].updatedAt = Date.now();\n        updatedWorkflows[workflowId] = this.workflows[workflowId];\n\n        if (!('isDisabled' in data)) return;\n\n        if (data.isDisabled) {\n          cleanWorkflowTriggers(workflowId);\n        } else {\n          const triggerBlock = this.workflows[workflowId].drawflow.nodes?.find(\n            (node) => node.label === 'trigger'\n          );\n          if (triggerBlock) {\n            registerWorkflowTrigger(id, triggerBlock);\n          }\n        }\n      };\n\n      if (isFunction) {\n        this.getWorkflows.forEach((workflow) => {\n          const isMatch = id(workflow) ?? false;\n          if (isMatch) workflowUpdater(workflow.id);\n        });\n      } else {\n        workflowUpdater(id);\n      }\n\n      await this.saveToStorage('workflows');\n\n      return updatedWorkflows;\n    },\n    async insertOrUpdate(\n      data = [],\n      { checkUpdateDate = false, duplicateId = false } = {}\n    ) {\n      const insertedData = {};\n\n      data.forEach((item) => {\n        const currentWorkflow = this.workflows[item.id];\n\n        if (currentWorkflow) {\n          let insert = true;\n          if (checkUpdateDate && currentWorkflow.createdAt && item.updatedAt) {\n            insert = dayjs(currentWorkflow.updatedAt).isBefore(item.updatedAt);\n          }\n\n          if (insert) {\n            const mergedData = deepmerge(this.workflows[item.id], item);\n\n            this.workflows[item.id] = mergedData;\n            insertedData[item.id] = mergedData;\n          }\n        } else {\n          const workflow = defaultWorkflow(item, { duplicateId });\n          this.workflows[workflow.id] = workflow;\n          insertedData[workflow.id] = workflow;\n        }\n      });\n\n      await this.saveToStorage('workflows');\n\n      return insertedData;\n    },\n    async delete(id) {\n      if (Array.isArray(id)) {\n        id.forEach((workflowId) => {\n          delete this.workflows[workflowId];\n        });\n      } else {\n        delete this.workflows[id];\n      }\n\n      await cleanWorkflowTriggers(id);\n\n      const userStore = useUserStore();\n\n      const hostedWorkflow = userStore.hostedWorkflows[id];\n      const backupIndex = userStore.backupIds.indexOf(id);\n\n      if (hostedWorkflow || backupIndex !== -1) {\n        const response = await fetchApi(`/me/workflows?id=${id}`, {\n          auth: true,\n          method: 'DELETE',\n        });\n        const result = await response.json();\n\n        if (!response.ok) {\n          throw new Error(result.message);\n        }\n\n        if (backupIndex !== -1) {\n          userStore.backupIds.splice(backupIndex, 1);\n          await browser.storage.local.set({ backupIds: userStore.backupIds });\n        }\n      }\n\n      await browser.storage.local.remove([\n        `state:${id}`,\n        `draft:${id}`,\n        `draft-team:${id}`,\n      ]);\n      await this.saveToStorage('workflows');\n\n      const { pinnedWorkflows } = await browser.storage.local.get(\n        'pinnedWorkflows'\n      );\n      const pinnedWorkflowIndex = pinnedWorkflows\n        ? pinnedWorkflows.indexOf(id)\n        : -1;\n      if (pinnedWorkflowIndex !== -1) {\n        pinnedWorkflows.splice(pinnedWorkflowIndex, 1);\n        await browser.storage.local.set({ pinnedWorkflows });\n      }\n\n      return id;\n    },\n  },\n});\n"
  },
  {
    "path": "src/utils/FindElement.js",
    "content": "import Sizzle from 'sizzle';\nimport {\n  querySelectorAllDeep,\n  querySelectorDeep,\n} from '@/lib/query-selector-shadow-dom';\n\n// Add a custom \"Sizzle\" pseudo-class selector\n// \":contains\": element content will be selected as long as it contains text\n// \":equal\" : element content must be exactly the same as text to be selected\n// Example: p.description:equal(\"cat\")\nSizzle.selectors.pseudos.equal = Sizzle.selectors.createPseudo(function (text) {\n  return function (elem) {\n    const elemText = elem.textContent || elem.innerText || '';\n    return elemText.trim() === text;\n  };\n});\n\nconst specialSelectors = [':contains', ':header', ':parent', ':equal'];\nconst specialSelectorsRegex = new RegExp(specialSelectors.join('|'));\n\nclass FindElement {\n  static cssSelector(data, documentCtx = document) {\n    const selector = data.markEl\n      ? `${data.selector.trim()}:not([${data.blockIdAttr}])`\n      : data.selector;\n\n    if (specialSelectorsRegex.test(selector)) {\n      // Fix Sizzle incorrect context in iframe, passed as context of iframe\n      const elements = Sizzle(selector, documentCtx);\n      if (!elements) return null;\n\n      return data.multiple ? elements : elements[0];\n    }\n\n    if (selector.includes('>>')) {\n      const newSelector = selector.replaceAll('>>', '');\n\n      return data.multiple\n        ? querySelectorAllDeep(newSelector)\n        : querySelectorDeep(newSelector);\n    }\n\n    if (data.multiple) {\n      const elements = documentCtx.querySelectorAll(selector);\n\n      if (elements.length === 0) return null;\n\n      return elements;\n    }\n\n    return documentCtx.querySelector(selector);\n  }\n\n  static xpath(data, documentCtx = document) {\n    const resultType = data.multiple\n      ? XPathResult.ORDERED_NODE_ITERATOR_TYPE\n      : XPathResult.FIRST_ORDERED_NODE_TYPE;\n\n    let result = null;\n    const elements = documentCtx.evaluate(\n      data.selector,\n      documentCtx,\n      null,\n      resultType,\n      null\n    );\n\n    if (data.multiple) {\n      result = [];\n      let element = elements.iterateNext();\n\n      while (element) {\n        result.push(element);\n\n        element = elements.iterateNext();\n      }\n    } else {\n      result = elements.singleNodeValue;\n    }\n\n    return result;\n  }\n}\n\nexport default FindElement;\n"
  },
  {
    "path": "src/utils/USKeyboardLayout.js",
    "content": "/*\n  Forked from:\n  https://github.com/puppeteer/puppeteer/blob/e11fe713407c2430526ae616d0adfb5dd278b5de/src/common/USKeyboardLayout.ts\n*/\n\n/**\n * Copyright 2017 Google Inc. All rights reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the 'License');\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an 'AS IS' BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * @internal\n */\nexport const keyDefinitions = {\n  0: { keyCode: 48, key: '0', code: 'Digit0' },\n  1: { keyCode: 49, key: '1', code: 'Digit1' },\n  2: { keyCode: 50, key: '2', code: 'Digit2' },\n  3: { keyCode: 51, key: '3', code: 'Digit3' },\n  4: { keyCode: 52, key: '4', code: 'Digit4' },\n  5: { keyCode: 53, key: '5', code: 'Digit5' },\n  6: { keyCode: 54, key: '6', code: 'Digit6' },\n  7: { keyCode: 55, key: '7', code: 'Digit7' },\n  8: { keyCode: 56, key: '8', code: 'Digit8' },\n  9: { keyCode: 57, key: '9', code: 'Digit9' },\n  Power: { key: 'Power', code: 'Power' },\n  Eject: { key: 'Eject', code: 'Eject' },\n  Abort: { keyCode: 3, code: 'Abort', key: 'Cancel' },\n  Help: { keyCode: 6, code: 'Help', key: 'Help' },\n  Backspace: { keyCode: 8, code: 'Backspace', key: 'Backspace' },\n  Tab: { keyCode: 9, code: 'Tab', key: 'Tab' },\n  Numpad5: {\n    keyCode: 12,\n    shiftKeyCode: 101,\n    key: 'Clear',\n    code: 'Numpad5',\n    shiftKey: '5',\n    location: 3,\n  },\n  NumpadEnter: {\n    keyCode: 13,\n    code: 'NumpadEnter',\n    key: 'Enter',\n    text: '\\r',\n    location: 3,\n  },\n  Enter: { keyCode: 13, code: 'Enter', key: 'Enter', text: '\\r' },\n  '\\r': { keyCode: 13, code: 'Enter', key: 'Enter', text: '\\r' },\n  '\\n': { keyCode: 13, code: 'Enter', key: 'Enter', text: '\\r' },\n  ShiftLeft: { keyCode: 16, code: 'ShiftLeft', key: 'Shift', location: 1 },\n  ShiftRight: { keyCode: 16, code: 'ShiftRight', key: 'Shift', location: 2 },\n  ControlLeft: {\n    keyCode: 17,\n    code: 'ControlLeft',\n    key: 'Control',\n    location: 1,\n  },\n  ControlRight: {\n    keyCode: 17,\n    code: 'ControlRight',\n    key: 'Control',\n    location: 2,\n  },\n  AltLeft: { keyCode: 18, code: 'AltLeft', key: 'Alt', location: 1 },\n  AltRight: { keyCode: 18, code: 'AltRight', key: 'Alt', location: 2 },\n  Pause: { keyCode: 19, code: 'Pause', key: 'Pause' },\n  CapsLock: { keyCode: 20, code: 'CapsLock', key: 'CapsLock' },\n  Escape: { keyCode: 27, code: 'Escape', key: 'Escape' },\n  Convert: { keyCode: 28, code: 'Convert', key: 'Convert' },\n  NonConvert: { keyCode: 29, code: 'NonConvert', key: 'NonConvert' },\n  Space: { keyCode: 32, code: 'Space', key: ' ' },\n  Numpad9: {\n    keyCode: 33,\n    shiftKeyCode: 105,\n    key: 'PageUp',\n    code: 'Numpad9',\n    shiftKey: '9',\n    location: 3,\n  },\n  PageUp: { keyCode: 33, code: 'PageUp', key: 'PageUp' },\n  Numpad3: {\n    keyCode: 34,\n    shiftKeyCode: 99,\n    key: 'PageDown',\n    code: 'Numpad3',\n    shiftKey: '3',\n    location: 3,\n  },\n  PageDown: { keyCode: 34, code: 'PageDown', key: 'PageDown' },\n  End: { keyCode: 35, code: 'End', key: 'End' },\n  Numpad1: {\n    keyCode: 35,\n    shiftKeyCode: 97,\n    key: 'End',\n    code: 'Numpad1',\n    shiftKey: '1',\n    location: 3,\n  },\n  Home: { keyCode: 36, code: 'Home', key: 'Home' },\n  Numpad7: {\n    keyCode: 36,\n    shiftKeyCode: 103,\n    key: 'Home',\n    code: 'Numpad7',\n    shiftKey: '7',\n    location: 3,\n  },\n  ArrowLeft: { keyCode: 37, code: 'ArrowLeft', key: 'ArrowLeft' },\n  Numpad4: {\n    keyCode: 37,\n    shiftKeyCode: 100,\n    key: 'ArrowLeft',\n    code: 'Numpad4',\n    shiftKey: '4',\n    location: 3,\n  },\n  Numpad8: {\n    keyCode: 38,\n    shiftKeyCode: 104,\n    key: 'ArrowUp',\n    code: 'Numpad8',\n    shiftKey: '8',\n    location: 3,\n  },\n  ArrowUp: { keyCode: 38, code: 'ArrowUp', key: 'ArrowUp' },\n  ArrowRight: { keyCode: 39, code: 'ArrowRight', key: 'ArrowRight' },\n  Numpad6: {\n    keyCode: 39,\n    shiftKeyCode: 102,\n    key: 'ArrowRight',\n    code: 'Numpad6',\n    shiftKey: '6',\n    location: 3,\n  },\n  Numpad2: {\n    keyCode: 40,\n    shiftKeyCode: 98,\n    key: 'ArrowDown',\n    code: 'Numpad2',\n    shiftKey: '2',\n    location: 3,\n  },\n  ArrowDown: { keyCode: 40, code: 'ArrowDown', key: 'ArrowDown' },\n  Select: { keyCode: 41, code: 'Select', key: 'Select' },\n  Open: { keyCode: 43, code: 'Open', key: 'Execute' },\n  PrintScreen: { keyCode: 44, code: 'PrintScreen', key: 'PrintScreen' },\n  Insert: { keyCode: 45, code: 'Insert', key: 'Insert' },\n  Numpad0: {\n    keyCode: 45,\n    shiftKeyCode: 96,\n    key: 'Insert',\n    code: 'Numpad0',\n    shiftKey: '0',\n    location: 3,\n  },\n  Delete: { keyCode: 46, code: 'Delete', key: 'Delete' },\n  NumpadDecimal: {\n    keyCode: 46,\n    shiftKeyCode: 110,\n    code: 'NumpadDecimal',\n    key: '\\u0000',\n    shiftKey: '.',\n    location: 3,\n  },\n  Digit0: { keyCode: 48, code: 'Digit0', shiftKey: ')', key: '0' },\n  Digit1: { keyCode: 49, code: 'Digit1', shiftKey: '!', key: '1' },\n  Digit2: { keyCode: 50, code: 'Digit2', shiftKey: '@', key: '2' },\n  Digit3: { keyCode: 51, code: 'Digit3', shiftKey: '#', key: '3' },\n  Digit4: { keyCode: 52, code: 'Digit4', shiftKey: '$', key: '4' },\n  Digit5: { keyCode: 53, code: 'Digit5', shiftKey: '%', key: '5' },\n  Digit6: { keyCode: 54, code: 'Digit6', shiftKey: '^', key: '6' },\n  Digit7: { keyCode: 55, code: 'Digit7', shiftKey: '&', key: '7' },\n  Digit8: { keyCode: 56, code: 'Digit8', shiftKey: '*', key: '8' },\n  Digit9: { keyCode: 57, code: 'Digit9', shiftKey: '(', key: '9' },\n  KeyA: { keyCode: 65, code: 'KeyA', shiftKey: 'A', key: 'a' },\n  KeyB: { keyCode: 66, code: 'KeyB', shiftKey: 'B', key: 'b' },\n  KeyC: { keyCode: 67, code: 'KeyC', shiftKey: 'C', key: 'c' },\n  KeyD: { keyCode: 68, code: 'KeyD', shiftKey: 'D', key: 'd' },\n  KeyE: { keyCode: 69, code: 'KeyE', shiftKey: 'E', key: 'e' },\n  KeyF: { keyCode: 70, code: 'KeyF', shiftKey: 'F', key: 'f' },\n  KeyG: { keyCode: 71, code: 'KeyG', shiftKey: 'G', key: 'g' },\n  KeyH: { keyCode: 72, code: 'KeyH', shiftKey: 'H', key: 'h' },\n  KeyI: { keyCode: 73, code: 'KeyI', shiftKey: 'I', key: 'i' },\n  KeyJ: { keyCode: 74, code: 'KeyJ', shiftKey: 'J', key: 'j' },\n  KeyK: { keyCode: 75, code: 'KeyK', shiftKey: 'K', key: 'k' },\n  KeyL: { keyCode: 76, code: 'KeyL', shiftKey: 'L', key: 'l' },\n  KeyM: { keyCode: 77, code: 'KeyM', shiftKey: 'M', key: 'm' },\n  KeyN: { keyCode: 78, code: 'KeyN', shiftKey: 'N', key: 'n' },\n  KeyO: { keyCode: 79, code: 'KeyO', shiftKey: 'O', key: 'o' },\n  KeyP: { keyCode: 80, code: 'KeyP', shiftKey: 'P', key: 'p' },\n  KeyQ: { keyCode: 81, code: 'KeyQ', shiftKey: 'Q', key: 'q' },\n  KeyR: { keyCode: 82, code: 'KeyR', shiftKey: 'R', key: 'r' },\n  KeyS: { keyCode: 83, code: 'KeyS', shiftKey: 'S', key: 's' },\n  KeyT: { keyCode: 84, code: 'KeyT', shiftKey: 'T', key: 't' },\n  KeyU: { keyCode: 85, code: 'KeyU', shiftKey: 'U', key: 'u' },\n  KeyV: { keyCode: 86, code: 'KeyV', shiftKey: 'V', key: 'v' },\n  KeyW: { keyCode: 87, code: 'KeyW', shiftKey: 'W', key: 'w' },\n  KeyX: { keyCode: 88, code: 'KeyX', shiftKey: 'X', key: 'x' },\n  KeyY: { keyCode: 89, code: 'KeyY', shiftKey: 'Y', key: 'y' },\n  KeyZ: { keyCode: 90, code: 'KeyZ', shiftKey: 'Z', key: 'z' },\n  MetaLeft: { keyCode: 91, code: 'MetaLeft', key: 'Meta', location: 1 },\n  MetaRight: { keyCode: 92, code: 'MetaRight', key: 'Meta', location: 2 },\n  ContextMenu: { keyCode: 93, code: 'ContextMenu', key: 'ContextMenu' },\n  NumpadMultiply: {\n    keyCode: 106,\n    code: 'NumpadMultiply',\n    key: '*',\n    location: 3,\n  },\n  NumpadAdd: { keyCode: 107, code: 'NumpadAdd', key: '+', location: 3 },\n  NumpadSubtract: {\n    keyCode: 109,\n    code: 'NumpadSubtract',\n    key: '-',\n    location: 3,\n  },\n  NumpadDivide: { keyCode: 111, code: 'NumpadDivide', key: '/', location: 3 },\n  F1: { keyCode: 112, code: 'F1', key: 'F1' },\n  F2: { keyCode: 113, code: 'F2', key: 'F2' },\n  F3: { keyCode: 114, code: 'F3', key: 'F3' },\n  F4: { keyCode: 115, code: 'F4', key: 'F4' },\n  F5: { keyCode: 116, code: 'F5', key: 'F5' },\n  F6: { keyCode: 117, code: 'F6', key: 'F6' },\n  F7: { keyCode: 118, code: 'F7', key: 'F7' },\n  F8: { keyCode: 119, code: 'F8', key: 'F8' },\n  F9: { keyCode: 120, code: 'F9', key: 'F9' },\n  F10: { keyCode: 121, code: 'F10', key: 'F10' },\n  F11: { keyCode: 122, code: 'F11', key: 'F11' },\n  F12: { keyCode: 123, code: 'F12', key: 'F12' },\n  F13: { keyCode: 124, code: 'F13', key: 'F13' },\n  F14: { keyCode: 125, code: 'F14', key: 'F14' },\n  F15: { keyCode: 126, code: 'F15', key: 'F15' },\n  F16: { keyCode: 127, code: 'F16', key: 'F16' },\n  F17: { keyCode: 128, code: 'F17', key: 'F17' },\n  F18: { keyCode: 129, code: 'F18', key: 'F18' },\n  F19: { keyCode: 130, code: 'F19', key: 'F19' },\n  F20: { keyCode: 131, code: 'F20', key: 'F20' },\n  F21: { keyCode: 132, code: 'F21', key: 'F21' },\n  F22: { keyCode: 133, code: 'F22', key: 'F22' },\n  F23: { keyCode: 134, code: 'F23', key: 'F23' },\n  F24: { keyCode: 135, code: 'F24', key: 'F24' },\n  NumLock: { keyCode: 144, code: 'NumLock', key: 'NumLock' },\n  ScrollLock: { keyCode: 145, code: 'ScrollLock', key: 'ScrollLock' },\n  AudioVolumeMute: {\n    keyCode: 173,\n    code: 'AudioVolumeMute',\n    key: 'AudioVolumeMute',\n  },\n  AudioVolumeDown: {\n    keyCode: 174,\n    code: 'AudioVolumeDown',\n    key: 'AudioVolumeDown',\n  },\n  AudioVolumeUp: { keyCode: 175, code: 'AudioVolumeUp', key: 'AudioVolumeUp' },\n  MediaTrackNext: {\n    keyCode: 176,\n    code: 'MediaTrackNext',\n    key: 'MediaTrackNext',\n  },\n  MediaTrackPrevious: {\n    keyCode: 177,\n    code: 'MediaTrackPrevious',\n    key: 'MediaTrackPrevious',\n  },\n  MediaStop: { keyCode: 178, code: 'MediaStop', key: 'MediaStop' },\n  MediaPlayPause: {\n    keyCode: 179,\n    code: 'MediaPlayPause',\n    key: 'MediaPlayPause',\n  },\n  Semicolon: { keyCode: 186, code: 'Semicolon', shiftKey: ':', key: ';' },\n  Equal: { keyCode: 187, code: 'Equal', shiftKey: '+', key: '=' },\n  NumpadEqual: { keyCode: 187, code: 'NumpadEqual', key: '=', location: 3 },\n  Comma: { keyCode: 188, code: 'Comma', shiftKey: '<', key: ',' },\n  Minus: { keyCode: 189, code: 'Minus', shiftKey: '_', key: '-' },\n  Period: { keyCode: 190, code: 'Period', shiftKey: '>', key: '.' },\n  Slash: { keyCode: 191, code: 'Slash', shiftKey: '?', key: '/' },\n  Backquote: { keyCode: 192, code: 'Backquote', shiftKey: '~', key: '`' },\n  BracketLeft: { keyCode: 219, code: 'BracketLeft', shiftKey: '{', key: '[' },\n  Backslash: { keyCode: 220, code: 'Backslash', shiftKey: '|', key: '\\\\' },\n  BracketRight: { keyCode: 221, code: 'BracketRight', shiftKey: '}', key: ']' },\n  Quote: { keyCode: 222, code: 'Quote', shiftKey: '\"', key: \"'\" },\n  AltGraph: { keyCode: 225, code: 'AltGraph', key: 'AltGraph' },\n  Props: { keyCode: 247, code: 'Props', key: 'CrSel' },\n  Cancel: { keyCode: 3, key: 'Cancel', code: 'Abort' },\n  Clear: { keyCode: 12, key: 'Clear', code: 'Numpad5', location: 3 },\n  Shift: { keyCode: 16, key: 'Shift', code: 'ShiftLeft', location: 1 },\n  Control: { keyCode: 17, key: 'Control', code: 'ControlLeft', location: 1 },\n  Alt: { keyCode: 18, key: 'Alt', code: 'AltLeft', location: 1 },\n  Accept: { keyCode: 30, key: 'Accept' },\n  ModeChange: { keyCode: 31, key: 'ModeChange' },\n  ' ': { keyCode: 32, key: ' ', code: 'Space' },\n  Print: { keyCode: 42, key: 'Print' },\n  Execute: { keyCode: 43, key: 'Execute', code: 'Open' },\n  '\\u0000': { keyCode: 46, key: '\\u0000', code: 'NumpadDecimal', location: 3 },\n  a: { keyCode: 65, key: 'a', code: 'KeyA' },\n  b: { keyCode: 66, key: 'b', code: 'KeyB' },\n  c: { keyCode: 67, key: 'c', code: 'KeyC' },\n  d: { keyCode: 68, key: 'd', code: 'KeyD' },\n  e: { keyCode: 69, key: 'e', code: 'KeyE' },\n  f: { keyCode: 70, key: 'f', code: 'KeyF' },\n  g: { keyCode: 71, key: 'g', code: 'KeyG' },\n  h: { keyCode: 72, key: 'h', code: 'KeyH' },\n  i: { keyCode: 73, key: 'i', code: 'KeyI' },\n  j: { keyCode: 74, key: 'j', code: 'KeyJ' },\n  k: { keyCode: 75, key: 'k', code: 'KeyK' },\n  l: { keyCode: 76, key: 'l', code: 'KeyL' },\n  m: { keyCode: 77, key: 'm', code: 'KeyM' },\n  n: { keyCode: 78, key: 'n', code: 'KeyN' },\n  o: { keyCode: 79, key: 'o', code: 'KeyO' },\n  p: { keyCode: 80, key: 'p', code: 'KeyP' },\n  q: { keyCode: 81, key: 'q', code: 'KeyQ' },\n  r: { keyCode: 82, key: 'r', code: 'KeyR' },\n  s: { keyCode: 83, key: 's', code: 'KeyS' },\n  t: { keyCode: 84, key: 't', code: 'KeyT' },\n  u: { keyCode: 85, key: 'u', code: 'KeyU' },\n  v: { keyCode: 86, key: 'v', code: 'KeyV' },\n  w: { keyCode: 87, key: 'w', code: 'KeyW' },\n  x: { keyCode: 88, key: 'x', code: 'KeyX' },\n  y: { keyCode: 89, key: 'y', code: 'KeyY' },\n  z: { keyCode: 90, key: 'z', code: 'KeyZ' },\n  Meta: { keyCode: 91, key: 'Meta', code: 'MetaLeft', location: 1 },\n  '*': { keyCode: 106, key: '*', code: 'NumpadMultiply', location: 3 },\n  '+': { keyCode: 107, key: '+', code: 'NumpadAdd', location: 3 },\n  '-': { keyCode: 109, key: '-', code: 'NumpadSubtract', location: 3 },\n  '/': { keyCode: 111, key: '/', code: 'NumpadDivide', location: 3 },\n  ';': { keyCode: 186, key: ';', code: 'Semicolon' },\n  '=': { keyCode: 187, key: '=', code: 'Equal' },\n  ',': { keyCode: 188, key: ',', code: 'Comma' },\n  '.': { keyCode: 190, key: '.', code: 'Period' },\n  '`': { keyCode: 192, key: '`', code: 'Backquote' },\n  '[': { keyCode: 219, key: '[', code: 'BracketLeft' },\n  '\\\\': { keyCode: 220, key: '\\\\', code: 'Backslash' },\n  ']': { keyCode: 221, key: ']', code: 'BracketRight' },\n  \"'\": { keyCode: 222, key: \"'\", code: 'Quote' },\n  Attn: { keyCode: 246, key: 'Attn' },\n  CrSel: { keyCode: 247, key: 'CrSel', code: 'Props' },\n  ExSel: { keyCode: 248, key: 'ExSel' },\n  EraseEof: { keyCode: 249, key: 'EraseEof' },\n  Play: { keyCode: 250, key: 'Play' },\n  ZoomOut: { keyCode: 251, key: 'ZoomOut' },\n  ')': { keyCode: 48, key: ')', code: 'Digit0' },\n  '!': { keyCode: 49, key: '!', code: 'Digit1' },\n  '@': { keyCode: 50, key: '@', code: 'Digit2' },\n  '#': { keyCode: 51, key: '#', code: 'Digit3' },\n  $: { keyCode: 52, key: '$', code: 'Digit4' },\n  '%': { keyCode: 53, key: '%', code: 'Digit5' },\n  '^': { keyCode: 54, key: '^', code: 'Digit6' },\n  '&': { keyCode: 55, key: '&', code: 'Digit7' },\n  '(': { keyCode: 57, key: '(', code: 'Digit9' },\n  A: { keyCode: 65, key: 'A', code: 'KeyA' },\n  B: { keyCode: 66, key: 'B', code: 'KeyB' },\n  C: { keyCode: 67, key: 'C', code: 'KeyC' },\n  D: { keyCode: 68, key: 'D', code: 'KeyD' },\n  E: { keyCode: 69, key: 'E', code: 'KeyE' },\n  F: { keyCode: 70, key: 'F', code: 'KeyF' },\n  G: { keyCode: 71, key: 'G', code: 'KeyG' },\n  H: { keyCode: 72, key: 'H', code: 'KeyH' },\n  I: { keyCode: 73, key: 'I', code: 'KeyI' },\n  J: { keyCode: 74, key: 'J', code: 'KeyJ' },\n  K: { keyCode: 75, key: 'K', code: 'KeyK' },\n  L: { keyCode: 76, key: 'L', code: 'KeyL' },\n  M: { keyCode: 77, key: 'M', code: 'KeyM' },\n  N: { keyCode: 78, key: 'N', code: 'KeyN' },\n  O: { keyCode: 79, key: 'O', code: 'KeyO' },\n  P: { keyCode: 80, key: 'P', code: 'KeyP' },\n  Q: { keyCode: 81, key: 'Q', code: 'KeyQ' },\n  R: { keyCode: 82, key: 'R', code: 'KeyR' },\n  S: { keyCode: 83, key: 'S', code: 'KeyS' },\n  T: { keyCode: 84, key: 'T', code: 'KeyT' },\n  U: { keyCode: 85, key: 'U', code: 'KeyU' },\n  V: { keyCode: 86, key: 'V', code: 'KeyV' },\n  W: { keyCode: 87, key: 'W', code: 'KeyW' },\n  X: { keyCode: 88, key: 'X', code: 'KeyX' },\n  Y: { keyCode: 89, key: 'Y', code: 'KeyY' },\n  Z: { keyCode: 90, key: 'Z', code: 'KeyZ' },\n  ':': { keyCode: 186, key: ':', code: 'Semicolon' },\n  '<': { keyCode: 188, key: '<', code: 'Comma' },\n  _: { keyCode: 189, key: '_', code: 'Minus' },\n  '>': { keyCode: 190, key: '>', code: 'Period' },\n  '?': { keyCode: 191, key: '?', code: 'Slash' },\n  '~': { keyCode: 192, key: '~', code: 'Backquote' },\n  '{': { keyCode: 219, key: '{', code: 'BracketLeft' },\n  '|': { keyCode: 220, key: '|', code: 'Backslash' },\n  '}': { keyCode: 221, key: '}', code: 'BracketRight' },\n  '\"': { keyCode: 222, key: '\"', code: 'Quote' },\n  SoftLeft: { key: 'SoftLeft', code: 'SoftLeft', location: 4 },\n  SoftRight: { key: 'SoftRight', code: 'SoftRight', location: 4 },\n  Camera: { keyCode: 44, key: 'Camera', code: 'Camera', location: 4 },\n  Call: { key: 'Call', code: 'Call', location: 4 },\n  EndCall: { keyCode: 95, key: 'EndCall', code: 'EndCall', location: 4 },\n  VolumeDown: {\n    keyCode: 182,\n    key: 'VolumeDown',\n    code: 'VolumeDown',\n    location: 4,\n  },\n  VolumeUp: { keyCode: 183, key: 'VolumeUp', code: 'VolumeUp', location: 4 },\n};\n"
  },
  {
    "path": "src/utils/api.js",
    "content": "import BrowserAPIService from '@/service/browser-api/BrowserAPIService';\nimport secrets from 'secrets';\nimport { isObject, parseJSON } from './helper';\n\nexport async function fetchApi(path, options = {}) {\n  const urlPath = path.startsWith('/') ? path : `/${path}`;\n  const headers = {\n    'Content-Type': 'application/json',\n    ...(options?.headers || {}),\n  };\n\n  const { session } = (await BrowserAPIService.storage.local.get(\n    'session'\n  )) || { session: null };\n  if (session && options?.auth) {\n    delete options.auth;\n\n    let token = session.access_token;\n\n    if (Date.now() > (session.expires_at - 2000) * 1000) {\n      const response = await fetch(\n        `${secrets.baseApiUrl}/me/refresh-auth-session?token=${session.refresh_token}`\n      );\n      const result = await response.json();\n      if (!response.ok) {\n        throw new Error(result.message);\n      }\n\n      await BrowserAPIService.storage.local.set({ session: result });\n      token = result.access_token;\n    }\n\n    headers.Authorization = `Bearer ${token}`;\n  }\n\n  const url = `${secrets.baseApiUrl}${urlPath}`;\n\n  return fetch(url, {\n    ...options,\n    headers,\n  });\n}\n\nexport async function cacheApi(key, callback, useCache = true) {\n  const isBoolOpts = typeof useCache === 'boolean';\n  const options = {\n    ttl: 10000 * 10,\n    storage: sessionStorage,\n    useCache: isBoolOpts ? useCache : true,\n  };\n  if (!isBoolOpts && isObject(useCache)) {\n    Object.assign(options, useCache);\n  }\n\n  const timeToLive = options.ttl;\n  const currentTime = Date.now() - timeToLive;\n\n  const timerKey = `cache-time:${key}`;\n  const cacheResult = parseJSON(options.storage.getItem(key), null);\n  const cacheTime = +options.storage.getItem(timerKey) || Date.now();\n\n  if (options.useCache && cacheResult && currentTime < cacheTime) {\n    return cacheResult;\n  }\n\n  const result = await callback();\n  let cacheData = result;\n\n  if (result?.cacheData) {\n    cacheData = result?.cacheData;\n  }\n\n  options.storage.setItem(timerKey, Date.now());\n  options.storage.setItem(key, JSON.stringify(cacheData));\n\n  return result;\n}\n\nexport async function getSharedWorkflows(useCache = true) {\n  return cacheApi(\n    'shared-workflows',\n    async () => {\n      try {\n        const response = await fetchApi('/me/workflows/shared?data=all');\n\n        if (response.status !== 200) throw new Error(response.statusText);\n\n        const result = await response.json();\n        const sharedWorkflows = result.reduce((acc, item) => {\n          item.drawflow = JSON.stringify(item.drawflow);\n          item.table = item.table || item.dataColumns || [];\n          item.createdAt = new Date(item.createdAt || Date.now()).getTime();\n\n          acc[item.id] = item;\n\n          return acc;\n        }, {});\n\n        return sharedWorkflows;\n      } catch (error) {\n        console.error(error);\n\n        return {};\n      }\n    },\n    useCache\n  );\n}\n\nexport async function getUserWorkflows(useCache = true) {\n  return cacheApi(\n    'user-workflows',\n    async () => {\n      try {\n        const { lastBackup } = await BrowserAPIService.storage.local.get(\n          'lastBackup'\n        );\n        const response = await fetchApi(\n          `/me/workflows?lastBackup=${(useCache && lastBackup) || null}`,\n          { auth: true }\n        );\n\n        if (!response.ok) throw new Error(response.statusText);\n\n        const result = await response.json();\n        const workflows = result.reduce(\n          (acc, workflow) => {\n            if (workflow.isHost) {\n              acc.hosted[workflow.id] = {\n                id: workflow.id,\n                hostId: workflow.hostId,\n              };\n            }\n\n            acc.backup.push(workflow);\n\n            return acc;\n          },\n          { hosted: {}, backup: [] }\n        );\n\n        workflows.cacheData = {\n          backup: [],\n          hosted: workflows.hosted,\n        };\n\n        return workflows;\n      } catch (error) {\n        console.error(error);\n\n        return {};\n      }\n    },\n    useCache\n  );\n}\n\nexport function validateOauthToken() {\n  let retryCount = 0;\n\n  const startFetch = async () => {\n    try {\n      const { sessionToken } = await BrowserAPIService.storage.local.get(\n        'sessionToken'\n      );\n      if (!sessionToken) return null;\n\n      const response = await fetch(\n        `https://www.googleapis.com/oauth2/v1/tokeninfo?access_token=${sessionToken.access}`\n      );\n      if (response.status === 400 && sessionToken.refresh && retryCount <= 3) {\n        const refreshResponse = await fetchApi(\n          `/me/refresh-session?token=${sessionToken.refresh}`,\n          { auth: true }\n        );\n        const refreshResult = await refreshResponse.json();\n\n        if (!refreshResponse.ok) {\n          throw new Error(refreshResult.message);\n        }\n\n        retryCount += 1;\n\n        const result = await startFetch();\n        return result;\n      }\n\n      return null;\n    } catch (error) {\n      console.error(error);\n    }\n\n    return null;\n  };\n\n  return startFetch();\n}\n\nexport async function fetchGapi(url, resource = {}, options = {}) {\n  const { sessionToken } = await BrowserAPIService.storage.local.get(\n    'sessionToken'\n  );\n  if (!sessionToken) throw new Error('unauthorized');\n\n  const { search, origin, pathname } = new URL(url);\n  const searchParams = new URLSearchParams(search);\n  searchParams.set('access_token', sessionToken.access);\n\n  let tryCount = 0;\n  const maxTry = options?.tryCount || 3;\n\n  const startFetch = async () => {\n    const response = await fetch(\n      `${origin}${pathname}?${searchParams.toString()}`,\n      resource\n    );\n\n    const result = parseJSON(await response.text(), null);\n    const insufficientScope =\n      response.status === 403 &&\n      result?.error?.message.includes('insufficient authentication scopes');\n    if (\n      (!sessionToken.access || response.status === 401 || insufficientScope) &&\n      sessionToken.refresh\n    ) {\n      const refreshResponse = await fetchApi(\n        `/me/refresh-session?token=${sessionToken.refresh}`,\n        { auth: true }\n      );\n      const refreshResult = await refreshResponse.json();\n\n      if (!refreshResponse.ok) {\n        throw new Error(refreshResult.message);\n      }\n\n      searchParams.set('access_token', refreshResult.token);\n      sessionToken.access = refreshResult.token;\n\n      await BrowserAPIService.storage.local.set({ sessionToken });\n\n      if (tryCount < maxTry) {\n        tryCount += 1;\n        const awaitResult = await startFetch();\n\n        return awaitResult;\n      }\n\n      throw new Error('unauthorized');\n    }\n    if (!response.ok) {\n      throw new Error(result?.error?.message, { cause: result });\n    }\n\n    if (options?.response) {\n      return { response, result };\n    }\n\n    return result;\n  };\n\n  const result = await startFetch();\n  return result;\n}\n"
  },
  {
    "path": "src/utils/callbackBridge.js",
    "content": "import { MessageListener } from './message';\n\n/**\n * Check if an object is a callback bridge handler\n * @param {*} obj - Object to check\n * @returns {boolean} - True if object is a callback bridge\n */\nexport function isCallbackBridge(obj) {\n  return obj && typeof obj === 'object' && obj.__type === 'callback_bridge';\n}\n\n/**\n * Simple callback bridge for cross-context communication in Chrome MV3\n * Uses Promise-based message passing instead of storing functions\n */\nclass CallbackBridge {\n  constructor() {\n    this.pendingCallbacks = new Map();\n    this.setupMessageHandlers();\n  }\n\n  setupMessageHandlers() {\n    const message = new MessageListener('callback-bridge');\n\n    message.on('resolve-callback', ({ callbackId, result, error }) => {\n      const pendingCallback = this.pendingCallbacks.get(callbackId);\n      if (pendingCallback) {\n        this.pendingCallbacks.delete(callbackId);\n        if (error) {\n          pendingCallback.reject(new Error(error));\n        } else {\n          pendingCallback.resolve(result);\n        }\n      }\n    });\n\n    // Set up message listener\n    if (typeof browser !== 'undefined' && browser.runtime) {\n      browser.runtime.onMessage.addListener(message.listener);\n    }\n  }\n\n  /**\n   * Convert callback function to Promise-based message\n   * @param {Function} callback - The callback function to convert\n   * @param {string} callbackId - Unique identifier for this callback\n   * @returns {Object} - Message-compatible callback handler\n   */\n  createCallbackHandler(callback, callbackId) {\n    // Store the original callback temporarily\n    const callbackPromise = new Promise((resolve, reject) => {\n      this.pendingCallbacks.set(callbackId, { resolve, reject });\n\n      // Add timeout to prevent memory leaks\n      setTimeout(() => {\n        if (this.pendingCallbacks.has(callbackId)) {\n          this.pendingCallbacks.delete(callbackId);\n          reject(new Error(`Callback ${callbackId} timed out`));\n        }\n      }, 300000); // 5 minutes timeout\n    });\n\n    // Execute original callback when promise resolves\n    callbackPromise\n      .then((result) => {\n        try {\n          callback(result);\n        } catch (error) {\n          console.error('Error executing callback:', error);\n        }\n      })\n      .catch((error) => {\n        console.error('Callback promise rejected:', error);\n      });\n\n    // Return message-compatible handler\n    return {\n      __type: 'callback_bridge',\n      __callbackId: callbackId,\n    };\n  }\n\n  /**\n   * Execute callback from message data\n   * @param {string} callbackId - The callback identifier\n   * @param {*} result - Result to pass to callback\n   * @param {string|null} error - Error message if any\n   */\n  static async executeCallback(callbackId, result, error = null) {\n    try {\n      await MessageListener.sendMessage(\n        'resolve-callback',\n        { callbackId, result, error },\n        'callback-bridge'\n      );\n    } catch (err) {\n      console.error(`Failed to execute callback ${callbackId}:`, err);\n    }\n  }\n\n  static generateCallbackId() {\n    return `cb_${Date.now()}_${Math.random().toString(36).slice(2, 11)}`;\n  }\n}\n\n// Global instance\nconst callbackBridge = new CallbackBridge();\n\n/**\n * Process data to convert callbacks to bridge handlers\n * @param {*} obj - Data to process\n * @param {CallbackBridge} bridge - Bridge instance\n * @returns {*} - Processed data\n */\nfunction processCallbacksInData(obj, bridge) {\n  if (typeof obj === 'function') {\n    const callbackId = CallbackBridge.generateCallbackId();\n    return bridge.createCallbackHandler(obj, callbackId);\n  }\n\n  if (Array.isArray(obj)) {\n    return obj.map((item) => processCallbacksInData(item, bridge));\n  }\n\n  if (obj && typeof obj === 'object') {\n    const result = {};\n    Object.keys(obj).forEach((key) => {\n      result[key] = processCallbacksInData(obj[key], bridge);\n    });\n    return result;\n  }\n\n  return obj;\n}\n\n/**\n * Enhanced message sending that handles callbacks automatically\n * @param {string} messageName - Message name\n * @param {Object} data - Message data (may contain callbacks)\n * @param {string} prefix - Message prefix\n * @returns {Promise} - Message response\n */\nexport async function sendMessageWithCallbacks(messageName, data, prefix = '') {\n  const processedData = processCallbacksInData(data, callbackBridge);\n  return MessageListener.sendMessage(messageName, processedData, prefix);\n}\n\n/**\n * Process received data to execute callbacks\n * @param {*} obj - Received data\n * @param {*} result - Result to pass to callbacks\n * @param {string|null} error - Error message if any\n */\nexport async function executeCallbacksInData(obj, result, error = null) {\n  if (isCallbackBridge(obj)) {\n    await CallbackBridge.executeCallback(obj.__callbackId, result, error);\n    return;\n  }\n\n  if (Array.isArray(obj)) {\n    await Promise.all(\n      obj.map((item) => executeCallbacksInData(item, result, error))\n    );\n    return;\n  }\n\n  if (obj && typeof obj === 'object') {\n    await Promise.all(\n      Object.values(obj).map((value) =>\n        executeCallbacksInData(value, result, error)\n      )\n    );\n  }\n}\n\n/**\n * Check if data contains any callback bridge handlers\n * @param {*} obj - Data to check\n * @returns {boolean} - True if data contains callback bridges\n */\nexport function hasCallbackBridges(obj) {\n  if (isCallbackBridge(obj)) {\n    return true;\n  }\n\n  if (Array.isArray(obj)) {\n    return obj.some(hasCallbackBridges);\n  }\n\n  if (obj && typeof obj === 'object') {\n    return Object.values(obj).some(hasCallbackBridges);\n  }\n\n  return false;\n}\n\n/**\n * Get all callback bridge IDs from data\n * @param {*} obj - Data to extract IDs from\n * @returns {string[]} - Array of callback IDs\n */\nexport function getCallbackBridgeIds(obj) {\n  const ids = [];\n\n  function collectIds(data) {\n    if (isCallbackBridge(data)) {\n      ids.push(data.__callbackId);\n      return;\n    }\n\n    if (Array.isArray(data)) {\n      data.forEach(collectIds);\n      return;\n    }\n\n    if (data && typeof data === 'object') {\n      Object.values(data).forEach(collectIds);\n    }\n  }\n\n  collectIds(obj);\n  return ids;\n}\n\nexport { callbackBridge };\n"
  },
  {
    "path": "src/utils/codeEditorAutocomplete.js",
    "content": "/* eslint-disable no-template-curly-in-string */\nimport { snippet } from '@codemirror/autocomplete';\nimport { syntaxTree } from '@codemirror/language';\n\nconst completePropertyAfter = ['PropertyName', '.', '?.'];\nconst excludeProps = ['chrome', 'Mousetrap'];\n\nfunction completeProperties(from, object) {\n  const options = [];\n  /* eslint-disable-next-line */\n  for (const name in object) {\n    if (\n      !name.startsWith('__') &&\n      !name.startsWith('webpack') &&\n      !excludeProps.includes(name)\n    )\n      options.push({\n        label: name,\n        type: typeof object[name] === 'function' ? 'function' : 'variable',\n      });\n  }\n  return {\n    from,\n    options,\n    validFor: /^[\\w$]*$/,\n  };\n}\n\nexport const dontCompleteIn = [\n  'String',\n  'TemplateString',\n  'LineComment',\n  'BlockComment',\n  'VariableDefinition',\n  'PropertyDefinition',\n];\nexport function completeFromGlobalScope(context) {\n  const nodeBefore = syntaxTree(context.state).resolveInner(context.pos, -1);\n\n  if (\n    completePropertyAfter.includes(nodeBefore.name) &&\n    nodeBefore.parent?.name === 'MemberExpression'\n  ) {\n    const object = nodeBefore.parent.getChild('Expression');\n    if (object?.name === 'VariableName') {\n      const from = /\\./.test(nodeBefore.name) ? nodeBefore.to : nodeBefore.from;\n      const variableName = context.state.sliceDoc(object.from, object.to);\n      if (typeof window[variableName] === 'object')\n        return completeProperties(from, window[variableName]);\n    }\n  } else if (nodeBefore.name === 'VariableName') {\n    return completeProperties(nodeBefore.from, window);\n  } else if (context.explicit && !dontCompleteIn.includes(nodeBefore.name)) {\n    return completeProperties(context.pos, window);\n  }\n  return null;\n}\n\nexport function automaFuncsCompletion(snippets) {\n  return function (context) {\n    const word = context.matchBefore(/\\w*/);\n    const nodeBefore = syntaxTree(context.state).resolveInner(context.pos, -1);\n\n    if (\n      (word.from === word.to && !context.explicit) ||\n      dontCompleteIn.includes(nodeBefore.name)\n    )\n      return null;\n\n    return {\n      from: word.from,\n      options: snippets,\n    };\n  };\n}\n\nexport const automaFuncsSnippets = {\n  automaNextBlock: {\n    label: 'automaNextBlock',\n    type: 'function',\n    apply: snippet('automaNextBlock(${data})'),\n    info: () => {\n      const container = document.createElement('div');\n\n      container.innerHTML = `\n        <code>automaNextBlock(<i>data</i>, <i>insert?</i>)</code>\n        <p class=\"mt-2\">\n          Execute the next block\n          <a href=\"https://docs.extension.automa.site/blocks/javascript-code.html#automanextblock-data\" target=\"_blank\" class=\"underline\">\n            Read more\n          </a>\n        </p>\n      `;\n\n      return container;\n    },\n  },\n  automaSetVariable: {\n    label: 'automaSetVariable',\n    type: 'function',\n    apply: snippet(\"automaSetVariable('${name}', ${value})\"),\n    info: () => {\n      const container = document.createElement('div');\n\n      container.innerHTML = `\n        <code>automaRefData(<i>name</i>, <i>value</i>)</code>\n        <p class=\"mt-2\">\n          Set the value of a variable\n        </p>\n      `;\n\n      return container;\n    },\n  },\n  automaFetch: {\n    label: 'automaFetch',\n    type: 'function',\n    apply: snippet(\"automaFetch('${json}', { url: '${}' })\"),\n    info: () => {\n      const container = document.createElement('div');\n\n      container.innerHTML = `\n        <code>automaFetch(<i>type</i>, <i>resource</i>)</code>\n      `;\n\n      return container;\n    },\n  },\n  automaRefData: {\n    label: 'automaRefData',\n    type: 'function',\n    apply: snippet(\"automaRefData('${keyword}', '${path}')\"),\n    info: () => {\n      const container = document.createElement('div');\n\n      container.innerHTML = `\n        <code>automaRefData(<i>keyword</i>, <i>path</i>)</code>\n        <p class=\"mt-2\">\n          Use this function to\n          <a href=\"https://docs.extension.automa.site/workflow/expressions.html\" target=\"_blank\" class=\"underline\">\n            reference data\n          </a>\n        </p>\n      `;\n\n      return container;\n    },\n  },\n  automaResetTimeout: {\n    label: 'automaResetTimeout',\n    type: 'function',\n    info: 'Reset javascript execution timeout',\n    apply: 'automaResetTimeout()',\n  },\n  automaExecWorkflow: {\n    label: 'automaExecWorkflow',\n    type: 'function',\n    apply: snippet(\"automaExecWorkflow({ id: '${workflowId}' })\"),\n    info: () => {\n      const container = document.createElement('div');\n\n      container.innerHTML = `\n        <code>automaRefData(<i>options</i>)</code>\n        <p class=\"mt-2\">\n          Execute a workflow\n        </p>\n      `;\n\n      return container;\n    },\n  },\n};\n"
  },
  {
    "path": "src/utils/compareBlockValue.js",
    "content": "const handlers = {\n  '==': (a, b) => a === b,\n  '!=': (a, b) => a !== b,\n  '>': (a, b) => a > b,\n  '>=': (a, b) => a >= b,\n  '<': (a, b) => a < b,\n  '<=': (a, b) => a <= b,\n  '()': (a, b) => a?.includes(b) ?? false,\n};\n\nexport default function (type, valueA, valueB) {\n  const handler = handlers[type];\n\n  if (handler) return handler(valueA, valueB);\n\n  return false;\n}\n"
  },
  {
    "path": "src/utils/constants/table.js",
    "content": "export const dataTypes = [\n  { id: 'any', name: 'Any' },\n  { id: 'string', name: 'Text' },\n  { id: 'integer', name: 'Number' },\n  { id: 'boolean', name: 'Boolean' },\n  { id: 'array', name: 'Array' },\n];\n"
  },
  {
    "path": "src/utils/convertWorkflowData.js",
    "content": "import { parseJSON, findTriggerBlock } from './helper';\n\nconst getFlowData = (workflow) =>\n  typeof workflow.drawflow === 'string'\n    ? parseJSON(workflow.drawflow, {})\n    : workflow.drawflow;\n\nexport default function (workflow) {\n  const data = getFlowData(workflow);\n  if (!data?.drawflow) return workflow;\n\n  const triggerBlock = findTriggerBlock(data);\n  if (!triggerBlock) return workflow;\n\n  const blocks = data.drawflow.Home.data;\n  const tracedBlocks = new Set();\n\n  const nodes = [];\n  const edges = [];\n\n  function extractBlock(blockId) {\n    if (tracedBlocks.has(blockId)) return;\n\n    const block = blocks[blockId];\n\n    nodes.push({\n      id: block.id,\n      type: block.html,\n      label: block.name,\n      position: {\n        x: block.pos_x,\n        y: block.pos_y,\n      },\n      data: block.data,\n    });\n\n    const nextBlockIds = [];\n    const outputs = Object.values(block.outputs);\n\n    outputs.forEach(({ connections }, outputIndex) => {\n      let outputName = outputIndex + 1;\n\n      const isLastIndex = outputs.length - 1 === outputIndex;\n      const isConditionsBlock = block.name === 'conditions';\n      const isFallbackBlock = block.html === 'BlockBasicWithFallback';\n      const isBlockFallback = block.html === 'BlockBasic' && outputName >= 2;\n      if (\n        (isConditionsBlock || isFallbackBlock || isBlockFallback) &&\n        isLastIndex\n      ) {\n        outputName = 'fallback';\n      }\n\n      if (isConditionsBlock && !isLastIndex) {\n        outputName = block.data.conditions[outputIndex].id;\n      }\n\n      connections.forEach(({ node: outputId, output }) => {\n        const sourceHandle = `${block.id}-output-${outputName}`;\n        const targetHandle = `${outputId}-${output.replace('_', '-')}`;\n\n        edges.push({\n          sourceHandle,\n          targetHandle,\n          source: block.id,\n          target: outputId,\n          updatable: true,\n          selectable: true,\n          id: `vueflow__edge-${sourceHandle}-${targetHandle}`,\n          class: `source-${sourceHandle} target-${targetHandle}`,\n        });\n\n        nextBlockIds.push(outputId);\n      });\n    });\n\n    tracedBlocks.add(blockId);\n\n    nextBlockIds.forEach((id) => {\n      extractBlock(id);\n    });\n  }\n  extractBlock(triggerBlock.id);\n\n  workflow.drawflow = { edges, nodes, x: 0, y: 0, zoom: 0 };\n\n  return workflow;\n}\n"
  },
  {
    "path": "src/utils/credentialUtil.js",
    "content": "import SHA256 from 'crypto-js/sha256';\nimport HmacSHA256 from 'crypto-js/hmac-sha256';\nimport AES from 'crypto-js/aes';\nimport encUtf8 from 'crypto-js/enc-utf8';\nimport getPassKey from './getPassKey';\nimport { parseJSON } from './helper';\n\nfunction encryptValue(value) {\n  const pass = getPassKey('credential');\n  const encryptedValue = AES.encrypt(value, pass).toString();\n  const hmac = HmacSHA256(encryptedValue, SHA256(pass)).toString();\n\n  return hmac + encryptedValue;\n}\n\nfunction decryptValue(value) {\n  const pass = getPassKey('credential');\n  const hmac = value.substring(0, 64);\n  const encryptedValue = value.substring(64);\n  const decryptedHmac = HmacSHA256(encryptedValue, SHA256(pass)).toString();\n\n  if (hmac !== decryptedHmac) return '';\n\n  const decryptedValue = AES.decrypt(encryptedValue, pass).toString(encUtf8);\n\n  return parseJSON(decryptedValue, decryptedValue);\n}\n\nexport default {\n  encrypt: encryptValue,\n  decrypt: decryptValue,\n};\n"
  },
  {
    "path": "src/utils/dataExporter.js",
    "content": "import Papa from 'papaparse';\nimport { fileSaver } from './helper';\n\nexport const files = {\n  'plain-text': {\n    mime: 'text/plain',\n    ext: '.txt',\n  },\n  json: {\n    mime: 'application/json',\n    ext: '.json',\n  },\n  csv: {\n    mime: 'text/csv',\n    ext: '.csv',\n  },\n};\n\nexport function generateJSON(keys, data) {\n  if (Array.isArray(data)) return data;\n\n  const result = [];\n\n  keys.forEach((key) => {\n    for (let index = 0; index < data[key].length; index += 1) {\n      const currData = data[key][index];\n\n      if (typeof result[index] === 'undefined') {\n        result.push({ [key]: currData });\n      } else {\n        result[index][key] = currData;\n      }\n    }\n  });\n\n  return result;\n}\n\nexport default function (\n  data,\n  { name, type, addBOMHeader, csvOptions, returnUrl, returnBlob },\n  converted\n) {\n  let result = data;\n\n  if (type === 'csv' || type === 'json') {\n    const jsonData = converted ? data : generateJSON(Object.keys(data), data);\n\n    result =\n      type === 'csv'\n        ? Papa.unparse(jsonData, csvOptions || {})\n        : JSON.stringify(jsonData, null, 2);\n  } else if (type === 'plain-text') {\n    const extractObj = (obj) => {\n      if (typeof obj !== 'object') return [obj];\n\n      // 需要处理深层对象 不然会返回:[object Object]\n      const kes = Object.keys(obj);\n      kes.forEach((key) => {\n        const itemValue = obj[key];\n        if (typeof itemValue === 'object') {\n          obj[key] = JSON.stringify(itemValue);\n        }\n      });\n\n      return Object.values(obj);\n    };\n\n    result = (\n      Array.isArray(data)\n        ? data.flatMap((item) => extractObj(item))\n        : extractObj(data)\n    ).join(' ');\n  }\n\n  const payload = [result];\n\n  if (type === 'csv' && addBOMHeader) {\n    payload.unshift(new Uint8Array([0xef, 0xbb, 0xbf]));\n  }\n\n  const { mime, ext } = files[type];\n  const blob = new Blob(payload, { type: mime });\n  if (returnBlob) return blob;\n\n  const blobUrl = URL.createObjectURL(blob);\n\n  if (!returnUrl) fileSaver(`${name || 'unnamed'}${ext}`, blobUrl);\n\n  return blobUrl;\n}\n"
  },
  {
    "path": "src/utils/dataMigration.js",
    "content": "import browser from 'webextension-polyfill';\nimport dbLogs from '@/db/logs';\n\nexport default async function () {\n  try {\n    const { logs, logsCtxData, migration } = await browser.storage.local.get([\n      'logs',\n      'migration',\n      'logsCtxData',\n    ]);\n    const hasMigrated = migration || {};\n    const backupData = {};\n\n    if (!hasMigrated.logs && logs) {\n      const ids = new Set();\n\n      const items = [];\n      const ctxData = [];\n      const logsData = [];\n      const histories = [];\n\n      for (let index = logs.length - 1; index > 0; index -= 1) {\n        const { data, history, ...item } = logs[index];\n        const logId = item.id;\n\n        if (!ids.has(logId) && ids.size < 500) {\n          items.push(item);\n          logsData.push({ logId, data });\n          histories.push({ logId, data: history });\n          ctxData.push({ logId, data: logsCtxData[logId] });\n\n          ids.add(logId);\n        }\n      }\n\n      await Promise.all([\n        dbLogs.items.bulkAdd(items),\n        dbLogs.ctxData.bulkAdd(ctxData),\n        dbLogs.logsData.bulkAdd(logsData),\n        dbLogs.histories.bulkAdd(histories),\n      ]);\n\n      backupData.logs = logs;\n      hasMigrated.logs = true;\n\n      await browser.storage.local.remove('logs');\n    }\n\n    await browser.storage.local.set({\n      migration: hasMigrated,\n      ...backupData,\n    });\n  } catch (error) {\n    console.error(error);\n  }\n}\n"
  },
  {
    "path": "src/utils/decryptFlow.js",
    "content": "import { nanoid } from 'nanoid';\nimport hmacSHA256 from 'crypto-js/hmac-sha256';\nimport AES from 'crypto-js/aes';\nimport encUtf8 from 'crypto-js/enc-utf8';\nimport { parseJSON } from './helper';\nimport getPassKey from './getPassKey';\n\nexport function getWorkflowPass(pass) {\n  const key = getPassKey(nanoid());\n  const decryptedPass = AES.decrypt(pass.substring(64), key).toString(encUtf8);\n\n  return decryptedPass;\n}\n\nexport default function ({ pass, drawflow }, password) {\n  const hmac = pass.substring(0, 64);\n  const decryptedHmac = hmacSHA256(pass.substring(64), password).toString();\n\n  if (hmac !== decryptedHmac)\n    return {\n      isError: true,\n      message: 'incorrect-password',\n    };\n\n  const isDecrypted = parseJSON(drawflow, null);\n  if (isDecrypted) return isDecrypted;\n\n  return AES.decrypt(drawflow, password).toString(encUtf8);\n}\n"
  },
  {
    "path": "src/utils/editor/DroppedNode.js",
    "content": "import { customAlphabet } from 'nanoid';\nimport { excludeOnError } from '../shared';\nimport { getBlocks } from '../getSharedData';\n\nconst nanoid = customAlphabet('1234567890abcdefghijklmnopqrstuvwxyz', 7);\n\nclass DroppedNode {\n  static isNode(target) {\n    if (target.closest('.vue-flow__handle')) return null;\n\n    return target.closest('.vue-flow__node');\n  }\n\n  static isHandle(target) {\n    return target.closest('.vue-flow__handle.source');\n  }\n\n  static isEdge(target) {\n    return target.closest('.vue-flow__edge');\n  }\n\n  static replaceNode(editor, { block, target: targetEl }) {\n    const targetNode = editor.getNode.value(targetEl.dataset.id);\n\n    if (targetNode.label === 'blocks-group' || block.fromBlockBasic) return;\n\n    let blockData = block;\n    if (block.fromBlockBasic) {\n      const blocks = getBlocks();\n      blockData = { ...blocks[block.id], id: block.id };\n    }\n\n    const onErrorEnabled =\n      targetNode.data?.onError?.enable &&\n      !excludeOnError.includes(blockData.id);\n    const newNodeData = onErrorEnabled\n      ? { ...blockData.data, onError: targetNode.data.onError }\n      : blockData.data;\n\n    const newNode = {\n      id: nanoid(),\n      data: newNodeData,\n      label: blockData.id,\n      type: blockData.component,\n      position: targetNode.position,\n    };\n\n    const edges = editor.getEdges.value.reduce(\n      (acc, { targetHandle, sourceHandle, target, source }) => {\n        let pushData = false;\n\n        if (target === targetNode.id) {\n          targetHandle = targetHandle.replace(target, newNode.id);\n          target = newNode.id;\n          pushData = true;\n        } else if (source === targetNode.id) {\n          sourceHandle = sourceHandle.replace(source, newNode.id);\n          source = newNode.id;\n          pushData = true;\n        }\n\n        if (pushData) {\n          acc.push({\n            source,\n            target,\n            sourceHandle,\n            targetHandle,\n            id: `edge-${nanoid()}`,\n            class: `source-${sourceHandle} target-${targetHandle}`,\n          });\n        }\n\n        return acc;\n      },\n      []\n    );\n\n    editor.removeNodes([targetNode]);\n    editor.addNodes([newNode]);\n    editor.addEdges(edges);\n  }\n\n  static appendNode(editor, { target, nodeId }) {\n    const { nodeid: source, handleid } = target.dataset;\n    if (!source || !handleid) return;\n\n    editor.addEdges([\n      {\n        source,\n        target: nodeId,\n        sourceHandle: handleid,\n        targetHandle: `${nodeId}-input-1`,\n      },\n    ]);\n  }\n\n  static insertBetweenNode(editor, { target, nodeId, outputs }) {\n    if (!target) return;\n\n    const edgesChanges = [];\n    const targetEdge = {\n      target: '',\n      source: '',\n      targetHandle: '',\n      sourceHandle: '',\n    };\n\n    target.classList.forEach((name) => {\n      if (name.startsWith('source-')) {\n        const sourceHandle = name.replace('source-', '');\n        const outputIndex = sourceHandle.indexOf('-output');\n        const sourceId = sourceHandle.slice(0, outputIndex);\n\n        targetEdge.source = sourceId;\n        targetEdge.sourceHandle = sourceHandle;\n\n        return;\n      }\n\n      if (name.startsWith('target-')) {\n        const targetHandle = name.replace('target-', '');\n        const inputIndex = targetHandle.indexOf('-input');\n        const targetId = targetHandle.slice(0, inputIndex);\n\n        targetEdge.target = targetId;\n        targetEdge.targetHandle = targetHandle;\n      }\n    });\n\n    editor.getEdges.value.forEach((edge) => {\n      const matchTarget = edge.targetHandle === targetEdge.targetHandle;\n      const matchSource = edge.sourceHandle === targetEdge.sourceHandle;\n\n      if (matchTarget && matchSource) {\n        edgesChanges.push({ type: 'remove', id: edge.id });\n      }\n    });\n\n    if (outputs > 0) {\n      edgesChanges.push({\n        type: 'add',\n        item: {\n          source: nodeId,\n          id: `edge-${nanoid()}`,\n          target: targetEdge.target,\n          sourceHandle: `${nodeId}-output-1`,\n          targetHandle: targetEdge.targetHandle,\n        },\n      });\n    }\n\n    edgesChanges.push({\n      type: 'add',\n      item: {\n        target: nodeId,\n        id: `edge-${nanoid()}`,\n        source: targetEdge.source,\n        targetHandle: `${nodeId}-input-1`,\n        sourceHandle: targetEdge.sourceHandle,\n      },\n    });\n\n    editor.applyEdgeChanges(edgesChanges);\n  }\n}\n\nexport default DroppedNode;\n"
  },
  {
    "path": "src/utils/editor/EditorCommands.js",
    "content": "class EditorCommands {\n  constructor(editor, initialStates = {}) {\n    this.editor = editor;\n    this.state = initialStates;\n  }\n\n  nodeAdded(addedNodes) {\n    const ids = [];\n    addedNodes.forEach((node) => {\n      ids.push(node.id);\n      this.state.nodes[node.id] = node;\n    });\n\n    return {\n      name: 'node-added',\n      execute: () => {\n        this.editor.addNodes(addedNodes);\n      },\n      undo: () => {\n        this.editor.removeNodes(ids);\n      },\n    };\n  }\n\n  nodeRemoved(ids) {\n    return {\n      name: 'node-removed',\n      execute: () => {\n        this.editor.removeNodes(ids);\n      },\n      undo: () => {\n        const nodes = ids.map((id) => this.state.nodes[id]);\n        this.editor.addNodes(nodes);\n      },\n    };\n  }\n\n  edgeAdded(addedEdges) {\n    const ids = [];\n    addedEdges.forEach((edge) => {\n      ids.push(edge.id);\n      this.state.edges[edge.id] = edge;\n    });\n\n    return {\n      name: 'edge-added',\n      execute: () => {\n        this.editor.addEdges(addedEdges);\n      },\n      undo: () => {\n        this.editor.removeEdges(ids);\n      },\n    };\n  }\n\n  edgeRemoved(ids) {\n    return {\n      name: 'edge-removed',\n      execute: () => {\n        this.editor.removeEdges(ids);\n      },\n      undo: () => {\n        const edges = ids.map((id) => this.state.edges[id]);\n        this.editor.addEdges(edges);\n      },\n    };\n  }\n}\n\nexport default EditorCommands;\n"
  },
  {
    "path": "src/utils/editor/editorAutocomplete.js",
    "content": "import { getBlocks } from '../getSharedData';\n\nconst blocks = getBlocks();\nconst autocompleteKeys = {\n  loopId: 'loopData',\n  refKey: 'googleSheets',\n  variableName: 'variables',\n};\n\nfunction getData(blockName, blockData) {\n  const keys = blocks[blockName]?.autocomplete;\n  const dataList = {};\n  if (!keys) return dataList;\n\n  keys.forEach((key) => {\n    const value = blockData[key];\n    if (!value) return;\n\n    const autocompleteKey = autocompleteKeys[key];\n    if (!dataList[autocompleteKey]) dataList[autocompleteKey] = {};\n\n    dataList[autocompleteKey][value] = '';\n  });\n\n  return dataList;\n}\n\nconst extractBlocksAutocomplete = {\n  trigger(blockId, data) {\n    if (!this[blockId].variables) this[blockId].variables = {};\n\n    data.parameters?.forEach((param) => {\n      this[blockId].variables[param.name] = '';\n    });\n\n    if (data.type === 'context-menu') {\n      Object.assign(this[blockId].variables, {\n        $ctxElSelector: '',\n        $ctxTextSelection: '',\n        $ctxLink: '',\n        $ctxMediaUrl: '',\n      });\n    }\n  },\n  'blocks-group': function (blockId, data) {\n    data.blocks.forEach((block) => {\n      this[block.itemId] = getData(block.id, block.data);\n    });\n  },\n  'insert-data': function (blockId, data) {\n    if (!this[blockId].variables) this[blockId].variables = {};\n\n    data.dataList.forEach((item) => {\n      if (item.type !== 'variable' || !item.name.trim()) return;\n\n      this[blockId].variables[item.name] = '';\n    });\n  },\n};\n\nexport default function (label, { data, id }) {\n  const autocompleteData = { [id]: {} };\n\n  if (extractBlocksAutocomplete[label]) {\n    extractBlocksAutocomplete[label].call(autocompleteData, id, data);\n  } else {\n    autocompleteData[id] = getData(label, data);\n  }\n\n  return autocompleteData;\n}\n"
  },
  {
    "path": "src/utils/firstWorkflows.js",
    "content": "import { nanoid } from 'nanoid';\n\nexport default [\n  {\n    id: nanoid(),\n    name: 'Twitter Trends to Google Sheets',\n    table: [\n      { id: '6QbkU', name: 'text', type: 'string' },\n      { id: 'Q-zII', name: 'tweets_count', type: 'integer' },\n    ],\n    drawflow: {\n      nodes: [\n        {\n          type: 'BlockBasic',\n          dimensions: { width: 192, height: 72 },\n          handleBounds: {\n            source: [\n              {\n                id: 'F_uKdk2VrnKslRle78d-C-output-1',\n                position: 'right',\n                x: 196,\n                y: 28,\n                width: 16,\n                height: 16,\n              },\n            ],\n          },\n          computedPosition: { x: 96, y: 75.5, z: 0 },\n          selected: false,\n          dragging: false,\n          resizing: false,\n          initialized: true,\n          data: {\n            disableBlock: false,\n            description: '',\n            type: 'manual',\n            interval: 60,\n            delay: 5,\n            date: '',\n            time: '00:00',\n            url: '',\n            shortcut: '',\n            activeInInput: false,\n            isUrlRegex: false,\n            days: [],\n            contextMenuName: '',\n            contextTypes: [],\n            parameters: [],\n            preferParamsInTab: false,\n            observeElement: {\n              selector: '',\n              baseSelector: '',\n              matchPattern: '',\n              targetOptions: {\n                subtree: false,\n                childList: true,\n                attributes: false,\n                attributeFilter: [],\n                characterData: false,\n              },\n              baseElOptions: {\n                subtree: false,\n                childList: true,\n                attributes: false,\n                attributeFilter: [],\n                characterData: false,\n              },\n            },\n          },\n          events: {},\n          position: { x: 96, y: 75.5 },\n          id: 'F_uKdk2VrnKslRle78d-C',\n          label: 'trigger',\n        },\n        {\n          type: 'BlockBasic',\n          dimensions: { width: 192, height: 72 },\n          handleBounds: {\n            source: [\n              {\n                id: '369o3ob-output-1',\n                position: 'right',\n                x: 196,\n                y: 28,\n                width: 16,\n                height: 16,\n              },\n            ],\n            target: [\n              {\n                id: '369o3ob-input-1',\n                position: 'left',\n                x: -20,\n                y: 28,\n                width: 16,\n                height: 16,\n              },\n            ],\n          },\n          computedPosition: { x: 388, y: 75.5, z: 0 },\n          selected: false,\n          dragging: false,\n          resizing: false,\n          initialized: true,\n          data: {\n            disableBlock: false,\n            description: '',\n            url: 'https://x.com/explore/tabs/trending',\n            userAgent: '',\n            active: true,\n            inGroup: false,\n            waitTabLoaded: false,\n            updatePrevTab: false,\n            customUserAgent: false,\n          },\n          events: {},\n          position: { x: 388, y: 75.5 },\n          label: 'new-tab',\n          id: '369o3ob',\n        },\n        {\n          type: 'BlockBasic',\n          dimensions: { width: 192, height: 72 },\n          handleBounds: {\n            source: [\n              {\n                id: 'f0ky2jr-output-1',\n                position: 'right',\n                x: 196,\n                y: 28,\n                width: 16,\n                height: 16,\n              },\n            ],\n            target: [\n              {\n                id: 'f0ky2jr-input-1',\n                position: 'left',\n                x: -20,\n                y: 28,\n                width: 16,\n                height: 16,\n              },\n            ],\n          },\n          computedPosition: { x: 680, y: 75.5, z: 0 },\n          selected: false,\n          dragging: false,\n          resizing: false,\n          initialized: true,\n          data: {\n            disableBlock: false,\n            loopId: 'items',\n            selector: '[data-testid=\"trend\"]',\n            maxLoop: '0',\n            description: '',\n            reverseLoop: false,\n            actionElSelector: '',\n            findBy: 'cssSelector',\n            actionElMaxWaitTime: 5,\n            actionPageMaxWaitTime: 10,\n            loadMoreAction: 'scroll',\n            scrollToBottom: true,\n            waitForSelector: true,\n            waitSelectorTimeout: 5000,\n          },\n          events: {},\n          id: 'f0ky2jr',\n          label: 'loop-elements',\n          position: { x: 680, y: 75.5 },\n        },\n        {\n          type: 'BlockBasic',\n          dimensions: { width: 192, height: 72 },\n          handleBounds: {\n            source: [\n              {\n                id: 'tpiq3ux-output-1',\n                position: 'right',\n                x: 196,\n                y: 28,\n                width: 16,\n                height: 16,\n              },\n            ],\n            target: [\n              {\n                id: 'tpiq3ux-input-1',\n                position: 'left',\n                x: -20,\n                y: 28,\n                width: 16,\n                height: 16,\n              },\n            ],\n          },\n          computedPosition: { x: 972, y: 75.5, z: 0 },\n          selected: false,\n          dragging: false,\n          resizing: false,\n          initialized: true,\n          data: {\n            disableBlock: false,\n            description: 'Trending text',\n            findBy: 'cssSelector',\n            waitForSelector: false,\n            waitSelectorTimeout: 5000,\n            selector: '{{loopData.items}} > div > div[dir=\"ltr\"]:nth-child(2)',\n            markEl: false,\n            multiple: false,\n            regex: '',\n            prefixText: '',\n            suffixText: '',\n            regexExp: [],\n            dataColumn: '6QbkU',\n            saveData: true,\n            includeTags: false,\n            addExtraRow: false,\n            assignVariable: false,\n            useTextContent: false,\n            variableName: '',\n            extraRowValue: '',\n            extraRowDataColumn: '',\n          },\n          events: {},\n          position: { x: 972, y: 75.5 },\n          label: 'get-text',\n          id: 'tpiq3ux',\n        },\n        {\n          type: 'BlockBasic',\n          dimensions: { width: 192, height: 72 },\n          handleBounds: {\n            source: [\n              {\n                id: 'zel6am7-output-1',\n                position: 'right',\n                x: 196,\n                y: 28,\n                width: 16,\n                height: 16,\n              },\n            ],\n            target: [\n              {\n                id: 'zel6am7-input-1',\n                position: 'left',\n                x: -20,\n                y: 28,\n                width: 16,\n                height: 16,\n              },\n            ],\n          },\n          computedPosition: { x: 1264, y: 75.5, z: 0 },\n          selected: false,\n          dragging: false,\n          resizing: false,\n          initialized: true,\n          data: {\n            disableBlock: false,\n            description: 'Trending tweets count',\n            findBy: 'cssSelector',\n            waitForSelector: false,\n            waitSelectorTimeout: 5000,\n            selector: '{{loopData.items}} > div > div[dir=\"ltr\"]:nth-child(3)',\n            markEl: false,\n            multiple: false,\n            regex: '',\n            prefixText: '',\n            suffixText: '',\n            regexExp: [],\n            dataColumn: 'Q-zII',\n            saveData: true,\n            includeTags: false,\n            addExtraRow: false,\n            assignVariable: false,\n            useTextContent: false,\n            variableName: '',\n            extraRowValue: '',\n            extraRowDataColumn: '',\n          },\n          events: {},\n          label: 'get-text',\n          id: 'zel6am7',\n          position: { x: 1264, y: 75.5 },\n        },\n        {\n          type: 'BlockLoopBreakpoint',\n          dimensions: { width: 192, height: 151 },\n          handleBounds: {\n            source: [\n              {\n                id: 'mfzs3rz-output-1',\n                position: 'right',\n                x: 196,\n                y: 67.6875,\n                width: 16,\n                height: 16,\n              },\n            ],\n            target: [\n              {\n                id: 'mfzs3rz-input-1',\n                position: 'left',\n                x: -20,\n                y: 67.6875,\n                width: 16,\n                height: 16,\n              },\n            ],\n          },\n          computedPosition: { x: 1556, y: 75.5, z: 0 },\n          selected: false,\n          dragging: false,\n          resizing: false,\n          initialized: true,\n          data: { disableBlock: false, loopId: 'items', clearLoop: false },\n          events: {},\n          position: { x: 1556, y: 75.5 },\n          label: 'loop-breakpoint',\n          id: 'mfzs3rz',\n        },\n        {\n          type: 'BlockBasic',\n          dimensions: { width: 192, height: 72 },\n          handleBounds: {\n            source: [\n              {\n                id: '8tor34x-output-1',\n                position: 'right',\n                x: 196.00022007042253,\n                y: 27.999996538337534,\n                width: 16,\n                height: 16,\n              },\n            ],\n            target: [\n              {\n                id: '8tor34x-input-1',\n                position: 'left',\n                x: -19.99993470685429,\n                y: 27.999996538337534,\n                width: 16,\n                height: 16,\n              },\n            ],\n          },\n          computedPosition: {\n            x: 1839.6148017621147,\n            y: 112.0740088105727,\n            z: 0,\n          },\n          selected: false,\n          dragging: false,\n          resizing: false,\n          initialized: true,\n          data: {\n            disableBlock: false,\n            range: '',\n            refKey: '',\n            type: 'update',\n            customData: '',\n            description: '',\n            spreadsheetId: '',\n            dataColumn: '',\n            saveData: true,\n            assignVariable: false,\n            variableName: '',\n            firstRowAsKey: false,\n            keysAsFirstRow: true,\n            valueInputOption: 'RAW',\n            InsertDataOption: 'INSERT_ROWS',\n            dataFrom: 'data-columns',\n          },\n          events: {},\n          position: { x: 1839.6148017621147, y: 112.0740088105727 },\n          label: 'google-sheets',\n          id: '8tor34x',\n        },\n        {\n          type: 'BlockNote',\n          dimensions: { width: 312, height: 251 },\n          handleBounds: {},\n          computedPosition: {\n            x: 322.1510553694428,\n            y: 207.59283268768826,\n            z: 0,\n          },\n          selected: false,\n          dragging: false,\n          resizing: false,\n          initialized: true,\n          data: {\n            disableBlock: false,\n            note: 'Read the documentation about the Google Sheets block before running this workflow',\n            drawing: false,\n            width: 280,\n            height: 168,\n            color: 'indigo',\n            fontSize: 'regular',\n          },\n          events: {},\n          position: { x: 322.1510553694428, y: 207.59283268768826 },\n          label: 'note',\n          id: '7rznotf',\n        },\n      ],\n      edges: [\n        {\n          id: 'vueflow__edge-F_uKdk2VrnKslRle78d-CF_uKdk2VrnKslRle78d-C-output-1-369o3ob369o3ob-input-1',\n          target: '369o3ob',\n          source: 'F_uKdk2VrnKslRle78d-C',\n          targetHandle: '369o3ob-input-1',\n          sourceHandle: 'F_uKdk2VrnKslRle78d-C-output-1',\n          type: 'custom',\n          updatable: true,\n          selectable: true,\n          markerEnd: 'arrowclosed',\n          data: {},\n          events: {},\n        },\n        {\n          id: 'edge-tczf2qq',\n          source: '369o3ob',\n          target: 'f0ky2jr',\n          sourceHandle: '369o3ob-output-1',\n          targetHandle: 'f0ky2jr-input-1',\n          type: 'custom',\n          updatable: true,\n          selectable: true,\n          markerEnd: 'arrowclosed',\n          data: {},\n          events: {},\n        },\n        {\n          id: 'vueflow__edge-f0ky2jrf0ky2jr-output-1-tpiq3uxtpiq3ux-input-1',\n          target: 'tpiq3ux',\n          source: 'f0ky2jr',\n          targetHandle: 'tpiq3ux-input-1',\n          sourceHandle: 'f0ky2jr-output-1',\n          type: 'custom',\n          updatable: true,\n          selectable: true,\n          markerEnd: 'arrowclosed',\n          data: {},\n          events: {},\n        },\n        {\n          id: 'vueflow__edge-tpiq3uxtpiq3ux-output-1-zel6am7zel6am7-input-1',\n          target: 'zel6am7',\n          source: 'tpiq3ux',\n          targetHandle: 'zel6am7-input-1',\n          sourceHandle: 'tpiq3ux-output-1',\n          type: 'custom',\n          updatable: true,\n          selectable: true,\n          markerEnd: 'arrowclosed',\n          data: {},\n          events: {},\n        },\n        {\n          id: 'vueflow__edge-zel6am7zel6am7-output-1-mfzs3rzmfzs3rz-input-1',\n          target: 'mfzs3rz',\n          source: 'zel6am7',\n          targetHandle: 'mfzs3rz-input-1',\n          sourceHandle: 'zel6am7-output-1',\n          type: 'custom',\n          updatable: true,\n          selectable: true,\n          markerEnd: 'arrowclosed',\n          data: {},\n          events: {},\n        },\n        {\n          id: 'vueflow__edge-mfzs3rzmfzs3rz-output-1-8tor34x8tor34x-input-1',\n          target: '8tor34x',\n          source: 'mfzs3rz',\n          targetHandle: '8tor34x-input-1',\n          sourceHandle: 'mfzs3rz-output-1',\n          type: 'custom',\n          updatable: true,\n          selectable: true,\n          markerEnd: 'arrowclosed',\n          data: {},\n          events: {},\n        },\n      ],\n      position: [-169.83007474880947, 65.03712768543713],\n      zoom: 0.9365333557481814,\n    },\n    settings: {\n      publicId: '',\n      aipowerToken: '',\n      blockDelay: 0,\n      saveLog: true,\n      debugMode: false,\n      restartTimes: 3,\n      notification: true,\n      execContext: 'popup',\n      reuseLastState: false,\n      inputAutocomplete: true,\n      onError: 'stop-workflow',\n      executedBlockOnWeb: false,\n      insertDefaultColumn: false,\n      defaultColumnName: 'column',\n    },\n    globalData: '{\\n\\t\"key\": \"value\"\\n}',\n    description: 'Import current twitter trends to Google Sheets',\n  },\n  {\n    id: nanoid(),\n    name: 'Google search',\n    createdAt: Date.now(),\n    drawflow: {\n      nodes: [\n        {\n          type: 'BlockBasic',\n          dimensions: { width: 192, height: 72 },\n          handleBounds: {\n            source: [\n              {\n                id: 'd634ff22-5dfe-44dc-83d2-842412bd9fbf-output-1',\n                position: 'right',\n                x: 196.00000657196182,\n                y: 28.000021560199762,\n                width: 16,\n                height: 16,\n              },\n            ],\n          },\n          computedPosition: { x: 50, y: 300, z: 0 },\n          id: 'd634ff22-5dfe-44dc-83d2-842412bd9fbf',\n          label: 'trigger',\n          position: { x: 50, y: 300 },\n          data: { type: 'manual', interval: 10 },\n          selected: false,\n        },\n        {\n          type: 'BlockBasic',\n          dimensions: { width: 192, height: 72 },\n          handleBounds: {\n            source: [\n              {\n                id: 'b9e7e0d4-e86a-4635-a352-31c63723fef4-output-1',\n                position: 'right',\n                x: 196.00006103515628,\n                y: 27.999992370605472,\n                width: 16,\n                height: 16,\n              },\n            ],\n            target: [\n              {\n                id: 'b9e7e0d4-e86a-4635-a352-31c63723fef4-input-1',\n                position: 'left',\n                x: -20,\n                y: 27.999992370605472,\n                width: 16,\n                height: 16,\n              },\n            ],\n          },\n          computedPosition: { x: 353, y: 298, z: 0 },\n          id: 'b9e7e0d4-e86a-4635-a352-31c63723fef4',\n          label: 'new-tab',\n          position: { x: 353, y: 298 },\n          data: {\n            disableBlock: false,\n            description: '',\n            url: 'https://google.com',\n            userAgent: '',\n            active: true,\n            inGroup: false,\n            waitTabLoaded: false,\n            updatePrevTab: false,\n            customUserAgent: false,\n            onError: {},\n          },\n          selected: false,\n        },\n        {\n          type: 'BlockBasic',\n          dimensions: { width: 192, height: 72 },\n          handleBounds: {\n            source: [\n              {\n                id: '09f3a14c-0514-4287-93b0-aa92b0064fba-output-1',\n                position: 'right',\n                x: 195.99997405489208,\n                y: 28.00001941411291,\n                width: 16,\n                height: 16,\n              },\n            ],\n            target: [\n              {\n                id: '09f3a14c-0514-4287-93b0-aa92b0064fba-input-1',\n                position: 'left',\n                x: -20.000021574075806,\n                y: 28.00001941411291,\n                width: 16,\n                height: 16,\n              },\n            ],\n          },\n          computedPosition: { x: 641, y: 290, z: 0 },\n          id: '09f3a14c-0514-4287-93b0-aa92b0064fba',\n          label: 'forms',\n          position: { x: 641, y: 290 },\n          data: {\n            description: 'Type query',\n            selector: \"[name='q']\",\n            markEl: false,\n            multiple: false,\n            selected: true,\n            type: 'text-field',\n            value: 'Automa Extension',\n            delay: '120',\n            events: [],\n          },\n          selected: false,\n        },\n        {\n          type: 'BlockBasic',\n          dimensions: { width: 192, height: 72 },\n          handleBounds: {\n            source: [\n              {\n                id: '5f76370d-aa3d-4258-8319-230fcfc49a3a-output-1',\n                position: 'right',\n                x: 196.00006103515628,\n                y: 27.999992370605472,\n                width: 16,\n                height: 16,\n              },\n            ],\n            target: [\n              {\n                id: '5f76370d-aa3d-4258-8319-230fcfc49a3a-input-1',\n                position: 'left',\n                x: -20,\n                y: 27.999992370605472,\n                width: 16,\n                height: 16,\n              },\n            ],\n          },\n          computedPosition: { x: 929, y: 293, z: 0 },\n          id: '5f76370d-aa3d-4258-8319-230fcfc49a3a',\n          label: 'event-click',\n          position: { x: 929, y: 293 },\n          data: {\n            description: 'Click search',\n            selector: 'center:nth-child(1) > .gNO89b',\n            markEl: false,\n            multiple: false,\n          },\n          selected: false,\n        },\n      ],\n      edges: [\n        {\n          id: 'edge-0',\n          sourceHandle: 'd634ff22-5dfe-44dc-83d2-842412bd9fbf-output-1',\n          targetHandle: 'b9e7e0d4-e86a-4635-a352-31c63723fef4-input-1',\n          source: 'd634ff22-5dfe-44dc-83d2-842412bd9fbf',\n          target: 'b9e7e0d4-e86a-4635-a352-31c63723fef4',\n          class:\n            'source-d634ff22-5dfe-44dc-83d2-842412bd9fbf-output-1 target-b9e7e0d4-e86a-4635-a352-31c63723fef4-input-1',\n          type: 'default',\n          z: 0,\n          sourceX: 0,\n          sourceY: 0,\n          targetX: 0,\n          targetY: 0,\n          markerEnd: 'arrowclosed',\n        },\n        {\n          id: 'edge-1',\n          sourceHandle: 'b9e7e0d4-e86a-4635-a352-31c63723fef4-output-1',\n          targetHandle: '09f3a14c-0514-4287-93b0-aa92b0064fba-input-1',\n          source: 'b9e7e0d4-e86a-4635-a352-31c63723fef4',\n          target: '09f3a14c-0514-4287-93b0-aa92b0064fba',\n          class:\n            'source-b9e7e0d4-e86a-4635-a352-31c63723fef4-output-1 target-09f3a14c-0514-4287-93b0-aa92b0064fba-input-1',\n          type: 'default',\n          z: 0,\n          sourceX: 0,\n          sourceY: 0,\n          targetX: 0,\n          targetY: 0,\n          markerEnd: 'arrowclosed',\n          animated: false,\n        },\n        {\n          id: 'edge-2',\n          sourceHandle: '09f3a14c-0514-4287-93b0-aa92b0064fba-output-1',\n          targetHandle: '5f76370d-aa3d-4258-8319-230fcfc49a3a-input-1',\n          source: '09f3a14c-0514-4287-93b0-aa92b0064fba',\n          target: '5f76370d-aa3d-4258-8319-230fcfc49a3a',\n          class:\n            'source-09f3a14c-0514-4287-93b0-aa92b0064fba-output-1 target-5f76370d-aa3d-4258-8319-230fcfc49a3a-input-1',\n          type: 'default',\n          z: 0,\n          sourceX: 0,\n          sourceY: 0,\n          targetX: 0,\n          targetY: 0,\n          markerEnd: 'arrowclosed',\n        },\n      ],\n      position: [-1.538468549623417, 35.22407674532957],\n      zoom: 0.7999999999999999,\n    },\n  },\n  {\n    id: nanoid(),\n    name: 'Generate lorem ipsum',\n    createdAt: Date.now(),\n    drawflow: {\n      nodes: [\n        {\n          type: 'BlockBasic',\n          dimensions: { width: 192, height: 72 },\n          handleBounds: {\n            source: [\n              {\n                id: 'c5774692-0be4-457f-82be-d5e4b3344ad7-output-1',\n                position: 'right',\n                x: 195.99998474121094,\n                y: 27.99999237060547,\n                width: 16,\n                height: 16,\n              },\n            ],\n          },\n          computedPosition: { x: 50, y: 300, z: 0 },\n          id: 'c5774692-0be4-457f-82be-d5e4b3344ad7',\n          label: 'trigger',\n          position: { x: 50, y: 300 },\n          data: {\n            disableBlock: false,\n            description: '',\n            type: 'manual',\n            interval: 60,\n            delay: 5,\n            date: '',\n            time: '00:00',\n            url: '',\n            shortcut: '',\n            activeInInput: false,\n            isUrlRegex: false,\n            days: [],\n          },\n        },\n        {\n          type: 'BlockBasic',\n          dimensions: { width: 192, height: 72 },\n          handleBounds: {\n            source: [\n              {\n                id: '10a0429e-b8c4-4c04-9ea3-df169cea78e4-output-1',\n                position: 'right',\n                x: 195.9999744943092,\n                y: 28.000021560199755,\n                width: 16,\n                height: 16,\n              },\n            ],\n            target: [\n              {\n                id: '10a0429e-b8c4-4c04-9ea3-df169cea78e4-input-1',\n                position: 'left',\n                x: -19.999903128358724,\n                y: 28.000021560199755,\n                width: 16,\n                height: 16,\n              },\n            ],\n          },\n          computedPosition: { x: 315, y: 297, z: 0 },\n          id: '10a0429e-b8c4-4c04-9ea3-df169cea78e4',\n          label: 'new-tab',\n          position: { x: 315, y: 297 },\n          data: {\n            disableBlock: false,\n            description: '',\n            url: 'http://lipsum.com',\n            userAgent: '',\n            active: true,\n            inGroup: false,\n            waitTabLoaded: false,\n            updatePrevTab: true,\n            customUserAgent: false,\n          },\n          selected: false,\n        },\n        {\n          type: 'BlockBasic',\n          dimensions: { width: 192, height: 72 },\n          handleBounds: {\n            source: [\n              {\n                id: '24bdec44-1e80-4cee-9139-00545b8d33d9-output-1',\n                position: 'right',\n                x: 195.99997198037403,\n                y: 28.000015439189703,\n                width: 16,\n                height: 16,\n              },\n            ],\n            target: [\n              {\n                id: '24bdec44-1e80-4cee-9139-00545b8d33d9-input-1',\n                position: 'left',\n                x: -20.000004547328174,\n                y: 28.000015439189703,\n                width: 16,\n                height: 16,\n              },\n            ],\n          },\n          computedPosition: { x: 596, y: 302, z: 0 },\n          id: '24bdec44-1e80-4cee-9139-00545b8d33d9',\n          label: 'element-scroll',\n          position: { x: 596, y: 302 },\n          data: {\n            disableBlock: false,\n            description: '',\n            findBy: 'cssSelector',\n            waitForSelector: true,\n            waitSelectorTimeout: 5000,\n            selector: '#amount',\n            markEl: false,\n            multiple: false,\n            scrollY: 0,\n            scrollX: 0,\n            incX: false,\n            incY: false,\n            smooth: true,\n            scrollIntoView: true,\n          },\n          selected: false,\n        },\n        {\n          type: 'BlockDelay',\n          dimensions: { width: 192, height: 117 },\n          handleBounds: {\n            source: [\n              {\n                id: 'df24edcc-4c29-49f5-8a29-0e572a4bc6ae-output-1',\n                position: 'right',\n                x: 196.00015343897923,\n                y: 50.687512658751125,\n                width: 16,\n                height: 16,\n              },\n            ],\n            target: [\n              {\n                id: 'df24edcc-4c29-49f5-8a29-0e572a4bc6ae-input-1',\n                position: 'left',\n                x: -19.999913818025576,\n                y: 50.687512658751125,\n                width: 16,\n                height: 16,\n              },\n            ],\n          },\n          computedPosition: { x: 878, y: 282, z: 0 },\n          id: 'df24edcc-4c29-49f5-8a29-0e572a4bc6ae',\n          label: 'delay',\n          position: { x: 878, y: 282 },\n          data: { time: '1000' },\n          selected: false,\n        },\n        {\n          type: 'BlockBasic',\n          dimensions: { width: 192, height: 72 },\n          handleBounds: {\n            source: [\n              {\n                id: '2d93c1de-42ca-4f39-8e61-e3e55529fbba-output-1',\n                position: 'right',\n                x: 195.99997198037403,\n                y: 28.000015439189703,\n                width: 16,\n                height: 16,\n              },\n            ],\n            target: [\n              {\n                id: '2d93c1de-42ca-4f39-8e61-e3e55529fbba-input-1',\n                position: 'left',\n                x: -20.000004547328174,\n                y: 28.000015439189703,\n                width: 16,\n                height: 16,\n              },\n            ],\n          },\n          computedPosition: { x: 1148, y: 297, z: 0 },\n          id: '2d93c1de-42ca-4f39-8e61-e3e55529fbba',\n          label: 'forms',\n          position: { x: 1148, y: 297 },\n          data: {\n            disableBlock: false,\n            description: 'Lipsum length',\n            findBy: 'cssSelector',\n            waitForSelector: false,\n            waitSelectorTimeout: 5000,\n            selector: '#amount',\n            markEl: false,\n            multiple: false,\n            selected: true,\n            clearValue: true,\n            getValue: false,\n            saveData: false,\n            dataColumn: '',\n            assignVariable: false,\n            variableName: '',\n            type: 'text-field',\n            value: '3',\n            delay: 0,\n            events: [],\n          },\n          selected: false,\n        },\n        {\n          type: 'BlockBasic',\n          dimensions: { width: 192, height: 72 },\n          handleBounds: {\n            source: [\n              {\n                id: '0f3e2baa-8d6d-4323-8ac7-362f1be39ecb-output-1',\n                position: 'right',\n                x: 195.99997198037403,\n                y: 27.999992756864053,\n                width: 16,\n                height: 16,\n              },\n            ],\n            target: [\n              {\n                id: '0f3e2baa-8d6d-4323-8ac7-362f1be39ecb-input-1',\n                position: 'left',\n                x: -20.00009527663077,\n                y: 27.999992756864053,\n                width: 16,\n                height: 16,\n              },\n            ],\n          },\n          computedPosition: { x: 1414, y: 299, z: 0 },\n          id: '0f3e2baa-8d6d-4323-8ac7-362f1be39ecb',\n          label: 'event-click',\n          position: { x: 1414, y: 299 },\n          data: {\n            disableBlock: false,\n            description: 'Generate button',\n            findBy: 'cssSelector',\n            waitForSelector: false,\n            waitSelectorTimeout: 5000,\n            selector: '#generate',\n            markEl: false,\n            multiple: false,\n          },\n          selected: false,\n        },\n        {\n          type: 'BlockDelay',\n          dimensions: { width: 192, height: 117 },\n          handleBounds: {\n            source: [\n              {\n                id: 'fb9be12f-8995-4876-8bfe-79323769474b-output-1',\n                position: 'right',\n                x: 195,\n                y: 50.68748474121094,\n                width: 16,\n                height: 16,\n              },\n            ],\n            target: [\n              {\n                id: 'fb9be12f-8995-4876-8bfe-79323769474b-input-1',\n                position: 'left',\n                x: -20,\n                y: 50.68748474121094,\n                width: 16,\n                height: 16,\n              },\n            ],\n          },\n          computedPosition: { x: 1686, y: 280, z: 0 },\n          id: 'fb9be12f-8995-4876-8bfe-79323769474b',\n          label: 'delay',\n          position: { x: 1686, y: 280 },\n          data: { disableBlock: false, time: 2000 },\n          selected: false,\n        },\n        {\n          type: 'BlockBasic',\n          dimensions: { width: 192, height: 72 },\n          handleBounds: {\n            source: [\n              {\n                id: '7205fcf2-deda-445e-9690-4e36adb52585-output-1',\n                position: 'right',\n                x: 195.99997449430924,\n                y: 28.00000552137348,\n                width: 16,\n                height: 16,\n              },\n            ],\n            target: [\n              {\n                id: '7205fcf2-deda-445e-9690-4e36adb52585-input-1',\n                position: 'left',\n                x: -20.000031438968968,\n                y: 28.00000552137348,\n                width: 16,\n                height: 16,\n              },\n            ],\n          },\n          computedPosition: { x: 1973, y: 307, z: 0 },\n          id: '7205fcf2-deda-445e-9690-4e36adb52585',\n          label: 'get-text',\n          position: { x: 1973, y: 307 },\n          data: {\n            disableBlock: false,\n            description: 'Get text result',\n            findBy: 'cssSelector',\n            waitForSelector: false,\n            waitSelectorTimeout: 5000,\n            selector: '#lipsum',\n            markEl: false,\n            multiple: false,\n            regex: '',\n            prefixText: '',\n            suffixText: '',\n            regexExp: ['g', 'g'],\n            dataColumn: '',\n            saveData: true,\n            includeTags: false,\n            addExtraRow: false,\n            assignVariable: false,\n            variableName: '',\n            extraRowValue: '',\n            extraRowDataColumn: '',\n          },\n          selected: false,\n        },\n        {\n          type: 'BlockRepeatTask',\n          dimensions: { width: 193, height: 149 },\n          handleBounds: {\n            source: [\n              {\n                id: '3d3e8fac-97fa-4c3d-84bc-a3db18740184-output-1',\n                position: 'right',\n                x: 197.2124006448874,\n                y: 66.6874815732158,\n                width: 16,\n                height: 16,\n              },\n              {\n                id: '3d3e8fac-97fa-4c3d-84bc-a3db18740184-output-2',\n                position: 'right',\n                x: 197.2124006448874,\n                y: 113.3875114484557,\n                width: 16,\n                height: 16,\n              },\n            ],\n            target: [\n              {\n                id: '3d3e8fac-97fa-4c3d-84bc-a3db18740184-input-1',\n                position: 'left',\n                x: -20.000129470007995,\n                y: 66.6874815732158,\n                width: 16,\n                height: 16,\n              },\n            ],\n          },\n          computedPosition: { x: 2253, y: 263.5, z: 0 },\n          id: '3d3e8fac-97fa-4c3d-84bc-a3db18740184',\n          label: 'repeat-task',\n          position: { x: 2253, y: 263.5 },\n          data: { disableBlock: false, repeatFor: 2 },\n          selected: false,\n        },\n        {\n          type: 'BlockBasic',\n          dimensions: { width: 192, height: 72 },\n          handleBounds: {\n            source: [\n              {\n                id: '4d39ecd5-f33f-4e57-b11d-2f26b1076334-output-1',\n                position: 'right',\n                x: 195.9998661589599,\n                y: 27.999992440129862,\n                width: 16,\n                height: 16,\n              },\n            ],\n            target: [\n              {\n                id: '4d39ecd5-f33f-4e57-b11d-2f26b1076334-input-1',\n                position: 'left',\n                x: -20.00023736594018,\n                y: 27.999992440129862,\n                width: 16,\n                height: 16,\n              },\n            ],\n          },\n          computedPosition: { x: 2529.75, y: 304, z: 0 },\n          id: '4d39ecd5-f33f-4e57-b11d-2f26b1076334',\n          label: 'export-data',\n          position: { x: 2529.75, y: 304 },\n          data: {\n            disableBlock: false,\n            name: 'Lipsum',\n            refKey: '',\n            type: 'plain-text',\n            description: '',\n            variableName: '',\n            addBOMHeader: false,\n            onConflict: 'uniquify',\n            dataToExport: 'data-columns',\n          },\n          selected: false,\n        },\n        {\n          type: 'BlockBasic',\n          dimensions: { width: 192, height: 96 },\n          handleBounds: {\n            source: [\n              {\n                id: '2f5fec61-a318-4e2b-b7d3-bc7328bd282c-output-1',\n                position: 'right',\n                x: 196.00006103515625,\n                y: 40.000038146972656,\n                width: 16,\n                height: 16,\n              },\n              {\n                id: '2f5fec61-a318-4e2b-b7d3-bc7328bd282c-output-fallback',\n                position: 'right',\n                x: 196.00006103515625,\n                y: 62.00000762939453,\n                width: 16,\n                height: 16,\n              },\n            ],\n            target: [\n              {\n                id: '2f5fec61-a318-4e2b-b7d3-bc7328bd282c-input-1',\n                position: 'left',\n                x: -20,\n                y: 40.000038146972656,\n                width: 16,\n                height: 16,\n              },\n            ],\n          },\n          computedPosition: { x: 1135.5, y: 628, z: 0 },\n          id: '2f5fec61-a318-4e2b-b7d3-bc7328bd282c',\n          label: 'go-back',\n          position: { x: 1135.5, y: 628 },\n          data: {\n            disableBlock: false,\n            onError: {\n              retry: false,\n              enable: true,\n              retryTimes: 1,\n              retryInterval: 2,\n              toDo: 'fallback',\n            },\n          },\n        },\n      ],\n      edges: [\n        {\n          id: 'edge-0',\n          sourceHandle: 'c5774692-0be4-457f-82be-d5e4b3344ad7-output-1',\n          targetHandle: '10a0429e-b8c4-4c04-9ea3-df169cea78e4-input-1',\n          source: 'c5774692-0be4-457f-82be-d5e4b3344ad7',\n          target: '10a0429e-b8c4-4c04-9ea3-df169cea78e4',\n          class:\n            'source-c5774692-0be4-457f-82be-d5e4b3344ad7-output-1 target-10a0429e-b8c4-4c04-9ea3-df169cea78e4-input-1',\n          type: 'default',\n          z: 0,\n          sourceX: 0,\n          sourceY: 0,\n          targetX: 0,\n          targetY: 0,\n          markerEnd: 'arrowclosed',\n        },\n        {\n          id: 'edge-1',\n          sourceHandle: '10a0429e-b8c4-4c04-9ea3-df169cea78e4-output-1',\n          targetHandle: '24bdec44-1e80-4cee-9139-00545b8d33d9-input-1',\n          source: '10a0429e-b8c4-4c04-9ea3-df169cea78e4',\n          target: '24bdec44-1e80-4cee-9139-00545b8d33d9',\n          class:\n            'source-10a0429e-b8c4-4c04-9ea3-df169cea78e4-output-1 target-24bdec44-1e80-4cee-9139-00545b8d33d9-input-1',\n          type: 'default',\n          z: 0,\n          sourceX: 0,\n          sourceY: 0,\n          targetX: 0,\n          targetY: 0,\n          markerEnd: 'arrowclosed',\n        },\n        {\n          id: 'edge-2',\n          sourceHandle: '24bdec44-1e80-4cee-9139-00545b8d33d9-output-1',\n          targetHandle: 'df24edcc-4c29-49f5-8a29-0e572a4bc6ae-input-1',\n          source: '24bdec44-1e80-4cee-9139-00545b8d33d9',\n          target: 'df24edcc-4c29-49f5-8a29-0e572a4bc6ae',\n          class:\n            'source-24bdec44-1e80-4cee-9139-00545b8d33d9-output-1 target-df24edcc-4c29-49f5-8a29-0e572a4bc6ae-input-1',\n          type: 'default',\n          z: 0,\n          sourceX: 0,\n          sourceY: 0,\n          targetX: 0,\n          targetY: 0,\n          markerEnd: 'arrowclosed',\n        },\n        {\n          id: 'edge-3',\n          sourceHandle: 'df24edcc-4c29-49f5-8a29-0e572a4bc6ae-output-1',\n          targetHandle: '2d93c1de-42ca-4f39-8e61-e3e55529fbba-input-1',\n          source: 'df24edcc-4c29-49f5-8a29-0e572a4bc6ae',\n          target: '2d93c1de-42ca-4f39-8e61-e3e55529fbba',\n          class:\n            'source-df24edcc-4c29-49f5-8a29-0e572a4bc6ae-output-1 target-2d93c1de-42ca-4f39-8e61-e3e55529fbba-input-1',\n          type: 'default',\n          z: 0,\n          sourceX: 0,\n          sourceY: 0,\n          targetX: 0,\n          targetY: 0,\n          markerEnd: 'arrowclosed',\n        },\n        {\n          id: 'edge-4',\n          sourceHandle: '2d93c1de-42ca-4f39-8e61-e3e55529fbba-output-1',\n          targetHandle: '0f3e2baa-8d6d-4323-8ac7-362f1be39ecb-input-1',\n          source: '2d93c1de-42ca-4f39-8e61-e3e55529fbba',\n          target: '0f3e2baa-8d6d-4323-8ac7-362f1be39ecb',\n          class:\n            'source-2d93c1de-42ca-4f39-8e61-e3e55529fbba-output-1 target-0f3e2baa-8d6d-4323-8ac7-362f1be39ecb-input-1',\n          type: 'default',\n          z: 0,\n          sourceX: 0,\n          sourceY: 0,\n          targetX: 0,\n          targetY: 0,\n          markerEnd: 'arrowclosed',\n        },\n        {\n          id: 'edge-5',\n          sourceHandle: '0f3e2baa-8d6d-4323-8ac7-362f1be39ecb-output-1',\n          targetHandle: 'fb9be12f-8995-4876-8bfe-79323769474b-input-1',\n          source: '0f3e2baa-8d6d-4323-8ac7-362f1be39ecb',\n          target: 'fb9be12f-8995-4876-8bfe-79323769474b',\n          class:\n            'source-0f3e2baa-8d6d-4323-8ac7-362f1be39ecb-output-1 target-fb9be12f-8995-4876-8bfe-79323769474b-input-1',\n          type: 'default',\n          z: 0,\n          sourceX: 0,\n          sourceY: 0,\n          targetX: 0,\n          targetY: 0,\n          markerEnd: 'arrowclosed',\n        },\n        {\n          id: 'edge-6',\n          sourceHandle: 'fb9be12f-8995-4876-8bfe-79323769474b-output-1',\n          targetHandle: '7205fcf2-deda-445e-9690-4e36adb52585-input-1',\n          source: 'fb9be12f-8995-4876-8bfe-79323769474b',\n          target: '7205fcf2-deda-445e-9690-4e36adb52585',\n          class:\n            'source-fb9be12f-8995-4876-8bfe-79323769474b-output-1 target-7205fcf2-deda-445e-9690-4e36adb52585-input-1',\n          type: 'default',\n          z: 0,\n          sourceX: 0,\n          sourceY: 0,\n          targetX: 0,\n          targetY: 0,\n          markerEnd: 'arrowclosed',\n        },\n        {\n          id: 'edge-7',\n          sourceHandle: '7205fcf2-deda-445e-9690-4e36adb52585-output-1',\n          targetHandle: '3d3e8fac-97fa-4c3d-84bc-a3db18740184-input-1',\n          source: '7205fcf2-deda-445e-9690-4e36adb52585',\n          target: '3d3e8fac-97fa-4c3d-84bc-a3db18740184',\n          class:\n            'source-7205fcf2-deda-445e-9690-4e36adb52585-output-1 target-3d3e8fac-97fa-4c3d-84bc-a3db18740184-input-1',\n          type: 'default',\n          z: 0,\n          sourceX: 0,\n          sourceY: 0,\n          targetX: 0,\n          targetY: 0,\n          markerEnd: 'arrowclosed',\n        },\n        {\n          id: 'edge-8',\n          sourceHandle: '3d3e8fac-97fa-4c3d-84bc-a3db18740184-output-1',\n          targetHandle: '4d39ecd5-f33f-4e57-b11d-2f26b1076334-input-1',\n          source: '3d3e8fac-97fa-4c3d-84bc-a3db18740184',\n          target: '4d39ecd5-f33f-4e57-b11d-2f26b1076334',\n          class:\n            'source-3d3e8fac-97fa-4c3d-84bc-a3db18740184-output-1 target-4d39ecd5-f33f-4e57-b11d-2f26b1076334-input-1',\n          type: 'default',\n          z: 0,\n          sourceX: 0,\n          sourceY: 0,\n          targetX: 0,\n          targetY: 0,\n          markerEnd: 'arrowclosed',\n        },\n        {\n          id: 'edge-9',\n          sourceHandle: '3d3e8fac-97fa-4c3d-84bc-a3db18740184-output-2',\n          targetHandle: '2f5fec61-a318-4e2b-b7d3-bc7328bd282c-input-1',\n          source: '3d3e8fac-97fa-4c3d-84bc-a3db18740184',\n          target: '2f5fec61-a318-4e2b-b7d3-bc7328bd282c',\n          class:\n            'source-3d3e8fac-97fa-4c3d-84bc-a3db18740184-output-2 target-2f5fec61-a318-4e2b-b7d3-bc7328bd282c-input-1',\n          type: 'default',\n          z: 0,\n          sourceX: 0,\n          sourceY: 0,\n          targetX: 0,\n          targetY: 0,\n          markerEnd: 'arrowclosed',\n        },\n        {\n          id: 'edge-10',\n          sourceHandle: '2f5fec61-a318-4e2b-b7d3-bc7328bd282c-output-1',\n          targetHandle: '24bdec44-1e80-4cee-9139-00545b8d33d9-input-1',\n          source: '2f5fec61-a318-4e2b-b7d3-bc7328bd282c',\n          target: '24bdec44-1e80-4cee-9139-00545b8d33d9',\n          class:\n            'source-2f5fec61-a318-4e2b-b7d3-bc7328bd282c-output-1 target-24bdec44-1e80-4cee-9139-00545b8d33d9-input-1',\n          type: 'default',\n          z: 0,\n          sourceX: 0,\n          sourceY: 0,\n          targetX: 0,\n          targetY: 0,\n          markerEnd: 'arrowclosed',\n        },\n        {\n          id: 'edge-11',\n          sourceHandle: '2f5fec61-a318-4e2b-b7d3-bc7328bd282c-output-fallback',\n          targetHandle: '10a0429e-b8c4-4c04-9ea3-df169cea78e4-input-1',\n          source: '2f5fec61-a318-4e2b-b7d3-bc7328bd282c',\n          target: '10a0429e-b8c4-4c04-9ea3-df169cea78e4',\n          class:\n            'source-2f5fec61-a318-4e2b-b7d3-bc7328bd282c-output-fallback target-10a0429e-b8c4-4c04-9ea3-df169cea78e4-input-1',\n          type: 'default',\n          z: 0,\n          sourceX: 0,\n          sourceY: 0,\n          targetX: 0,\n          targetY: 0,\n          markerEnd: 'arrowclosed',\n        },\n      ],\n      position: [29, 97],\n      zoom: 0.5,\n    },\n  },\n  {\n    id: nanoid(),\n    name: 'Search in ProductHunt',\n    createdAt: Date.now(),\n    drawflow: {\n      nodes: [\n        {\n          type: 'BlockBasic',\n          dimensions: {\n            width: 192,\n            height: 72,\n          },\n          handleBounds: {\n            source: [\n              {\n                id: 'MeMnYgI8DFrfORUfmREmq-output-1',\n                position: 'right',\n                x: 196.00003756009605,\n                y: 28.000018780048062,\n                width: 16,\n                height: 16,\n              },\n            ],\n          },\n          computedPosition: {\n            x: 96,\n            y: 36,\n            z: 0,\n          },\n          position: {\n            x: 96,\n            y: 36,\n          },\n          id: 'MeMnYgI8DFrfORUfmREmq',\n          label: 'trigger',\n          data: {\n            disableBlock: false,\n            description: '',\n            type: 'context-menu',\n            interval: 60,\n            delay: 5,\n            date: '',\n            time: '00:00',\n            url: '',\n            shortcut: '',\n            activeInInput: false,\n            isUrlRegex: false,\n            days: [],\n            contextMenuName: 'Search in ProductHunt',\n            contextTypes: ['selection'],\n            parameters: [],\n            observeElement: {\n              selector: '',\n              baseSelector: '',\n              matchPattern: '',\n              targetOptions: {\n                subtree: false,\n                childList: true,\n                attributes: false,\n                attributeFilter: [],\n                characterData: false,\n              },\n              baseElOptions: {\n                subtree: false,\n                childList: true,\n                attributes: false,\n                attributeFilter: [],\n                characterData: false,\n              },\n            },\n          },\n          selected: false,\n        },\n        {\n          type: 'BlockBasic',\n          dimensions: {\n            width: 192,\n            height: 72,\n          },\n          handleBounds: {\n            source: [\n              {\n                id: 'pdeg5g8-output-1',\n                position: 'right',\n                x: 196.00003756009605,\n                y: 28.000018780048062,\n                width: 16,\n                height: 16,\n              },\n            ],\n            target: [\n              {\n                id: 'pdeg5g8-input-1',\n                position: 'left',\n                x: -19.99999999999999,\n                y: 28.000018780048062,\n                width: 16,\n                height: 16,\n              },\n            ],\n          },\n          computedPosition: {\n            x: 388,\n            y: 36,\n            z: 0,\n          },\n          position: {\n            x: 388,\n            y: 36,\n          },\n          label: 'new-tab',\n          data: {\n            disableBlock: false,\n            description: '',\n            url: 'https://www.producthunt.com/search?q={{variables@$ctxTextSelection}}',\n            userAgent: '',\n            active: true,\n            inGroup: false,\n            waitTabLoaded: false,\n            updatePrevTab: false,\n            customUserAgent: false,\n          },\n          id: 'pdeg5g8',\n          selected: false,\n        },\n      ],\n      edges: [\n        {\n          id: 'vueflow__edge-MeMnYgI8DFrfORUfmREmqMeMnYgI8DFrfORUfmREmq-output-1-pdeg5g8pdeg5g8-input-1',\n          source: 'MeMnYgI8DFrfORUfmREmq',\n          target: 'pdeg5g8',\n          sourceHandle: 'MeMnYgI8DFrfORUfmREmq-output-1',\n          targetHandle: 'pdeg5g8-input-1',\n          updatable: true,\n          selectable: true,\n          type: 'default',\n          markerEnd: '',\n          z: 0,\n          sourceX: 0,\n          sourceY: 0,\n          targetX: 0,\n          targetY: 0,\n        },\n      ],\n      position: [136.59999999999997, 279.20001831054685],\n      zoom: 1.3,\n    },\n  },\n  {\n    id: nanoid(),\n    createdAt: Date.now(),\n    name: 'Google Keyword Research',\n    table: [\n      {\n        id: '25D2q',\n        name: 'keyword',\n        type: 'string',\n      },\n      {\n        id: '38NQr',\n        name: 'related',\n        type: 'string',\n      },\n    ],\n    drawflow: {\n      nodes: [\n        {\n          computedPosition: {\n            x: 96,\n            y: 36,\n            z: 0,\n          },\n          data: {\n            activeInInput: false,\n            contextMenuName: '',\n            contextTypes: [],\n            date: '',\n            days: [],\n            delay: 5,\n            description: '',\n            disableBlock: false,\n            interval: 60,\n            isUrlRegex: false,\n            observeElement: {\n              baseElOptions: {\n                attributeFilter: [],\n                attributes: false,\n                characterData: false,\n                childList: true,\n                subtree: false,\n              },\n              baseSelector: '',\n              matchPattern: '',\n              selector: '',\n              targetOptions: {\n                attributeFilter: [],\n                attributes: false,\n                characterData: false,\n                childList: true,\n                subtree: false,\n              },\n            },\n            parameters: [\n              {\n                defaultValue: '',\n                name: 'keywords',\n                placeholder: 'keyword 1,keyword 2',\n                type: 'string',\n              },\n            ],\n            shortcut: '',\n            time: '00:00',\n            type: 'manual',\n            url: '',\n          },\n          dimensions: {\n            width: 192,\n            height: 72,\n          },\n          handleBounds: {\n            source: [\n              {\n                id: '2VtN_ZVBleyrXpIe6hHqm-output-1',\n                position: 'right',\n                x: 196.000030930837,\n                y: 28.00001817279392,\n                width: 16,\n                height: 16,\n              },\n            ],\n          },\n          id: '2VtN_ZVBleyrXpIe6hHqm',\n          label: 'trigger',\n          position: {\n            x: 96,\n            y: 36,\n          },\n          selected: false,\n          type: 'BlockBasic',\n        },\n        {\n          computedPosition: {\n            x: 388,\n            y: 36,\n            z: 0,\n          },\n          data: {\n            active: true,\n            customUserAgent: false,\n            description: '',\n            disableBlock: false,\n            inGroup: false,\n            updatePrevTab: false,\n            url: 'https://www.google.com/',\n            userAgent: '',\n            waitTabLoaded: false,\n          },\n          dimensions: {\n            width: 192,\n            height: 72,\n          },\n          handleBounds: {\n            source: [\n              {\n                id: 'q2ayq0p-output-1',\n                position: 'right',\n                x: 195.99998279147678,\n                y: 28.00001817279392,\n                width: 16,\n                height: 16,\n              },\n            ],\n            target: [\n              {\n                id: 'q2ayq0p-input-1',\n                position: 'left',\n                x: -19.999978595309788,\n                y: 28.00001817279392,\n                width: 16,\n                height: 16,\n              },\n            ],\n          },\n          id: 'q2ayq0p',\n          label: 'new-tab',\n          position: {\n            x: 388,\n            y: 36,\n          },\n          selected: false,\n          type: 'BlockBasic',\n        },\n        {\n          computedPosition: {\n            x: 680,\n            y: 36,\n            z: 0,\n          },\n          data: {\n            code: \"const keywords = automaRefData('variables', 'keywords');\\nautomaSetVariable(\\n  'keywords', \\n  keywords.split(',').map((keyword) => keyword.trim())\\n)\",\n            description: '',\n            disableBlock: false,\n            everyNewTab: false,\n            preloadScripts: [],\n            runBeforeLoad: false,\n            timeout: 20000,\n          },\n          dimensions: {\n            width: 192,\n            height: 72,\n          },\n          handleBounds: {\n            source: [\n              {\n                id: 'ls02uyr-output-1',\n                position: 'right',\n                x: 196.00007907019722,\n                y: 28.00001817279392,\n                width: 16,\n                height: 16,\n              },\n            ],\n            target: [\n              {\n                id: 'ls02uyr-input-1',\n                position: 'left',\n                x: -19.999882316589357,\n                y: 28.00001817279392,\n                width: 16,\n                height: 16,\n              },\n            ],\n          },\n          id: 'ls02uyr',\n          label: 'javascript-code',\n          position: {\n            x: 680,\n            y: 36,\n          },\n          selected: false,\n          type: 'BlockBasic',\n        },\n        {\n          computedPosition: {\n            x: 972,\n            y: 36,\n            z: 0,\n          },\n          data: {\n            description: '',\n            disableBlock: false,\n            elementSelector: '',\n            fromNumber: 1,\n            loopData: '[]',\n            loopId: 'keywords',\n            loopThrough: 'variable',\n            maxLoop: 0,\n            referenceKey: '',\n            resumeLastWorkflow: false,\n            startIndex: 0,\n            toNumber: 10,\n            variableName: 'keywords',\n            waitForSelector: false,\n            waitSelectorTimeout: 5000,\n          },\n          dimensions: {\n            width: 192,\n            height: 72,\n          },\n          handleBounds: {\n            source: [\n              {\n                id: 'xk53h8i-output-1',\n                position: 'right',\n                x: 196.00007907019722,\n                y: 28.00001817279392,\n                width: 16,\n                height: 16,\n              },\n            ],\n            target: [\n              {\n                id: 'xk53h8i-input-1',\n                position: 'left',\n                x: -19.999978595309788,\n                y: 28.00001817279392,\n                width: 16,\n                height: 16,\n              },\n            ],\n          },\n          id: 'xk53h8i',\n          label: 'loop-data',\n          position: {\n            x: 972,\n            y: 36,\n          },\n          selected: false,\n          type: 'BlockBasic',\n        },\n        {\n          computedPosition: {\n            x: 680,\n            y: 158,\n            z: 0,\n          },\n          data: {\n            dataList: [\n              {\n                name: '25D2q',\n                type: 'table',\n                value: '{{loopData@keywords}}',\n              },\n            ],\n            description: '',\n            disableBlock: false,\n          },\n          dimensions: {\n            width: 192,\n            height: 72,\n          },\n          handleBounds: {\n            source: [\n              {\n                id: 'f1fo1xh-output-1',\n                position: 'right',\n                x: 196.00007907019722,\n                y: 28.00001817279392,\n                width: 16,\n                height: 16,\n              },\n            ],\n            target: [\n              {\n                id: 'f1fo1xh-input-1',\n                position: 'left',\n                x: -19.999882316589357,\n                y: 28.00001817279392,\n                width: 16,\n                height: 16,\n              },\n            ],\n          },\n          id: 'f1fo1xh',\n          label: 'insert-data',\n          position: {\n            x: 680,\n            y: 158,\n          },\n          selected: false,\n          type: 'BlockBasic',\n        },\n        {\n          computedPosition: {\n            x: 103.18645833333312,\n            y: 284.19444444444446,\n            z: 0,\n          },\n          data: {\n            disableBlock: false,\n            loopId: 'keywords',\n          },\n          dimensions: {\n            width: 192,\n            height: 117,\n          },\n          handleBounds: {\n            source: [\n              {\n                id: 'xcsy0gj-output-1',\n                position: 'right',\n                x: 196.00007907019722,\n                y: 50.68752097023858,\n                width: 16,\n                height: 16,\n              },\n            ],\n            target: [\n              {\n                id: 'xcsy0gj-input-1',\n                position: 'left',\n                x: -20.000026734670005,\n                y: 50.68752097023858,\n                width: 16,\n                height: 16,\n              },\n            ],\n          },\n          id: 'xcsy0gj',\n          label: 'loop-breakpoint',\n          position: {\n            x: 103.18645833333312,\n            y: 284.19444444444446,\n          },\n          selected: false,\n          type: 'BlockLoopBreakpoint',\n        },\n        {\n          computedPosition: {\n            x: 96,\n            y: 158,\n            z: 0,\n          },\n          data: {\n            assignVariable: false,\n            clearValue: true,\n            dataColumn: '',\n            delay: 70,\n            description: '',\n            disableBlock: false,\n            events: [],\n            findBy: 'cssSelector',\n            getValue: false,\n            markEl: false,\n            multiple: false,\n            optionPosition: '1',\n            saveData: false,\n            selectOptionBy: 'value',\n            selected: true,\n            selector: 'input[name=\"q\"]',\n            type: 'text-field',\n            value: '{{loopData@keywords}}',\n            variableName: '',\n            waitForSelector: false,\n            waitSelectorTimeout: 5000,\n          },\n          dimensions: {\n            width: 192,\n            height: 72,\n          },\n          handleBounds: {\n            source: [\n              {\n                id: 'uoxxd4x-output-1',\n                position: 'right',\n                x: 196.000030930837,\n                y: 28.00001817279392,\n                width: 16,\n                height: 16,\n              },\n            ],\n            target: [\n              {\n                id: 'uoxxd4x-input-1',\n                position: 'left',\n                x: -19.999978595309788,\n                y: 28.00001817279392,\n                width: 16,\n                height: 16,\n              },\n            ],\n          },\n          id: 'uoxxd4x',\n          label: 'forms',\n          position: {\n            x: 96,\n            y: 158,\n          },\n          selected: false,\n          type: 'BlockBasic',\n        },\n        {\n          computedPosition: {\n            x: 388,\n            y: 158,\n            z: 0,\n          },\n          data: {\n            disableBlock: false,\n            loopId: 'options',\n            maxLoop: 0,\n            toNumber: 10,\n            fromNumber: 1,\n            startIndex: 0,\n            loopData: '[]',\n            description: '',\n            variableName: '',\n            referenceKey: '',\n            elementSelector: '[role=\"listbox\"] [role=\"option\"]',\n            waitForSelector: false,\n            waitSelectorTimeout: 5000,\n            resumeLastWorkflow: false,\n            loopThrough: 'elements',\n          },\n          dimensions: {\n            width: 192,\n            height: 72,\n          },\n          handleBounds: {\n            source: [\n              {\n                id: 'kz3rbur-output-1',\n                position: 'right',\n                x: 195.99998279147678,\n                y: 28.00001817279392,\n                width: 16,\n                height: 16,\n              },\n            ],\n            target: [\n              {\n                id: 'kz3rbur-input-1',\n                position: 'left',\n                x: -19.999978595309788,\n                y: 28.00001817279392,\n                width: 16,\n                height: 16,\n              },\n            ],\n          },\n          id: 'kz3rbur',\n          label: 'loop-data',\n          position: {\n            x: 388,\n            y: 158,\n          },\n          selected: false,\n          type: 'BlockBasic',\n        },\n        {\n          computedPosition: {\n            x: 972,\n            y: 158,\n            z: 0,\n          },\n          data: {\n            disableBlock: false,\n            description: '',\n            findBy: 'cssSelector',\n            waitForSelector: false,\n            waitSelectorTimeout: 5000,\n            selector: '{{loopData@options}}',\n            markEl: false,\n            multiple: false,\n            regex: '',\n            prefixText: '',\n            suffixText: '',\n            regexExp: ['g', 'g', 'g'],\n            dataColumn: '38NQr',\n            saveData: true,\n            includeTags: false,\n            addExtraRow: false,\n            assignVariable: false,\n            useTextContent: false,\n            variableName: '',\n            extraRowValue: '',\n            extraRowDataColumn: '',\n          },\n          dimensions: {\n            width: 192,\n            height: 72,\n          },\n          handleBounds: {\n            source: [\n              {\n                id: '5odnjn3-output-1',\n                position: 'right',\n                x: 196.00007907019722,\n                y: 28.00001817279392,\n                width: 16,\n                height: 16,\n              },\n            ],\n            target: [\n              {\n                id: '5odnjn3-input-1',\n                position: 'left',\n                x: -19.999978595309788,\n                y: 28.00001817279392,\n                width: 16,\n                height: 16,\n              },\n            ],\n          },\n          id: '5odnjn3',\n          label: 'get-text',\n          position: {\n            x: 972,\n            y: 158,\n          },\n          selected: false,\n          type: 'BlockBasic',\n        },\n        {\n          computedPosition: {\n            x: 1227.7190972222222,\n            y: 134.33854166666666,\n            z: 1000,\n          },\n          data: {\n            disableBlock: false,\n            loopId: 'options',\n          },\n          dimensions: {\n            width: 192,\n            height: 117,\n          },\n          handleBounds: {\n            source: [\n              {\n                id: 'th64xxb-output-1',\n                position: 'right',\n                x: 195.99998279147678,\n                y: 50.68747283087836,\n                width: 16,\n                height: 16,\n              },\n            ],\n            target: [\n              {\n                id: 'th64xxb-input-1',\n                position: 'left',\n                x: -19.999978595309788,\n                y: 50.68747283087836,\n                width: 16,\n                height: 16,\n              },\n            ],\n          },\n          id: 'th64xxb',\n          label: 'loop-breakpoint',\n          position: {\n            x: 1227.7190972222222,\n            y: 134.33854166666666,\n          },\n          selected: true,\n          type: 'BlockLoopBreakpoint',\n        },\n        {\n          type: 'BlockBasic',\n          dimensions: {\n            width: 192,\n            height: 72,\n          },\n          handleBounds: {\n            source: [\n              {\n                id: 'tts9dbc-output-1',\n                position: 'right',\n                x: 196.00007907019722,\n                y: 28.00001817279392,\n                width: 16,\n                height: 16,\n              },\n            ],\n            target: [\n              {\n                id: 'tts9dbc-input-1',\n                position: 'left',\n                x: -19.999882316589357,\n                y: 28.00001817279392,\n                width: 16,\n                height: 16,\n              },\n            ],\n          },\n          computedPosition: {\n            x: 376.2090277777778,\n            y: 307.24879056082835,\n            z: 0,\n          },\n          position: {\n            x: 376.2090277777778,\n            y: 307.24879056082835,\n          },\n          label: 'export-data',\n          data: {\n            disableBlock: false,\n            name: 'google-keywords',\n            refKey: '',\n            type: 'csv',\n            description: '',\n            variableName: '',\n            csvDelimiter: ',',\n            addBOMHeader: true,\n            onConflict: 'uniquify',\n            dataToExport: 'data-columns',\n          },\n          id: 'tts9dbc',\n          selected: false,\n        },\n      ],\n      edges: [\n        {\n          id: 'vueflow__edge-2VtN_ZVBleyrXpIe6hHqm2VtN_ZVBleyrXpIe6hHqm-output-1-q2ayq0pq2ayq0p-input-1',\n          class: 'source-2VtN_ZVBleyrXpIe6hHqm-output-1 target-q2ayq0p-input-1',\n          markerEnd: '',\n          selectable: true,\n          source: '2VtN_ZVBleyrXpIe6hHqm',\n          sourceHandle: '2VtN_ZVBleyrXpIe6hHqm-output-1',\n          sourceX: 0,\n          sourceY: 0,\n          target: 'q2ayq0p',\n          targetHandle: 'q2ayq0p-input-1',\n          targetX: 0,\n          targetY: 0,\n          type: 'default',\n          updatable: true,\n          z: 0,\n        },\n        {\n          id: 'vueflow__edge-q2ayq0pq2ayq0p-output-1-ls02uyrls02uyr-input-1',\n          markerEnd: '',\n          selectable: true,\n          selected: false,\n          source: 'q2ayq0p',\n          sourceHandle: 'q2ayq0p-output-1',\n          sourceX: 0,\n          sourceY: 0,\n          target: 'ls02uyr',\n          targetHandle: 'ls02uyr-input-1',\n          targetX: 0,\n          targetY: 0,\n          type: 'default',\n          updatable: true,\n          z: 0,\n        },\n        {\n          id: 'vueflow__edge-ls02uyrls02uyr-output-1-xk53h8ixk53h8i-input-1',\n          markerEnd: '',\n          selectable: true,\n          source: 'ls02uyr',\n          sourceHandle: 'ls02uyr-output-1',\n          sourceX: 0,\n          sourceY: 0,\n          target: 'xk53h8i',\n          targetHandle: 'xk53h8i-input-1',\n          targetX: 0,\n          targetY: 0,\n          type: 'default',\n          updatable: true,\n          z: 0,\n        },\n        {\n          id: 'edge-6k9nnc2',\n          class: 'source-uoxxd4x-output-1 target-kz3rbur-input-1',\n          markerEnd: '',\n          selectable: true,\n          selected: false,\n          source: 'uoxxd4x',\n          sourceHandle: 'uoxxd4x-output-1',\n          sourceX: 0,\n          sourceY: 0,\n          target: 'kz3rbur',\n          targetHandle: 'kz3rbur-input-1',\n          targetX: 0,\n          targetY: 0,\n          type: 'default',\n          updatable: true,\n          z: 0,\n        },\n        {\n          id: 'vueflow__edge-kz3rburkz3rbur-output-1-f1fo1xhf1fo1xh-input-1',\n          class: 'source-kz3rbur-output-1 target-f1fo1xh-input-1',\n          markerEnd: '',\n          selectable: true,\n          selected: false,\n          source: 'kz3rbur',\n          sourceHandle: 'kz3rbur-output-1',\n          sourceX: 0,\n          sourceY: 0,\n          target: 'f1fo1xh',\n          targetHandle: 'f1fo1xh-input-1',\n          targetX: 0,\n          targetY: 0,\n          type: 'default',\n          updatable: true,\n          z: 0,\n        },\n        {\n          id: 'vueflow__edge-f1fo1xhf1fo1xh-output-1-5odnjn35odnjn3-input-1',\n          class: 'source-f1fo1xh-output-1 target-5odnjn3-input-1',\n          markerEnd: '',\n          selectable: true,\n          selected: false,\n          source: 'f1fo1xh',\n          sourceHandle: 'f1fo1xh-output-1',\n          sourceX: 0,\n          sourceY: 0,\n          target: '5odnjn3',\n          targetHandle: '5odnjn3-input-1',\n          targetX: 0,\n          targetY: 0,\n          type: 'default',\n          updatable: true,\n          z: 0,\n        },\n        {\n          id: 'vueflow__edge-5odnjn35odnjn3-output-1-th64xxbth64xxb-input-1',\n          class: 'source-5odnjn3-output-1 target-th64xxb-input-1',\n          markerEnd: '',\n          selectable: true,\n          source: '5odnjn3',\n          sourceHandle: '5odnjn3-output-1',\n          sourceX: 0,\n          sourceY: 0,\n          target: 'th64xxb',\n          targetHandle: 'th64xxb-input-1',\n          targetX: 0,\n          targetY: 0,\n          type: 'default',\n          updatable: true,\n          z: 0,\n        },\n        {\n          id: 'vueflow__edge-th64xxbth64xxb-output-1-xcsy0gjxcsy0gj-input-1',\n          class: 'source-th64xxb-output-1 target-xcsy0gj-input-1',\n          markerEnd: '',\n          selectable: true,\n          source: 'th64xxb',\n          sourceHandle: 'th64xxb-output-1',\n          sourceX: 0,\n          sourceY: 0,\n          target: 'xcsy0gj',\n          targetHandle: 'xcsy0gj-input-1',\n          targetX: 0,\n          targetY: 0,\n          type: 'default',\n          updatable: true,\n          z: 0,\n        },\n        {\n          id: 'vueflow__edge-xk53h8ixk53h8i-output-1-uoxxd4xuoxxd4x-input-1',\n          class: 'source-xk53h8i-output-1 target-uoxxd4x-input-1',\n          markerEnd: '',\n          selectable: true,\n          source: 'xk53h8i',\n          sourceHandle: 'xk53h8i-output-1',\n          sourceX: 0,\n          sourceY: 0,\n          target: 'uoxxd4x',\n          targetHandle: 'uoxxd4x-input-1',\n          targetX: 0,\n          targetY: 0,\n          type: 'default',\n          updatable: true,\n          z: 0,\n        },\n        {\n          id: 'vueflow__edge-xcsy0gjxcsy0gj-output-1-tts9dbctts9dbc-input-1',\n          source: 'xcsy0gj',\n          target: 'tts9dbc',\n          sourceHandle: 'xcsy0gj-output-1',\n          targetHandle: 'tts9dbc-input-1',\n          updatable: true,\n          selectable: true,\n          type: 'default',\n          markerEnd: '',\n          z: 0,\n          sourceX: 0,\n          sourceY: 0,\n          targetX: 0,\n          targetY: 0,\n        },\n      ],\n      position: [-7.494827206691525, 274.2219861731927],\n      zoom: 0.6339423288575831,\n    },\n  },\n];\n"
  },
  {
    "path": "src/utils/getAIPoweredInfo.js",
    "content": "import secrets from 'secrets';\n\nconst BASE_URL = secrets.apApiUrl;\n\n/**\n * @typedef {object} AIWorkflowInputOutput\n * @property {string} label - The display name of the node.\n * @property {string} name - The parameter name for the runtime API call.\n * @property {'TEXT'|'IMAGE'|'FILE'|'VIDEO'|'AUDIO'} type - The parameter type.\n * @property {string} [accept] - The acceptable file types (comma-separated).\n * @property {number} [maxSize] - The maximum file size in MB.\n */\n\n/**\n * @typedef {object} AIWorkflowDetail\n * @property {string} flowUuid - The UUID of the AI workflow.\n * @property {string} name - The name of the AI workflow.\n * @property {AIWorkflowInputOutput[]} inputs - The array of input nodes.\n * @property {AIWorkflowInputOutput[]} output - The array of output nodes.\n */\n\n/**\n * @typedef {object} APIDetailResponse\n * @property {number} code - Business status code (200 for success).\n * @property {boolean} success - Indicates if the request was successful.\n * @property {string} msg - Failure message.\n * @property {AIWorkflowDetail} data - The entity data.\n * @property {string} requestId - The request ID.\n */\n\n/**\n * Fetches the details of an AI Power workflow.\n * @param {string} flowUuid - The UUID of the AI workflow.\n * @param {string} token - The authorization token.\n * @returns {Promise<APIDetailResponse>} The API response containing the workflow details.\n */\nexport const getAPWorkflowDetail = async (flowUuid, token) => {\n  const url = new URL(`${BASE_URL}/oapi/power/v1/flow/detail`);\n  url.searchParams.append('flowUuid', flowUuid);\n\n  const response = await fetch(url.toString(), {\n    method: 'GET',\n    headers: {\n      'Content-Type': 'application/json; charset=utf-8',\n      Authorization: `Bearer ${token}`,\n    },\n  });\n\n  if (!response.ok) {\n    const errorData = await response.text();\n    console.error('Failed to fetch AI Power detail:', {\n      status: response.status,\n      data: errorData,\n    });\n    throw new Error(`HTTP error! status: ${response.status}`);\n  }\n\n  return response.json();\n};\n\n/**\n * @typedef {object} AIWorkflowListItem\n * @property {string} flowUuid - The UUID of the AI workflow.\n * @property {string} name - The name of the AI workflow.\n */\n\n/**\n * @typedef {object} PaginationInfo\n * @property {number} total - Total number of records.\n * @property {number} size - Number of records per page.\n * @property {number} pages - Total number of pages.\n */\n\n/**\n * @typedef {object} APIListResponse\n * @property {number} code - Business status code (200 for success).\n * @property {boolean} success - Indicates if the request was successful.\n * @property {string} msg - Failure message.\n * @property {AIWorkflowListItem[]} data - The list of AI workflows.\n * @property {PaginationInfo} page - Pagination information.\n * @property {string} requestId - The request ID.\n */\n\n/**\n * @typedef {object} GetAPFlowListParams\n * @property {number} page - The page number.\n * @property {number} size - The number of items per page.\n * @property {string} [name] - The name to search for (fuzzy search).\n */\n\n/**\n * Fetches a paginated list of AI Power workflows.\n * @param {GetAPFlowListParams} params - The pagination and search parameters.\n * @param {string} token - The authorization token.\n * @returns {Promise<APIListResponse>} The API response containing the list of workflows.\n */\nexport const getAPFlowList = async (params, token) => {\n  const { page, size, name } = params;\n  const url = new URL(`${BASE_URL}/oapi/power/v1/flow/page`);\n  url.searchParams.append('page', String(page));\n  url.searchParams.append('size', String(size));\n  if (name) {\n    url.searchParams.append('name', name);\n  }\n\n  const response = await fetch(url.toString(), {\n    method: 'GET',\n    headers: {\n      'Content-Type': 'application/json; charset=utf-8',\n      Authorization: `Bearer ${token}`,\n    },\n  });\n\n  if (!response.ok) {\n    const errorData = await response.text();\n    console.error('Failed to fetch AI Power flow list:', {\n      status: response.status,\n      data: errorData,\n    });\n    throw new Error(`HTTP error! status: ${response.status}`);\n  }\n\n  return response.json();\n};\n\n/**\n * @typedef {object} AIWorkflowStatus\n * @property {'pending'|'success'|'failed'} status - The execution status.\n * @property {object} result - The execution result as a JSON object.\n * @property {string} [failReason] - The reason for failure.\n */\n\n/**\n * @typedef {object} APIStatusResponse\n * @property {number} code - Business status code (200 for success).\n * @property {boolean} success - Indicates if the request was successful.\n * @property {string} msg - Failure message.\n * @property {AIWorkflowStatus} data - The status data.\n * @property {string} requestId - The request ID.\n */\n\n/**\n * Fetches the execution status of an AI Power workflow.\n * @param {number | string} runRecordId - The run record ID of the AI workflow.\n * @param {string} token - The authorization token.\n * @returns {Promise<APIStatusResponse>} The API response containing the execution status.\n */\nexport const getAPFlowStatus = async (runRecordId, token) => {\n  const url = new URL(`${BASE_URL}/oapi/power/v1/rest/flow/execute/result`);\n  url.searchParams.append('runRecordId', String(runRecordId));\n\n  const response = await fetch(url.toString(), {\n    method: 'GET',\n    headers: {\n      'Content-Type': 'application/json; charset=utf-8',\n      Authorization: `Bearer ${token}`,\n    },\n  });\n\n  if (!response.ok) {\n    const errorData = await response.text();\n    console.error('Failed to fetch AI Power flow status:', {\n      status: response.status,\n      data: errorData,\n    });\n    throw new Error(`HTTP error! status: ${response.status}`);\n  }\n\n  return response.json();\n};\n\n/**\n * @typedef {object} AIWorkflowExecuteResult\n * @property {number} runRecordId - The ID of the workflow run record.\n * @property {object} result - The output node data as a JSON object.\n */\n\n/**\n * @typedef {object} APIExecuteResponse\n * @property {number} code - Business status code (200 for success).\n * @property {boolean} success - Indicates if the request was successful.\n * @property {string} msg - Failure message.\n * @property {AIWorkflowExecuteResult} data - The execution result data.\n * @property {string} requestId - The request ID.\n */\n\n/**\n * @typedef {object} PostRunAPWorkflowParams\n * @property {string} flowUuid - The UUID of the AI workflow.\n * @property {object} input - The input parameters for the AI workflow.\n */\n\n/**\n * Executes an AI Power workflow synchronously.\n * @param {PostRunAPWorkflowParams} params - The parameters for executing the workflow.\n * @param {string} token - The authorization token.\n * @returns {Promise<APIExecuteResponse>} The API response containing the execution result.\n */\nexport const postRunAPWorkflow = async ({ flowUuid, input }, token) => {\n  const url = `${BASE_URL}/oapi/power/v1/rest/flow/${flowUuid}/execute`;\n\n  const response = await fetch(url, {\n    method: 'POST',\n    headers: {\n      'Content-Type': 'application/json; charset=utf-8',\n      Authorization: `Bearer ${token}`,\n    },\n    body: JSON.stringify({\n      input,\n      source: 'automa_extension',\n    }),\n  });\n\n  if (!response.ok) {\n    const errorData = await response.text();\n    console.error('Failed to execute AI Power workflow:', {\n      status: response.status,\n      data: errorData,\n    });\n    throw new Error(`HTTP error! status: ${response.status}`);\n  }\n\n  return response.json();\n};\n\n/**\n * @typedef {object} FileUploadResult\n * @property {string} fileReadUrl - The URL of the uploaded file.\n */\n\n/**\n * @typedef {object} APIUploadResponse\n * @property {number} code - Business status code (200 for success).\n * @property {boolean} success - Indicates if the request was successful.\n * @property {string} msg - Failure message.\n * @property {FileUploadResult} data - The result of the file upload.\n * @property {string} requestId - The request ID.\n */\n\n/**\n * Uploads a file to the AI Power server.\n * The request is sent as `multipart/form-data`.\n * @param {File} file - The file object to upload.\n * @param {string} token - The authorization token.\n * @returns {Promise<APIUploadResponse>} The API response containing the upload result.\n */\nexport const postUploadFile = async (file, token) => {\n  const url = `${BASE_URL}/oapi/power/v1/file/upload`;\n\n  const formData = new FormData();\n  formData.append('file', file);\n\n  const response = await fetch(url, {\n    method: 'POST',\n    headers: {\n      Authorization: `Bearer ${token}`,\n    },\n    body: formData,\n  });\n\n  if (!response.ok) {\n    const errorData = await response.text();\n    console.error('Failed to upload file:', {\n      status: response.status,\n      data: errorData,\n    });\n    throw new Error(`HTTP error! status: ${response.status}`);\n  }\n\n  return response.json();\n};\n"
  },
  {
    "path": "src/utils/getBlockMessage.js",
    "content": "import locale from '../locales/en/newtab.json';\n\nexport default function ({ message, ...data }) {\n  const normalize = (value) => value.join('');\n  const interpolate = (key) => data[key];\n  const named = (key) => key;\n\n  const localeMessage = locale.log.messages[message];\n  if (localeMessage) return localeMessage({ normalize, interpolate, named });\n\n  return message;\n}\n"
  },
  {
    "path": "src/utils/getFile.js",
    "content": "export function readFileAsBase64(blob) {\n  return new Promise((resolve) => {\n    const reader = new FileReader();\n    reader.onload = () => {\n      resolve(reader.result);\n    };\n    reader.readAsDataURL(blob);\n  });\n}\n\nasync function downloadFile(url, options) {\n  const response = await fetch(url);\n  if (!response.ok) throw new Error(response.statusText);\n\n  const type = options.responseType || 'blob';\n  const result = await response[type]();\n\n  if (options.returnValue) {\n    return result;\n  }\n\n  if (URL.createObjectURL) {\n    const objUrl = URL.createObjectURL(result);\n    return { objUrl, path: url, type: result.type };\n  }\n  const base64 = await readFileAsBase64(result);\n  return { path: url, objUrl: base64, type: result.type };\n}\nfunction getLocalFile(path, options) {\n  return new Promise((resolve, reject) => {\n    const isFile = /\\.(.*)/.test(path);\n\n    if (!isFile) {\n      reject(new Error(`\"${path}\" is invalid file path.`));\n      return;\n    }\n\n    const fileUrl = path?.startsWith('file://') ? path : `file://${path}`;\n\n    /* eslint-disable-next-line */\n    if ('XMLHttpRequest' in self) {\n      const xhr = new XMLHttpRequest();\n      xhr.responseType = options.responseType || 'blob';\n      xhr.onreadystatechange = () => {\n        if (xhr.readyState === XMLHttpRequest.DONE) {\n          if (xhr.status === 0 || xhr.status === 200) {\n            if (options.returnValue) {\n              resolve(xhr.response);\n              return;\n            }\n\n            const objUrl = URL.createObjectURL(xhr.response);\n            resolve({ path, objUrl, type: xhr.response.type });\n          } else {\n            reject(new Error(xhr.statusText));\n          }\n        }\n      };\n      xhr.onerror = function () {\n        reject(\n          new Error(xhr.statusText || `Can't find a file with \"${path}\" path`)\n        );\n      };\n      xhr.open('GET', fileUrl);\n      xhr.send();\n    } else {\n      fetch(fileUrl)\n        .then((response) => {\n          if (!response.ok) throw new Error(response.statusText);\n\n          if (options.returnValue) return response.text();\n\n          return response.blob();\n        })\n        .then((blob) => {\n          if (options.returnValue) {\n            resolve(blob);\n            return;\n          }\n          if (!blob) return;\n\n          if (URL.createObjectURL) {\n            const objUrl = URL.createObjectURL(blob);\n            resolve({ path, objUrl, type: blob.type });\n          } else {\n            const reader = new FileReader();\n            reader.onload = () => {\n              resolve({ path, objUrl: reader.result, type: blob.type });\n            };\n            reader.readAsDataURL(blob);\n          }\n        })\n        .catch(reject);\n    }\n  });\n}\n\nexport default function (path, options = {}) {\n  if (path.startsWith('http')) return downloadFile(path, options);\n\n  return getLocalFile(path, options);\n}\n"
  },
  {
    "path": "src/utils/getSharedData.js",
    "content": "import customBlocks from '@business/blocks';\nimport { tasks } from './shared';\n\nexport function getBlocks() {\n  return { ...tasks, ...customBlocks() };\n}\n"
  },
  {
    "path": "src/utils/getTranslateLog.js",
    "content": "import { getBlocks } from '@/utils/getSharedData';\nimport dayjs from '@/lib/dayjs';\nimport vueI18n from '@/lib/vueI18n';\nimport { countDuration } from '@/utils/helper';\nimport { messageHasReferences } from '@/utils/shared';\n\nconst blocks = getBlocks();\n\n/**\n * 转换日志\n * @param {*} log\n * @returns\n */\nfunction translateLog(log) {\n  const copyLog = { ...log };\n  const getTranslatation = (path, def) => {\n    const params = typeof path === 'string' ? { path } : path;\n\n    return vueI18n.global.te(params.path)\n      ? vueI18n.global.t(params.path, params.params)\n      : def;\n  };\n\n  if (['finish', 'stop'].includes(log.type)) {\n    copyLog.name = vueI18n.global.t(`log.types.${log.type}`);\n  } else {\n    copyLog.name = getTranslatation(\n      `workflow.blocks.${log.name}.name`,\n      blocks[log.name].name\n    );\n  }\n\n  if (copyLog.message && messageHasReferences.includes(copyLog.message)) {\n    copyLog.messageId = `${copyLog.message}`;\n  }\n\n  copyLog.message = getTranslatation(\n    { path: `log.messages.${log.message}`, params: log },\n    log.message\n  );\n\n  return copyLog;\n}\n\nfunction getDataSnapshot(propsCtxData, refData) {\n  if (!propsCtxData?.dataSnapshot) return;\n\n  const data = propsCtxData.dataSnapshot;\n  const getData = (key) => {\n    const currentData = refData[key];\n    if (typeof currentData !== 'string') return currentData;\n\n    return data[currentData] ?? {};\n  };\n\n  refData.loopData = getData('loopData');\n  refData.variables = getData('variables');\n}\n\n/**\n * 获取日志\n * @param {*} dataType 日志数据类型\n * @param {*} translatedLog 转换后的日志\n * @returns\n */\nfunction getLogs(dataType, translatedLog, curStateCtxData) {\n  let data = dataType === 'plain-text' ? '' : [];\n  const getItemData = {\n    'plain-text': ([\n      timestamp,\n      duration,\n      status,\n      name,\n      description,\n      message,\n      ctxData,\n    ]) => {\n      data += `${timestamp}(${countDuration(\n        0,\n        duration || 0\n      ).trim()}) - ${status} - ${name} - ${description} - ${message} - ${JSON.stringify(\n        ctxData\n      )} \\n`;\n    },\n    json: ([\n      timestamp,\n      duration,\n      status,\n      name,\n      description,\n      message,\n      ctxData,\n    ]) => {\n      data.push({\n        timestamp,\n        duration: countDuration(0, duration || 0).trim(),\n        status,\n        name,\n        description,\n        message,\n        data: ctxData,\n      });\n    },\n  };\n  translatedLog.forEach((item, index) => {\n    let logData = curStateCtxData;\n    if (logData.ctxData) logData = logData.ctxData;\n\n    const itemData = logData[item.id] || null;\n    if (itemData) getDataSnapshot(curStateCtxData, itemData.referenceData);\n\n    getItemData[dataType](\n      [\n        dayjs(item.timestamp || Date.now()).format('YYYY-MM-DD HH:mm:ss'),\n        item.duration,\n        item.type.toUpperCase(),\n        item.name,\n        item.description || 'NULL',\n        item.message || 'NULL',\n        itemData,\n      ],\n      index\n    );\n  });\n  return data;\n}\n\n/**\n * 获取日志数据\n * @param {*} curState 当前工作流状态\n * @param {*} dataType 日志数据类型 plain-text 和 json\n * @returns\n */\nexport default function (curState, dataType = 'plain-text') {\n  const { logs: curStateHistory, ctxData: curStateCtxData } = curState;\n  // 经过转换后的日志\n  const translatedLog = curStateHistory.map(translateLog);\n  // 获取日志\n  const logs = getLogs(dataType, translatedLog, curStateCtxData);\n  // 获取日志\n  return logs;\n}\n"
  },
  {
    "path": "src/utils/googleSheetsApi.js",
    "content": "import { fetchGapi, fetchApi } from './api';\n\nfunction queryBuilder(obj) {\n  let str = '';\n\n  Object.entries(obj).forEach(([key, value], index) => {\n    if (index !== 0) str += `&`;\n\n    str += `${key}=${value}`;\n  });\n\n  return str;\n}\n\nexport const googleSheetNative = {\n  getUrl(path) {\n    return `https://sheets.googleapis.com/v4/spreadsheets${path}`;\n  },\n  getValues({ spreadsheetId, range }) {\n    const url = googleSheetNative.getUrl(`/${spreadsheetId}/values/${range}`);\n\n    return fetchGapi(url);\n  },\n  getRange({ spreadsheetId, range }) {\n    const url = googleSheetNative.getUrl(\n      `/${spreadsheetId}/values/${range}:append?valueInputOption=RAW&includeValuesInResponse=false&insertDataOption=INSERT_ROWS`\n    );\n\n    return fetchGapi(url, {\n      method: 'POST',\n    });\n  },\n  clearValues({ spreadsheetId, range }) {\n    const url = googleSheetNative.getUrl(\n      `/${spreadsheetId}/values/${range}:clear`\n    );\n\n    return fetchGapi(url, { method: 'POST' });\n  },\n  updateValues({ spreadsheetId, range, options, append }) {\n    let url = '';\n    let method = '';\n\n    if (append) {\n      url = googleSheetNative.getUrl(\n        `/${spreadsheetId}/values/${range}:append`\n      );\n      method = 'POST';\n    } else {\n      url = googleSheetNative.getUrl(`/${spreadsheetId}/values/${range}`);\n      method = 'PUT';\n    }\n\n    const payload = { method };\n    if (options.body) payload.body = options.body;\n\n    return fetchGapi(`${url}?${queryBuilder(options?.queries || {})}`, payload);\n  },\n  create(name) {\n    const url = googleSheetNative.getUrl('');\n\n    return fetchGapi(url, {\n      method: 'POST',\n      body: JSON.stringify({\n        properties: {\n          title: name,\n        },\n      }),\n    });\n  },\n  addSheet({ sheetName, spreadsheetId }) {\n    const url = googleSheetNative.getUrl(`/${spreadsheetId}:batchUpdate`);\n    return fetchGapi(url, {\n      method: 'POST',\n      body: JSON.stringify({\n        requests: [\n          {\n            addSheet: {\n              properties: { title: sheetName },\n            },\n          },\n        ],\n      }),\n    });\n  },\n};\n\nexport const googleSheets = {\n  getUrl(spreadsheetId, range) {\n    return `/services/google-sheets?spreadsheetId=${spreadsheetId}&range=${range}`;\n  },\n  getValues({ spreadsheetId, range }) {\n    const url = this.getUrl(spreadsheetId, range);\n\n    return fetchApi(url);\n  },\n  getRange({ spreadsheetId, range }) {\n    return googleSheets.updateValues({\n      range,\n      append: true,\n      spreadsheetId,\n      options: {\n        body: JSON.stringify({ values: [] }),\n        queries: {\n          valueInputOption: 'RAW',\n          includeValuesInResponse: false,\n          insertDataOption: 'INSERT_ROWS',\n        },\n      },\n    });\n  },\n  clearValues({ spreadsheetId, range }) {\n    return fetchApi(this.getUrl(spreadsheetId, range), {\n      method: 'DELETE',\n    });\n  },\n  updateValues({ spreadsheetId, range, options = {}, append }) {\n    const url = `${this.getUrl(spreadsheetId, range)}&${queryBuilder(\n      options?.queries || {}\n    )}`;\n\n    return fetchApi(url, {\n      ...options,\n      method: append ? 'POST' : 'PUT',\n    });\n  },\n};\n\nexport default function (isDriveSheet = false) {\n  return isDriveSheet ? googleSheetNative : googleSheets;\n}\n"
  },
  {
    "path": "src/utils/handleFormElement.js",
    "content": "import { sleep } from '@/utils/helper';\nimport { keyDefinitions } from '@/utils/USKeyboardLayout';\nimport simulateEvent from './simulateEvent';\n\nconst nativeInputValueSetter = Object.getOwnPropertyDescriptor(\n  window.HTMLInputElement.prototype,\n  'value'\n).set;\nfunction reactJsEvent(element, value) {\n  if (!element._valueTracker) return;\n\n  const previousValue = element.value;\n  nativeInputValueSetter.call(element, value);\n  element._valueTracker.setValue(previousValue);\n}\n\nfunction formEvent(element, data) {\n  if (data.type === 'text-field') {\n    const currentKey = /\\s/.test(data.value) ? 'Space' : data.value;\n    const { key, keyCode, code } = keyDefinitions[currentKey] || {\n      key: currentKey,\n      keyCode: 0,\n      code: `Key${currentKey}`,\n    };\n\n    simulateEvent(element, 'input', {\n      inputType: 'insertText',\n      data: data.value,\n      bubbles: true,\n      cancelable: true,\n    });\n\n    simulateEvent(element, 'keydown', {\n      key,\n      code,\n      keyCode,\n      bubbles: true,\n      cancelable: true,\n    });\n    simulateEvent(element, 'keyup', {\n      key,\n      code,\n      keyCode,\n      bubbles: true,\n      cancelable: true,\n    });\n  }\n\n  simulateEvent(element, 'input', {\n    inputType: 'insertText',\n    data: data.value,\n    bubbles: true,\n    cancelable: true,\n  });\n\n  if (data.type !== 'text-field') {\n    element.dispatchEvent(\n      new Event('change', { bubbles: true, cancelable: true })\n    );\n  }\n}\nasync function inputText({ data, element, isEditable }) {\n  element?.focus();\n  element?.click();\n  const elementKey = isEditable ? 'textContent' : 'value';\n\n  if (data.delay > 0 && !document.hidden) {\n    for (let index = 0; index < data.value.length; index += 1) {\n      if (elementKey === 'value') reactJsEvent(element, element.value);\n\n      const currentChar = data.value[index];\n      element[elementKey] += currentChar;\n\n      formEvent(element, {\n        type: 'text-field',\n        value: currentChar,\n        isEditable,\n      });\n\n      await sleep(data.delay);\n    }\n  } else {\n    if (elementKey === 'value') reactJsEvent(element, element.value);\n\n    element[elementKey] += data.value;\n\n    formEvent(element, {\n      isEditable,\n      type: 'text-field',\n      value: data.value[0] ?? '',\n    });\n  }\n\n  element.dispatchEvent(\n    new Event('change', { bubbles: true, cancelable: true })\n  );\n\n  element?.blur();\n}\n\nexport default async function (element, data) {\n  const textFields = ['INPUT', 'TEXTAREA'];\n  const isEditable =\n    element.hasAttribute('contenteditable') && element.isContentEditable;\n\n  if (isEditable) {\n    if (data.clearValue) element.innerText = '';\n\n    await inputText({ data, element, isEditable });\n    return;\n  }\n\n  if (data.type === 'text-field' && textFields.includes(element.tagName)) {\n    if (data.clearValue) {\n      element?.select();\n      reactJsEvent(element, '');\n      element.value = '';\n    }\n\n    await inputText({ data, element });\n    return;\n  }\n\n  element?.focus();\n\n  if (data.type === 'checkbox' || data.type === 'radio') {\n    element.checked = data.selected;\n    formEvent(element, { type: data.type, value: data.selected });\n  } else if (data.type === 'select') {\n    let optionValue = data.value;\n\n    const options = element.querySelectorAll('option');\n    const getOptionValue = (index) => {\n      if (!options) return element.value;\n\n      let optionIndex = index;\n      const maxIndex = options.length - 1;\n\n      if (index < 0) optionIndex = 0;\n      else if (index > maxIndex) optionIndex = maxIndex;\n\n      return options[optionIndex]?.value || element.value;\n    };\n\n    switch (data.selectOptionBy) {\n      case 'first-option':\n        optionValue = getOptionValue(0);\n        break;\n      case 'last-option':\n        optionValue = getOptionValue(options.length - 1);\n        break;\n      case 'custom-position':\n        optionValue = getOptionValue(+data.optionPosition - 1 ?? 0);\n        break;\n      default:\n    }\n\n    if (optionValue) {\n      element.value = optionValue;\n      formEvent(element, data);\n    }\n  }\n\n  element?.blur();\n}\n"
  },
  {
    "path": "src/utils/helper.js",
    "content": "import BrowserAPIService from '@/service/browser-api/BrowserAPIService';\nimport browser from 'webextension-polyfill';\n\nexport async function getActiveTab() {\n  try {\n    const tabsQuery = {\n      active: true,\n      url: ['*://*/*', 'file://*'],\n    };\n\n    const window = await browser.windows.getLastFocused({\n      populate: true,\n      windowTypes: ['normal'],\n    });\n    const windowId = window.id;\n\n    if (windowId) tabsQuery.windowId = windowId;\n    else tabsQuery.lastFocusedWindow = true;\n\n    const [tab] = await browser.tabs.query(tabsQuery);\n\n    return tab;\n  } catch (error) {\n    console.error(error);\n    return null;\n  }\n}\n\nexport function isXPath(str) {\n  const regex = /^([(/@]|id\\()/;\n\n  return regex.test(str);\n}\n\nexport function visibleInViewport(element) {\n  const { top, left, bottom, right, height, width } =\n    element.getBoundingClientRect();\n\n  if (height === 0 || width === 0) return false;\n\n  return (\n    top >= 0 &&\n    left >= 0 &&\n    bottom <= (window.innerHeight || document.documentElement.clientHeight) &&\n    right <= (window.innerWidth || document.documentElement.clientWidth)\n  );\n}\n\nexport function sleep(timeout = 500) {\n  return new Promise((resolve) => {\n    setTimeout(() => {\n      resolve();\n    }, timeout);\n  });\n}\n\nexport function findTriggerBlock(drawflow = {}) {\n  if (!drawflow) return null;\n\n  if (drawflow.drawflow) {\n    const blocks = Object.values(drawflow.drawflow?.Home?.data ?? {});\n    if (!blocks) return null;\n\n    return blocks.find(({ name }) => name === 'trigger');\n  }\n  if (drawflow.nodes) {\n    return drawflow.nodes.find((node) => node.label === 'trigger');\n  }\n\n  return null;\n}\n\nexport function throttle(callback, limit) {\n  let waiting = false;\n\n  return (...args) => {\n    if (!waiting) {\n      callback.apply(this, args);\n      waiting = true;\n      setTimeout(() => {\n        waiting = false;\n      }, limit);\n    }\n  };\n}\n\nexport function convertArrObjTo2DArr(arr) {\n  const keyIndex = new Map();\n  const values = [[]];\n\n  arr.forEach((obj) => {\n    const keys = Object.keys(obj);\n    const row = [];\n\n    keys.forEach((key) => {\n      if (!keyIndex.has(key)) {\n        keyIndex.set(key, keyIndex.size);\n        values[0].push(key);\n      }\n\n      const value = obj[key];\n\n      const rowIndex = keyIndex.get(key);\n      row[rowIndex] = typeof value === 'object' ? JSON.stringify(value) : value;\n    });\n\n    values.push([...row]);\n  });\n\n  return values;\n}\n\nexport function convert2DArrayToArrayObj(values) {\n  let keyIndex = 0;\n  const keys = values.shift();\n  const result = [];\n\n  for (let columnIndex = 0; columnIndex < values.length; columnIndex += 1) {\n    const currentColumn = {};\n\n    for (\n      let rowIndex = 0;\n      rowIndex < values[columnIndex].length;\n      rowIndex += 1\n    ) {\n      let key = keys[rowIndex];\n\n      if (!key) {\n        keyIndex += 1;\n        key = `_row${keyIndex}`;\n        keys.push(key);\n      }\n\n      currentColumn[key] = values[columnIndex][rowIndex];\n    }\n\n    result.push(currentColumn);\n  }\n\n  return result;\n}\n\nexport function parseJSON(data, def) {\n  try {\n    const result = JSON.parse(data);\n\n    return result;\n  } catch (error) {\n    return def;\n  }\n}\n\nexport function parseFlow(flow) {\n  const obj = typeof flow === 'string' ? parseJSON(flow, {}) : flow;\n\n  return obj;\n}\n\nexport function replaceMustache(str, replacer) {\n  /* eslint-disable-next-line */\n  return str.replace(/\\{\\{(.*?)\\}\\}/g, replacer);\n}\n\nexport function openFilePicker(acceptedFileTypes = [], attrs = {}) {\n  return new Promise((resolve) => {\n    const input = document.createElement('input');\n    input.type = 'file';\n    input.accept = Array.isArray(acceptedFileTypes)\n      ? acceptedFileTypes.join(',')\n      : acceptedFileTypes;\n\n    Object.entries(attrs).forEach(([key, value]) => {\n      input[key] = value;\n    });\n\n    input.onchange = (event) => {\n      const { files } = event.target;\n      const validFiles = [];\n\n      Array.from(files).forEach((file) => {\n        if (!acceptedFileTypes.includes(file.type)) return;\n\n        validFiles.push(file);\n      });\n\n      resolve(validFiles);\n    };\n\n    input.click();\n  });\n}\n\nexport function fileSaver(filename, data) {\n  const anchor = document.createElement('a');\n  anchor.download = filename;\n  anchor.href = data;\n\n  anchor.dispatchEvent(new MouseEvent('click'));\n  anchor.remove();\n}\n\nexport function countDuration(started, ended) {\n  const duration = Math.round((ended - started) / 1000);\n  const minutes = Math.floor(duration / 60);\n  const seconds = Math.floor(duration % 60);\n\n  const getText = (num, suffix) => (num > 0 ? `${num}${suffix}` : '');\n\n  return `${getText(minutes, 'm')} ${seconds}s`;\n}\n\nexport function toCamelCase(str, capitalize = false) {\n  const result = str.replace(/(?:^\\w|[A-Z]|\\b\\w)/g, (letter, index) => {\n    return index === 0 && !capitalize\n      ? letter.toLowerCase()\n      : letter.toUpperCase();\n  });\n\n  return result.replace(/\\s+|[-]/g, '');\n}\n\nexport function isObject(obj) {\n  return typeof obj === 'object' && obj !== null && !Array.isArray(obj);\n}\n\nexport function objectHasKey(obj, key) {\n  return Object.prototype.hasOwnProperty.call(obj, key);\n}\n\nexport function isWhitespace(str) {\n  return !/\\S/.test(str);\n}\n\nexport function debounce(callback, time = 200) {\n  let interval;\n\n  return (...args) => {\n    clearTimeout(interval);\n\n    return new Promise((resolve) => {\n      interval = setTimeout(() => {\n        interval = null;\n\n        callback(...args);\n        resolve();\n      }, time);\n    });\n  };\n}\n\nexport async function clearCache(workflow) {\n  try {\n    await BrowserAPIService.storage.local.remove(`state:${workflow.id}`);\n\n    const flows = parseJSON(workflow.drawflow, null);\n    const blocks = flows && flows.drawflow.Home.data;\n\n    if (blocks) {\n      Object.values(blocks).forEach(({ name, id }) => {\n        if (name !== 'loop-data') return;\n\n        localStorage.removeItem(`index:${id}`);\n      });\n    }\n\n    return true;\n  } catch (error) {\n    console.error(error);\n    return false;\n  }\n}\n\nexport function arraySorter({ data, key, order = 'asc' }) {\n  let runCounts = {};\n  const copyData = data.slice();\n\n  if (key === 'mostUsed') {\n    runCounts = parseJSON(localStorage.getItem('runCounts'), {}) || {};\n  }\n\n  return copyData.sort((a, b) => {\n    let comparison = 0;\n    let itemA = a[key] || a;\n    let itemB = b[key] || b;\n\n    if (key === 'mostUsed') {\n      itemA = runCounts[a.id] || 0;\n      itemB = runCounts[b.id] || 0;\n    }\n\n    if (itemA > itemB) {\n      comparison = 1;\n    } else if (itemA < itemB) {\n      comparison = -1;\n    }\n\n    return order === 'desc' ? comparison * -1 : comparison;\n  });\n}\n"
  },
  {
    "path": "src/utils/message.js",
    "content": "import browser from 'webextension-polyfill';\n\nconst nameBuilder = (prefix, name) => (prefix ? `${prefix}--${name}` : name);\nconst isFirefox = BROWSER_TYPE === 'firefox';\n\n/**\n *\n * @param {string=} name\n * @param {*=} data\n * @param {string=} prefix\n *\n * @returns {Promise<*>}\n */\nexport function sendMessage(name = '', data = {}, prefix = '') {\n  let payload = {\n    name: nameBuilder(prefix, name),\n    data,\n  };\n\n  if (isFirefox) {\n    payload = JSON.stringify(payload);\n  }\n\n  return browser.runtime.sendMessage(payload);\n}\n\nexport class MessageListener {\n  static sendMessage = sendMessage;\n\n  constructor(prefix = '') {\n    this.listeners = {};\n    this.prefix = prefix;\n\n    this.listener = this.listener.bind(this);\n  }\n\n  on(name, listener) {\n    if (Object.hasOwn(this.listeners, name)) {\n      console.error(`You already added ${name}`);\n      return this.on;\n    }\n\n    this.listeners[nameBuilder(this.prefix, name)] = listener;\n\n    return this.on;\n  }\n\n  listener(message, sender) {\n    try {\n      if (isFirefox) message = JSON.parse(message);\n\n      const listener = this.listeners[message.name];\n      const response =\n        listener && listener.call({ message, sender }, message.data, sender);\n\n      const _prefix = message.name.split('--')[0];\n      // 如果消息有明确的前缀\n      if (_prefix && _prefix !== message.name) {\n        // 只有当前缀匹配时才处理\n        if (_prefix === this.prefix) {\n          if (!response) return Promise.resolve();\n          if (!(response instanceof Promise)) return Promise.resolve(response);\n          return response;\n        }\n        // 对于不匹配的前缀消息，不返回任何响应\n        // eslint-disable-next-line consistent-return\n        return;\n      }\n\n      // 对于没有前缀的消息，保持原有行为\n      if (!response) return Promise.resolve();\n      if (!(response instanceof Promise)) return Promise.resolve(response);\n      return response;\n    } catch (err) {\n      return Promise.reject(\n        new Error(`Unhandled Background Error: ${String(err)}`)\n      );\n    }\n  }\n\n  /**\n   *\n   * @param {string} name\n   * @param {*} data\n   *\n   * @returns {Promise<*>}\n   */\n  sendMessage(name, data) {\n    return sendMessage(name, data, this.prefix);\n  }\n}\n"
  },
  {
    "path": "src/utils/openGDriveFilePicker.js",
    "content": "import browser from 'webextension-polyfill';\n\n/**\n *\n * get all google sheets files in current user's google drive\n * @returns {Promise<Array>} file list [{ id, name, mimeType }]\n */\nexport default async function fetchGDriveSheets() {\n  const { sessionToken, session } = await browser.storage.local.get([\n    'sessionToken',\n    'session',\n  ]);\n  if (!sessionToken || !sessionToken.access) return [];\n\n  const isGoogleProvider =\n    session?.user?.user_metadata?.iss.includes('google.com');\n  if (!isGoogleProvider) return [];\n\n  const accessToken = sessionToken.access;\n  const endpoint =\n    'https://www.googleapis.com/drive/v3/files?fields=files(id%2Cname%2CmimeType)&q=mimeType%3D%27application%2Fvnd.google-apps.spreadsheet%27&spaces=drive&pageSize=1000';\n\n  try {\n    const res = await fetch(endpoint, {\n      headers: {\n        Authorization: `Bearer ${accessToken}`,\n      },\n    });\n    if (!res.ok) throw new Error('Failed to fetch Google Sheets list');\n    const data = await res.json();\n    return data.files || [];\n  } catch (e) {\n    // handle token expired or other exceptions\n    return [];\n  }\n}\n\n/**\n * open google picker popup to get user authorized file\n * @param {string} accessToken\n * @returns {Promise<{id, name, mimeType}>}\n */\nexport function openGDrivePickerPopup(accessToken) {\n  return new Promise((resolve, reject) => {\n    const pickerUrl = `https://extension.automa.site/picker?access_token=${accessToken}`;\n    const popup = window.open(pickerUrl, '_blank', 'width=600,height=600');\n    function handleMessage(event) {\n      if (!event.origin.startsWith('https://extension.automa.site')) return;\n      if (event.data && event.data.type === 'GDRIVE_PICKER_RESULT') {\n        window.removeEventListener('message', handleMessage);\n        popup.close();\n        resolve(event.data.file);\n      }\n    }\n    window.addEventListener('message', handleMessage);\n    const timer = setInterval(() => {\n      if (popup.closed) {\n        clearInterval(timer);\n        window.removeEventListener('message', handleMessage);\n        reject(new Error('Picker window closed'));\n      }\n    }, 500);\n  });\n}\n"
  },
  {
    "path": "src/utils/recordKeys.js",
    "content": "import { toCamelCase } from './helper';\n\nconst modifierKeys = ['Control', 'Alt', 'Shift', 'Meta'];\nexport function recordPressedKey(\n  { repeat, shiftKey, metaKey, altKey, ctrlKey, key },\n  callback\n) {\n  if (repeat || modifierKeys.includes(key)) return;\n\n  let pressedKey = key.length > 1 || shiftKey ? toCamelCase(key, true) : key;\n\n  if (pressedKey === ' ') pressedKey = 'Space';\n  else if (pressedKey === '+') pressedKey = 'NumpadAdd';\n\n  const keys = [pressedKey];\n\n  if (shiftKey) keys.unshift('Shift');\n  if (metaKey) keys.unshift('Meta');\n  if (altKey) keys.unshift('Alt');\n  if (ctrlKey) keys.unshift('Control');\n\n  if (callback) callback(keys);\n}\n\nconst allowedKeys = {\n  '+': 'plus',\n  Delete: 'del',\n  Insert: 'ins',\n  ArrowDown: 'down',\n  ArrowLeft: 'left',\n  ArrowUp: 'up',\n  ArrowRight: 'right',\n  Escape: 'escape',\n  Enter: 'enter',\n};\nexport function recordShortcut(\n  { ctrlKey, altKey, metaKey, shiftKey, key, repeat },\n  callback\n) {\n  if (repeat) return;\n\n  const keys = [];\n\n  if (ctrlKey || metaKey) keys.push('mod');\n  if (altKey) keys.push('option');\n  if (shiftKey) keys.push('shift');\n\n  const isValidKey = !!allowedKeys[key] || /^[a-z0-9,./;'[\\]\\-=`]$/i.test(key);\n\n  if (isValidKey) {\n    keys.push(allowedKeys[key] || key.toLowerCase());\n\n    callback(keys);\n  }\n}\n"
  },
  {
    "path": "src/utils/serialization.js",
    "content": "export function serializeFunctions(obj) {\n  if (typeof obj === 'function') {\n    return {\n      __type: 'function',\n      __value: obj.toString(),\n    };\n  }\n\n  if (Array.isArray(obj)) {\n    return obj.map((item) => serializeFunctions(item));\n  }\n\n  if (obj && typeof obj === 'object') {\n    const result = {};\n    for (const key in obj) {\n      if (Object.prototype.hasOwnProperty.call(obj, key)) {\n        result[key] = serializeFunctions(obj[key]);\n      }\n    }\n    return result;\n  }\n\n  return obj;\n}\n\nexport function deserializeFunctions(obj) {\n  if (obj && typeof obj === 'object') {\n    if (obj.__type === 'function') {\n      // eslint-disable-next-line no-new-func, prefer-template\n      return new Function('return ' + obj.__value)();\n    }\n\n    if (Array.isArray(obj)) {\n      return obj.map((item) => deserializeFunctions(item));\n    }\n\n    const result = {};\n    for (const key in obj) {\n      if (Object.prototype.hasOwnProperty.call(obj, key)) {\n        result[key] = deserializeFunctions(obj[key]);\n      }\n    }\n    return result;\n  }\n\n  return obj;\n}\n"
  },
  {
    "path": "src/utils/shared.js",
    "content": "export const tasks = {\n  trigger: {\n    name: 'Trigger',\n    description: 'Block where workflow will start executing',\n    icon: 'riFlashlightLine',\n    component: 'BlockBasic',\n    editComponent: 'EditTrigger',\n    category: 'general',\n    inputs: 0,\n    outputs: 1,\n    allowedInputs: true,\n    maxConnection: 1,\n    refDataKeys: ['url'],\n    data: {\n      disableBlock: false,\n      description: '',\n      type: 'manual',\n      interval: 60,\n      delay: 5,\n      date: '',\n      time: '00:00',\n      url: '',\n      shortcut: '',\n      activeInInput: false,\n      isUrlRegex: false,\n      days: [],\n      contextMenuName: '',\n      contextTypes: [],\n      parameters: [],\n      preferParamsInTab: false,\n      observeElement: {\n        selector: '',\n        baseSelector: '',\n        matchPattern: '',\n        targetOptions: {\n          subtree: false,\n          childList: true,\n          attributes: false,\n          attributeFilter: [],\n          characterData: false,\n        },\n        baseElOptions: {\n          subtree: false,\n          childList: true,\n          attributes: false,\n          attributeFilter: [],\n          characterData: false,\n        },\n      },\n    },\n  },\n  'ai-workflow': {\n    name: 'AI Workflow',\n    description: 'A workflow that is created by AI-Power',\n    icon: 'https://winrobot-pub-a-1302949341.cos.ap-shanghai.myqcloud.com/image/20250717194249/10e0c06a7b243d15ac9a9385b07ce4e2.svg',\n    tag: 'AI',\n    component: 'BlockBasic',\n    editComponent: 'EditAiWorkflow',\n    category: 'general',\n    inputs: 1,\n    outputs: 1,\n    allowedInputs: true,\n    maxConnection: 1,\n    data: {\n      disableBlock: false,\n      flowUuid: '',\n      flowLabel: '',\n      description: '',\n      inputs: [],\n      outputs: [],\n      assignVariable: false,\n      variableName: '',\n\n      saveData: false,\n      dataColumn: '',\n    },\n  },\n  'execute-workflow': {\n    name: 'Execute workflow',\n    description: '',\n    icon: 'riFlowChart',\n    component: 'BlockBasic',\n    category: 'general',\n    editComponent: 'EditExecuteWorkflow',\n    inputs: 1,\n    outputs: 1,\n    allowedInputs: true,\n    maxConnection: 1,\n    refDataKeys: ['globalData'],\n    data: {\n      disableBlock: false,\n      executeId: '',\n      workflowId: '',\n      globalData: '',\n      description: '',\n      insertAllVars: false,\n      insertAllGlobalData: false,\n    },\n  },\n  'active-tab': {\n    name: 'Active tab',\n    description: \"Set current tab that you're in as an active tab\",\n    icon: 'riWindowLine',\n    component: 'BlockBasic',\n    category: 'browser',\n    disableEdit: true,\n    inputs: 1,\n    outputs: 1,\n    allowedInputs: true,\n    maxConnection: 1,\n    data: {\n      disableBlock: false,\n    },\n  },\n  'new-tab': {\n    name: 'New tab',\n    description: 'Create a new tab',\n    icon: 'riGlobalLine',\n    component: 'BlockBasic',\n    editComponent: 'EditNewTab',\n    category: 'browser',\n    inputs: 1,\n    outputs: 1,\n    allowedInputs: true,\n    maxConnection: 1,\n    refDataKeys: ['url', 'userAgent'],\n    data: {\n      disableBlock: false,\n      description: '',\n      url: '',\n      userAgent: '',\n      active: true,\n      tabZoom: 1,\n      inGroup: false,\n      waitTabLoaded: false,\n      updatePrevTab: false,\n      customUserAgent: false,\n    },\n  },\n  'switch-tab': {\n    name: 'Switch tab',\n    description: 'Switch active tab',\n    icon: 'riArrowLeftRightLine',\n    component: 'BlockBasic',\n    editComponent: 'EditSwitchTab',\n    category: 'browser',\n    inputs: 1,\n    outputs: 1,\n    allowedInputs: true,\n    maxConnection: 1,\n    refDataKeys: ['url', 'matchPattern', 'tabTitle'],\n    data: {\n      disableBlock: false,\n      description: '',\n      url: '',\n      tabIndex: 0,\n      tabTitle: '',\n      matchPattern: '',\n      activeTab: true,\n      createIfNoMatch: false,\n      findTabBy: 'match-patterns',\n    },\n  },\n  'new-window': {\n    name: 'New window',\n    description: 'Create a new window',\n    icon: 'riWindow2Line',\n    component: 'BlockBasic',\n    editComponent: 'EditNewWindow',\n    category: 'browser',\n    inputs: 1,\n    outputs: 1,\n    allowedInputs: true,\n    maxConnection: 1,\n    refDataKeys: ['url'],\n    data: {\n      disableBlock: false,\n      description: '',\n      top: 0,\n      left: 0,\n      width: 0,\n      url: '',\n      height: 0,\n      type: 'normal',\n      incognito: false,\n      windowState: 'normal',\n    },\n  },\n  proxy: {\n    name: 'Proxy',\n    description: 'Set the proxy of the browser',\n    icon: 'riShieldKeyholeLine',\n    component: 'BlockBasic',\n    category: 'browser',\n    editComponent: 'EditProxy',\n    inputs: 1,\n    outputs: 1,\n    maxConnection: 1,\n    allowedInputs: true,\n    refDataKeys: ['host', 'port', 'scheme'],\n    data: {\n      description: '',\n      disableBlock: false,\n      scheme: 'https',\n      host: '',\n      port: 443,\n      bypassList: '',\n      clearProxy: false,\n    },\n  },\n  'go-back': {\n    name: 'Go back',\n    description: 'Go back to the previous page',\n    icon: 'riArrowGoBackLine',\n    component: 'BlockBasic',\n    category: 'browser',\n    inputs: 1,\n    outputs: 1,\n    maxConnection: 1,\n    disableEdit: true,\n    allowedInputs: true,\n    data: {\n      disableBlock: false,\n    },\n  },\n  'forward-page': {\n    name: 'Go forward',\n    description: 'Go forward to the next page',\n    icon: 'riArrowGoForwardLine',\n    component: 'BlockBasic',\n    category: 'browser',\n    inputs: 1,\n    outputs: 1,\n    maxConnection: 1,\n    disableEdit: true,\n    allowedInputs: true,\n    data: {\n      disableBlock: false,\n    },\n  },\n  'close-tab': {\n    name: 'Close tab/window',\n    icon: 'riCloseCircleLine',\n    component: 'BlockBasic',\n    category: 'browser',\n    editComponent: 'EditCloseTab',\n    inputs: 1,\n    outputs: 1,\n    maxConnection: 1,\n    allowedInputs: true,\n    refDataKeys: ['url'],\n    data: {\n      disableBlock: false,\n      url: '',\n      description: '',\n      activeTab: true,\n      closeType: 'tab',\n      allWindows: false,\n    },\n  },\n  'take-screenshot': {\n    name: 'Take screenshot',\n    description: 'Take a screenshot of current active tab',\n    icon: 'riImageLine',\n    component: 'BlockBasic',\n    category: 'browser',\n    editComponent: 'EditTakeScreenshot',\n    inputs: 1,\n    outputs: 1,\n    maxConnection: 1,\n    allowedInputs: true,\n    refDataKeys: ['fileName', 'selector', 'variableName'],\n    autocomplete: ['variableName'],\n    data: {\n      description: '',\n      disableBlock: false,\n      fileName: '',\n      ext: 'png',\n      quality: 100,\n      dataColumn: '',\n      variableName: '',\n      selector: '',\n      fullPage: false,\n      saveToColumn: false,\n      saveToComputer: true,\n      assignVariable: false,\n      captureActiveTab: true,\n    },\n  },\n  'browser-event': {\n    name: 'Browser event',\n    description: 'Wait until the selected event is triggered',\n    icon: 'riLightbulbLine',\n    component: 'BlockBasic',\n    category: 'browser',\n    editComponent: 'EditBrowserEvent',\n    inputs: 1,\n    outputs: 1,\n    maxConnection: 1,\n    allowedInputs: true,\n    data: {\n      disableBlock: false,\n      description: '',\n      timeout: 10000,\n      eventName: 'tab:loaded',\n      setAsActiveTab: true,\n      activeTabLoaded: true,\n      tabLoadedUrl: '',\n      tabUrl: '',\n      fileQuery: '',\n    },\n  },\n  'event-click': {\n    name: 'Click element',\n    icon: 'riCursorLine',\n    component: 'BlockBasic',\n    editComponent: 'EditInteractionBase',\n    category: 'interaction',\n    inputs: 1,\n    outputs: 1,\n    allowedInputs: true,\n    maxConnection: 1,\n    refDataKeys: ['selector'],\n    data: {\n      disableBlock: false,\n      description: '',\n      findBy: 'cssSelector',\n      waitForSelector: false,\n      waitSelectorTimeout: 5000,\n      selector: '',\n      markEl: false,\n      multiple: false,\n    },\n  },\n  delay: {\n    name: 'Delay',\n    description: 'Add delay before executing the next block',\n    icon: 'riTimerLine',\n    component: 'BlockDelay',\n    editComponent: 'EditDelay',\n    category: 'general',\n    inputs: 1,\n    outputs: 1,\n    allowedInputs: true,\n    maxConnection: 1,\n    refDataKeys: ['time'],\n    data: {\n      disableBlock: false,\n      time: 500,\n    },\n  },\n  'get-text': {\n    name: 'Get text',\n    description: 'Get text from an element',\n    icon: 'riParagraph',\n    component: 'BlockBasic',\n    editComponent: 'EditGetText',\n    category: 'interaction',\n    inputs: 1,\n    outputs: 1,\n    allowedInputs: true,\n    maxConnection: 1,\n    refDataKeys: [\n      'selector',\n      'variableName',\n      'prefixText',\n      'suffixText',\n      'extraRowValue',\n    ],\n    autocomplete: ['variableName'],\n    data: {\n      disableBlock: false,\n      description: '',\n      findBy: 'cssSelector',\n      waitForSelector: false,\n      waitSelectorTimeout: 5000,\n      selector: '',\n      markEl: false,\n      multiple: false,\n      regex: '',\n      prefixText: '',\n      suffixText: '',\n      regexExp: [],\n      dataColumn: '',\n      saveData: true,\n      includeTags: false,\n      addExtraRow: false,\n      assignVariable: false,\n      useTextContent: false,\n      variableName: '',\n      extraRowValue: '',\n      extraRowDataColumn: '',\n    },\n  },\n  'export-data': {\n    name: 'Export data',\n    icon: 'riDownloadLine',\n    component: 'BlockBasic',\n    editComponent: 'EditExportData',\n    category: 'general',\n    inputs: 1,\n    outputs: 1,\n    allowedInputs: true,\n    maxConnection: 1,\n    refDataKeys: ['name', 'variableName'],\n    data: {\n      disableBlock: false,\n      name: '',\n      refKey: '',\n      type: 'json',\n      description: '',\n      variableName: '',\n      csvDelimiter: ',',\n      addBOMHeader: true,\n      onConflict: 'uniquify',\n      dataToExport: 'data-columns',\n    },\n  },\n  'element-scroll': {\n    name: 'Scroll element',\n    icon: 'riMouseLine',\n    component: 'BlockBasic',\n    editComponent: 'EditScrollElement',\n    category: 'interaction',\n    inputs: 1,\n    outputs: 1,\n    allowedInputs: true,\n    maxConnection: 1,\n    refDataKeys: ['selector'],\n    data: {\n      disableBlock: false,\n      description: '',\n      findBy: 'cssSelector',\n      waitForSelector: false,\n      waitSelectorTimeout: 5000,\n      selector: 'html',\n      markEl: false,\n      multiple: false,\n      scrollY: 0,\n      scrollX: 0,\n      incX: false,\n      incY: false,\n      smooth: false,\n      scrollIntoView: false,\n    },\n  },\n  link: {\n    name: 'Link',\n    description: 'Open link element',\n    icon: 'riLink',\n    component: 'BlockBasic',\n    editComponent: 'EditLink',\n    category: 'interaction',\n    inputs: 1,\n    outputs: 1,\n    allowedInputs: true,\n    maxConnection: 1,\n    refDataKeys: ['selector'],\n    data: {\n      disableBlock: false,\n      description: '',\n      findBy: 'cssSelector',\n      waitForSelector: false,\n      waitSelectorTimeout: 5000,\n      selector: '',\n      markEl: false,\n      disableMultiple: true,\n      openInNewTab: false,\n    },\n  },\n  'attribute-value': {\n    name: 'Attribute value',\n    description: 'Get attribute value of an element',\n    icon: 'riBracketsLine',\n    component: 'BlockBasic',\n    editComponent: 'EditAttributeValue',\n    category: 'interaction',\n    inputs: 1,\n    outputs: 1,\n    allowedInputs: true,\n    maxConnection: 1,\n    refDataKeys: [\n      'selector',\n      'variableName',\n      'attributeName',\n      'extraRowValue',\n      'attributeValue',\n    ],\n    autocomplete: ['variableName'],\n    data: {\n      disableBlock: false,\n      description: '',\n      findBy: 'cssSelector',\n      waitForSelector: false,\n      waitSelectorTimeout: 5000,\n      selector: '',\n      markEl: false,\n      multiple: false,\n      attributeValue: '',\n      attributeName: '',\n      assignVariable: false,\n      variableName: '',\n      dataColumn: '',\n      saveData: true,\n      action: 'get',\n      addExtraRow: false,\n      extraRowValue: '',\n      extraRowDataColumn: '',\n    },\n  },\n  forms: {\n    name: 'Forms',\n    icon: 'riInputCursorMove',\n    description: 'Manipulate form(input, select, checkbox, and radio) element',\n    component: 'BlockBasic',\n    editComponent: 'EditForms',\n    category: 'interaction',\n    inputs: 1,\n    outputs: 1,\n    allowedInputs: true,\n    maxConnection: 1,\n    refDataKeys: [\n      'selector',\n      'variableName',\n      'value',\n      'optionPosition',\n      'delay',\n    ],\n    autocomplete: ['variableName'],\n    data: {\n      disableBlock: false,\n      description: '',\n      findBy: 'cssSelector',\n      waitForSelector: false,\n      waitSelectorTimeout: 5000,\n      selector: '',\n      markEl: false,\n      multiple: false,\n      selected: true,\n      clearValue: true,\n      getValue: false,\n      saveData: false,\n      dataColumn: '',\n      selectOptionBy: 'value',\n      optionPosition: '1',\n      assignVariable: false,\n      variableName: '',\n      type: 'text-field',\n      value: '',\n      delay: 0,\n      events: [],\n    },\n  },\n  'repeat-task': {\n    name: 'Repeat task',\n    icon: 'riRepeat2Line',\n    component: 'BlockRepeatTask',\n    category: 'conditions',\n    inputs: 1,\n    outputs: 2,\n    allowedInputs: true,\n    maxConnection: 1,\n    refDataKeys: ['repeatFor'],\n    data: {\n      disableBlock: false,\n      repeatFor: '1',\n    },\n  },\n  'javascript-code': {\n    name: 'JavaScript code',\n    description: 'Execute your custom javascript code in a webpage',\n    icon: 'riCodeSSlashLine',\n    component: 'BlockBasic',\n    editComponent: 'EditJavascriptCode',\n    category: 'interaction',\n    inputs: 1,\n    outputs: 1,\n    allowedInputs: true,\n    maxConnection: 1,\n    data: {\n      disableBlock: false,\n      description: '',\n      timeout: 20000,\n      context: 'website',\n      code: 'console.log(\"Hello world!\");\\nautomaNextBlock()',\n      preloadScripts: [],\n      everyNewTab: false,\n      runBeforeLoad: false,\n    },\n  },\n  'trigger-event': {\n    name: 'Trigger event',\n    description: 'Trigger event',\n    icon: 'riLightbulbFlashLine',\n    component: 'BlockBasic',\n    editComponent: 'EditTriggerEvent',\n    category: 'interaction',\n    inputs: 1,\n    outputs: 1,\n    allowedInputs: true,\n    maxConnection: 1,\n    refDataKeys: ['selector', 'eventParams.clientX', 'eventParams.clientY'],\n    data: {\n      disableBlock: false,\n      description: '',\n      findBy: 'cssSelector',\n      waitForSelector: false,\n      waitSelectorTimeout: 5000,\n      selector: 'html',\n      markEl: false,\n      multiple: false,\n      eventName: '',\n      eventType: '',\n      eventParams: { bubbles: true, cancelable: false },\n    },\n  },\n  'google-sheets': {\n    name: 'Google sheets',\n    description: 'Read Google Sheets data',\n    icon: 'mdiGoogleSheet',\n    component: 'BlockBasic',\n    editComponent: 'EditGoogleSheets',\n    category: 'onlineServices',\n    inputs: 1,\n    outputs: 1,\n    allowedInputs: true,\n    maxConnection: 1,\n    refDataKeys: ['customData', 'range', 'spreadsheetId', 'variableName'],\n    autocomplete: ['refKey'],\n    data: {\n      disableBlock: false,\n      range: '',\n      refKey: '',\n      type: 'get',\n      customData: '',\n      description: '',\n      spreadsheetId: '',\n      dataColumn: '',\n      saveData: true,\n      assignVariable: false,\n      variableName: '',\n      firstRowAsKey: false,\n      keysAsFirstRow: true,\n      valueInputOption: 'RAW',\n      InsertDataOption: 'INSERT_ROWS',\n      dataFrom: 'data-columns',\n    },\n  },\n  'google-sheets-drive': {\n    name: 'Google sheets (GDrive)',\n    description: 'Read Google Sheets data',\n    icon: 'riDriveFill',\n    component: 'BlockBasic',\n    editComponent: 'EditGoogleSheetsDrive',\n    category: 'onlineServices',\n    inputs: 1,\n    outputs: 1,\n    allowedInputs: true,\n    maxConnection: 1,\n    refDataKeys: [\n      'customData',\n      'range',\n      'spreadsheetId',\n      'sheetName',\n      'variableName',\n    ],\n    autocomplete: ['refKey'],\n    data: {\n      disableBlock: false,\n      range: '',\n      refKey: '',\n      type: 'get',\n      customData: '',\n      description: '',\n      spreadsheetId: '',\n      dataColumn: '',\n      inputSpreadsheetId: 'connected',\n      saveData: true,\n      sheetName: '',\n      assignVariable: false,\n      variableName: '',\n      firstRowAsKey: false,\n      keysAsFirstRow: true,\n      valueInputOption: 'RAW',\n      InsertDataOption: 'INSERT_ROWS',\n      dataFrom: 'data-columns',\n    },\n  },\n  'google-drive': {\n    name: 'Google drive',\n    description: 'Upload files to Google Drive',\n    icon: 'riDriveLine',\n    component: 'BlockBasic',\n    editComponent: 'EditGoogleDrive',\n    category: 'onlineServices',\n    inputs: 1,\n    outputs: 1,\n    allowedInputs: true,\n    maxConnection: 1,\n    refDataKeys: [],\n    autocomplete: ['refKey'],\n    data: {\n      disableBlock: false,\n      action: 'upload',\n      filePaths: [],\n    },\n  },\n  conditions: {\n    name: 'Conditions',\n    description: 'Conditional block',\n    icon: 'riAB',\n    component: 'BlockConditions',\n    editComponent: 'EditConditions',\n    category: 'conditions',\n    inputs: 1,\n    outputs: 0,\n    allowedInputs: true,\n    maxConnection: 1,\n    data: {\n      description: '',\n      disableBlock: false,\n      conditions: [],\n      retryConditions: false,\n      retryCount: 10,\n      retryTimeout: 1000,\n    },\n  },\n  'element-exists': {\n    name: 'Element exists',\n    description: 'Check if an element is exists',\n    icon: 'riFocus3Line',\n    component: 'BlockElementExists',\n    editComponent: 'EditElementExists',\n    category: 'conditions',\n    inputs: 1,\n    outputs: 2,\n    allowedInputs: true,\n    maxConnection: 1,\n    refDataKeys: ['selector'],\n    data: {\n      disableBlock: false,\n      description: '',\n      findBy: 'cssSelector',\n      selector: '',\n      tryCount: 1,\n      timeout: 500,\n      markEl: false,\n      throwError: false,\n    },\n  },\n  webhook: {\n    name: 'HTTP Request',\n    description: 'make an HTTP request',\n    icon: 'riEarthLine',\n    component: 'BlockBasicWithFallback',\n    editComponent: 'EditWebhook',\n    category: 'general',\n    inputs: 1,\n    outputs: 2,\n    allowedInputs: true,\n    maxConnection: 1,\n    refDataKeys: ['body', 'url', 'variableName'],\n    autocomplete: ['variableName'],\n    data: {\n      disableBlock: false,\n      description: '',\n      url: '',\n      body: '{}',\n      headers: [],\n      method: 'POST',\n      timeout: 10000,\n      dataPath: '',\n      contentType: 'json',\n      variableName: '',\n      assignVariable: false,\n      saveData: false,\n      dataColumn: '',\n      responseType: 'json',\n    },\n  },\n  'while-loop': {\n    name: 'While loop',\n    description: 'Execute blocks while the condition is met',\n    icon: 'riRefreshFill',\n    component: 'BlockBasicWithFallback',\n    editComponent: 'EditWhileLoop',\n    category: 'conditions',\n    inputs: 1,\n    outputs: 2,\n    allowedInputs: true,\n    maxConnection: 1,\n    data: {\n      disableBlock: false,\n      description: '',\n      conditions: null,\n    },\n  },\n  'loop-data': {\n    name: 'Loop data',\n    icon: 'riRefreshLine',\n    component: 'BlockBasic',\n    editComponent: 'EditLoopData',\n    category: 'conditions',\n    inputs: 1,\n    outputs: 1,\n    allowedInputs: true,\n    maxConnection: 1,\n    refDataKeys: [\n      'maxLoop',\n      'loopData',\n      'selector',\n      'startIndex',\n      'variableName',\n      'referenceKey',\n      'elementSelector',\n    ],\n    autocomplete: ['variableName', 'loopId'],\n    data: {\n      disableBlock: false,\n      loopId: '',\n      maxLoop: 0,\n      toNumber: 10,\n      fromNumber: 1,\n      startIndex: 0,\n      loopData: '[]',\n      description: '',\n      variableName: '',\n      referenceKey: '',\n      reverseLoop: false,\n      elementSelector: '',\n      waitForSelector: false,\n      waitSelectorTimeout: 5000,\n      resumeLastWorkflow: false,\n      loopThrough: 'data-columns',\n    },\n  },\n  'loop-elements': {\n    name: 'Loop elements',\n    icon: 'riRestartLine',\n    component: 'BlockBasic',\n    editComponent: 'EditLoopElements',\n    category: 'conditions',\n    inputs: 1,\n    outputs: 1,\n    allowedInputs: true,\n    maxConnection: 1,\n    refDataKeys: [\n      'maxLoop',\n      'selector',\n      'variableName',\n      'elementSelector',\n      'actionElSelector',\n    ],\n    autocomplete: ['loopId'],\n    data: {\n      disableBlock: false,\n      loopId: '',\n      selector: '',\n      maxLoop: '0',\n      description: '',\n      reverseLoop: false,\n      actionElSelector: '',\n      findBy: 'cssSelector',\n      actionElMaxWaitTime: 5,\n      actionPageMaxWaitTime: 10,\n      loadMoreAction: 'none',\n      scrollToBottom: true,\n      waitForSelector: false,\n      waitSelectorTimeout: 5000,\n    },\n  },\n  'loop-breakpoint': {\n    name: 'Loop breakpoint',\n    description: 'To tell where loop data must stop',\n    icon: 'riStopLine',\n    component: 'BlockLoopBreakpoint',\n    category: 'conditions',\n    disableEdit: true,\n    inputs: 1,\n    outputs: 1,\n    allowedInputs: true,\n    maxConnection: 1,\n    data: {\n      disableBlock: false,\n      loopId: '',\n      clearLoop: false,\n    },\n  },\n  'blocks-group': {\n    name: 'Blocks group',\n    description: 'Grouping blocks',\n    icon: 'riFolderZipLine',\n    component: 'BlockGroup',\n    category: 'general',\n    disableEdit: true,\n    inputs: 1,\n    outputs: 1,\n    allowedInputs: true,\n    maxConnection: 1,\n    data: {\n      disableBlock: false,\n      name: '',\n      blocks: [],\n    },\n  },\n  // 'blocks-group-2': {\n  //   name: 'Blocks group',\n  //   description: 'Grouping blocks',\n  //   icon: 'riFolderZipLine',\n  //   component: 'BlockGroup2',\n  //   category: 'general',\n  //   disableEdit: true,\n  //   inputs: 1,\n  //   outputs: 1,\n  //   allowedInputs: true,\n  //   maxConnection: 1,\n  //   data: {\n  //     disableBlock: false,\n  //     name: '',\n  //     width: 400,\n  //     height: 300,\n  //     borderColor: '#2563eb',\n  //     backgroundColor: 'rgb(37, 99, 235, 0.3)',\n  //   },\n  // },\n  clipboard: {\n    name: 'Clipboard',\n    description: 'Get the copied text from the clipboard',\n    icon: 'riClipboardLine',\n    component: 'BlockBasic',\n    category: 'general',\n    editComponent: 'EditClipboard',\n    inputs: 1,\n    outputs: 1,\n    allowedInputs: true,\n    maxConnection: 1,\n    autocomplete: ['variableName'],\n    refDataKeys: ['dataToCopy', 'variableName'],\n    data: {\n      disableBlock: false,\n      description: '',\n      type: 'get',\n      assignVariable: false,\n      variableName: '',\n      saveData: true,\n      dataColumn: '',\n      dataToCopy: '',\n      copySelectedText: false,\n    },\n  },\n  'insert-data': {\n    name: 'Insert data',\n    description: 'Insert data into table or variable',\n    icon: 'riDatabase2Line',\n    component: 'BlockBasic',\n    category: 'data',\n    editComponent: 'EditInsertData',\n    inputs: 1,\n    outputs: 1,\n    allowedInputs: true,\n    maxConnection: 1,\n    data: {\n      disableBlock: false,\n      description: '',\n      dataList: [],\n    },\n  },\n  'switch-to': {\n    name: 'Switch frame',\n    description: 'Switch between main window and iframe',\n    icon: 'riArrowUpDownLine',\n    component: 'BlockBasic',\n    editComponent: 'EditSwitchTo',\n    category: 'interaction',\n    inputs: 1,\n    outputs: 1,\n    allowedInputs: true,\n    maxConnection: 1,\n    refDataKeys: ['selector'],\n    data: {\n      disableBlock: false,\n      findBy: 'cssSelector',\n      selector: '',\n      windowType: 'main-window',\n    },\n  },\n  'upload-file': {\n    name: 'Upload file',\n    description: 'Upload file into <input type=\"file\"> element',\n    icon: 'riFileUploadLine',\n    component: 'BlockBasic',\n    editComponent: 'EditUploadFile',\n    category: 'interaction',\n    inputs: 1,\n    outputs: 1,\n    allowedInputs: true,\n    maxConnection: 1,\n    refDataKeys: ['selector', 'filePaths'],\n    data: {\n      disableBlock: false,\n      findBy: 'cssSelector',\n      waitForSelector: false,\n      waitSelectorTimeout: 5000,\n      selector: '',\n      filePaths: [],\n    },\n  },\n  'hover-element': {\n    name: 'Hover element',\n    description: 'Hover over an element',\n    icon: 'mdiCursorDefaultClickOutline',\n    component: 'BlockBasic',\n    editComponent: 'EditInteractionBase',\n    category: 'interaction',\n    inputs: 1,\n    outputs: 1,\n    allowedInputs: true,\n    maxConnection: 1,\n    refDataKeys: ['selector'],\n    data: {\n      disableBlock: false,\n      description: '',\n      findBy: 'cssSelector',\n      waitForSelector: false,\n      waitSelectorTimeout: 5000,\n      selector: '',\n      markEl: false,\n      multiple: false,\n    },\n  },\n  'save-assets': {\n    name: 'Save assets',\n    description:\n      'Save assets (image, video, audio, or file) from an element or URL',\n    icon: 'riImageLine',\n    component: 'BlockBasic',\n    editComponent: 'EditSaveAssets',\n    category: 'interaction',\n    inputs: 1,\n    outputs: 1,\n    allowedInputs: true,\n    maxConnection: 1,\n    refDataKeys: ['selector', 'url', 'filename', 'variableName'],\n    data: {\n      disableBlock: false,\n      description: '',\n      findBy: 'cssSelector',\n      waitForSelector: false,\n      waitSelectorTimeout: 5000,\n      selector: '',\n      markEl: false,\n      multiple: false,\n      type: 'element',\n      url: '',\n      filename: '',\n      saveDownloadIds: false,\n      onConflict: 'uniquify',\n      dataColumn: '',\n      saveData: true,\n      assignVariable: false,\n      variableName: '',\n      saveToGDrive: false,\n    },\n  },\n  'press-key': {\n    name: 'Press key',\n    description: 'Press a key or a combination',\n    icon: 'riKeyboardLine',\n    component: 'BlockBasic',\n    editComponent: 'EditPressKey',\n    category: 'interaction',\n    inputs: 1,\n    outputs: 1,\n    allowedInputs: true,\n    maxConnection: 1,\n    refDataKeys: ['selector', 'keys', 'keysToPress', 'pressTime'],\n    data: {\n      disableBlock: false,\n      keys: '',\n      selector: '',\n      pressTime: '0',\n      description: '',\n      keysToPress: '',\n      action: 'press-key',\n    },\n  },\n  'handle-dialog': {\n    name: 'Handle dialog',\n    description:\n      'Accepts or dismisses a JavaScript initiated dialog (alert, confirm, prompt, or onbeforeunload).',\n    icon: 'riChat3Line',\n    component: 'BlockBasic',\n    editComponent: 'EditHandleDialog',\n    category: 'browser',\n    inputs: 1,\n    outputs: 1,\n    allowedInputs: true,\n    maxConnection: 1,\n    refDataKeys: ['promptText'],\n    data: {\n      disableBlock: false,\n      description: '',\n      accept: true,\n      promptText: '',\n    },\n  },\n  'handle-download': {\n    name: 'Handle download',\n    description: 'Handle downloaded file',\n    icon: 'riFileDownloadLine',\n    component: 'BlockBasic',\n    editComponent: 'EditHandleDownload',\n    category: 'browser',\n    inputs: 1,\n    outputs: 1,\n    allowedInputs: true,\n    maxConnection: 1,\n    refDataKeys: ['filename', 'downloadId', 'variableName'],\n    autocomplete: ['variableName'],\n    data: {\n      disableBlock: false,\n      description: '',\n      filename: '',\n      timeout: 20000,\n      onConflict: 'uniquify',\n      waitForDownload: true,\n      dataColumn: '',\n      saveData: true,\n      assignVariable: false,\n      variableName: '',\n      downloadId: '',\n    },\n  },\n  'reload-tab': {\n    name: 'Reload tab',\n    description: 'Reload the active tab',\n    icon: 'riRestartLine',\n    component: 'BlockBasic',\n    category: 'browser',\n    inputs: 1,\n    outputs: 1,\n    allowedInputs: true,\n    maxConnection: 1,\n    disableEdit: true,\n    data: {\n      disableBlock: false,\n    },\n  },\n  'delete-data': {\n    name: 'Delete data',\n    description: 'Delete table or variable data',\n    icon: 'riDeleteBin7Line',\n    editComponent: 'EditDeleteData',\n    component: 'BlockBasic',\n    category: 'data',\n    inputs: 1,\n    outputs: 1,\n    allowedInputs: true,\n    maxConnection: 1,\n    data: {\n      disableBlock: false,\n      description: '',\n      deleteList: [],\n    },\n  },\n  'wait-connections': {\n    name: 'Wait connections',\n    description: 'Wait for all connections before continuing to the next block',\n    icon: 'riTimerFlashLine',\n    editComponent: 'EditWaitConnections',\n    component: 'BlockBasic',\n    category: 'general',\n    inputs: 1,\n    outputs: 1,\n    allowedInputs: true,\n    maxConnection: 1,\n    data: {\n      disableBlock: false,\n      description: '',\n      timeout: 10000,\n      specificFlow: false,\n      flowBlockId: '',\n    },\n  },\n  notification: {\n    name: 'Notification',\n    description: 'Display a notification',\n    icon: 'riNotification3Line',\n    editComponent: 'EditNotification',\n    component: 'BlockBasic',\n    category: 'general',\n    inputs: 1,\n    outputs: 1,\n    allowedInputs: true,\n    maxConnection: 1,\n    refDataKeys: ['message', 'title', 'iconUrl', 'imageUrl'],\n    data: {\n      disableBlock: false,\n      description: '',\n      message: '',\n      iconUrl: '',\n      imageUrl: '',\n      title: 'Hello world!',\n    },\n  },\n  'log-data': {\n    name: 'Get log data',\n    description: 'Get the latest log data of a workflow',\n    icon: 'riFileHistoryLine',\n    editComponent: 'EditLogData',\n    component: 'BlockBasic',\n    category: 'data',\n    inputs: 1,\n    outputs: 1,\n    allowedInputs: true,\n    maxConnection: 1,\n    data: {\n      disableBlock: false,\n      description: '',\n      workflowId: '',\n      dataColumn: '',\n      saveData: true,\n      assignVariable: false,\n      variableName: '',\n    },\n  },\n  'tab-url': {\n    name: 'Get tab URL',\n    description: 'Get the tab URL',\n    icon: 'riLinksLine',\n    editComponent: 'EditTabURL',\n    component: 'BlockBasic',\n    category: 'browser',\n    inputs: 1,\n    outputs: 1,\n    allowedInputs: true,\n    maxConnection: 1,\n    refDataKeys: ['variableName'],\n    data: {\n      disableBlock: false,\n      description: '',\n      type: 'active-tab',\n      dataColumn: '',\n      saveData: true,\n      assignVariable: false,\n      variableName: '',\n      qTitle: '',\n      qMatchPatterns: '',\n    },\n  },\n  'slice-variable': {\n    name: 'Slice variable',\n    description: 'Extracts a section of a variable value',\n    icon: 'riSliceLine',\n    editComponent: 'EditSliceVariable',\n    component: 'BlockBasic',\n    category: 'data',\n    inputs: 1,\n    outputs: 1,\n    allowedInputs: true,\n    maxConnection: 1,\n    data: {\n      disableBlock: false,\n      description: '',\n      endIdxEnabled: false,\n      startIdxEnabled: true,\n      endIndex: 0,\n      startIndex: 0,\n      variableName: '',\n    },\n  },\n  'increase-variable': {\n    name: 'Increase variable',\n    description: 'Increase the value of a variable by specific amount',\n    icon: 'riIncreaseDecreaseLine',\n    editComponent: 'EditIncreaseVariable',\n    component: 'BlockBasic',\n    category: 'data',\n    inputs: 1,\n    outputs: 1,\n    allowedInputs: true,\n    maxConnection: 1,\n    data: {\n      disableBlock: false,\n      description: '',\n      increaseBy: 1,\n      variableName: '',\n    },\n  },\n  'regex-variable': {\n    name: 'RegEx variable',\n    description: 'Matching a variable value against a regular expression',\n    icon: 'mdiRegex',\n    editComponent: 'EditRegexVariable',\n    component: 'BlockBasic',\n    category: 'data',\n    inputs: 1,\n    outputs: 1,\n    allowedInputs: true,\n    maxConnection: 1,\n    refDataKeys: ['replaceVal'],\n    data: {\n      disableBlock: false,\n      method: 'match',\n      replaceVal: '',\n      description: '',\n      expression: '',\n      flag: [],\n    },\n  },\n  'data-mapping': {\n    name: 'Data mapping',\n    description: 'Map data of a variable or table',\n    icon: 'riMindMap',\n    editComponent: 'EditDataMapping',\n    component: 'BlockBasic',\n    category: 'data',\n    inputs: 1,\n    outputs: 1,\n    allowedInputs: true,\n    maxConnection: 1,\n    data: {\n      disableBlock: false,\n      description: '',\n      dataSource: 'table',\n      sources: [],\n      varSourceName: '',\n      dataColumn: '',\n      saveData: false,\n      assignVariable: false,\n      variableName: '',\n    },\n  },\n  'sort-data': {\n    name: 'Sort data',\n    description: 'Sort the items of data',\n    icon: 'riSortAsc',\n    editComponent: 'EditSortData',\n    component: 'BlockBasic',\n    category: 'data',\n    inputs: 1,\n    outputs: 1,\n    allowedInputs: true,\n    maxConnection: 1,\n    data: {\n      disableBlock: false,\n      description: '',\n      sortByProperty: false,\n      itemProperties: [],\n      dataSource: 'table',\n      varSourceName: '',\n      dataColumn: '',\n      saveData: false,\n      assignVariable: false,\n      variableName: '',\n    },\n  },\n  'create-element': {\n    name: 'Create element',\n    description: 'Create an element and insert it into the page',\n    icon: 'riHtml5Line',\n    editComponent: 'EditCreateElement',\n    component: 'BlockBasic',\n    category: 'interaction',\n    inputs: 1,\n    outputs: 1,\n    allowedInputs: true,\n    maxConnection: 1,\n    refDataKeys: ['html', 'css', 'selector'],\n    data: {\n      disableBlock: false,\n      description: '',\n      javascript: '',\n      html: '',\n      css: '',\n      preloadScripts: [],\n      findBy: 'cssSelector',\n      insertAt: 'after',\n      runBeforeLoad: false,\n      waitForSelector: false,\n      waitSelectorTimeout: 5000,\n      selector: 'body',\n    },\n  },\n  cookie: {\n    name: 'Cookie',\n    description: 'Get, set, or remove cookies',\n    icon: 'mdiCookieOutline',\n    editComponent: 'EditCookie',\n    component: 'BlockBasic',\n    category: 'browser',\n    inputs: 1,\n    outputs: 1,\n    allowedInputs: true,\n    maxConnection: 1,\n    refDataKeys: [\n      'domain',\n      'expirationDate',\n      'path',\n      'sameSite',\n      'name',\n      'url',\n      'value',\n      'jsonCode',\n      'variableName',\n    ],\n    data: {\n      disableBlock: false,\n      description: '',\n      type: 'get',\n      jsonCode: '{\\n\\n}',\n      useJson: false,\n      getAll: false,\n      domain: '',\n      expirationDate: '',\n      path: '',\n      sameSite: '',\n      name: '',\n      url: '',\n      value: '',\n      httpOnly: false,\n      secure: false,\n      session: false,\n      assignVariable: false,\n      variableName: '',\n      saveData: true,\n      dataColumn: '',\n    },\n  },\n  'block-package': {\n    name: 'Block package',\n    description: 'Block package',\n    icon: 'riHtml5Line',\n    editComponent: 'EditPackage',\n    component: 'BlockPackage',\n    category: 'package',\n    inputs: 1,\n    outputs: 1,\n    allowedInputs: true,\n    maxConnection: 1,\n    data: {},\n  },\n  note: {\n    name: 'Note',\n    description: '',\n    icon: 'riFileEditLine',\n    component: 'BlockNote',\n    category: 'general',\n    disableEdit: true,\n    inputs: 1,\n    outputs: 1,\n    allowedInputs: true,\n    maxConnection: 1,\n    data: {\n      disableBlock: false,\n      note: '',\n      drawing: false,\n      width: 280,\n      height: 168,\n      color: 'white',\n      fontSize: 'regular',\n    },\n  },\n  'workflow-state': {\n    name: 'Workflow State',\n    description: 'Manage workflows states',\n    icon: 'riSettings3Line',\n    component: 'BlockBasic',\n    editComponent: 'EditWorkflowState',\n    category: 'general',\n    inputs: 1,\n    outputs: 1,\n    allowedInputs: true,\n    maxConnection: 1,\n    refDataKeys: ['errorMessage'],\n    data: {\n      disableBlock: false,\n      description: '',\n      type: 'stop-current',\n      exceptCurrent: false,\n      workflowsToStop: [],\n      throwError: false,\n      errorMessage: '',\n    },\n  },\n  'parameter-prompt': {\n    name: 'Parameter prompt',\n    description: '',\n    icon: 'riCommandLine',\n    component: 'BlockBasic',\n    category: 'general',\n    editComponent: 'EditParameterPrompt',\n    inputs: 1,\n    outputs: 1,\n    maxConnection: 1,\n    allowedInputs: true,\n    data: {\n      disableBlock: false,\n      description: '',\n      timeout: 60000,\n      parameters: [],\n    },\n  },\n};\n\nexport const categories = {\n  interaction: {\n    name: 'Web interaction',\n    border: 'border-green-200 dark:border-green-300',\n    color: 'bg-green-200 dark:bg-green-300 fill-green-200 dark:fill-green-300',\n  },\n  browser: {\n    name: 'Browser',\n    border: 'border-orange-200 dark:border-orange-300',\n    color:\n      'bg-orange-200 dark:bg-orange-300 fill-orange-200 dark:fill-orange-300',\n  },\n  general: {\n    name: 'General',\n    border: 'border-yellow-200 dark:border-yellow-300',\n    color:\n      'bg-yellow-200 dark:bg-yellow-300 fill-yellow-200 dark:fill-yellow-300',\n  },\n  onlineServices: {\n    name: 'Online services',\n    border: 'border-red-200 dark:border-red-300',\n    color: 'bg-red-200 dark:bg-red-300 fill-red-200 dark:fill-red-300',\n  },\n  data: {\n    name: 'Data',\n    border: 'border-lime-200 dark:border-lime-300',\n    color: 'bg-lime-200 dark:bg-lime-300 fill-lime-200 dark:fill-lime-300',\n  },\n  conditions: {\n    name: 'Control flow',\n    border: 'border-blue-200 dark:border-blue-300',\n    color: 'bg-blue-200 dark:bg-blue-300 fill-blue-200 dark:fill-blue-300',\n  },\n  package: {\n    name: 'Packages',\n    border: 'border-cyan-200 dark:border-cyan-300',\n    color: 'bg-cyan-200 dark:bg-cyan-300 fill-cyan-200 dark:fill-cyan-300',\n  },\n};\n\nexport const tagColors = {\n  stage: 'bg-yellow-200 dark:bg-yellow-300',\n  production: 'bg-green-200 dark:bg-green-300',\n};\n\nexport const eventList = [\n  { id: 'click', name: 'Click', type: 'mouse-event' },\n  { id: 'dblclick', name: 'Double Click', type: 'mouse-event' },\n  { id: 'mouseup', name: 'Mouseup', type: 'mouse-event' },\n  { id: 'mousedown', name: 'Mousedown', type: 'mouse-event' },\n  { id: 'mouseenter', name: 'Mouseenter', type: 'mouse-event' },\n  { id: 'mouseleave', name: 'Mouseleave', type: 'mouse-event' },\n  { id: 'mouseover', name: 'Mouseover', type: 'mouse-event' },\n  { id: 'mouseout', name: 'Mouseout', type: 'mouse-event' },\n  { id: 'mousemove', name: 'Mousemove', type: 'mouse-event' },\n  { id: 'focus', name: 'Focus', type: 'focus-event' },\n  { id: 'blur', name: 'Blur', type: 'focus-event' },\n  { id: 'input', name: 'Input', type: 'input-event' },\n  { id: 'change', name: 'Change', type: 'event' },\n  { id: 'touchstart', name: 'Touch start', type: 'touch-event' },\n  { id: 'touchend', name: 'Touch end', type: 'touch-event' },\n  { id: 'touchmove', name: 'Touch move', type: 'touch-event' },\n  { id: 'touchcancel', name: 'Touch cancel', type: 'touch-event' },\n  { id: 'keydown', name: 'Keydown', type: 'keyboard-event' },\n  { id: 'keyup', name: 'Keyup', type: 'keyboard-event' },\n  { id: 'submit', name: 'Submit', type: 'submit-event' },\n  { id: 'wheel', name: 'Wheel', type: 'wheel-event' },\n];\n\nexport const dataExportTypes = [\n  { name: 'JSON', id: 'json' },\n  { name: 'CSV', id: 'csv' },\n  { name: 'Plain text', id: 'plain-text' },\n];\n\nexport const workflowCategories = {\n  scrape: 'Scraping',\n  automation: 'Automation',\n  productivity: 'Productivity',\n};\n\nexport const excludeOnError = [\n  'note',\n  'delay',\n  'webhook',\n  'trigger',\n  'while-loop',\n  'conditions',\n  'blocks-group',\n  'block-package',\n  'element-exists',\n];\n\nexport const contentTypes = [\n  { name: 'text/plain', value: 'text' },\n  { name: 'application/json', value: 'json' },\n  { name: 'multipart/form-data', value: 'form-data' },\n  { name: 'application/x-www-form-urlencoded', value: 'form' },\n];\n\nexport const supportLocales = [\n  { id: 'en', name: 'English' },\n  { id: 'fr', name: 'Français' },\n  { id: 'it', name: 'Italiano' },\n  { id: 'uk', name: 'Українська' },\n  { id: 'vi', name: 'Tiếng Việt' },\n  { id: 'zh', name: '简体中文' },\n  { id: 'zh-TW', name: '繁體中文' },\n  { id: 'tr', name: 'Türkçe' },\n  { id: 'es', name: 'Spanish' },\n  { id: 'pt-BR', name: 'Portuguese' },\n];\n\nexport const communities = [\n  {\n    name: 'GitHub',\n    icon: 'riGithubFill',\n    url: 'https://github.com/AutomaApp/automa',\n  },\n  {\n    name: 'Twitter',\n    icon: 'riTwitterLine',\n    url: 'https://twitter.com/AutomaApp',\n  },\n  {\n    name: 'Discord',\n    icon: 'riDiscordLine',\n    url: 'https://discord.gg/C6khwwTE84',\n  },\n  {\n    name: 'YouTube',\n    icon: 'riYoutubeLine',\n    url: 'https://www.youtube.com/channel/UCL3qU64hW0fsIj2vOayOQUQ',\n  },\n];\n\nexport const elementsHighlightData = {\n  selectedElements: {\n    stroke: '#2563EB',\n    activeStroke: '#f87171',\n    fill: 'rgba(37, 99, 235, 0.1)',\n    activeFill: 'rgba(248, 113, 113, 0.1)',\n  },\n  hoveredElements: {\n    stroke: '#fbbf24',\n    fill: 'rgba(251, 191, 36, 0.1)',\n  },\n};\n\nexport const excludeGroupBlocks = [\n  'trigger',\n  'repeat-task',\n  'loop-data',\n  'loop-breakpoint',\n  'blocks-group',\n  'conditions',\n  'webhook',\n  'element-exists',\n  'while-loop',\n  'block-package',\n];\n\nexport const conditionBuilder = {\n  valueTypes: [\n    {\n      id: 'value',\n      category: 'value',\n      name: 'Value',\n      compareable: true,\n      data: { value: '' },\n    },\n    {\n      id: 'code',\n      category: 'value',\n      name: 'Code',\n      compareable: false,\n      data: { code: '\\nreturn true;', context: 'background' },\n    },\n    {\n      id: 'data#exists',\n      category: 'value',\n      name: 'Data exists',\n      compareable: false,\n      valueKey: 'dataPath',\n      data: { dataPath: '' },\n    },\n    {\n      id: 'element#text',\n      category: 'element',\n      name: 'Element text',\n      compareable: true,\n      data: { selector: '' },\n    },\n    {\n      id: 'element#exists',\n      category: 'element',\n      name: 'Element exists',\n      compareable: false,\n      data: { selector: '' },\n    },\n    {\n      id: 'element#notExists',\n      category: 'element',\n      name: 'Element not exists',\n      compareable: false,\n      data: { selector: '' },\n    },\n    {\n      id: 'element#visible',\n      category: 'element',\n      name: 'Element visible',\n      compareable: false,\n      data: { selector: '' },\n    },\n    {\n      id: 'element#visibleScreen',\n      category: 'element',\n      name: 'Element visible in screen',\n      compareable: false,\n      data: { selector: '' },\n    },\n    {\n      id: 'element#invisible',\n      category: 'element',\n      name: 'Element hidden in screen',\n      compareable: false,\n      data: { selector: '' },\n    },\n    {\n      id: 'element#attribute',\n      category: 'element',\n      name: 'Element attribute value',\n      compareable: true,\n      data: { selector: '', attrName: '' },\n    },\n  ],\n  compareTypes: [\n    { id: 'eq', name: 'Equals', needValue: true, category: 'basic' },\n    {\n      id: 'eqi',\n      name: 'Equals (case insensitive)',\n      needValue: true,\n      category: 'basic',\n    },\n    { id: 'nq', name: 'Not equals', needValue: true, category: 'basic' },\n    { id: 'gt', name: 'Greater than', needValue: true, category: 'number' },\n    {\n      id: 'gte',\n      name: 'Greater than or equal',\n      needValue: true,\n      category: 'number',\n    },\n    { id: 'lt', name: 'Less than', needValue: true, category: 'number' },\n    {\n      id: 'lte',\n      name: 'Less than or equal',\n      needValue: true,\n      category: 'number',\n    },\n    { id: 'cnt', name: 'Contains', needValue: true, category: 'text' },\n    {\n      id: 'cni',\n      name: 'Contains (case insensitive)',\n      needValue: true,\n      category: 'text',\n    },\n    { id: 'nct', name: 'Not contains', needValue: true, category: 'text' },\n    {\n      id: 'nci',\n      name: 'Not contains (case insensitive)',\n      needValue: true,\n      category: 'text',\n    },\n    { id: 'stw', name: 'Starts with', needValue: true, category: 'text' },\n    { id: 'enw', name: 'Ends with', needValue: true, category: 'text' },\n    { id: 'rgx', name: 'Match with RegEx', needValue: true, category: 'text' },\n    { id: 'itr', name: 'Is truthy', needValue: false, category: 'boolean' },\n    { id: 'ifl', name: 'Is falsy', needValue: false, category: 'boolean' },\n  ],\n  inputTypes: {\n    selector: {\n      placeholder: '.class',\n      label: 'CSS selector or XPath',\n    },\n    value: {\n      label: 'Value',\n      placeholder: 'abc123',\n    },\n    attrName: {\n      label: 'Attribute name',\n      placeholder: 'name',\n    },\n    dataPath: {\n      label: 'variables@variableName',\n      placeholder: '',\n    },\n  },\n};\n\nexport const messageHasReferences = [\n  'no-tab',\n  'invalid-active-tab',\n  'no-match-tab',\n  'invalid-body',\n  'element-not-found',\n];\n"
  },
  {
    "path": "src/utils/simulateEvent/index.js",
    "content": "import { eventList } from '../shared';\n\nexport function getEventObj(name, params) {\n  const eventType = eventList.find(({ id }) => id === name)?.type ?? '';\n  let event;\n\n  switch (eventType) {\n    case 'mouse-event':\n      event = new MouseEvent(name, { ...params, view: window });\n      break;\n    case 'focus-event':\n      event = new FocusEvent(name, params);\n      break;\n    case 'touch-event':\n      event = new TouchEvent(name, params);\n      break;\n    case 'keyboard-event':\n      event = new KeyboardEvent(name, params);\n      break;\n    case 'wheel-event':\n      event = new WheelEvent(name, params);\n      break;\n    case 'input-event':\n      event = new InputEvent(name, params);\n      break;\n    default:\n      event = new Event(name, params);\n  }\n\n  return event;\n}\n\nexport default function (element, name, params) {\n  const event = getEventObj(name, params);\n  const useNativeMethods = ['focus', 'submit', 'blur'];\n\n  if (useNativeMethods.includes(name) && element[name]) {\n    element[name]();\n  } else {\n    element.dispatchEvent(event);\n  }\n}\n"
  },
  {
    "path": "src/utils/simulateEvent/mouseEvent.js",
    "content": "export default function ({ sendCommand, commandParams }) {\n  async function mousedown() {\n    commandParams.type = 'mousePressed';\n    await sendCommand('Input.dispatchMouseEvent', commandParams);\n  }\n  async function mouseup() {\n    commandParams.type = 'mouseReleased';\n    await sendCommand('Input.dispatchMouseEvent', commandParams);\n  }\n  async function click() {\n    if (!commandParams.clickCount) commandParams.clickCount = 1;\n\n    await mousedown();\n    await mouseup();\n  }\n  async function dblclick() {\n    commandParams.clickCount = 2;\n    await click();\n  }\n  async function mousemove() {\n    commandParams.type = 'mouseMoved';\n    await sendCommand('Input.dispatchMouseEvent', commandParams);\n  }\n  async function mouseenter() {\n    await mousemove();\n  }\n  async function mouseleave() {\n    await mousemove();\n\n    commandParams.x = -100;\n    commandParams.y = -100;\n    await mousemove();\n  }\n\n  return {\n    mousedown,\n    mouseup,\n    click,\n    dblclick,\n    mousemove,\n    mouseenter,\n    mouseleave,\n  };\n}\n"
  },
  {
    "path": "src/utils/triggerText.js",
    "content": "import browser from 'webextension-polyfill';\nimport dayjs from '@/lib/dayjs';\nimport { getReadableShortcut } from '@/composable/shortcut';\n\nexport default async function (trigger, t, workflowId, includeManual = false) {\n  if (!trigger || (trigger.type === 'manual' && !includeManual)) return null;\n\n  const triggerLocale = t('workflow.blocks.trigger.name');\n\n  if (trigger.type === 'manual') {\n    return `${triggerLocale}: ${t('workflow.blocks.trigger.items.manual')}`;\n  }\n\n  const triggerName = t(`workflow.blocks.trigger.items.${trigger.type}`);\n  let text = '';\n\n  if (trigger.type === 'keyboard-shortcut') {\n    text = getReadableShortcut(trigger.shortcut);\n  } else if (trigger.type === 'visit-web') {\n    text = trigger.url;\n  } else if (['specific-day', 'date'].includes(trigger.type)) {\n    const triggerTime = (await browser.alarms.get(workflowId))?.scheduledTime;\n\n    text = dayjs(triggerTime || Date.now()).format('DD-MMM-YYYY, hh:mm A');\n  }\n\n  text = text && `: \\n ${text}`;\n\n  return `${triggerLocale} (${triggerName})${text}`;\n}\n"
  },
  {
    "path": "src/utils/workflowData.js",
    "content": "import browser from 'webextension-polyfill';\nimport { useWorkflowStore } from '@/stores/workflow';\nimport { registerWorkflowTrigger } from './workflowTrigger';\nimport {\n  parseJSON,\n  fileSaver,\n  openFilePicker,\n  findTriggerBlock,\n} from './helper';\n\nconst contextMenuPermission =\n  BROWSER_TYPE === 'firefox' ? 'menus' : 'contextMenus';\nconst checkPermission = (permissions) =>\n  browser.permissions.contains({ permissions });\nconst requiredPermissions = {\n  trigger: {\n    name: contextMenuPermission,\n    hasPermission({ data }) {\n      const permissions = [];\n\n      if (data.triggers) {\n        data.triggers.forEach((trigger) => {\n          if (trigger.type !== 'context-menu') return;\n\n          permissions.push(contextMenuPermission);\n        });\n      } else if (data.type === 'context-menu') {\n        permissions.push(contextMenuPermission);\n      }\n\n      return checkPermission(permissions);\n    },\n  },\n  clipboard: {\n    name: 'clipboardRead',\n    hasPermission() {\n      const clipboardPermissions = ['clipboardRead'];\n      if (BROWSER_TYPE === 'firefox')\n        clipboardPermissions.push('clipboardWrite');\n\n      return checkPermission(clipboardPermissions);\n    },\n  },\n  notification: {\n    name: 'notifications',\n    hasPermission() {\n      return checkPermission(['notifications']);\n    },\n  },\n  'handle-download': {\n    name: 'downloads',\n    hasPermission() {\n      return checkPermission(['downloads']);\n    },\n  },\n  'save-assets': {\n    name: 'downloads',\n    hasPermission() {\n      return checkPermission(['downloads']);\n    },\n  },\n  cookie: {\n    name: 'cookies',\n    hasPermission() {\n      return checkPermission(['cookies']);\n    },\n  },\n};\n\nexport async function getWorkflowPermissions(drawflow) {\n  let blocks = [];\n  const permissions = [];\n  const drawflowData =\n    typeof drawflow === 'string' ? parseJSON(drawflow) : drawflow;\n\n  if (drawflowData.nodes) {\n    blocks = drawflowData.nodes;\n  } else {\n    blocks = Object.values(drawflowData.drawflow?.Home?.data || {});\n  }\n\n  for (const block of blocks) {\n    const name = block.label || block.name;\n    const permission = requiredPermissions[name];\n\n    if (permission && !permissions.includes(permission.name)) {\n      const hasPermission = await permission.hasPermission(block);\n      if (!hasPermission) permissions.push(permission.name);\n    }\n  }\n\n  return permissions;\n}\n\nexport function importWorkflow(attrs = {}) {\n  return new Promise((resolve, reject) => {\n    openFilePicker(['application/json'], attrs)\n      .then((files) => {\n        const handleOnLoadReader = ({ target }) => {\n          const workflow = JSON.parse(target.result);\n          const workflowStore = useWorkflowStore();\n\n          if (workflow.includedWorkflows) {\n            Object.keys(workflow.includedWorkflows).forEach((workflowId) => {\n              const isWorkflowExists = Boolean(\n                workflowStore.workflows[workflowId]\n              );\n\n              if (isWorkflowExists) return;\n\n              const currentWorkflow = workflow.includedWorkflows[workflowId];\n              currentWorkflow.table =\n                currentWorkflow.table || currentWorkflow.dataColumns;\n              delete currentWorkflow.dataColumns;\n\n              workflowStore.insert(\n                {\n                  ...currentWorkflow,\n                  id: workflowId,\n                  createdAt: Date.now(),\n                },\n                { duplicateId: true }\n              );\n            });\n\n            delete workflow.includedWorkflows;\n          }\n\n          workflow.table = workflow.table || workflow.dataColumns;\n          delete workflow.dataColumns;\n\n          if (typeof workflow.drawflow === 'string') {\n            workflow.drawflow = parseJSON(workflow.drawflow, {});\n          }\n\n          workflowStore\n            .insert({\n              ...workflow,\n              createdAt: Date.now(),\n            })\n            .then((result) => {\n              Object.values(result).forEach((item) => {\n                const triggerBlock = findTriggerBlock(item.drawflow);\n                registerWorkflowTrigger(item.id, triggerBlock);\n              });\n\n              resolve(result);\n            });\n        };\n\n        files.forEach((file) => {\n          const reader = new FileReader();\n\n          reader.onload = handleOnLoadReader;\n          reader.readAsText(file);\n        });\n      })\n      .catch((error) => {\n        console.error(error);\n        reject(error);\n      });\n  });\n}\n\nconst defaultValue = {\n  name: '',\n  icon: '',\n  table: [],\n  settings: {},\n  globalData: '',\n  dataColumns: [],\n  description: '',\n  drawflow: { nodes: [], edges: [] },\n  version: browser.runtime.getManifest().version,\n};\n\nexport function convertWorkflow(workflow, additionalKeys = []) {\n  if (!workflow) return null;\n\n  const keys = [\n    'name',\n    'icon',\n    'table',\n    'version',\n    'drawflow',\n    'settings',\n    'globalData',\n    'description',\n    ...additionalKeys,\n  ];\n  const content = {\n    extVersion: browser.runtime.getManifest().version,\n  };\n\n  keys.forEach((key) => {\n    content[key] = workflow[key] ?? defaultValue[key];\n  });\n\n  return content;\n}\nfunction findIncludedWorkflows(\n  { drawflow },\n  store,\n  maxDepth = 3,\n  workflows = {}\n) {\n  if (maxDepth === 0) return workflows;\n\n  const flow = parseJSON(drawflow, drawflow);\n  const blocks = flow?.drawflow?.Home.data ?? flow.nodes ?? null;\n  if (!blocks) return workflows;\n\n  const checkWorkflow = (type, workflowId) => {\n    if (type !== 'execute-workflow' || workflows[workflowId]) return;\n\n    const workflow = store.getById(workflowId);\n    if (workflow) {\n      workflows[workflowId] = convertWorkflow(workflow);\n      findIncludedWorkflows(workflow, store, maxDepth - 1, workflows);\n    }\n  };\n\n  if (flow.nodes) {\n    flow.nodes.forEach((node) => {\n      checkWorkflow(node.label, node.data.workflowId);\n    });\n  } else {\n    Object.values(blocks).forEach(({ data, name }) => {\n      checkWorkflow(name, data.workflowId);\n    });\n  }\n\n  return workflows;\n}\nexport function exportWorkflow(workflow) {\n  if (workflow.isProtected) return;\n\n  const workflowStore = useWorkflowStore();\n  const includedWorkflows = findIncludedWorkflows(workflow, workflowStore);\n  const content = convertWorkflow(workflow);\n\n  content.includedWorkflows = includedWorkflows;\n\n  const blob = new Blob([JSON.stringify(content)], {\n    type: 'application/json',\n  });\n  const url = URL.createObjectURL(blob);\n\n  fileSaver(`${workflow.name}.automa.json`, url);\n}\n\nexport default {\n  export: exportWorkflow,\n  import: importWorkflow,\n};\n"
  },
  {
    "path": "src/utils/workflowTrigger.js",
    "content": "import cronParser from 'cron-parser';\nimport dayjs from 'dayjs';\nimport browser from 'webextension-polyfill';\nimport { isObject } from './helper';\n\nexport function registerContextMenu(triggerId, data) {\n  return new Promise((resolve, reject) => {\n    const documentUrlPatterns = ['https://*/*', 'http://*/*'];\n    const contextTypes =\n      !data.contextTypes || data.contextTypes.length === 0\n        ? ['all']\n        : data.contextTypes;\n\n    const isFirefox = BROWSER_TYPE === 'firefox';\n    const browserContext = isFirefox ? browser.menus : browser.contextMenus;\n\n    if (!browserContext) {\n      resolve();\n      return;\n    }\n\n    const workflowId = triggerId.includes(':')\n      ? triggerId.split(':')[1]\n      : triggerId;\n\n    browserContext.create(\n      {\n        id: workflowId,\n        documentUrlPatterns,\n        contexts: contextTypes,\n        title: data.contextMenuName,\n        parentId: 'automaContextMenu',\n      },\n      () => {\n        const error = browser.runtime.lastError;\n        if (error) {\n          if (error.message.includes('automaContextMenu')) {\n            browserContext.create(\n              {\n                documentUrlPatterns,\n                contexts: ['all'],\n                id: 'automaContextMenu',\n                title: 'Run Automa workflow',\n              },\n              () => {\n                registerContextMenu(workflowId, data)\n                  .then(resolve)\n                  .catch(reject);\n              }\n            );\n            resolve();\n            return;\n          }\n          if (error.message.includes('Duplicate id')) {\n            browserContext.remove(triggerId).then(() => {\n              registerContextMenu(workflowId, data).then(resolve).catch(reject);\n            });\n            return;\n          }\n\n          reject(error.message);\n        } else {\n          if (browserContext.refresh) browserContext.refresh();\n          resolve();\n        }\n      }\n    );\n  });\n}\n\nasync function removeFromWorkflowQueue(workflowId) {\n  const { workflowQueue } = await browser.storage.local.get('workflowQueue');\n  const queueIndex = (workflowQueue || []).findIndex((id) =>\n    id.includes(workflowId)\n  );\n\n  if (!workflowQueue || queueIndex === -1) return;\n\n  workflowQueue.splice(queueIndex, 1);\n\n  await browser.storage.local.set({ workflowQueue });\n}\n\nexport async function cleanWorkflowTriggers(workflowId, triggers) {\n  try {\n    const alarms = await browser.alarms.getAll();\n    for (const alarm of alarms) {\n      if (alarm.name.includes(workflowId)) {\n        await browser.alarms.clear(alarm.name);\n      }\n    }\n\n    const { visitWebTriggers, onStartupTriggers, shortcuts } =\n      await browser.storage.local.get([\n        'shortcuts',\n        'visitWebTriggers',\n        'onStartupTriggers',\n      ]);\n\n    const keyboardShortcuts = Array.isArray(shortcuts) ? {} : shortcuts || {};\n    Object.keys(keyboardShortcuts).forEach((shortcutId) => {\n      if (!shortcutId.includes(workflowId)) return;\n\n      delete keyboardShortcuts[shortcutId];\n    });\n\n    const startupTriggers = (onStartupTriggers || []).filter(\n      (id) => !id.includes(workflowId)\n    );\n    const filteredVisitWebTriggers = visitWebTriggers?.filter(\n      (item) => !item.id.includes(workflowId)\n    );\n\n    await removeFromWorkflowQueue(workflowId);\n\n    await browser.storage.local.set({\n      shortcuts: keyboardShortcuts,\n      onStartupTriggers: startupTriggers,\n      visitWebTriggers: filteredVisitWebTriggers,\n    });\n\n    const browserContextMenu =\n      BROWSER_TYPE === 'firefox' ? browser.menus : browser.contextMenus;\n    const removeFromContextMenu = async () => {\n      try {\n        let promises = [];\n\n        if (triggers) {\n          promises = triggers.map(async (trigger) => {\n            if (trigger.type !== 'context-menu') return;\n\n            const triggerId = `trigger:${workflowId}:${trigger.id}`;\n            await browserContextMenu.remove(triggerId);\n          });\n        }\n\n        promises.push(browserContextMenu.remove(workflowId));\n\n        await Promise.allSettled(promises);\n      } catch (error) {\n        // Do nothing\n      }\n    };\n    if (browserContextMenu) await removeFromContextMenu();\n  } catch (error) {\n    console.error(error);\n  }\n}\n\nexport function registerSpecificDay(workflowId, data) {\n  if (data.days.length === 0) return null;\n\n  const getDate = (dayId, time) => {\n    const [hour, minute, seconds] = time.split(':');\n    const date = dayjs()\n      .day(dayId)\n      .hour(hour)\n      .minute(minute)\n      .second(seconds || 0);\n\n    return date.valueOf();\n  };\n\n  const dates = data.days\n    .reduce((acc, item) => {\n      if (isObject(item)) {\n        item.times.forEach((time) => {\n          acc.push(getDate(item.id, time));\n        });\n      } else {\n        acc.push(getDate(item, data.time));\n      }\n\n      return acc;\n    }, [])\n    .sort();\n\n  const findDate =\n    dates.find((date) => date > Date.now()) ||\n    dayjs(dates[0]).add(7, 'day').valueOf();\n\n  return browser.alarms.create(workflowId, {\n    when: findDate,\n  });\n}\n\nexport function registerInterval(workflowId, data) {\n  const alarmInfo = {\n    periodInMinutes: data.interval,\n  };\n\n  if (data.delay > 0 && !data.fixedDelay) alarmInfo.delayInMinutes = data.delay;\n\n  return browser.alarms.create(workflowId, alarmInfo);\n}\n\nexport async function registerSpecificDate(workflowId, data) {\n  let date = Date.now() + 60000;\n\n  if (data.date) {\n    const [hour, minute, second] = data.time.split(':');\n    date = dayjs(data.date)\n      .hour(hour)\n      .minute(minute)\n      .second(second || 0)\n      .valueOf();\n  }\n\n  if (Date.now() > date) return;\n\n  await browser.alarms.create(workflowId, {\n    when: date,\n  });\n}\n\nexport async function registerVisitWeb(workflowId, data) {\n  try {\n    if (data.url.trim() === '') return;\n\n    const visitWebTriggers =\n      (await browser.storage.local.get('visitWebTriggers'))?.visitWebTriggers ||\n      [];\n\n    const index = visitWebTriggers.findIndex((item) => item.id === workflowId);\n    const payload = {\n      id: workflowId,\n      url: data.url,\n      isRegex: data.isUrlRegex,\n      supportSPA: data.supportSPA ?? false,\n    };\n\n    if (index === -1) {\n      visitWebTriggers.unshift(payload);\n    } else {\n      visitWebTriggers[index] = payload;\n    }\n\n    await browser.storage.local.set({ visitWebTriggers });\n  } catch (error) {\n    console.error(error);\n  }\n}\n\nexport async function registerKeyboardShortcut(workflowId, data) {\n  try {\n    const { shortcuts } = await browser.storage.local.get('shortcuts');\n    const keyboardShortcuts = Array.isArray(shortcuts) ? {} : shortcuts || {};\n\n    keyboardShortcuts[workflowId] = data.shortcut;\n\n    await browser.storage.local.set({ shortcuts: keyboardShortcuts });\n  } catch (error) {\n    console.error(error);\n  }\n}\n\nexport async function registerOnStartup() {\n  // Do nothing\n}\n\nexport async function registerCronJob(workflowId, data) {\n  try {\n    const cronExpression = cronParser.parseExpression(data.expression);\n    const nextSchedule = cronExpression.next();\n\n    await browser.alarms.create(workflowId, { when: nextSchedule.getTime() });\n  } catch (error) {\n    console.error(error);\n  }\n}\n\nexport const workflowTriggersMap = {\n  interval: registerInterval,\n  date: registerSpecificDate,\n  'cron-job': registerCronJob,\n  'visit-web': registerVisitWeb,\n  'on-startup': registerOnStartup,\n  'specific-day': registerSpecificDay,\n  'context-menu': registerContextMenu,\n  'keyboard-shortcut': registerKeyboardShortcut,\n};\n\nexport async function registerWorkflowTrigger(workflowId, { data }) {\n  try {\n    await cleanWorkflowTriggers(workflowId, data && data?.triggers);\n\n    if (data.triggers) {\n      for (const trigger of data.triggers) {\n        const handler = workflowTriggersMap[trigger.type];\n        if (handler)\n          await handler(`trigger:${workflowId}:${trigger.id}`, trigger.data);\n      }\n    } else if (workflowTriggersMap[data.type]) {\n      await workflowTriggersMap[data.type](workflowId, data);\n    }\n  } catch (error) {\n    console.error(error);\n    throw error;\n  }\n}\n\nexport default {\n  cleanUp: cleanWorkflowTriggers,\n  register: registerWorkflowTrigger,\n};\n"
  },
  {
    "path": "src/workflowEngine/WorkflowEngine.js",
    "content": "import dbStorage from '@/db/storage';\nimport BrowserAPIService, {\n  IS_BROWSER_API_AVAILABLE,\n} from '@/service/browser-api/BrowserAPIService';\nimport { fetchApi } from '@/utils/api';\nimport { getBlocks } from '@/utils/getSharedData';\nimport { clearCache, isObject, parseJSON, sleep } from '@/utils/helper';\nimport { MessageListener } from '@/utils/message';\nimport cloneDeep from 'lodash.clonedeep';\nimport { nanoid } from 'nanoid';\nimport WorkflowWorker from './WorkflowWorker';\n\nlet blocks = getBlocks();\n\nclass WorkflowEngine {\n  constructor(workflow, { states, logger, blocksHandler, isPopup, options }) {\n    this.id = nanoid();\n    this.states = states;\n    this.logger = logger;\n    this.workflow = workflow;\n    this.isPopup = isPopup ?? true;\n    // this.isMV2 = IS_MV2;\n    this.blocksHandler = blocksHandler;\n    this.isTestingMode = workflow.testingMode;\n    this.parentWorkflow = options?.parentWorkflow;\n    this.saveLog = workflow.settings?.saveLog ?? true;\n\n    this.workerId = 0;\n    this.workers = new Map();\n\n    this.packagesCache = {};\n    this.extractedGroup = {};\n    this.connectionsMap = {};\n    this.waitConnections = {};\n\n    this.isDestroyed = false;\n    this.isUsingProxy = false;\n    this.isInBreakpoint = false;\n\n    this.triggerBlockId = null;\n\n    this.blocks = {};\n    this.history = [];\n    this.columnsId = {};\n    this.historyCtxData = {};\n    this.eventListeners = {};\n    this.preloadScripts = [];\n\n    this.columns = {\n      column: {\n        index: 0,\n        type: 'any',\n        name: this.workflow.settings?.defaultColumnName || 'column',\n      },\n    };\n    this.rowData = {};\n\n    this.logsLimit = 1001;\n    this.logHistoryId = 0;\n\n    let variables = {};\n    let { globalData } = workflow;\n    if (options && options?.data) {\n      globalData = options.data.globalData || globalData;\n      variables = isObject(options.data.variables)\n        ? options?.data.variables\n        : {};\n\n      options.data = { globalData, variables };\n    }\n    this.options = options;\n\n    this.refDataSnapshots = {};\n    this.refDataSnapshotsKeys = {\n      loopData: {\n        index: 0,\n        key: '##loopData0',\n      },\n      variables: {\n        index: 0,\n        key: '##variables0',\n      },\n    };\n    this.referenceData = {\n      variables,\n      table: [],\n      secrets: {},\n      loopData: {},\n      workflow: {},\n      googleSheets: {},\n      globalData: parseJSON(globalData, globalData),\n    };\n\n    this.onDebugEvent = ({ tabId }, method, params) => {\n      let isActiveTabEvent = false;\n      this.workers.forEach((worker) => {\n        if (isActiveTabEvent) return;\n\n        isActiveTabEvent = worker.activeTab.id === tabId;\n      });\n\n      if (!isActiveTabEvent) return;\n\n      (this.eventListeners[method] || []).forEach((listener) => {\n        listener(params);\n      });\n    };\n    this.onWorkflowStopped = (id) => {\n      if (this.id !== id || this.isDestroyed) return;\n      this.stop();\n    };\n    this.onResumeExecution = ({ id, nextBlock }) => {\n      if (this.id !== id || this.isDestroyed) return;\n\n      this.workers.forEach((worker) => {\n        worker.resume(nextBlock);\n      });\n    };\n\n    // this.messageListener = new MessageListener('workflow-engine');\n  }\n\n  async init() {\n    try {\n      if (this.workflow.isDisabled) return;\n\n      if (!this.states) {\n        console.error(`\"${this.workflow.name}\" workflow doesn't have states`);\n        this.destroy('error');\n        return;\n      }\n\n      const { nodes, edges } = this.workflow.drawflow;\n      if (!nodes || nodes.length === 0) {\n        console.error(`${this.workflow.name} doesn't have blocks`);\n        return;\n      }\n\n      const triggerBlock = nodes.find((node) => {\n        if (this.options?.blockId) return node.id === this.options.blockId;\n\n        return node.label === 'trigger';\n      });\n      if (!triggerBlock) {\n        console.error(`${this.workflow.name} doesn't have a trigger block`);\n        return;\n      }\n\n      if (!this.workflow.settings) {\n        this.workflow.settings = {};\n      }\n\n      blocks = getBlocks();\n\n      const checkParams = this.options?.checkParams ?? true;\n      const hasParams =\n        checkParams && triggerBlock.data?.parameters?.length > 0;\n      if (hasParams) {\n        this.eventListeners = {};\n\n        if (triggerBlock.data.preferParamsInTab) {\n          const [activeTab] = await BrowserAPIService.tabs.query({\n            active: true,\n            url: '*://*/*',\n            lastFocusedWindow: true,\n          });\n          if (activeTab) {\n            const result = await BrowserAPIService.tabs.sendMessage(\n              activeTab.id,\n              {\n                type: 'input-workflow-params',\n                data: {\n                  workflow: this.workflow,\n                  params: triggerBlock.data.parameters,\n                },\n              }\n            );\n\n            if (result) return;\n          }\n        }\n\n        const paramUrl = BrowserAPIService.runtime.getURL('params.html');\n\n        let tabs;\n        if (!IS_BROWSER_API_AVAILABLE) {\n          tabs = await BrowserAPIService.tabs.query({});\n        } else {\n          try {\n            tabs = await BrowserAPIService.tabs.query({});\n            if (!tabs || !Array.isArray(tabs)) {\n              tabs = await MessageListener.sendMessage(\n                'browser-api',\n                { name: 'tabs.query', args: [{}] },\n                'background'\n              );\n            }\n          } catch (e) {\n            tabs = await MessageListener.sendMessage(\n              'browser-api',\n              { name: 'tabs.query', args: [{}] },\n              'background'\n            );\n          }\n        }\n\n        const paramTab = tabs.find((tab) => tab.url?.includes(paramUrl));\n\n        if (paramTab) {\n          await BrowserAPIService.tabs.sendMessage(paramTab.id, {\n            name: 'workflow:params',\n            data: this.workflow,\n          });\n          await BrowserAPIService.windows.update(paramTab.windowId, {\n            focused: true,\n          });\n        } else {\n          let workflowId = '';\n          if (this.workflow.hostId) {\n            workflowId = `hosted:${this.workflow.hostId}`;\n          } else {\n            workflowId = this.workflow.id;\n          }\n\n          BrowserAPIService.windows.create({\n            type: 'popup',\n            width: 480,\n            height: 700,\n            url: BrowserAPIService.runtime.getURL(\n              `/params.html?workflowId=${workflowId}`\n            ),\n          });\n        }\n        return;\n      }\n\n      this.triggerBlockId = triggerBlock.id;\n\n      this.blocks = nodes.reduce((acc, node) => {\n        acc[node.id] = node;\n\n        return acc;\n      }, {});\n      this.connectionsMap = edges.reduce(\n        (acc, { sourceHandle, target, targetHandle }) => {\n          if (!acc[sourceHandle]) acc[sourceHandle] = new Map();\n          acc[sourceHandle].set(target, {\n            id: target,\n            targetHandle,\n            sourceHandle,\n          });\n\n          return acc;\n        },\n        {}\n      );\n\n      const workflowTable =\n        this.workflow.table || this.workflow.dataColumns || [];\n      let columns = Array.isArray(workflowTable)\n        ? workflowTable\n        : Object.values(workflowTable);\n\n      if (this.workflow.connectedTable) {\n        const connectedTable = await dbStorage.tablesItems\n          .where('id')\n          .equals(this.workflow.connectedTable)\n          .first();\n        const connectedTableData = await dbStorage.tablesData\n          .where('tableId')\n          .equals(connectedTable?.id)\n          .first();\n        if (connectedTable && connectedTableData) {\n          columns = Object.values(connectedTable.columns);\n          Object.assign(this.columns, connectedTableData.columnsIndex);\n          this.referenceData.table = connectedTableData.items || [];\n        } else {\n          this.workflow.connectedTable = null;\n        }\n      }\n\n      columns.forEach(({ name, type, id }) => {\n        const columnId = id || name;\n\n        this.rowData[name] = null;\n\n        this.columnsId[name] = columnId;\n        if (!this.columns[columnId])\n          this.columns[columnId] = { index: 0, name, type };\n      });\n\n      if (BROWSER_TYPE !== 'chrome') {\n        this.workflow.settings.debugMode = false;\n      } else if (this.workflow.settings.debugMode) {\n        BrowserAPIService.debugger.onEvent.addListener(this.onDebugEvent);\n      }\n      if (\n        this.workflow.settings.reuseLastState &&\n        !this.workflow.connectedTable\n      ) {\n        const lastStateKey = `state:${this.workflow.id}`;\n        const value = await BrowserAPIService.storage.local.get(lastStateKey);\n        const lastState = value[lastStateKey];\n\n        if (lastState) {\n          Object.assign(this.columns, lastState.columns);\n          Object.assign(this.referenceData, lastState.referenceData);\n        }\n      }\n\n      const { settings: userSettings = {} } =\n        (await BrowserAPIService.storage.local.get('settings')) || {};\n      this.logsLimit = userSettings?.logsLimit || 1001;\n\n      this.workflow.table = columns;\n      this.startedTimestamp = Date.now();\n\n      this.states.on('stop', this.onWorkflowStopped);\n      this.states.on('resume', this.onResumeExecution);\n\n      const credentials = await dbStorage.credentials.toArray();\n      credentials.forEach(({ name, value }) => {\n        this.referenceData.secrets[name] = value;\n      });\n\n      const variables = await dbStorage.variables.toArray();\n      variables.forEach(({ name, value }) => {\n        this.referenceData.variables[`$$${name}`] = value;\n      });\n\n      this.addRefDataSnapshot('variables');\n\n      await this.states.add(this.id, {\n        id: this.id,\n        status: 'running',\n        state: this.state,\n        workflowId: this.workflow.id,\n        parentState: this.parentWorkflow,\n        teamId: this.workflow.teamId || null,\n      });\n\n      this.addWorker({ blockId: triggerBlock.id });\n    } catch (error) {\n      console.error('WorkflowEngine init error:', error);\n    }\n  }\n\n  addRefDataSnapshot(key) {\n    this.refDataSnapshotsKeys[key].index += 1;\n    this.refDataSnapshotsKeys[key].key = key;\n\n    const keyName = this.refDataSnapshotsKeys[key].key;\n    this.refDataSnapshots[keyName] = cloneDeep(this.referenceData[key]);\n  }\n\n  addWorker(detail) {\n    this.workerId += 1;\n\n    const workerId = `worker-${this.workerId}`;\n    const worker = new WorkflowWorker(workerId, this, { blocksDetail: blocks });\n    worker.init(detail);\n\n    this.workers.set(worker.id, worker);\n  }\n\n  addLogHistory(detail) {\n    if (detail.name === 'blocks-group') return;\n\n    const isLimit = this.history?.length >= this.logsLimit;\n    const notErrorLog = detail.type !== 'error';\n\n    if ((isLimit || !this.saveLog) && notErrorLog) return;\n\n    this.logHistoryId += 1;\n    detail.id = this.logHistoryId;\n\n    if (\n      detail.name !== 'delay' ||\n      detail.replacedValue ||\n      detail.name === 'javascript-code' ||\n      (blocks[detail.name]?.refDataKeys && this.saveLog)\n    ) {\n      const { variables, loopData } = this.refDataSnapshotsKeys;\n\n      this.historyCtxData[this.logHistoryId] = {\n        referenceData: {\n          loopData: loopData.key,\n          variables: variables.key,\n          activeTabUrl: detail.activeTabUrl,\n          prevBlockData: detail.prevBlockData || '',\n        },\n        replacedValue: cloneDeep(detail.replacedValue),\n        ...(detail?.ctxData || {}),\n      };\n\n      delete detail.replacedValue;\n    }\n\n    this.history.push(detail);\n  }\n\n  async stop() {\n    try {\n      if (this.childWorkflowId) {\n        await this.states.stop(this.childWorkflowId);\n      }\n\n      await this.destroy('stopped');\n    } catch (error) {\n      console.error(error);\n    }\n  }\n\n  async executeQueue() {\n    const { workflowQueue } = (await BrowserAPIService.storage.local.get(\n      'workflowQueue'\n    )) || { workflowQueue: [] };\n    const queueIndex = (workflowQueue || []).indexOf(this.workflow?.id);\n\n    if (!workflowQueue || queueIndex === -1) return;\n\n    const engine = new WorkflowEngine(this.workflow, {\n      logger: this.logger,\n      states: this.states,\n      blocksHandler: this.blocksHandler,\n    });\n    engine.init();\n\n    workflowQueue.splice(queueIndex, 1);\n\n    await BrowserAPIService.storage.local.set({ workflowQueue });\n  }\n\n  async destroyWorker(workerId) {\n    // is last worker\n    if (this.workers.size === 1 && this.workers.has(workerId)) {\n      this.addLogHistory({\n        type: 'finish',\n        name: 'finish',\n      });\n      this.dispatchEvent('finish');\n      await this.destroy('success');\n    }\n    // wait detach debugger\n    this.workers.delete(workerId);\n\n    // No active workers, destroying workflow\n    if (this.workers.size === 0) {\n      this.destroy('success');\n    }\n  }\n\n  async destroy(status, message, blockDetail) {\n    const cleanUp = () => {\n      this.id = null;\n      this.states = null;\n      this.logger = null;\n      this.saveLog = null;\n      this.workflow = null;\n      this.blocksHandler = null;\n      this.parentWorkflow = null;\n\n      this.isDestroyed = true;\n      this.referenceData = null;\n      this.eventListeners = null;\n      this.packagesCache = null;\n      this.extractedGroup = null;\n      this.connectionsMap = null;\n      this.waitConnections = null;\n      this.blocks = null;\n      this.history = null;\n      this.columnsId = null;\n      this.historyCtxData = null;\n      this.preloadScripts = null;\n    };\n\n    try {\n      if (this.isDestroyed) return;\n      if (this.isUsingProxy) BrowserAPIService.proxy.clearSettings({});\n      if (this.workflow.settings.debugMode && BROWSER_TYPE === 'chrome') {\n        BrowserAPIService.debugger.onEvent.removeListener(this.onDebugEvent);\n\n        await sleep(1000);\n\n        this.workers.forEach((worker) => {\n          if (!worker.debugAttached) return;\n\n          BrowserAPIService.debugger.detach({ tabId: worker.activeTab.id });\n        });\n      }\n\n      const endedTimestamp = Date.now();\n      this.workers.clear();\n      this.executeQueue();\n\n      this.states.off('stop', this.onWorkflowStopped);\n      await this.states.delete(this.id);\n\n      if (!this.workflow.settings?.debugMode) {\n        const { user } = (await BrowserAPIService.storage.local.get(\n          'user'\n        )) || { user: null };\n\n        const logDto = {\n          workflowId: this.workflow.id,\n          workflowName: this.workflow.name,\n          nodesCount: this.workflow.drawflow.nodes.length,\n          status,\n          message: message || '',\n          startedAt: new Date(this.startedTimestamp).toISOString(),\n          endedAt: new Date(endedTimestamp).toISOString(),\n          userId: user?.id,\n        };\n\n        try {\n          const response = await fetchApi('/workflows/logs/report', {\n            method: 'POST',\n            headers: { 'Content-Type': 'application/json' },\n            body: JSON.stringify(logDto),\n            auth: true,\n          });\n\n          if (!response.ok) {\n            throw new Error(`API request failed: ${response.status}`);\n          }\n        } catch (err) {\n          console.error('Failed to report workflow execution:', err);\n        }\n      }\n\n      this.dispatchEvent('destroyed', {\n        status,\n        message,\n        blockDetail,\n        id: this.id,\n        endedTimestamp,\n        history: this.history,\n        startedTimestamp: this.startedTimestamp,\n      });\n\n      if (this.workflow.settings.reuseLastState) {\n        const workflowState = {\n          [`state:${this.workflow.id}`]: {\n            columns: this.columns,\n            referenceData: {\n              table: this.referenceData.table,\n              variables: this.referenceData.variables,\n            },\n          },\n        };\n\n        BrowserAPIService.storage.local.set(workflowState);\n      } else if (status === 'success') {\n        clearCache(this.workflow);\n      }\n\n      const { table, variables } = this.referenceData;\n      const tableId = this.workflow.connectedTable;\n\n      // Merge all table and variables from all workflows\n      Object.values(this.referenceData.workflow).forEach((data) => {\n        Object.assign(table, data.table);\n        Object.assign(variables, data.variables);\n      });\n\n      await dbStorage.transaction(\n        'rw',\n        dbStorage.tablesItems,\n        dbStorage.tablesData,\n        async () => {\n          if (!tableId) return;\n\n          await dbStorage.tablesItems.update(tableId, {\n            modifiedAt: Date.now(),\n            rowsCount: table.length,\n          });\n          await dbStorage.tablesData.where('tableId').equals(tableId).modify({\n            items: table,\n            columnsIndex: this.columns,\n          });\n        }\n      );\n\n      if (!this.workflow?.isTesting) {\n        const { name, id, teamId } = this.workflow;\n\n        await this.logger.add({\n          detail: {\n            name,\n            status,\n            teamId,\n            message,\n            id: this.id,\n            workflowId: id,\n            saveLog: this.saveLog,\n            endedAt: endedTimestamp,\n            parentLog: this.parentWorkflow,\n            startedAt: this.startedTimestamp,\n          },\n          history: {\n            logId: this.id,\n            data: this.saveLog ? this.history : [],\n          },\n          ctxData: {\n            logId: this.id,\n            data: {\n              ctxData: this.historyCtxData,\n              dataSnapshot: this.refDataSnapshots,\n            },\n          },\n          data: {\n            logId: this.id,\n            data: {\n              table: [...this.referenceData.table],\n              variables: { ...this.referenceData.variables },\n            },\n          },\n        });\n      }\n\n      cleanUp();\n    } catch (error) {\n      console.error('workflowEngine error', error);\n      cleanUp();\n    }\n  }\n\n  async updateState(data) {\n    const state = {\n      ...data,\n      tabIds: [],\n      currentBlock: [],\n      name: this.workflow.name,\n      logs: this.history,\n      ctxData: {\n        ctxData: this.historyCtxData,\n        dataSnapshot: this.refDataSnapshots,\n      },\n      startedTimestamp: this.startedTimestamp,\n    };\n\n    this.workers.forEach((worker) => {\n      const { id, label, startedAt } = worker.currentBlock;\n\n      state.currentBlock.push({ id, name: label, startedAt });\n      state.tabIds.push(worker.activeTab.id);\n    });\n\n    await this.states.update(this.id, { state });\n    this.dispatchEvent('update', { state });\n  }\n\n  dispatchEvent(name, params) {\n    const listeners = this.eventListeners[name];\n\n    if (!listeners) return;\n\n    listeners.forEach((callback) => {\n      callback(params);\n    });\n  }\n\n  on(name, listener) {\n    (this.eventListeners[name] = this.eventListeners[name] || []).push(\n      listener\n    );\n  }\n}\n\nexport default WorkflowEngine;\n"
  },
  {
    "path": "src/workflowEngine/WorkflowLogger.js",
    "content": "import dbLogs, { defaultLogItem } from '@/db/logs';\n/* eslint-disable class-methods-use-this */\nclass WorkflowLogger {\n  async add({ detail, history, ctxData, data }) {\n    const logDetail = { ...defaultLogItem, ...detail };\n\n    await Promise.all([\n      dbLogs.logsData.add(data),\n      dbLogs.ctxData.add(ctxData),\n      dbLogs.items.add(logDetail),\n      dbLogs.histories.add(history),\n    ]);\n  }\n}\n\nexport default WorkflowLogger;\n"
  },
  {
    "path": "src/workflowEngine/WorkflowManager.js",
    "content": "import dayjs from '@/lib/dayjs';\nimport BrowserAPIService from '@/service/browser-api/BrowserAPIService';\nimport { fetchApi } from '@/utils/api';\nimport convertWorkflowData from '@/utils/convertWorkflowData';\nimport getBlockMessage from '@/utils/getBlockMessage';\nimport blocksHandler from './blocksHandler';\nimport WorkflowEngine from './WorkflowEngine';\nimport WorkflowEvent from './workflowEvent';\nimport WorkflowLogger from './WorkflowLogger';\nimport WorkflowState from './WorkflowState';\n\nconst workflowStateStorage = {\n  get() {\n    return BrowserAPIService.storage.local\n      .get('workflowStates')\n      .then(({ workflowStates }) => workflowStates || []);\n  },\n  set(key, value) {\n    const states = Object.values(value);\n\n    return BrowserAPIService.storage.local.set({ workflowStates: states });\n  },\n};\n\nclass WorkflowManager {\n  /** @type {WorkflowManager} */\n  static #_instance;\n\n  /**\n   * WorkflowManager singleton\n   * @type {WorkflowManager}\n   */\n  static get instance() {\n    if (!this.#_instance) this.#_instance = new WorkflowManager();\n\n    return this.#_instance;\n  }\n\n  /** @type {WorkflowState} */\n  #state;\n\n  /** @type {WorkflowLogger} */\n  #logger;\n\n  constructor() {\n    this.#logger = new WorkflowLogger();\n    this.#state = new WorkflowState({ storage: workflowStateStorage });\n  }\n\n  execute(workflowData, options) {\n    if (workflowData.testingMode) {\n      for (const value of this.#state.states.values()) {\n        if (value.workflowId === workflowData.id) return null;\n      }\n    }\n\n    const convertedWorkflow = convertWorkflowData(workflowData);\n    const engine = new WorkflowEngine(convertedWorkflow, {\n      options,\n      states: this.#state,\n      logger: this.#logger,\n      blocksHandler: blocksHandler(),\n    });\n\n    engine.init();\n    engine.on('destroyed', ({ id, status, history, blockDetail, ...rest }) => {\n      if (status !== 'stopped') {\n        BrowserAPIService.permissions\n          .contains({ permissions: ['notifications'] })\n          .then((hasPermission) => {\n            if (!hasPermission || !workflowData.settings.notification) return;\n\n            const name = workflowData.name.slice(0, 32);\n\n            BrowserAPIService.notifications.create(`logs:${id}`, {\n              type: 'basic',\n              iconUrl: BrowserAPIService.runtime.getURL('icon-128.png'),\n              title: status === 'success' ? 'Success' : 'Error',\n              message: `${\n                status === 'success' ? 'Successfully' : 'Failed'\n              } ran the \"${name}\" workflow`,\n            });\n          });\n      }\n\n      if (convertedWorkflow.settings?.events) {\n        const workflowHistory = history.map((item) => {\n          delete item.logId;\n          delete item.prevBlockData;\n          delete item.workerId;\n\n          item.description = item.description || '';\n\n          return item;\n        });\n        const workflowRefData = {\n          status,\n          startedAt: rest.startedTimestamp,\n          endedAt: rest.endedTimestamp\n            ? rest.endedTimestamp - rest.startedTimestamp\n            : null,\n          logs: workflowHistory,\n          errorMessage:\n            status === 'error' ? getBlockMessage(blockDetail) : null,\n        };\n\n        convertedWorkflow.settings.events.forEach((event) => {\n          if (status === 'success' && !event.events.includes('finish:success'))\n            return;\n          if (status === 'error' && !event.events.includes('finish:failed'))\n            return;\n\n          WorkflowEvent.handle(event.action, {\n            workflow: workflowRefData,\n            variables: { ...engine.referenceData.variables },\n            globalData: { ...engine.referenceData.globalData },\n          });\n        });\n      }\n    });\n\n    BrowserAPIService.storage.local\n      .get('checkStatus')\n      .then((res) => {\n        const { checkStatus } = res || { checkStatus: null };\n        const isSameDay = checkStatus\n          ? dayjs().isSame(checkStatus, 'day')\n          : false;\n        if (!isSameDay || !checkStatus) {\n          fetchApi('/status')\n            .then((response) => response.json())\n            .then(() => {\n              BrowserAPIService.storage.local.set({\n                checkStatus: new Date().toString(),\n              });\n            })\n            .catch((error) => {\n              console.error('Failed to check status:', error);\n            });\n        }\n      })\n      .catch((error) => {\n        console.error('Failed to get checkStatus:', error);\n      });\n\n    return engine;\n  }\n\n  /**\n   * Stop workflow execution\n   * @param {string} stateId\n   * @returns {Promise<void>}\n   */\n  stopExecution(stateId) {\n    return this.#state.stop(stateId);\n  }\n\n  /**\n   * Resume workflow execution\n   * @param {string} id\n   * @param {object} nextBlock\n   * @returns {Promise<void>}\n   */\n  resumeExecution(id, nextBlock) {\n    return this.#state.resume(id, nextBlock);\n  }\n\n  /**\n   * Resume workflow execution\n   * @param {string} id\n   * @param {object} stateData\n   * @returns {Promise<void>}\n   */\n  updateExecution(id, stateData) {\n    return this.#state.update(id, stateData);\n  }\n}\n\nexport default WorkflowManager;\n"
  },
  {
    "path": "src/workflowEngine/WorkflowState.js",
    "content": "/* eslint-disable  no-param-reassign */\nimport BrowserAPIService from '@/service/browser-api/BrowserAPIService';\n\nclass WorkflowState {\n  constructor({ storage, key = 'workflowState' }) {\n    this.key = key;\n    this.storage = storage;\n\n    this.states = new Map();\n    this.eventListeners = {};\n\n    this.storageTimeout = null;\n  }\n\n  _updateBadge() {\n    BrowserAPIService.browserAction.setBadgeText({\n      text: (this.states.size || '').toString(),\n    });\n  }\n\n  _saveToStorage() {\n    if (this.storageTimeout) return;\n\n    this.storageTimeout = setTimeout(() => {\n      this.storageTimeout = null;\n\n      const states = Object.fromEntries(this.states);\n      this.storage.set(this.key, states);\n    }, 1000);\n  }\n\n  dispatchEvent(name, params) {\n    const listeners = this.eventListeners[name];\n\n    if (!listeners) return;\n\n    listeners.forEach((callback) => {\n      callback(params);\n    });\n  }\n\n  on(name, listener) {\n    (this.eventListeners[name] = this.eventListeners[name] || []).push(\n      listener\n    );\n  }\n\n  off(name, listener) {\n    const listeners = this.eventListeners[name];\n    if (!listeners) return;\n\n    const index = listeners.indexOf(listener);\n    if (index !== -1) listeners.splice(index, 1);\n  }\n\n  get getAll() {\n    return this.states;\n  }\n\n  async get(stateId) {\n    let { states } = this;\n\n    if (typeof stateId === 'function') {\n      states = Array.from(states.entries()).find(({ 1: state }) =>\n        stateId(state)\n      );\n    } else if (stateId) {\n      states = this.states.get(stateId);\n    }\n\n    return states;\n  }\n\n  async add(id, data = {}) {\n    this.states.set(id, data);\n    this._updateBadge();\n    this._saveToStorage(this.key);\n  }\n\n  async stop(id) {\n    const isStateExist = await this.get(id);\n    if (!isStateExist) {\n      await this.delete(id);\n      this.dispatchEvent('stop', id);\n      return id;\n    }\n\n    await this.update(id, { isDestroyed: true });\n    this.dispatchEvent('stop', id);\n    return id;\n  }\n\n  async resume(id, nextBlock) {\n    const state = this.states.get(id);\n    if (!state) return;\n\n    this.states.set(id, {\n      ...state,\n      status: 'running',\n    });\n    this._saveToStorage();\n\n    this.dispatchEvent('resume', { id, nextBlock });\n  }\n\n  async update(id, data = {}) {\n    const state = this.states.get(id);\n    if (!state) return;\n\n    if (data?.state?.status) {\n      state.status = data.state.status;\n      delete data.state.status;\n    }\n\n    this.states.set(id, { ...state, ...data });\n    this.dispatchEvent('update', { id, data });\n    this._saveToStorage();\n  }\n\n  async delete(id) {\n    this.states.delete(id);\n    this.dispatchEvent('delete', id);\n    this._updateBadge();\n    this._saveToStorage();\n  }\n}\n\nexport default WorkflowState;\n"
  },
  {
    "path": "src/workflowEngine/WorkflowWorker.js",
    "content": "import dbStorage from '@/db/storage';\nimport BrowserAPIService from '@/service/browser-api/BrowserAPIService';\nimport {\n  isObject,\n  objectHasKey,\n  parseJSON,\n  sleep,\n  toCamelCase,\n} from '@/utils/helper';\nimport cloneDeep from 'lodash.clonedeep';\nimport { convertData, waitTabLoaded } from './helper';\nimport templating from './templating';\nimport renderString from './templating/renderString';\n\nfunction blockExecutionWrapper(blockHandler, blockData) {\n  return new Promise((resolve, reject) => {\n    let timeout = null;\n    const timeoutMs = blockData?.settings?.blockTimeout;\n    if (timeoutMs && timeoutMs > 0) {\n      timeout = setTimeout(() => {\n        reject(new Error('Timeout'));\n      }, timeoutMs);\n    }\n\n    blockHandler()\n      .then((result) => {\n        resolve(result);\n      })\n      .catch((error) => {\n        reject(error);\n      })\n      .finally(() => {\n        if (timeout) clearTimeout(timeout);\n      });\n  });\n}\n\nclass WorkflowWorker {\n  constructor(id, engine, options = {}) {\n    this.id = id;\n    this.engine = engine;\n    this.settings = engine.workflow.settings;\n    this.blocksDetail = options.blocksDetail || {};\n\n    this.loopEls = [];\n    this.loopList = {};\n    this.repeatedTasks = {};\n    this.preloadScripts = [];\n    this.breakpointState = null;\n\n    this.windowId = null;\n    this.currentBlock = null;\n    this.childWorkflowId = null;\n\n    this.debugAttached = false;\n\n    this.activeTab = {\n      url: '',\n      frameId: 0,\n      frames: {},\n      groupId: null,\n      id: engine.options?.tabId,\n    };\n  }\n\n  init({ blockId, execParam, state }) {\n    if (state) {\n      Object.keys(state).forEach((key) => {\n        this[key] = state[key];\n      });\n    }\n\n    const block = this.engine.blocks[blockId];\n    this.executeBlock(block, execParam);\n  }\n\n  addDataToColumn(key, value) {\n    if (Array.isArray(key)) {\n      key.forEach((item) => {\n        if (!isObject(item)) return;\n\n        Object.entries(item).forEach(([itemKey, itemValue]) => {\n          this.addDataToColumn(itemKey, itemValue);\n        });\n      });\n\n      return;\n    }\n\n    const insertDefault = this.settings.insertDefaultColumn ?? true;\n    const columnId =\n      (this.engine.columns[key] ? key : this.engine.columnsId[key]) || 'column';\n\n    if (columnId === 'column' && !insertDefault) return;\n\n    const currentColumn = this.engine.columns[columnId];\n    const columnName = currentColumn.name || 'column';\n    const convertedValue = convertData(value, currentColumn.type);\n\n    if (objectHasKey(this.engine.referenceData.table, currentColumn.index)) {\n      this.engine.referenceData.table[currentColumn.index][columnName] =\n        convertedValue;\n    } else {\n      this.engine.referenceData.table.push({\n        [columnName]: convertedValue,\n      });\n    }\n\n    currentColumn.index += 1;\n  }\n\n  async setVariable(name, value) {\n    let variableName = name;\n    const vars = this.engine.referenceData.variables;\n\n    if (name.startsWith('$push:')) {\n      const { 1: varName } = name.split('$push:');\n\n      if (!objectHasKey(vars, varName)) vars[varName] = [];\n      else if (!Array.isArray(vars[varName])) vars[varName] = [vars[varName]];\n\n      vars[varName].push(value);\n      variableName = varName;\n    } else {\n      vars[name] = value;\n    }\n\n    if (variableName.startsWith('$$')) {\n      variableName = variableName.slice(2);\n\n      const findStorageVar = await dbStorage.variables.get({\n        name: variableName,\n      });\n\n      if (findStorageVar)\n        await dbStorage.variables.update(findStorageVar.id, { value });\n      else await dbStorage.variables.add({ name: variableName, value });\n    }\n\n    this.engine.addRefDataSnapshot('variables');\n  }\n\n  getBlockConnections(blockId, outputIndex = 1) {\n    if (this.engine.isDestroyed) return null;\n\n    const outputId = `${blockId}-output-${outputIndex}`;\n    const connections = this.engine.connectionsMap[outputId];\n\n    if (!connections) return null;\n\n    return [...connections.values()];\n  }\n\n  executeNextBlocks(\n    connections,\n    prevBlockData,\n    nextBlockBreakpointCount = null\n  ) {\n    // pre check\n    for (const connection of connections) {\n      const id = typeof connection === 'string' ? connection : connection.id;\n\n      const block = this.engine.blocks[id];\n\n      if (!block) {\n        console.error(`Block ${id} doesn't exist`);\n        this.engine.destroy('stopped');\n        return;\n      }\n\n      // pass disabled block\n      // eslint-disable-next-line no-continue\n      if (block.data.disableBlock) continue;\n\n      // check if the next block is breakpoint\n      if (block.data?.$breakpoint) {\n        // set breakpoint state\n        nextBlockBreakpointCount = 0;\n      }\n    }\n\n    connections.forEach((connection, index) => {\n      const { id, targetHandle, sourceHandle } =\n        typeof connection === 'string'\n          ? { id: connection, targetHandle: '', sourceHandle: '' }\n          : connection;\n      const execParam = {\n        prevBlockData,\n        targetHandle,\n        sourceHandle,\n        nextBlockBreakpointCount,\n      };\n\n      if (index === 0) {\n        this.executeBlock(this.engine.blocks[id], {\n          prevBlockData,\n          ...execParam,\n        });\n      } else {\n        const state = cloneDeep({\n          windowId: this.windowId,\n          loopList: this.loopList,\n          activeTab: this.activeTab,\n          currentBlock: this.currentBlock,\n          repeatedTasks: this.repeatedTasks,\n          preloadScripts: this.preloadScripts,\n          debugAttached: this.debugAttached,\n        });\n\n        this.engine.addWorker({\n          state,\n          execParam,\n          blockId: id,\n        });\n      }\n    });\n  }\n\n  resume(nextBlock) {\n    if (!this.breakpointState) return;\n\n    const { block, execParam, isRetry } = this.breakpointState;\n    const payload = { ...execParam, resume: true };\n\n    payload.nextBlockBreakpointCount = nextBlock ? 1 : null;\n\n    this.executeBlock(block, payload, isRetry);\n\n    this.breakpointState = null;\n  }\n\n  async executeBlock(block, execParam = {}, isRetry = false) {\n    const currentState = await this.engine.states.get(this.engine.id);\n\n    if (!currentState || currentState.isDestroyed) {\n      if (this.engine.isDestroyed) return;\n\n      await this.engine.destroy('stopped');\n      return;\n    }\n\n    const startExecuteTime = Date.now();\n    const prevBlock = this.currentBlock;\n    this.currentBlock = { ...block, startedAt: startExecuteTime };\n\n    const isInBreakpoint =\n      this.engine.isTestingMode &&\n      ((block.data?.$breakpoint && !execParam.resume) ||\n        execParam.nextBlockBreakpointCount === 0);\n\n    if (!isRetry) {\n      const payload = {\n        activeTabUrl: this.activeTab.url,\n        childWorkflowId: this.childWorkflowId,\n        nextBlockBreakpoint: Boolean(execParam.nextBlockBreakpointCount),\n      };\n      if (isInBreakpoint && currentState.status !== 'breakpoint')\n        payload.status = 'breakpoint';\n\n      await this.engine.updateState(payload);\n    }\n\n    if (execParam.nextBlockBreakpointCount) {\n      execParam.nextBlockBreakpointCount -= 1;\n    }\n\n    if (isInBreakpoint || currentState.status === 'breakpoint') {\n      this.engine.isInBreakpoint = true;\n      this.breakpointState = { block, execParam, isRetry };\n\n      return;\n    }\n\n    const blockHandler = this.engine.blocksHandler[toCamelCase(block.label)];\n    const handler =\n      !blockHandler && this.blocksDetail[block.label].category === 'interaction'\n        ? this.engine.blocksHandler.interactionBlock\n        : blockHandler;\n\n    if (!handler) {\n      console.error(`${block.label} doesn't have handler`);\n      this.engine.destroy('stopped');\n      return;\n    }\n\n    const { prevBlockData } = execParam;\n    const refData = {\n      prevBlockData,\n      ...this.engine.referenceData,\n      activeTabUrl: this.activeTab.url,\n    };\n\n    const replacedBlock = await templating({\n      block,\n      data: refData,\n      isPopup: this.engine.isPopup,\n      refKeys:\n        isRetry || block.data.disableBlock\n          ? null\n          : this.blocksDetail[block.label].refDataKeys,\n    });\n\n    const blockDelay = this.settings?.blockDelay || 0;\n    const addBlockLog = (status, obj = {}) => {\n      let { description } = block.data;\n\n      if (block.label === 'loop-breakpoint') description = block.data.loopId;\n      else if (block.label === 'block-package') description = block.data.name;\n\n      this.engine.addLogHistory({\n        description,\n        prevBlockData,\n        type: status,\n        name: block.label,\n        blockId: block.id,\n        workerId: this.id,\n        timestamp: startExecuteTime,\n        activeTabUrl: this.activeTab?.url,\n        replacedValue: replacedBlock.replacedValue,\n        duration: Math.round(Date.now() - startExecuteTime),\n        ...obj,\n      });\n    };\n\n    const executeBlocks = (blocks, data) => {\n      return this.executeNextBlocks(\n        blocks,\n        data,\n        execParam.nextBlockBreakpointCount\n      );\n    };\n\n    try {\n      let result;\n\n      if (block.data.disableBlock) {\n        result = {\n          data: '',\n          nextBlockId: this.getBlockConnections(block.id),\n        };\n      } else {\n        const bindedHandler = handler.bind(this, replacedBlock, {\n          refData,\n          prevBlock,\n          ...(execParam || {}),\n        });\n        result = await blockExecutionWrapper(bindedHandler, block.data);\n\n        if (this.engine.isDestroyed) return;\n\n        if (result.replacedValue) {\n          replacedBlock.replacedValue = result.replacedValue;\n        }\n\n        addBlockLog(result.status || 'success', {\n          logId: result.logId,\n          ctxData: result?.ctxData,\n        });\n      }\n\n      if (result.nextBlockId && !result.destroyWorker) {\n        if (blockDelay > 0) {\n          setTimeout(() => {\n            executeBlocks(result.nextBlockId, result.data);\n          }, blockDelay);\n        } else {\n          executeBlocks(result.nextBlockId, result.data);\n        }\n      } else {\n        this.engine.destroyWorker(this.id);\n      }\n    } catch (error) {\n      console.error(error);\n\n      const errorLogData = {\n        message: error.message,\n        ...(error.data || {}),\n        ...(error.ctxData || {}),\n      };\n\n      const { onError: blockOnError } = replacedBlock.data;\n      if (blockOnError && blockOnError.enable) {\n        if (blockOnError.retry && blockOnError.retryTimes) {\n          await sleep(blockOnError.retryInterval * 1000);\n          blockOnError.retryTimes -= 1;\n          await this.executeBlock(replacedBlock, execParam, true);\n\n          return;\n        }\n\n        if (blockOnError.insertData) {\n          for (const item of blockOnError.dataToInsert) {\n            let value = (\n              await renderString(item.value, refData, this.engine.isPopup)\n            )?.value;\n            value = parseJSON(value, value);\n\n            if (item.type === 'variable') {\n              await this.setVariable(item.name, value);\n            } else {\n              this.addDataToColumn(item.name, value);\n            }\n          }\n        }\n\n        const nextBlocks = this.getBlockConnections(\n          block.id,\n          blockOnError.toDo === 'continue' ? 1 : 'fallback'\n        );\n        if (blockOnError.toDo !== 'error' && nextBlocks) {\n          addBlockLog('error', errorLogData);\n\n          executeBlocks(nextBlocks, prevBlockData);\n\n          return;\n        }\n\n        // 抛出错误并且存在自定义的错误信息\n        if (blockOnError.toDo === 'error' && blockOnError.errorMessage.trim()) {\n          errorLogData.message = blockOnError.errorMessage;\n          error.message = blockOnError.errorMessage;\n        }\n      }\n\n      const errorLogItem = errorLogData;\n      addBlockLog('error', errorLogItem);\n\n      errorLogItem.blockId = block.id;\n\n      const { onError } = this.settings;\n      const nodeConnections = this.getBlockConnections(block.id);\n\n      if (onError === 'keep-running' && nodeConnections) {\n        setTimeout(() => {\n          executeBlocks(nodeConnections, error.data || '');\n        }, blockDelay);\n      } else if (onError === 'restart-workflow' && !this.parentWorkflow) {\n        const restartCount = this.engine.restartWorkersCount[this.id] || 0;\n        const maxRestart = this.settings.restartTimes ?? 3;\n\n        if (restartCount >= maxRestart) {\n          delete this.engine.restartWorkersCount[this.id];\n          this.engine.destroy('error', error.message, errorLogItem);\n          return;\n        }\n\n        this.reset();\n\n        const triggerBlock = this.engine.blocks[this.engine.triggerBlockId];\n        if (triggerBlock) this.executeBlock(triggerBlock, execParam);\n\n        this.engine.restartWorkersCount[this.id] = restartCount + 1;\n      } else {\n        this.engine.destroy('error', error.message, errorLogItem);\n      }\n    }\n  }\n\n  reset() {\n    this.loopList = {};\n    this.repeatedTasks = {};\n\n    this.windowId = null;\n    this.currentBlock = null;\n    this.childWorkflowId = null;\n\n    this.engine.history = [];\n    this.engine.preloadScripts = [];\n    this.engine.columns = {\n      column: {\n        index: 0,\n        type: 'any',\n        name: this.settings?.defaultColumnName || 'column',\n      },\n    };\n\n    this.activeTab = {\n      url: '',\n      frameId: 0,\n      frames: {},\n      groupId: null,\n      id: this.options?.tabId,\n    };\n    this.engine.referenceData = {\n      table: [],\n      loopData: {},\n      workflow: {},\n      googleSheets: {},\n      variables: this.engine.options?.variables || {},\n      globalData: this.engine.referenceData.globalData,\n    };\n  }\n\n  async _sendMessageToTab(payload, options = {}, runBeforeLoad = false) {\n    try {\n      if (!this.activeTab.id) {\n        const error = new Error('no-tab');\n        error.workflowId = this.id;\n\n        throw error;\n      }\n\n      if (!runBeforeLoad) {\n        await waitTabLoaded({\n          tabId: this.activeTab.id,\n          ms: this.settings?.tabLoadTimeout ?? 30000,\n        });\n      }\n\n      const { executedBlockOnWeb, debugMode } = this.settings;\n      const messagePayload = {\n        isBlock: true,\n        debugMode,\n        executedBlockOnWeb,\n        loopEls: this.loopEls,\n        activeTabId: this.activeTab.id,\n        frameSelector: this.frameSelector,\n        ...payload,\n      };\n      const data = await BrowserAPIService.tabs.sendMessage(\n        this.activeTab.id,\n        messagePayload,\n        { frameId: this.activeTab.frameId, ...options }\n      );\n\n      return data;\n    } catch (error) {\n      console.error(error);\n      const noConnection = error.message?.includes(\n        'Could not establish connection'\n      );\n      const channelClosed = error.message?.includes('message channel closed');\n\n      if (noConnection || channelClosed) {\n        const isScriptInjected = await BrowserAPIService.contentScript.inject({\n          file: './contentScript.bundle.js',\n          target: {\n            tabId: this.activeTab.id,\n            frameId: this.activeTab.frameId,\n          },\n          waitUntilInjected: true,\n        });\n\n        if (isScriptInjected) {\n          const result = await this._sendMessageToTab(\n            payload,\n            options,\n            runBeforeLoad\n          );\n          return result;\n        }\n        error.message = 'Could not establish connection to the active tab';\n      } else if (error.message?.startsWith('No tab')) {\n        error.message = 'active-tab-removed';\n      }\n\n      throw error;\n    }\n  }\n}\n\nexport default WorkflowWorker;\n"
  },
  {
    "path": "src/workflowEngine/blocksHandler/handlerActiveTab.js",
    "content": "import { sleep } from '@/utils/helper';\nimport BrowserAPIService from '@/service/browser-api/BrowserAPIService';\nimport { attachDebugger, injectPreloadScript } from '../helper';\n\nasync function activeTab(block) {\n  try {\n    const data = {\n      data: '',\n      nextBlockId: this.getBlockConnections(block.id),\n    };\n\n    if (this.activeTab.id) {\n      await BrowserAPIService.tabs.update(this.activeTab.id, { active: true });\n      return data;\n    }\n\n    const tabsQuery = {\n      active: true,\n      url: ['*://*/*', 'file://*'],\n    };\n\n    if (BROWSER_TYPE === 'firefox') {\n      tabsQuery.currentWindow = true;\n    } else if (this.engine.isPopup) {\n      let windowId = null;\n      const extURL = BrowserAPIService.runtime.getURL('');\n      const windows = await BrowserAPIService.windows.getAll({\n        populate: true,\n      });\n      for (const browserWindow of windows) {\n        const [tab] = browserWindow.tabs;\n        const isDashboard =\n          browserWindow.tabs.length === 1 && tab.url?.includes(extURL);\n\n        if (isDashboard) {\n          await BrowserAPIService.windows.update(browserWindow.id, {\n            focused: false,\n          });\n        } else if (browserWindow.focused) {\n          windowId = browserWindow.id;\n        }\n      }\n\n      if (windowId) tabsQuery.windowId = windowId;\n      else if (windows.length > 2) tabsQuery.lastFocusedWindow = true;\n    } else {\n      const dashboardTabs = await BrowserAPIService.tabs.query({\n        url: BrowserAPIService.runtime.getURL('/newtab.html'),\n      });\n      await Promise.all(\n        dashboardTabs.map((item) =>\n          BrowserAPIService.windows.update(item.windowId, {\n            focused: false,\n          })\n        )\n      );\n\n      tabsQuery.currentWindow = true;\n    }\n\n    const [tab] = await BrowserAPIService.tabs.query(tabsQuery);\n    if (!tab) {\n      throw new Error(\"Can't find active tab\");\n    }\n    const isHttpUrl = tab?.url?.startsWith('http');\n    const isFileUrl = tab?.url?.startsWith('file://');\n    if (!isHttpUrl && !isFileUrl) {\n      const error = new Error('invalid-active-tab');\n      error.data = { url: tab?.url };\n\n      throw error;\n    }\n\n    this.activeTab = {\n      ...this.activeTab,\n      frameId: 0,\n      id: tab.id,\n      url: tab.url,\n    };\n    this.windowId = tab.windowId;\n\n    if (this.settings.debugMode) {\n      await attachDebugger(tab.id, this.activeTab.id);\n      this.debugAttached = true;\n    }\n\n    if (this.preloadScripts.length > 0) {\n      if (this.engine.isMV2) {\n        await this._sendMessageToTab({\n          isPreloadScripts: true,\n          label: 'javascript-code',\n          data: { scripts: this.preloadScripts },\n        });\n      } else {\n        await injectPreloadScript({\n          scripts: this.preloadScripts,\n          frameSelector: this.frameSelector,\n          target: {\n            tabId: this.activeTab.id,\n            frameIds: [this.activeTab.frameId || 0],\n          },\n        });\n      }\n    }\n\n    await BrowserAPIService.tabs.update(tab.id, { active: true });\n    await BrowserAPIService.windows.update(tab.windowId, { focused: true });\n\n    await sleep(200);\n\n    return data;\n  } catch (error) {\n    console.error(error);\n    error.data = error.data || {};\n\n    throw error;\n  }\n}\n\nexport default activeTab;\n"
  },
  {
    "path": "src/workflowEngine/blocksHandler/handlerAiWorkflow.js",
    "content": "import { postRunAPWorkflow } from '@/utils/getAIPoweredInfo';\nimport renderString from '../templating/renderString';\n\nasync function aiWorkflow(block, { refData }) {\n  const {\n    flowUuid,\n    inputs,\n    assignVariable,\n    variableName,\n    saveData,\n    dataColumn,\n  } = block.data;\n\n  const replacedValueList = {};\n  const aipowerToken = this.engine.workflow.settings?.aipowerToken;\n\n  if (!aipowerToken) {\n    throw new Error('AI Power token is not set');\n  }\n\n  const inputForAPI = {};\n  for (const item of inputs) {\n    if (typeof item.value === 'object' && item.value !== null) {\n      // For file objects, we don't render them as strings.\n      // We assume they contain the necessary structure like { filename, url }.\n      inputForAPI[item.name] = item.value;\n    } else {\n      // For strings, we render them using the templating engine.\n      const renderedValue = await renderString(\n        item.value,\n        refData,\n        this.engine.isPopup\n      );\n      inputForAPI[item.name] = renderedValue.value;\n      Object.assign(replacedValueList, renderedValue.list);\n    }\n  }\n\n  try {\n    const runResponse = await postRunAPWorkflow(\n      { flowUuid, input: inputForAPI },\n      aipowerToken\n    );\n    const { success, msg } = runResponse;\n\n    if (!success) {\n      throw new Error(msg || 'AI workflow execution failed');\n    }\n\n    if (assignVariable) {\n      this.setVariable(variableName, runResponse.data.result);\n    }\n\n    if (saveData) {\n      this.addDataToColumn(dataColumn, runResponse.data.result);\n    }\n\n    const nextBlockId = this.getBlockConnections(block.id);\n\n    return {\n      data: runResponse.data.result,\n      nextBlockId,\n      replacedValue: replacedValueList,\n    };\n  } catch (error) {\n    console.error('AI workflow execution failed:', error);\n    throw new Error(error.message);\n  }\n}\n\nexport default aiWorkflow;\n"
  },
  {
    "path": "src/workflowEngine/blocksHandler/handlerBlockPackage.js",
    "content": "export default async function (\n  { data, id },\n  { targetHandle: prevTarget, prevBlockData }\n) {\n  if (!this.engine.packagesCache[id]) {\n    this.engine.packagesCache[id] = { extracted: false, nodes: {} };\n  }\n\n  const pkgCache = this.engine.packagesCache[id];\n\n  const { 1: targetId } = prevTarget.split('input-');\n  const addBlockPrefix = (itemId) => `${id}__${itemId}`;\n  const hasCache = pkgCache.nodes[targetId];\n  if (hasCache)\n    return {\n      data: prevBlockData,\n      nextBlockId: [{ id: hasCache }],\n    };\n\n  const input = data.inputs.find((item) => item.id === targetId);\n  if (!input) {\n    throw new Error('Input not found');\n  }\n  const block = data.data.nodes.find((node) => node.id === input.blockId);\n  pkgCache.nodes[targetId] = addBlockPrefix(block.id);\n\n  const connections = {};\n\n  if (!pkgCache.extracted) {\n    const outputsMap = new Set();\n\n    data.inputs.forEach((item) => {\n      connections[addBlockPrefix(item.id)] = new Map([\n        [\n          item.id,\n          {\n            id: addBlockPrefix(item.blockId),\n            targetId: `${addBlockPrefix(block.id)}-input-1`,\n          },\n        ],\n      ]);\n    });\n    data.outputs.forEach((output) => {\n      const connection =\n        this.engine.connectionsMap[`${id}-output-${output.id}`];\n      if (!connection) return;\n\n      connections[addBlockPrefix(output.handleId)] = new Map(connection);\n      outputsMap.add(output.handleId);\n    });\n\n    data.data.nodes.forEach((node) => {\n      const newNodeId = addBlockPrefix(node.id);\n      this.engine.blocks[newNodeId] = { ...node, id: newNodeId };\n    });\n\n    if (!block) {\n      throw new Error(`Can't find block for this input`);\n    }\n\n    data.data.edges.forEach(({ sourceHandle, target, targetHandle }) => {\n      if (outputsMap.has(sourceHandle)) return;\n\n      const nodeSourceHandle = addBlockPrefix(sourceHandle);\n      if (!connections[nodeSourceHandle])\n        connections[nodeSourceHandle] = new Map();\n\n      const connectionId = addBlockPrefix(target);\n      connections[nodeSourceHandle].set(connectionId, {\n        id: connectionId,\n        sourceHandle: nodeSourceHandle,\n        targetHandle: addBlockPrefix(targetHandle),\n      });\n    });\n\n    pkgCache.extracted = true;\n  }\n\n  Object.assign(this.engine.connectionsMap, connections);\n\n  return {\n    data: prevBlockData,\n    nextBlockId: [{ id: addBlockPrefix(block.id) }],\n  };\n}\n"
  },
  {
    "path": "src/workflowEngine/blocksHandler/handlerBlocksGroup.js",
    "content": "function blocksGroup({ data, id }, { prevBlockData }) {\n  return new Promise((resolve) => {\n    const nextBlockId = this.getBlockConnections(id);\n\n    if (data.blocks.length === 0) {\n      resolve({\n        nextBlockId,\n        data: prevBlockData,\n      });\n\n      return;\n    }\n\n    const { blocks, connections } = data.blocks.reduce(\n      (acc, block, index) => {\n        const nextBlock = data.blocks[index + 1]?.itemId;\n\n        acc.blocks[block.itemId] = {\n          label: block.id,\n          data: block.data,\n          id: nextBlock ? block.itemId : id,\n        };\n\n        if (nextBlock) {\n          const outputId = `${block.itemId}-output-1`;\n\n          if (!acc.connections[outputId]) {\n            acc.connections[outputId] = new Map();\n          }\n          acc.connections[outputId].set(nextBlock, { id: nextBlock });\n        }\n\n        return acc;\n      },\n      { blocks: {}, connections: {} }\n    );\n\n    Object.assign(this.engine.blocks, blocks);\n    Object.assign(this.engine.connectionsMap, connections);\n\n    resolve({\n      data: prevBlockData,\n      nextBlockId: [{ id: data.blocks[0].itemId }],\n    });\n  });\n}\n\nexport default blocksGroup;\n"
  },
  {
    "path": "src/workflowEngine/blocksHandler/handlerBrowserEvent.js",
    "content": "import { isWhitespace } from '@/utils/helper';\nimport BrowserAPIService from '@/service/browser-api/BrowserAPIService';\n\nfunction handleEventListener(target, validate) {\n  return (data, activeTab) => {\n    return new Promise((resolve) => {\n      let resolved = false;\n      const eventListener = (event) => {\n        if (resolved) return;\n        if (validate && !validate(event, { data, activeTab })) return;\n\n        target.removeListener(eventListener);\n        resolve(event);\n      };\n\n      setTimeout(() => {\n        resolved = true;\n        target.removeListener(eventListener);\n        resolve('');\n      }, data.timeout || 10000);\n\n      target.addListener(eventListener);\n    });\n  };\n}\n\nfunction onTabLoaded({ tabLoadedUrl, activeTabLoaded, timeout }, { id }) {\n  return new Promise((resolve, reject) => {\n    let resolved = false;\n\n    const checkActiveTabStatus = () => {\n      if (resolved) return;\n      if (!id) {\n        reject(new Error('no-tab'));\n        return;\n      }\n\n      BrowserAPIService.tabs\n        .get(id)\n        .then((tab) => {\n          if (tab.status === 'complete') {\n            resolve();\n            return;\n          }\n\n          setTimeout(checkActiveTabStatus, 1000);\n        })\n        .catch(reject);\n    };\n\n    const url = isWhitespace(tabLoadedUrl)\n      ? '<all_urls>'\n      : tabLoadedUrl.replace(/\\s/g, '').split(',');\n    const checkTabsStatus = () => {\n      BrowserAPIService.tabs\n        .query({\n          url,\n          status: 'loading',\n        })\n        .then((tabs) => {\n          if (resolved) return;\n          if (tabs.length === 0) {\n            resolve();\n            return;\n          }\n\n          setTimeout(checkTabsStatus, 1000);\n        })\n        .catch(reject);\n    };\n\n    if (activeTabLoaded) checkActiveTabStatus();\n    else checkTabsStatus();\n\n    setTimeout(() => {\n      resolved = true;\n      reject(new Error('timeout'));\n    }, timeout || 10000);\n  });\n}\n\nconst validateCreatedTab = ({ url }, { data }) => {\n  if (!isWhitespace(data.tabUrl)) {\n    const regex = new RegExp(data.tabUrl, 'gi');\n\n    if (!regex.test(url)) return false;\n  }\n\n  return true;\n};\nconst events = {\n  'tab:loaded': onTabLoaded,\n  'tab:close': handleEventListener(BrowserAPIService.tabs.onRemoved),\n  'tab:create': handleEventListener(\n    BrowserAPIService.webNavigation.onCreatedNavigationTarget,\n    validateCreatedTab\n  ),\n  'window:create': handleEventListener(\n    BrowserAPIService.webNavigation.onCreatedNavigationTarget,\n    validateCreatedTab\n  ),\n  'window:close': handleEventListener(BrowserAPIService.windows.onRemoved),\n};\n\nexport default async function ({ data, id }) {\n  const currentEvent = events[data.eventName];\n\n  if (!currentEvent) {\n    throw new Error(`Can't find ${data.eventName} event`);\n  }\n\n  const result = await currentEvent(data, this.activeTab);\n\n  if (data.eventName === 'tab:create' && data.setAsActiveTab) {\n    this.activeTab.id = result.tabId;\n    this.activeTab.url = result.url;\n  }\n\n  return {\n    data: result || '',\n    nextBlockId: this.getBlockConnections(id),\n  };\n}\n"
  },
  {
    "path": "src/workflowEngine/blocksHandler/handlerClipboard.js",
    "content": "import { IS_FIREFOX } from '@/common/utils/constant';\nimport BrowserAPIService from '@/service/browser-api/BrowserAPIService';\nimport { copyTextToClipboard } from '../helper';\n\nfunction doCommand(command, value) {\n  const textarea = document.createElement('textarea');\n  document.body.appendChild(textarea);\n\n  if (command === 'paste') {\n    textarea.focus();\n    document.execCommand('paste');\n    value = textarea.value;\n  } else if (command === 'copy') {\n    textarea.value = value;\n    textarea.select();\n    document.execCommand('copy');\n    textarea.blur();\n  }\n\n  textarea.remove();\n\n  return value;\n}\n\nexport default async function ({ data, id, label }) {\n  if (!IS_FIREFOX && !this.engine?.isPopup && !this.engine?.isMV2)\n    throw new Error('Clipboard block is not supported in background execution');\n\n  const permissions = ['clipboardRead'];\n  if (IS_FIREFOX) {\n    permissions.push('clipboardWrite');\n  }\n\n  const hasPermission = await BrowserAPIService.permissions.contains({\n    permissions,\n  });\n\n  if (!hasPermission) {\n    throw new Error('no-clipboard-acces');\n  }\n\n  let valueToReturn = '';\n\n  if (!data.type || data.type === 'get') {\n    const copiedText = doCommand('paste');\n    valueToReturn = copiedText;\n\n    if (data.assignVariable) {\n      await this.setVariable(data.variableName, copiedText);\n    }\n    if (data.saveData) {\n      this.addDataToColumn(data.dataColumn, copiedText);\n    }\n  } else if (data.type === 'insert') {\n    let text = '';\n\n    if (data.copySelectedText) {\n      if (!this.activeTab.id) throw new Error('no-tab');\n\n      text = await this._sendMessageToTab({\n        id,\n        label,\n      });\n    } else {\n      text = data.dataToCopy;\n    }\n\n    valueToReturn = text;\n\n    if (IS_FIREFOX) {\n      await copyTextToClipboard(text);\n    } else {\n      doCommand('copy', text);\n    }\n  }\n\n  return {\n    data: valueToReturn,\n    nextBlockId: this.getBlockConnections(id),\n  };\n}\n"
  },
  {
    "path": "src/workflowEngine/blocksHandler/handlerCloseTab.js",
    "content": "import BrowserAPIService from '@/service/browser-api/BrowserAPIService';\n\nasync function closeWindow(data, windowId) {\n  const windowIds = [];\n\n  if (data.allWindows) {\n    const windows = await BrowserAPIService.windows.getAll();\n\n    windows.forEach(({ id }) => {\n      windowIds.push(id);\n    });\n  } else {\n    let currentWindowId;\n\n    if (windowId && typeof windowId === 'number') {\n      currentWindowId = windowId;\n    } else {\n      currentWindowId = (await BrowserAPIService.windows.getCurrent()).id;\n    }\n\n    windowIds.push(currentWindowId);\n  }\n\n  await Promise.allSettled(\n    windowIds.map((id) => BrowserAPIService.windows.remove(id))\n  );\n}\n\nasync function closeTab(data, tabId) {\n  let tabIds;\n\n  if (data.activeTab && tabId) {\n    tabIds = tabId;\n  } else if (data.url) {\n    tabIds = (await BrowserAPIService.tabs.query({ url: data.url })).map(\n      (tab) => tab.id\n    );\n  }\n\n  if (tabIds) await BrowserAPIService.tabs.remove(tabIds);\n}\n\nexport default async function ({ data, id }) {\n  if (data.closeType === 'window') {\n    await closeWindow(data, this.windowId);\n\n    this.windowId = null;\n  } else {\n    await closeTab(data, this.activeTab.id);\n\n    if (data.activeTab) {\n      this.activeTab.id = null;\n    }\n  }\n\n  return {\n    data: '',\n    nextBlockId: this.getBlockConnections(id),\n  };\n}\n"
  },
  {
    "path": "src/workflowEngine/blocksHandler/handlerConditions.js",
    "content": "import compareBlockValue from '@/utils/compareBlockValue';\nimport testConditions from '../utils/testConditions';\nimport renderString from '../templating/renderString';\nimport checkCodeCondition from '../utils/conditionCode';\n\nconst isMV2 = false;\n\nfunction checkConditions(data, conditionOptions) {\n  return new Promise((resolve, reject) => {\n    let retryCount = 1;\n    const replacedValue = {};\n\n    const testAllConditions = async () => {\n      try {\n        for (let index = 0; index < data.conditions.length; index += 1) {\n          const result = await testConditions(\n            data.conditions[index].conditions,\n            conditionOptions\n          );\n\n          Object.assign(replacedValue, result?.replacedValue || {});\n\n          if (result.isMatch) {\n            resolve({ match: true, index, replacedValue });\n            return;\n          }\n        }\n\n        if (data.retryConditions && retryCount <= data.retryCount) {\n          retryCount += 1;\n\n          setTimeout(() => {\n            testAllConditions();\n          }, data.retryTimeout);\n        } else {\n          resolve({ match: false, replacedValue });\n        }\n      } catch (error) {\n        reject(error);\n      }\n    };\n\n    testAllConditions();\n  });\n}\n\nasync function conditions({ data, id }, { prevBlockData, refData }) {\n  if (data.conditions.length === 0) {\n    throw new Error('conditions-empty');\n  }\n\n  let resultData = '';\n  let isConditionMet = false;\n  let outputId = 'fallback';\n\n  const replacedValue = {};\n  const condition = data.conditions[0];\n  const prevData = Array.isArray(prevBlockData)\n    ? prevBlockData[0]\n    : prevBlockData;\n\n  const { debugMode } = this.engine.workflow?.settings || {};\n\n  if (condition && condition.conditions) {\n    const conditionPayload = {\n      isMV2,\n      refData,\n      isPopup: this.engine.isPopup,\n      checkCodeCondition: (payload) => {\n        payload.debugMode = debugMode;\n        return checkCodeCondition(this.activeTab, payload);\n      },\n      sendMessage: (payload) =>\n        this._sendMessageToTab({ ...payload.data, label: 'conditions', id }),\n    };\n\n    const conditionsResult = await checkConditions(data, conditionPayload);\n\n    if (conditionsResult.replacedValue) {\n      Object.assign(replacedValue, conditionsResult.replacedValue);\n    }\n    if (conditionsResult.match) {\n      isConditionMet = true;\n      outputId = data.conditions[conditionsResult.index].id;\n    }\n  } else {\n    for (const { type, value, compareValue, id: itemId } of data.conditions) {\n      if (isConditionMet) break;\n\n      const firstValue = (\n        await renderString(\n          compareValue ?? prevData,\n          refData,\n          this.engine.isPopup\n        )\n      ).value;\n      const secondValue = (\n        await renderString(value, refData, this.engine.isPopup)\n      ).value;\n\n      Object.assign(replacedValue, firstValue.list, secondValue.list);\n\n      const isMatch = compareBlockValue(\n        type,\n        firstValue.value,\n        secondValue.value\n      );\n\n      if (isMatch) {\n        outputId = itemId;\n        resultData = value;\n        isConditionMet = true;\n      }\n    }\n  }\n\n  return {\n    replacedValue,\n    data: resultData,\n    nextBlockId: this.getBlockConnections(id, outputId),\n  };\n}\n\nexport default conditions;\n"
  },
  {
    "path": "src/workflowEngine/blocksHandler/handlerCookie.js",
    "content": "import BrowserAPIService from '@/service/browser-api/BrowserAPIService';\nimport { parseJSON } from '@/utils/helper';\n\nfunction getValues(data, keys) {\n  const values = {};\n  keys.forEach((key) => {\n    const value = data[key];\n\n    if (!value) return;\n\n    values[key] = value;\n  });\n\n  return values;\n}\n\nconst keys = {\n  get: ['name', 'url'],\n  remove: ['name', 'url'],\n  getAll: ['domain', 'name', 'path', 'secure', 'url'],\n  set: [\n    'name',\n    'url',\n    'expirationDate',\n    'domain',\n    'path',\n    'sameSite',\n    'secure',\n    'url',\n    'value',\n    'httpOnly',\n  ],\n};\n\nasync function cookie({ data, id }) {\n  const hasPermission = await BrowserAPIService.permissions.contains({\n    permissions: ['cookies'],\n  });\n\n  if (!hasPermission) {\n    const error = new Error('no-permission');\n    error.data = { permission: 'cookies' };\n\n    throw error;\n  }\n\n  let key = data.type;\n  if (key === 'get' && data.getAll) key = 'getAll';\n\n  let result = null;\n\n  if (data.useJson) {\n    const obj = parseJSON(data.jsonCode, null);\n    if (!obj) throw new Error('Invalid JSON format');\n\n    result = await BrowserAPIService.cookies[key](obj);\n  } else {\n    const values = getValues(data, keys[key]);\n    if (values.expirationDate) {\n      values.expirationDate = Date.now() / 1000 + +values.expirationDate;\n    }\n\n    if (data.type === 'remove' && !data.name) {\n      const cookies = await BrowserAPIService.cookies.getAll({ url: data.url });\n      const removePromise = cookies.map(({ name }) =>\n        BrowserAPIService.cookies.remove({ name, url: data.url })\n      );\n      await Promise.allSettled(removePromise);\n\n      result = cookies;\n    } else {\n      result = await BrowserAPIService.cookies[key](values);\n    }\n  }\n\n  if (data.type === 'get') {\n    if (data.assignVariable) {\n      await this.setVariable(data.variableName, result);\n    }\n    if (data.saveData) {\n      this.addDataToColumn(data.dataColumn, result);\n    }\n  }\n\n  return {\n    data: result,\n    nextBlockId: this.getBlockConnections(id),\n  };\n}\n\nexport default cookie;\n"
  },
  {
    "path": "src/workflowEngine/blocksHandler/handlerCreateElement.js",
    "content": "import { MessageListener } from '@/utils/message';\nimport { customAlphabet } from 'nanoid/non-secure';\nimport { automaRefDataStr, checkCSPAndInject } from '../helper';\n\nconst nanoid = customAlphabet('1234567890abcdef', 5);\n\nfunction getAutomaScript(refData) {\n  const varName = `automa${nanoid()}`;\n\n  const str = `\nconst ${varName} = ${JSON.stringify(refData)};\n${automaRefDataStr(varName)}\nfunction automaSetVariable(name, value) {\n  const variables = ${varName}.variables;\n  if (!variables) ${varName}.variables = {}\n\n  ${varName}.variables[name] = value;\n}\nfunction automaExecWorkflow(options = {}) {\n  window.dispatchEvent(new CustomEvent('automa:execute-workflow', { detail: options }));\n}\n  `;\n\n  return str;\n}\n\nfunction createElementScript(code, blockId, $automaScript, $preloadScripts) {\n  // fixme: 这里是有问题的，如果按现在方案会至少创建两个script标签，\n  // 一个是preloadScripts，一个是automaScript\n  // 那么在现在的方案中 VM 可能导致不共享 window 变量\n  const str = `\n    const baseId = 'automa-${blockId}';\n\n    ${JSON.stringify($preloadScripts)}.forEach((item) => {\n      if (item.type === 'style') return;\n\n      const script = document.createElement(item.type);\n      script.id = \\`\\${baseId}-script\\`;\n      script.textContent = item.script;\n\n      document.body.appendChild(script);\n    });\n\n    const script = document.createElement('script');\n    script.id = \\`\\${baseId}-javascript\\`;\n    script.textContent = \\`(() => { ${$automaScript}\\n${code} })()\\`;\n\n    document.body.appendChild(script);\n  `;\n  return str;\n}\n\nasync function handleCreateElement(block, { refData }) {\n  if (!this.activeTab.id) throw new Error('no-tab');\n\n  const { data } = block;\n  const preloadScriptsPromise = await Promise.allSettled(\n    data.preloadScripts.map((item) => {\n      if (!item.src.startsWith('http'))\n        return Promise.reject(new Error('Invalid URL'));\n\n      return fetch(item.src)\n        .then((response) => response.text())\n        .then((result) => ({ type: item.type, script: result }));\n    })\n  );\n  const preloadScripts = preloadScriptsPromise.reduce((acc, item) => {\n    if (item.status === 'rejected') return acc;\n\n    acc.push(item.value);\n\n    return acc;\n  }, []);\n\n  data.preloadScripts = preloadScripts;\n\n  // (data.javascript || data.preloadScripts.length > 0) &&\n  const isMV3 = !this.engine.isMV2;\n  const payload = {\n    ...block,\n    data: {\n      ...data,\n      automaScript: getAutomaScript({ ...refData, secrets: {} }),\n    },\n    preloadCSS: data.preloadScripts.filter((item) => item.type === 'style'),\n  };\n\n  if (isMV3) {\n    payload.data.dontInjectJS = true;\n  }\n\n  await this._sendMessageToTab(payload, {}, data.runBeforeLoad ?? false);\n\n  if (isMV3) {\n    const target = {\n      tabId: this.activeTab.id,\n      frameIds: [this.activeTab.frameId || 0],\n    };\n\n    const { debugMode = false } = this.engine.workflow?.settings || {};\n\n    // 创建一个简单的回调函数，直接返回要执行的代码\n    const callbackFunction = `\n      function() {\n        // 创建一个包含所有预加载脚本的代码块\n        const preloadScripts = ${JSON.stringify(preloadScripts)};\n        const blockId = \"${block.id}\";\n        const automaScript = ${JSON.stringify(\n          payload.data?.automaScript || ''\n        )};\n        const javascript = ${JSON.stringify(data.javascript || '')};\n\n        // 返回一个完整的、可执行的JavaScript代码字符串\n        return \\`\n          // 执行预加载脚本\n          \\${preloadScripts.map(item => {\n            if (item.type === 'style') return '';\n            return \\`try { eval(\\${JSON.stringify(item.script)}); } catch(e) { console.error('预加载脚本执行错误:', e); }\\`;\n          }).join('\\\\n')}\n\n          // 执行主要JavaScript代码\n          try {\n            (function() {\n              \\${automaScript}\n              \\${javascript}\n            })();\n          } catch(error) {\n            console.error('执行JavaScript代码时出错:', error);\n          }\n\n          // 返回一个值，确保不是undefined\n          true;\n        \\`;\n      }\n    `;\n\n    // 使用checkCSPAndInject函数处理CSP限制\n    const result = await checkCSPAndInject(\n      {\n        target,\n        debugMode,\n        options: {\n          awaitPromise: false,\n          returnByValue: true, // 确保返回值是JavaScript代码字符串\n        },\n      },\n      callbackFunction\n    );\n\n    if (!result.isBlocked) {\n      const jsCode = createElementScript(\n        data.javascript,\n        block.id,\n        payload.data?.automaScript || '',\n        preloadScripts || []\n      );\n      await MessageListener.sendMessage(\n        'script:execute-callback',\n        {\n          target,\n          callback: jsCode,\n        },\n        'background'\n      );\n    } else {\n      console.log('CreateElement: 使用CSP绕过方法执行代码');\n    }\n  }\n\n  return {\n    data: '',\n    nextBlockId: this.getBlockConnections(block.id),\n  };\n}\n\nexport default handleCreateElement;\n"
  },
  {
    "path": "src/workflowEngine/blocksHandler/handlerDataMapping.js",
    "content": "import objectPath from 'object-path';\nimport { objectHasKey, isObject } from '@/utils/helper';\n\nfunction mapData(data, sources) {\n  const mappedData = {};\n\n  sources.forEach((source) => {\n    const dataExist = objectPath.has(data, source.name);\n    if (!dataExist) return;\n\n    const value = objectPath.get(data, source.name);\n\n    source.destinations.forEach(({ name }) => {\n      objectPath.set(mappedData, name, value);\n    });\n  });\n\n  return mappedData;\n}\n\nexport async function dataMapping({ id, data }) {\n  let dataToMap = null;\n\n  if (data.dataSource === 'table') {\n    dataToMap = this.engine.referenceData.table;\n  } else if (data.dataSource === 'variable') {\n    const { variables } = this.engine.referenceData;\n\n    if (!objectHasKey(variables, data.varSourceName)) {\n      throw new Error(`Cant find \"${data.varSourceName}\" variable`);\n    }\n\n    dataToMap = variables[data.varSourceName];\n  }\n\n  if (!isObject(dataToMap) && !Array.isArray(dataToMap)) {\n    const dataType = dataToMap === null ? 'null' : typeof dataToMap;\n\n    throw new Error(`Can't map data with \"${dataType}\" data type`);\n  }\n\n  if (isObject(dataToMap)) {\n    dataToMap = mapData(dataToMap, data.sources);\n  } else {\n    dataToMap = dataToMap.map((item) => mapData(item, data.sources));\n  }\n\n  if (data.assignVariable) {\n    await this.setVariable(data.variableName, dataToMap);\n  }\n  if (data.saveData) {\n    this.addDataToColumn(data.dataColumn, dataToMap);\n  }\n\n  return {\n    data: dataToMap,\n    nextBlockId: this.getBlockConnections(id),\n  };\n}\n\nexport default dataMapping;\n"
  },
  {
    "path": "src/workflowEngine/blocksHandler/handlerDelay.js",
    "content": "function delay(block) {\n  return new Promise((resolve) => {\n    const delayTime = +block.data.time || 500;\n    setTimeout(() => {\n      resolve({\n        data: '',\n        nextBlockId: this.getBlockConnections(block.id),\n      });\n    }, delayTime);\n  });\n}\n\nexport default delay;\n"
  },
  {
    "path": "src/workflowEngine/blocksHandler/handlerDeleteData.js",
    "content": "function deleteData({ data, id }) {\n  return new Promise((resolve) => {\n    let variableDeleted = false;\n\n    data.deleteList.forEach((item) => {\n      if (item.type === 'table') {\n        if (item.columnId === '[all]') {\n          this.engine.referenceData.table = [];\n\n          Object.keys(this.engine.columns).forEach((key) => {\n            this.engine.columns[key].index = 0;\n          });\n        } else {\n          const columnName = this.engine.columns[item.columnId].name;\n\n          this.engine.referenceData.table.forEach((_, index) => {\n            const row = this.engine.referenceData.table[index];\n            delete row[columnName];\n\n            if (!row || Object.keys(row).length === 0) {\n              this.engine.referenceData.table[index] = {};\n            }\n          });\n\n          this.engine.columns[item.columnId].index = 0;\n        }\n      } else if (item.variableName) {\n        delete this.engine.referenceData.variables[item.variableName];\n        variableDeleted = true;\n      }\n    });\n\n    if (variableDeleted) this.engine.addRefDataSnapshot('variables');\n\n    resolve({\n      data: '',\n      nextBlockId: this.getBlockConnections(id),\n    });\n  });\n}\n\nexport default deleteData;\n"
  },
  {
    "path": "src/workflowEngine/blocksHandler/handlerElementExists.js",
    "content": "function elementExists(block) {\n  return new Promise((resolve, reject) => {\n    this._sendMessageToTab(block)\n      .then((data) => {\n        if (!data && block.data.throwError) {\n          const error = new Error('element-not-found');\n          error.data = { selector: block.data.selector };\n\n          reject(error);\n          return;\n        }\n\n        resolve({\n          data,\n          nextBlockId: this.getBlockConnections(block.id, data ? 1 : 2),\n        });\n      })\n      .catch((error) => {\n        reject(error);\n      });\n  });\n}\n\nexport default elementExists;\n"
  },
  {
    "path": "src/workflowEngine/blocksHandler/handlerExecuteWorkflow.js",
    "content": "import { isWhitespace, parseJSON } from '@/utils/helper';\nimport decryptFlow, { getWorkflowPass } from '@/utils/decryptFlow';\nimport convertWorkflowData from '@/utils/convertWorkflowData';\nimport { nanoid } from 'nanoid';\nimport BrowserAPIService from '@/service/browser-api/BrowserAPIService';\nimport WorkflowEngine from '../WorkflowEngine';\n\nfunction workflowListener(workflow, options) {\n  return new Promise((resolve, reject) => {\n    if (workflow.isProtected) {\n      const flow = parseJSON(workflow.drawflow, null);\n\n      if (!flow) {\n        const pass = getWorkflowPass(workflow.pass);\n\n        workflow.drawflow = decryptFlow(workflow, pass);\n      }\n    }\n\n    const engine = new WorkflowEngine(workflow, options);\n    engine.init();\n    engine.on('destroyed', ({ id, status, message }) => {\n      options.events.onDestroyed(engine);\n\n      if (status === 'error') {\n        const error = new Error(message);\n        error.data = { logId: id };\n\n        reject(error);\n        return;\n      }\n\n      resolve({ id, status, message });\n    });\n\n    options.events.onInit(engine);\n  });\n}\n\nfunction findWorkflow(workflows, workflowId) {\n  const workflow = Array.isArray(workflows)\n    ? workflows.find(({ id }) => id === workflowId)\n    : workflows[workflowId];\n\n  return workflow;\n}\n\nasync function executeWorkflow({ id: blockId, data }, { refData }) {\n  if (data.workflowId === '') throw new Error('empty-workflow');\n\n  const { workflows, teamWorkflows } =\n    await BrowserAPIService.storage.local.get(['workflows', 'teamWorkflows']);\n  let workflow = null;\n\n  if (data.workflowId.startsWith('team')) {\n    const teamWorkflowsArr = Object.values(\n      Object.values(teamWorkflows || {})[0] ?? {}\n    );\n    workflow = findWorkflow(teamWorkflowsArr, data.workflowId);\n  } else {\n    workflow = findWorkflow(workflows, data.workflowId);\n  }\n\n  if (!workflow) {\n    const errorInstance = new Error('no-workflow');\n    errorInstance.data = { workflowId: data.workflowId };\n\n    throw errorInstance;\n  }\n\n  workflow = convertWorkflowData(workflow);\n  const optionsParams = { variables: {} };\n\n  if (workflow.testingMode) workflow.testingMode = false;\n\n  if (data.insertAllGlobalData) {\n    optionsParams.globalData = refData.globalData;\n  }\n\n  if (!isWhitespace(data.globalData)) {\n    // shallow copy\n    optionsParams.globalData = {\n      ...optionsParams.globalData,\n      ...JSON.parse(data.globalData),\n    };\n  }\n\n  if (data.insertAllVars) {\n    optionsParams.variables = JSON.parse(\n      JSON.stringify(this.engine.referenceData.variables)\n    );\n  } else if (data.insertVars) {\n    const varsName = data.insertVars.split(',');\n    varsName.forEach((name) => {\n      const varName = name.trim();\n      const value = this.engine.referenceData.variables[varName];\n\n      if (!value && typeof value !== 'boolean') return;\n\n      optionsParams.variables[varName] = value;\n    });\n  }\n\n  const options = {\n    options: {\n      data: optionsParams,\n      parentWorkflow: {\n        id: this.engine.id,\n        name: this.engine.workflow.name,\n      },\n    },\n    events: {\n      onInit: (engine) => {\n        this.childWorkflowId = engine.id;\n      },\n      onDestroyed: (engine) => {\n        const { variables, table } = engine.referenceData;\n\n        this.engine.referenceData.workflow[\n          data.executeId || `${engine.id}-${nanoid(8)}`\n        ] = {\n          table,\n          variables,\n        };\n      },\n    },\n    states: this.engine.states,\n    logger: this.engine.logger,\n    blocksHandler: this.engine.blocksHandler,\n  };\n\n  const isWorkflowIncluded = workflow.drawflow.nodes.some(\n    (node) =>\n      node.label === 'execute-workflow' &&\n      node.data.workflowId === this.engine.workflow.id\n  );\n  if (isWorkflowIncluded) {\n    throw new Error('workflow-infinite-loop');\n  }\n\n  const result = await workflowListener(workflow, options);\n\n  return {\n    data: '',\n    logId: result.id,\n    nextBlockId: this.getBlockConnections(blockId),\n  };\n}\n\nexport default executeWorkflow;\n"
  },
  {
    "path": "src/workflowEngine/blocksHandler/handlerExportData.js",
    "content": "import { default as dataExporter, files } from '@/utils/dataExporter';\nimport BrowserAPIService from '@/service/browser-api/BrowserAPIService';\n\nfunction blobToBase64(blob) {\n  return new Promise((resolve) => {\n    const reader = new FileReader();\n    reader.onloadend = () => resolve(reader.result);\n    reader.readAsDataURL(blob);\n  });\n}\n\nasync function exportData({ data, id }, { refData }) {\n  const dataToExport = data.dataToExport || 'data-columns';\n  let payload = refData.table;\n\n  if (dataToExport === 'google-sheets') {\n    payload = refData.googleSheets[data.refKey] || [];\n  } else if (dataToExport === 'variable') {\n    payload = refData.variables[data.variableName] || [];\n\n    if (!Array.isArray(payload)) {\n      payload = [payload];\n\n      if (data.type === 'csv' && typeof payload[0] !== 'object')\n        payload = [payload];\n    }\n  }\n\n  const isDOMAvailable = typeof document !== 'undefined';\n  let blobUrl = dataExporter(payload, {\n    ...data,\n    csvOptions: {\n      delimiter: data.csvDelimiter || ',',\n    },\n    returnUrl: !isDOMAvailable,\n  });\n\n  const hasDownloadAccess =\n    !isDOMAvailable &&\n    (await BrowserAPIService.permissions.contains({\n      permissions: ['downloads'],\n    }));\n  if (hasDownloadAccess) {\n    blobUrl = await blobToBase64(blobUrl);\n\n    const filename = `${data.name || 'unnamed'}${files[data.type].ext}`;\n    const options = {\n      filename,\n      conflictAction: data.onConflict || 'uniquify',\n    };\n\n    await BrowserAPIService.downloads.download({\n      ...options,\n      url: blobUrl,\n    });\n  }\n\n  return {\n    data: '',\n    nextBlockId: this.getBlockConnections(id),\n  };\n}\n\nexport default exportData;\n"
  },
  {
    "path": "src/workflowEngine/blocksHandler/handlerForwardPage.js",
    "content": "import BrowserAPIService from '@/service/browser-api/BrowserAPIService';\n\nexport async function goBack({ id }) {\n  if (!this.activeTab.id) throw new Error('no-tab');\n\n  await BrowserAPIService.tabs.goForward(this.activeTab.id);\n\n  return {\n    data: '',\n    nextBlockId: this.getBlockConnections(id),\n  };\n}\n\nexport default goBack;\n"
  },
  {
    "path": "src/workflowEngine/blocksHandler/handlerGoBack.js",
    "content": "import BrowserAPIService from '@/service/browser-api/BrowserAPIService';\n\nexport async function goBack({ id }) {\n  if (!this.activeTab.id) throw new Error('no-tab');\n\n  await BrowserAPIService.tabs.goBack(this.activeTab.id);\n\n  return {\n    data: '',\n    nextBlockId: this.getBlockConnections(id),\n  };\n}\n\nexport default goBack;\n"
  },
  {
    "path": "src/workflowEngine/blocksHandler/handlerGoogleDrive.js",
    "content": "import { fetchGapi, validateOauthToken } from '@/utils/api';\nimport getFile from '@/utils/getFile';\nimport BrowserAPIService from '@/service/browser-api/BrowserAPIService';\nimport renderString from '../templating/renderString';\n\nfunction getFilename(url) {\n  try {\n    const filename = new URL(url).pathname.split('/').pop();\n    const hasExtension = /\\.[0-9a-z]+$/i.test(filename);\n\n    if (!hasExtension) return null;\n\n    return filename;\n  } catch (e) {\n    return null;\n  }\n}\n\nexport async function googleDrive({ id, data }, { refData }) {\n  const { sessionToken } = await BrowserAPIService.storage.local.get(\n    'sessionToken'\n  );\n  if (!sessionToken) throw new Error(\"You haven't connect Google Drive\");\n\n  await validateOauthToken();\n\n  const resultPromise = data.filePaths.map(async (item) => {\n    let path = (await renderString(item.path, refData, this.engine.isPopup))\n      .value;\n    if (item.type === 'downloadId') {\n      const [downloadItem] = await BrowserAPIService.downloads.search({\n        id: +path,\n        exists: true,\n        state: 'complete',\n      });\n      if (!downloadItem || !downloadItem.filename)\n        throw new Error(`Can't find download item with \"${item.path}\" id`);\n\n      path = downloadItem.filename;\n    }\n\n    const name =\n      (await renderString(item.name || '', refData, this.engine.isPopup))\n        .value || getFilename(path);\n\n    const blob = await getFile(path, { returnValue: true });\n    const { response: sessionResponse } = await fetchGapi(\n      'https://www.googleapis.com/upload/drive/v3/files?uploadType=resumable',\n      {\n        method: 'POST',\n        headers: {\n          'Content-Type': 'application/json',\n        },\n        body: JSON.stringify({\n          name,\n          mimeType: blob.type,\n        }),\n      },\n      { response: true }\n    );\n    const locationUri = sessionResponse.headers.get('location');\n\n    const buffer = await new Promise((resolve) => {\n      const reader = new FileReader();\n      reader.onload = () => {\n        resolve(reader.result);\n      };\n      reader.readAsArrayBuffer(blob);\n    });\n\n    const result = await fetchGapi(locationUri, {\n      method: 'PUT',\n      headers: {\n        'Content-Length': blob.size,\n      },\n      body: buffer,\n    });\n\n    return result;\n  });\n  const result = await Promise.all(resultPromise);\n\n  return {\n    data: result,\n    nextBlockId: this.getBlockConnections(id),\n  };\n}\n\nexport default googleDrive;\n"
  },
  {
    "path": "src/workflowEngine/blocksHandler/handlerGoogleSheets.js",
    "content": "import googleSheetsApi from '@/utils/googleSheetsApi';\nimport {\n  convert2DArrayToArrayObj,\n  convertArrObjTo2DArr,\n  isWhitespace,\n  parseJSON,\n} from '@/utils/helper';\n\nasync function getSpreadsheetValues({\n  spreadsheetId,\n  range,\n  firstRowAsKey,\n  isDriveSheet,\n}) {\n  const response = await googleSheetsApi(isDriveSheet).getValues({\n    spreadsheetId,\n    range,\n  });\n\n  const result = isDriveSheet ? response : await response.json();\n  if (!isDriveSheet && !response.ok) {\n    throw new Error(result.message);\n  }\n\n  const sheetsData = firstRowAsKey\n    ? convert2DArrayToArrayObj(result.values)\n    : result.values;\n\n  return sheetsData;\n}\nasync function getSpreadsheetRange({ spreadsheetId, range, isDriveSheet }) {\n  const response = await googleSheetsApi(isDriveSheet).getRange({\n    spreadsheetId,\n    range,\n  });\n\n  const result = isDriveSheet ? response : await response.json();\n  if (!isDriveSheet && !response.ok) {\n    throw new Error(result.message);\n  }\n\n  const data = {\n    tableRange: result.tableRange || null,\n    lastRange: result.updates.updatedRange,\n  };\n\n  return data;\n}\nasync function clearSpreadsheetValues({ spreadsheetId, range, isDriveSheet }) {\n  const response = await googleSheetsApi(isDriveSheet).clearValues({\n    spreadsheetId,\n    range,\n  });\n\n  const result = isDriveSheet ? response : await response.json();\n  if (!isDriveSheet && !response.ok) {\n    throw new Error(result.message);\n  }\n\n  return result;\n}\nasync function updateSpreadsheetValues(\n  {\n    range,\n    append,\n    dataFrom,\n    customData,\n    isDriveSheet,\n    spreadsheetId,\n    keysAsFirstRow,\n    insertDataOption,\n    valueInputOption,\n  },\n  columns\n) {\n  let values = [];\n  if (['data-columns', 'table'].includes(dataFrom)) {\n    if (keysAsFirstRow) {\n      values = convertArrObjTo2DArr(columns);\n    } else {\n      values = columns.map((item) => Object.values(item));\n    }\n  } else if (dataFrom === 'custom') {\n    values = parseJSON(customData, customData);\n  }\n\n  if (Array.isArray(values)) {\n    const validTypes = ['boolean', 'string', 'number'];\n    values.forEach((row, rowIndex) => {\n      row.forEach((column, colIndex) => {\n        if (column && validTypes.includes(typeof column)) return;\n\n        values[rowIndex][colIndex] = ' ';\n      });\n    });\n  }\n\n  const queries = {\n    valueInputOption: valueInputOption || 'RAW',\n  };\n\n  if (append) {\n    Object.assign(queries, {\n      includeValuesInResponse: false,\n      insertDataOption: insertDataOption || 'INSERT_ROWS',\n    });\n  }\n\n  const response = await googleSheetsApi(isDriveSheet).updateValues({\n    range,\n    append,\n    spreadsheetId,\n    options: {\n      queries,\n      body: JSON.stringify({ values }),\n    },\n  });\n\n  const result = isDriveSheet ? response : await response.json();\n  if (!isDriveSheet && !response.ok) {\n    throw new Error(result.message);\n  }\n}\n\nexport default async function ({ data, id }, { refData }) {\n  const isNotCreateAction = !['create', 'add-sheet'].includes(data.type);\n\n  if (isWhitespace(data.spreadsheetId) && isNotCreateAction)\n    throw new Error('empty-spreadsheet-id');\n  if (isWhitespace(data.range) && isNotCreateAction)\n    throw new Error('empty-spreadsheet-range');\n\n  let result = [];\n  const handleUpdate = async (append = false) => {\n    result = await updateSpreadsheetValues(\n      {\n        ...data,\n        append,\n      },\n      refData.table\n    );\n  };\n  const actionHandlers = {\n    get: async () => {\n      const spreadsheetValues = await getSpreadsheetValues(data);\n\n      result = spreadsheetValues;\n\n      if (data.refKey && !isWhitespace(data.refKey)) {\n        refData.googleSheets[data.refKey] = spreadsheetValues;\n      }\n    },\n    getRange: async () => {\n      result = await getSpreadsheetRange(data);\n\n      if (data.assignVariable) {\n        await this.setVariable(data.variableName, result);\n      }\n      if (data.saveData) {\n        this.addDataToColumn(data.dataColumn, result);\n      }\n    },\n    update: () => handleUpdate(),\n    append: () => handleUpdate(true),\n    clear: async () => {\n      result = await clearSpreadsheetValues(data);\n    },\n    create: async () => {\n      const { spreadsheetId } = await googleSheetsApi(true).create(\n        data.sheetName\n      );\n      result = spreadsheetId;\n\n      if (data.assignVariable) {\n        await this.setVariable(data.variableName, result);\n      }\n      if (data.saveData) {\n        this.addDataToColumn(data.dataColumn, result);\n      }\n    },\n    'add-sheet': async () => {\n      result = await googleSheetsApi(true).addSheet(data);\n      result = result.replies[0].addSheet.properties;\n    },\n  };\n  await actionHandlers[data.type]();\n\n  return {\n    data: result,\n    nextBlockId: this.getBlockConnections(id),\n  };\n}\n"
  },
  {
    "path": "src/workflowEngine/blocksHandler/handlerGoogleSheetsDrive.js",
    "content": "import handlerGoogleSheets from './handlerGoogleSheets';\n\nexport default function (blockData, additionalData) {\n  blockData.data.isDriveSheet = true;\n  return handlerGoogleSheets.call(this, blockData, additionalData);\n}\n"
  },
  {
    "path": "src/workflowEngine/blocksHandler/handlerHandleDialog.js",
    "content": "import { MessageListener } from '@/utils/message';\nimport { checkCSPAndInject, sendDebugCommand } from '../helper';\n\nconst overwriteDialog = (accept, promptText) => `\n  const realConfirm = window.confirm;\n  window.confirm = function() {\n    return ${accept};\n  };\n\n  const realAlert = window.alert;\n  window.alert = function() {\n    return ${accept};\n  };\n\n  const realPrompt = window.prompt;\n  window.prompt = function() {\n    return ${accept} ? \"${promptText}\" : null;\n  }\n`;\n\nasync function handleDialog({ data, id: blockId }) {\n  if (!this.settings.debugMode || BROWSER_TYPE !== 'chrome') {\n    const isScriptExist = this.preloadScripts.some(({ id }) => id === blockId);\n\n    if (!isScriptExist) {\n      const jsCode = overwriteDialog(data.accept, data.promptText);\n      const payload = {\n        id: blockId,\n        isBlock: true,\n        name: 'javascript-code',\n        isPreloadScripts: true,\n        data: {\n          everyNewTab: true,\n          scripts: [{ data: { code: jsCode }, id: blockId }],\n        },\n      };\n\n      if (this.engine.isMV2) {\n        this.preloadScripts.push(payload);\n        await this._sendMessageToTab(payload, {}, true);\n      } else {\n        const target = { tabId: this.activeTab.id, allFrames: true };\n        const { debugMode } = this.engine.workflow.settings;\n        const cspResult = await checkCSPAndInject({\n          target,\n          debugMode,\n          injectOptions: {\n            injectImmediately: true,\n          },\n        });\n        if (!cspResult.isBlocked) {\n          MessageListener.sendMessage(\n            'script:execute-callback',\n            {\n              target,\n              callback: jsCode,\n            },\n            'background'\n          );\n        }\n      }\n    }\n  } else {\n    this.dialogParams = {\n      accept: data.accept,\n      promptText: data.promptText,\n    };\n\n    const methodName = 'Page.javascriptDialogOpening';\n    if (!this.engine.eventListeners[methodName]) {\n      this.engine.on(methodName, () => {\n        sendDebugCommand(\n          this.activeTab.id,\n          'Page.handleJavaScriptDialog',\n          this.dialogParams\n        );\n      });\n    }\n  }\n\n  return {\n    data: '',\n    nextBlockId: this.getBlockConnections(blockId),\n  };\n}\n\nexport default handleDialog;\n"
  },
  {
    "path": "src/workflowEngine/blocksHandler/handlerHandleDownload.js",
    "content": "import BrowserAPIService from '@/service/browser-api/BrowserAPIService';\nimport { sendMessageWithCallbacks } from '@/utils/callbackBridge';\nimport { MessageListener } from '@/utils/message';\nimport browser from 'webextension-polyfill';\n\nconst DOWNLOADS_STORAGE_KEY = 'automa-rename-downloaded-files';\n\n/**\n *\n * @returns {Promise<Object>}\n */\nasync function getDownloadFilesFromStorage() {\n  try {\n    const result = await browser.storage.session.get(DOWNLOADS_STORAGE_KEY);\n    return result[DOWNLOADS_STORAGE_KEY] || {};\n  } catch (error) {\n    console.error('Failed to get downloads from storage:', error);\n    return {};\n  }\n}\n\n/**\n *\n * @param {Object} filesData\n */\nasync function saveDownloadFilesToStorage(filesData) {\n  try {\n    await browser.storage.session.set({\n      [DOWNLOADS_STORAGE_KEY]: filesData,\n    });\n  } catch (error) {\n    console.error('Failed to save downloads to storage:', error);\n  }\n}\n\n/**\n *\n * @param {number} downloadId\n */\nasync function removeDownloadFromStorage(downloadId) {\n  try {\n    const filesData = await getDownloadFilesFromStorage();\n    delete filesData[downloadId];\n    await saveDownloadFilesToStorage(filesData);\n  } catch (error) {\n    console.error('Failed to remove download from storage:', error);\n  }\n}\n\n/**\n * env: background\n */\nasync function registerDownloadListeners() {\n  try {\n    const hasPermission = await BrowserAPIService.permissions.contains({\n      permissions: ['downloads'],\n    });\n\n    if (!hasPermission) {\n      const granted = await BrowserAPIService.permissions.request({\n        permissions: ['downloads'],\n      });\n      if (!granted) {\n        throw new Error('Download feature requires download permission');\n      }\n    }\n\n    return MessageListener.sendMessage(\n      'downloads:register-listeners',\n      null,\n      'background'\n    );\n  } catch (error) {\n    console.error('Failed to register download listeners:', error);\n\n    throw error;\n  }\n}\n\nasync function handleDownload({ data, id: blockId }) {\n  const nextBlockId = this.getBlockConnections(blockId);\n\n  try {\n    const hasPermission = await BrowserAPIService.permissions.contains({\n      permissions: ['downloads'],\n    });\n    if (!hasPermission) {\n      const granted = await BrowserAPIService.permissions.request({\n        permissions: ['downloads'],\n      });\n      if (!granted) {\n        throw new Error('Download feature requires download permission');\n      }\n    }\n\n    const processedData = {\n      ...data,\n      filename: data.filename?.trim() || '',\n    };\n\n    let downloadId = null;\n    if (processedData.downloadId?.trim()) {\n      if (Number.isNaN(+processedData.downloadId))\n        throw new Error('Download id is not a number');\n\n      const [downloadItem] = await BrowserAPIService.downloads.search({\n        id: +processedData.downloadId,\n      });\n\n      if (!downloadItem)\n        throw new Error(\n          `Can't find download item with ${processedData.downloadId} id`\n        );\n\n      if (downloadItem.state === 'complete') {\n        if (processedData.saveData) {\n          this.addDataToColumn(processedData.dataColumn, downloadItem.filename);\n        }\n        if (processedData.assignVariable) {\n          await this.setVariable(\n            processedData.variableName,\n            downloadItem.filename\n          );\n        }\n\n        return {\n          nextBlockId,\n          data: downloadItem.filename,\n        };\n      }\n\n      downloadId = +processedData.downloadId;\n    }\n\n    await registerDownloadListeners();\n\n    const result = await new Promise((resolve) => {\n      if (!this.activeTab.id) throw new Error('no-tab');\n\n      (async () => {\n        try {\n          if (!downloadId) {\n            const downloadCompletePromise = new Promise((completeResolve) => {\n              sendMessageWithCallbacks(\n                'downloads:watch-created',\n                {\n                  downloadData: processedData,\n                  tabId: this.activeTab.id,\n\n                  onComplete: (response) => {\n                    completeResolve(response);\n                  },\n                },\n                'background'\n              ).catch((err) => {\n                completeResolve({ error: true, message: err.message });\n              });\n            });\n\n            if (!processedData.waitForDownload) {\n              resolve({\n                nextBlockId,\n                data: processedData.filename,\n              });\n              return;\n            }\n\n            const timeoutPromise = new Promise((timeoutResolve) => {\n              setTimeout(() => {\n                timeoutResolve({\n                  timedOut: true,\n                  filename: processedData.filename,\n                });\n              }, processedData.timeout);\n            });\n\n            const downloadResult = await Promise.race([\n              downloadCompletePromise,\n              timeoutPromise,\n            ]);\n\n            let finalFilename = processedData.filename;\n            if (downloadResult.filename) {\n              finalFilename = downloadResult.filename;\n            }\n\n            if (processedData.saveData) {\n              this.addDataToColumn(processedData.dataColumn, finalFilename);\n            }\n            if (processedData.assignVariable) {\n              await this.setVariable(processedData.variableName, finalFilename);\n            }\n\n            resolve({\n              nextBlockId,\n              data: finalFilename,\n            });\n          } else {\n            const filesData = await getDownloadFilesFromStorage();\n            filesData[downloadId] = processedData;\n            await saveDownloadFilesToStorage(filesData);\n\n            if (!processedData.waitForDownload) {\n              resolve({\n                nextBlockId,\n                data: processedData.filename,\n              });\n              return;\n            }\n\n            let isResolved = false;\n            let currentFilename = processedData.filename;\n\n            const timeout = setTimeout(() => {\n              if (isResolved) return;\n              isResolved = true;\n\n              resolve({\n                nextBlockId,\n                data: currentFilename,\n              });\n            }, processedData.timeout);\n\n            await sendMessageWithCallbacks(\n              'downloads:watch-changed',\n              {\n                downloadId,\n                tabId: this.activeTab.id,\n                onComplete: async (response) => {\n                  try {\n                    if (isResolved) return;\n\n                    if (response.filename) {\n                      currentFilename = response.filename;\n                    }\n\n                    if (processedData.saveData) {\n                      this.addDataToColumn(\n                        processedData.dataColumn,\n                        currentFilename\n                      );\n                    }\n                    if (processedData.assignVariable) {\n                      await this.setVariable(\n                        processedData.variableName,\n                        currentFilename\n                      );\n                    }\n\n                    clearTimeout(timeout);\n                    isResolved = true;\n\n                    if (response.downloadId) {\n                      await removeDownloadFromStorage(response.downloadId);\n                    }\n\n                    resolve({\n                      nextBlockId,\n                      data: currentFilename,\n                    });\n                  } catch (err) {\n                    if (!isResolved) {\n                      isResolved = true;\n                      resolve({\n                        nextBlockId,\n                        data: { $error: true, message: err.message },\n                      });\n                    }\n                  }\n                },\n              },\n              'background'\n            );\n          }\n        } catch (err) {\n          resolve({\n            nextBlockId,\n            data: { $error: true, message: err.message },\n          });\n        }\n      })().catch((err) => {\n        resolve({\n          nextBlockId,\n          data: { $error: true, message: err.message },\n        });\n      });\n    });\n\n    return result;\n  } catch (error) {\n    return {\n      nextBlockId,\n      data: { $error: true, message: error.message },\n    };\n  }\n}\n\nexport default handleDownload;\n"
  },
  {
    "path": "src/workflowEngine/blocksHandler/handlerHoverElement.js",
    "content": "import BrowserAPIService from '@/service/browser-api/BrowserAPIService';\nimport { attachDebugger } from '../helper';\n\nexport async function hoverElement(block) {\n  if (!this.activeTab.id) throw new Error('no-tab');\n  if (BROWSER_TYPE !== 'chrome') {\n    const error = new Error('browser-not-supported');\n    error.data = { browser: BROWSER_TYPE };\n\n    throw error;\n  }\n\n  const { debugMode, executedBlockOnWeb } = this.settings;\n\n  if (!debugMode) {\n    await attachDebugger(this.activeTab.id);\n  }\n\n  await this._sendMessageToTab({\n    ...block,\n    debugMode,\n    executedBlockOnWeb,\n    activeTabId: this.activeTab.id,\n    frameSelector: this.frameSelector,\n  });\n\n  if (!debugMode) {\n    BrowserAPIService.debugger.detach({ tabId: this.activeTab.id });\n  }\n\n  return {\n    data: '',\n    nextBlockId: this.getBlockConnections(block.id),\n  };\n}\n\nexport default hoverElement;\n"
  },
  {
    "path": "src/workflowEngine/blocksHandler/handlerIncreaseVariable.js",
    "content": "import objectPath from 'object-path';\n\nexport async function increaseVariable({ id, data }) {\n  const refVariables = this.engine.referenceData.variables;\n  const variableExist = objectPath.has(refVariables, data.variableName);\n\n  if (!variableExist) {\n    throw new Error(`Cant find \"${data.variableName}\" variable`);\n  }\n\n  const currentVar = +objectPath.get(refVariables, data.variableName);\n  if (Number.isNaN(currentVar)) {\n    throw new Error(\n      `The \"${data.variableName}\" variable value is not a number`\n    );\n  }\n\n  objectPath.set(\n    this.engine.referenceData.variables,\n    data.variableName,\n    currentVar + data.increaseBy\n  );\n\n  return {\n    data: refVariables[data.variableName],\n    nextBlockId: this.getBlockConnections(id),\n  };\n}\n\nexport default increaseVariable;\n"
  },
  {
    "path": "src/workflowEngine/blocksHandler/handlerInsertData.js",
    "content": "import { read as readXlsx, utils as utilsXlsx } from 'xlsx';\nimport Papa from 'papaparse';\nimport { parseJSON } from '@/utils/helper';\nimport getFile, { readFileAsBase64 } from '@/utils/getFile';\nimport renderString from '../templating/renderString';\n\nasync function insertData({ id, data }, { refData }) {\n  const replacedValueList = {};\n\n  for (const item of data.dataList) {\n    let value = '';\n\n    if (item.isFile) {\n      const replacedPath = await renderString(\n        item.filePath || '',\n        refData,\n        this.engine.isPopup\n      );\n      const path = replacedPath.value;\n      const isExcel = /.xlsx?$/.test(path);\n      const isJSON = path.endsWith('.json');\n\n      const action = item.action || item.csvAction || 'default';\n      let responseType = 'text';\n\n      if (isJSON) responseType = 'json';\n      else if (action === 'base64' || (isExcel && action !== 'default'))\n        responseType = 'blob';\n\n      let result = await getFile(path, {\n        responseType,\n        returnValue: true,\n      });\n\n      const readAsJson = action.includes('json');\n\n      if (action === 'base64') {\n        result = await readFileAsBase64(result);\n      } else if (result && path.endsWith('.csv') && readAsJson) {\n        const parsedCSV = Papa.parse(result, {\n          header: action.includes('header'),\n        });\n        result = parsedCSV.data || [];\n      } else if (isExcel && readAsJson) {\n        const base64Xls = await readFileAsBase64(result);\n        const wb = readXlsx(base64Xls.slice(base64Xls.indexOf(',')), {\n          type: 'base64',\n        });\n\n        const inputtedSheet = (item.xlsSheet || '').trim();\n        const sheetName = wb.SheetNames.includes(inputtedSheet)\n          ? inputtedSheet\n          : wb.SheetNames[0];\n\n        const options = {};\n        if (item.xlsRange) options.range = item.xlsRange;\n        if (!action.includes('header')) options.header = 1;\n\n        const sheetData = utilsXlsx.sheet_to_json(\n          wb.Sheets[sheetName],\n          options\n        );\n        result = sheetData;\n      }\n\n      value = result;\n      Object.assign(replacedValueList, replacedPath.list);\n    } else {\n      const replacedValue = await renderString(\n        item.value,\n        refData,\n        this.engine.isPopup\n      );\n      value = parseJSON(replacedValue.value, replacedValue.value);\n      Object.assign(replacedValueList, replacedValue.list);\n    }\n\n    if (item.type === 'table') {\n      const values = typeof value === 'string' ? value.split('||') : [value];\n      values.forEach((tableValue) => {\n        this.addDataToColumn(item.name, tableValue);\n      });\n    } else {\n      const variableName = await renderString(\n        item.name,\n        refData,\n        this.engine.isPopup\n      );\n      await this.setVariable(variableName.value, value);\n    }\n  }\n\n  return {\n    data: '',\n    replacedValue: replacedValueList,\n    nextBlockId: this.getBlockConnections(id),\n  };\n}\n\nexport default insertData;\n"
  },
  {
    "path": "src/workflowEngine/blocksHandler/handlerInteractionBlock.js",
    "content": "import { objectHasKey } from '@/utils/helper';\nimport BrowserAPIService from '@/service/browser-api/BrowserAPIService';\nimport { attachDebugger } from '../helper';\n\nasync function checkAccess(blockName) {\n  if (blockName === 'upload-file') {\n    const hasFileAccess =\n      await BrowserAPIService.extension.isAllowedFileSchemeAccess();\n\n    if (hasFileAccess) return true;\n\n    throw new Error('no-file-access');\n  } else if (blockName === 'clipboard') {\n    const hasPermission = await BrowserAPIService.permissions.contains({\n      permissions: ['clipboardRead'],\n    });\n\n    if (!hasPermission) {\n      throw new Error('no-clipboard-acces');\n    }\n  }\n\n  return true;\n}\n\nasync function interactionHandler(block) {\n  await checkAccess(block.label);\n\n  const debugMode =\n    (block.data.settings?.debugMode ?? false) && !this.settings.debugMode;\n  const isChrome = BROWSER_TYPE === 'chrome';\n\n  try {\n    if (debugMode && isChrome) {\n      await attachDebugger(this.activeTab.id);\n      block.debugMode = true;\n    }\n\n    const data = await this._sendMessageToTab(block, {\n      frameId: this.activeTab.frameId || 0,\n    });\n\n    if (\n      (block.data.saveData && block.label !== 'forms') ||\n      (block.data.getValue && block.data.saveData)\n    ) {\n      const currentColumnType =\n        this.engine.columns[block.data.dataColumn]?.type || 'any';\n      const insertDataToColumn = (value) => {\n        this.addDataToColumn(block.data.dataColumn, value);\n\n        const addExtraRow =\n          objectHasKey(block.data, 'extraRowDataColumn') &&\n          block.data.addExtraRow;\n        if (addExtraRow) {\n          this.addDataToColumn(\n            block.data.extraRowDataColumn,\n            block.data.extraRowValue\n          );\n        }\n      };\n\n      if (Array.isArray(data) && currentColumnType !== 'array') {\n        data.forEach((value) => {\n          insertDataToColumn(value);\n        });\n      } else {\n        insertDataToColumn(data);\n      }\n    }\n\n    if (block.data.assignVariable) {\n      await this.setVariable(block.data.variableName, data);\n    }\n\n    if (debugMode && isChrome) {\n      BrowserAPIService.debugger.detach({ tabId: this.activeTab.id });\n    }\n\n    return {\n      data,\n      nextBlockId: this.getBlockConnections(block.id),\n    };\n  } catch (error) {\n    if (debugMode && isChrome) {\n      BrowserAPIService.debugger.detach({ tabId: this.activeTab.id });\n    }\n\n    error.data = {\n      name: block.label,\n      selector: block.data.selector,\n    };\n\n    throw error;\n  }\n}\n\nexport default interactionHandler;\n"
  },
  {
    "path": "src/workflowEngine/blocksHandler/handlerJavascriptCode.js",
    "content": "import { isObject, parseJSON } from '@/utils/helper';\nimport { MessageListener } from '@/utils/message';\nimport cloneDeep from 'lodash.clonedeep';\nimport { customAlphabet } from 'nanoid/non-secure';\nimport {\n  automaRefDataStr,\n  checkCSPAndInject,\n  messageSandbox,\n  waitTabLoaded,\n} from '../helper';\nimport { automaFetchClient } from '../utils/javascriptBlockUtil';\n\nconst nanoid = customAlphabet('1234567890abcdef', 5);\n\nfunction getAutomaScript({ varName, refData, everyNewTab, isEval = false }) {\n  let str = `\nconst ${varName} = ${JSON.stringify(refData)};\n${automaRefDataStr(varName)}\nfunction automaSetVariable(name, value) {\n  const variables = ${varName}.variables;\n  if (!variables) ${varName}.variables = {}\n\n  ${varName}.variables[name] = value;\n}\nfunction automaNextBlock(data, insert = true) {\n  if (${isEval}) {\n    $automaResolve({\n      columns: {\n        data,\n        insert,\n      },\n      variables: ${varName}.variables,\n    });\n  } else{\n    document.body.dispatchEvent(new CustomEvent('__automa-next-block__', { detail: { data, insert, refData: ${varName} } }));\n  }\n}\nfunction automaResetTimeout() {\n  if (${isEval}) {\n    clearTimeout($automaTimeout);\n    $automaTimeout = setTimeout(() => {\n      resolve();\n    }, $automaTimeoutMs);\n  } else {\n    document.body.dispatchEvent(new CustomEvent('__automa-reset-timeout__'));\n  }\n}\n${automaFetchClient.toString()}\n\nfunction automaFetch(type, resource) {\n  return automaFetchClient('${varName}', { type, resource });\n}\n  `;\n\n  if (everyNewTab) str = automaRefDataStr(varName);\n\n  return str;\n}\nasync function executeInWebpage(args, target, worker) {\n  if (!target.tabId) {\n    throw new Error('no-tab');\n  }\n\n  if (worker.engine.isMV2) {\n    args[0] = cloneDeep(args[0]);\n\n    const result = await worker._sendMessageToTab({\n      label: 'javascript-code',\n      data: args,\n    });\n\n    return result;\n  }\n\n  const { debugMode } = worker.engine.workflow.settings;\n\n  // 提取需要的数据并序列化\n  const serializedBlockData = JSON.stringify(args[0]);\n  const serializedPreloadScripts = JSON.stringify(args[1]);\n  const serializedVarName = JSON.stringify(args[3]);\n\n  // 创建一个简单的回调函数字符串\n  const callbackFunction = `\n    function() {\n      try {\n        // 解析序列化的数据\n        const _blockData = ${serializedBlockData};\n        const _preloadScripts = ${serializedPreloadScripts};\n        const _varName = ${serializedVarName};\n\n        // 获取自动化脚本\n        const _automaScript = (function(_varName, _refData, _everyNewTab, _isEval) {\n          // 这里是getAutomaScript的简化版本\n          const _automaRefDataStr = function(_varName) {\n            return \\`\n              function findData(obj, path) {\n                const paths = path.split('.');\n                const isWhitespace = paths.length === 1 && !/\\\\\\\\S/.test(paths[0]);\n                if (path.startsWith('$last') && Array.isArray(obj)) {\n                  paths[0] = obj.length - 1;\n                }\n                if (paths.length === 0 || isWhitespace) return obj;\n                else if (paths.length === 1) return obj[paths[0]];\n                let result = obj;\n                for (let i = 0; i < paths.length; i++) {\n                  if (result[paths[i]] == undefined) {\n                    return undefined;\n                  } else {\n                    result = result[paths[i]];\n                  }\n                }\n                return result;\n              }\n              function automaRefData(keyword, path = '') {\n                const data = \\${_varName}[keyword];\n                if (!data) return;\n                return findData(data, path);\n              }\n            \\`;\n          };\n\n          let _str = \\`\n            const \\${_varName} = \\${JSON.stringify(_refData)};\n            \\${_automaRefDataStr(_varName)}\n            function automaSetVariable(name, value) {\n              const variables = \\${_varName}.variables;\n              if (!variables) \\${_varName}.variables = {}\n              \\${_varName}.variables[name] = value;\n            }\n            function automaNextBlock(data, insert = true) {\n              if (\\${_isEval}) {\n                $automaResolve({\n                  columns: {\n                    data,\n                    insert,\n                  },\n                  variables: \\${_varName}.variables,\n                });\n              } else{\n                document.body.dispatchEvent(new CustomEvent('__automa-next-block__', { detail: { data, insert, refData: \\${_varName} } }));\n              }\n            }\n            function automaResetTimeout() {\n              if (\\${_isEval}) {\n                clearTimeout($automaTimeout);\n                $automaTimeout = setTimeout(() => {\n                  resolve();\n                }, $automaTimeoutMs);\n              } else{\n                document.body.dispatchEvent(new CustomEvent('__automa-reset-timeout__'));\n              }\n            }\n            function automaFetchClient(id, { type, resource }) {\n              return new Promise((resolve, reject) => {\n                const validType = ['text', 'json', 'base64'];\n                if (!type || !validType.includes(type)) {\n                  reject(new Error('The \"type\" must be \"text\" or \"json\"'));\n                  return;\n                }\n                const eventName = \\\\\\`__automa-fetch-response-\\\\\\${id}__\\\\\\`;\n                const eventListener = ({ detail }) => {\n                  if (detail.id !== id) return;\n                  window.removeEventListener(eventName, eventListener);\n                  if (detail.isError) {\n                    reject(new Error(detail.result));\n                  } else {\n                    resolve(detail.result);\n                  }\n                };\n                window.addEventListener(eventName, eventListener);\n                window.dispatchEvent(\n                  new CustomEvent(\\\\\\`__automa-fetch__\\\\\\`, {\n                    detail: {\n                      id,\n                      type,\n                      resource,\n                    },\n                  })\n                );\n              });\n            }\n            function automaFetch(type, resource) {\n              return automaFetchClient('\\${_varName}', { type, resource });\n            }\n          \\`;\n\n          if (_everyNewTab) _str = _automaRefDataStr(_varName);\n\n          return _str;\n        })(_varName, _blockData.refData, _blockData.data.everyNewTab, true);\n\n        // 生成JavaScript代码\n        const _jsCode = (function(_blockData, _automaScript, _preloadScripts) {\n          // 这里是jsContentHandlerEval的简化版本\n          const _preloadScriptsStr = _preloadScripts\n            .map(function(item) { return item.script; })\n            .join('\\\\n');\n\n          return \\`(() => {\n            \\${_preloadScriptsStr}\n            return new Promise(($automaResolve) => {\n              const $automaTimeoutMs = \\${_blockData.data.timeout};\n              let $automaTimeout = setTimeout(() => {\n                $automaResolve();\n              }, $automaTimeoutMs);\n              \\${_automaScript}\n              try {\n                \\${_blockData.data.code}\n                \\${\n                  _blockData.data.code.includes('automaNextBlock')\n                    ? ''\n                    : 'automaNextBlock()'\n                }\n              } catch (error) {\n                return { columns: { data: { $error: true, message: error.message } } };\n              }\n            }).catch((error) => {\n              return { columns: { data: { $error: true, message: error.message } } };\n            });\n          })();\\`;\n        })(_blockData, _automaScript, _preloadScripts);\n\n        return _jsCode;\n      } catch (error) {\n        console.error('回调函数内部错误:', error);\n        throw error;\n      }\n    }\n  `;\n\n  const cspResult = await checkCSPAndInject(\n    { target, debugMode },\n    callbackFunction\n  );\n\n  if (cspResult.isBlocked) {\n    return cspResult.value;\n  }\n\n  const { 0: blockData, 1: preloadScripts, 3: varName } = args;\n\n  const [{ result }] = await MessageListener.sendMessage(\n    'script:execute',\n    {\n      target,\n      blockData,\n      preloadScripts,\n      varName,\n    },\n    'background'\n  );\n\n  if (typeof result?.columns?.data === 'string') {\n    result.columns.data = parseJSON(result.columns.data, {});\n  }\n\n  return result;\n}\n\nexport async function javascriptCode({ outputs, data, ...block }, { refData }) {\n  let nextBlockId = this.getBlockConnections(block.id);\n\n  if (data.everyNewTab) {\n    const isScriptExist = this.preloadScripts.some(({ id }) => id === block.id);\n\n    if (!isScriptExist)\n      this.preloadScripts.push({ id: block.id, data: cloneDeep(data) });\n    if (!this.activeTab.id) return { data: '', nextBlockId };\n  } else if (!this.activeTab.id && data.context !== 'background') {\n    throw new Error('no-tab');\n  }\n\n  const payload = {\n    ...block,\n    data,\n    refData: { variables: {} },\n    frameSelector: this.frameSelector,\n  };\n  if (data.code.includes('automaRefData')) {\n    const newRefData = {};\n    Object.keys(refData).forEach((keyword) => {\n      if (!data.code.includes(keyword)) return;\n\n      newRefData[keyword] = refData[keyword];\n    });\n\n    payload.refData = { ...newRefData, secrets: {} };\n  }\n\n  const preloadScriptsPromise = await Promise.allSettled(\n    data.preloadScripts.map(async (script) => {\n      const { protocol } = new URL(script.src);\n      const isValidUrl = /https?/.test(protocol);\n      if (!isValidUrl) return null;\n\n      const response = await fetch(script.src);\n      if (!response.ok) throw new Error(response.statusText);\n\n      const result = await response.text();\n\n      return {\n        script: result,\n        id: `automa-script-${nanoid()}`,\n        removeAfterExec: script.removeAfterExec,\n      };\n    })\n  );\n  const preloadScripts = preloadScriptsPromise.reduce((acc, item) => {\n    if (item.status === 'fulfilled') acc.push(item.value);\n\n    return acc;\n  }, []);\n\n  const instanceId = `automa${nanoid()}`;\n  const automaScript =\n    data.everyNewTab && (!data.context || data.context !== 'background')\n      ? ''\n      : getAutomaScript({\n          varName: instanceId,\n          refData: payload.refData,\n          everyNewTab: data.everyNewTab,\n        });\n\n  if (data.context !== 'background') {\n    await waitTabLoaded({\n      tabId: this.activeTab.id,\n      ms: this.settings?.tabLoadTimeout ?? 30000,\n    });\n  }\n\n  const inSandbox =\n    (this.engine.isMV2 || this.engine.isPopup) &&\n    BROWSER_TYPE !== 'firefox' &&\n    data.context === 'background';\n\n  const result = await (inSandbox\n    ? messageSandbox('javascriptBlock', {\n        instanceId,\n        preloadScripts,\n        refData: payload.refData,\n        blockData: cloneDeep(payload.data),\n      })\n    : executeInWebpage(\n        [payload, preloadScripts, automaScript, instanceId],\n        {\n          tabId: this.activeTab.id,\n          frameIds: [this.activeTab.frameId || 0],\n        },\n        this\n      ));\n\n  if (result) {\n    if (result.columns.data?.$error) {\n      throw new Error(result.columns.data.message);\n    }\n\n    if (result.variables) {\n      await Promise.allSettled(\n        Object.keys(result.variables).map(async (varName) => {\n          await this.setVariable(varName, result.variables[varName]);\n        })\n      );\n    }\n\n    let insert = true;\n    let replaceTable = false;\n    if (isObject(result.columns.insert)) {\n      const {\n        insert: insertData,\n        nextBlockId: inputNextBlockId,\n        replaceTable: replaceTableParam,\n      } = result.columns.insert;\n\n      replaceTable = Boolean(replaceTableParam);\n      insert = typeof insertData === 'boolean' ? insertData : true;\n\n      if (inputNextBlockId) {\n        let customNextBlockId = this.getBlockConnections(inputNextBlockId);\n\n        const nextBlock = this.engine.blocks[inputNextBlockId];\n        if (!customNextBlockId && nextBlock) {\n          customNextBlockId = [\n            {\n              id: inputNextBlockId,\n              blockId: inputNextBlockId,\n              connections: new Map([]),\n            },\n          ];\n        }\n\n        if (!customNextBlockId)\n          throw new Error(`Can't find block with \"${inputNextBlockId}\" id`);\n\n        nextBlockId = customNextBlockId;\n      }\n    } else {\n      insert = result.columns.insert;\n    }\n\n    const columnData = result.columns.data;\n    if (insert && columnData) {\n      const columnDataObj =\n        typeof columnData === 'string'\n          ? parseJSON(columnData, null)\n          : columnData;\n      if (columnDataObj) {\n        const params = Array.isArray(columnDataObj)\n          ? columnDataObj\n          : [columnDataObj];\n\n        if (replaceTable) {\n          this.engine.referenceData.table = [];\n          Object.keys(this.engine.columns).forEach((key) => {\n            this.engine.columns[key].index = 0;\n          });\n        }\n\n        this.addDataToColumn(params);\n      }\n    }\n  }\n\n  return {\n    nextBlockId,\n    data: result?.columns.data || {},\n  };\n}\n\nexport default javascriptCode;\n"
  },
  {
    "path": "src/workflowEngine/blocksHandler/handlerLink.js",
    "content": "import BrowserAPIService from '@/service/browser-api/BrowserAPIService';\n\nexport default async function ({ data, id, label }) {\n  const url = await this._sendMessageToTab({\n    id,\n    data,\n    label,\n  });\n\n  if (data.openInNewTab) {\n    const tab = await BrowserAPIService.tabs.create({\n      url,\n      windowId: this.activeTab.windowId,\n    });\n\n    this.activeTab.url = url;\n    this.activeTab.frameId = 0;\n    this.activeTab.id = tab.id;\n  }\n\n  return {\n    data: url,\n    nextBlockId: this.getBlockConnections(id),\n  };\n}\n"
  },
  {
    "path": "src/workflowEngine/blocksHandler/handlerLogData.js",
    "content": "import getTranslateLog from '@/utils/getTranslateLog';\n\nexport async function logData({ id, data }) {\n  if (!data.workflowId) {\n    throw new Error('No workflow is selected');\n  }\n\n  // 工作流状态数组\n  // block handler is inside WorkflowWorker scope. See WorkflowWorker.js:343\n  const { states } = this.engine.states;\n  let logs = [];\n  if (states) {\n    // 转换为数组\n    const stateValues = Object.values(Object.fromEntries(states));\n    // 当前工作流状态\n    const curWorkflowState = stateValues.find(\n      (item) => item.workflowId === data.workflowId\n    )?.state;\n\n    if (curWorkflowState) {\n      // 当前工作流最新日志\n      logs = getTranslateLog(curWorkflowState, 'json');\n\n      if (data.assignVariable) {\n        await this.setVariable(data.variableName, logs);\n      }\n      if (data.saveData) {\n        this.addDataToColumn(data.dataColumn, logs);\n      }\n    }\n  }\n\n  return {\n    data: logs,\n    nextBlockId: this.getBlockConnections(id),\n  };\n}\n\nexport default logData;\n"
  },
  {
    "path": "src/workflowEngine/blocksHandler/handlerLoopBreakpoint.js",
    "content": "import { waitTabLoaded } from '../helper';\n\nasync function loopBreakpoint(block, { prevBlockData }) {\n  const currentLoop = this.loopList[block.data.loopId];\n\n  let validLoopData = false;\n\n  if (currentLoop) {\n    validLoopData =\n      currentLoop.type === 'numbers'\n        ? true\n        : currentLoop.index < currentLoop.data.length - 1;\n  } else {\n    throw new Error(`Can't find a loop with \"${block.data.loopId}\" loop id`);\n  }\n\n  const notReachMaxLoop =\n    currentLoop && currentLoop.maxLoop > 0\n      ? currentLoop.index < currentLoop.maxLoop - 1\n      : true;\n  if (!block.data.clearLoop && validLoopData && notReachMaxLoop) {\n    return {\n      data: '',\n      nextBlockId: [{ id: currentLoop.blockId }],\n    };\n  }\n  if (currentLoop.type === 'elements') {\n    if (currentLoop.loadMoreAction && notReachMaxLoop) {\n      const isClickLink = currentLoop.loadMoreAction.type === 'click-link';\n      let result = await this._sendMessageToTab({\n        id: currentLoop.blockId,\n        label: 'loop-elements',\n        data: {\n          ...currentLoop.loadMoreAction,\n          index: currentLoop.index,\n          onlyClickLink: isClickLink,\n        },\n      });\n\n      if (!result.continue && isClickLink) {\n        await waitTabLoaded({\n          tabId: this.activeTab.id,\n          ms: currentLoop.loadMoreAction.actionPageMaxWaitTime * 1000,\n        });\n        result = await this._sendMessageToTab({\n          id: currentLoop.blockId,\n          label: 'loop-elements',\n          data: {\n            ...currentLoop.loadMoreAction,\n            index: currentLoop.index,\n          },\n        });\n      }\n\n      if (!result.continue && result.length > 0) {\n        this.loopList[block.data.loopId].data.push(...result);\n        return {\n          data: '',\n          nextBlockId: [{ id: currentLoop.blockId }],\n        };\n      }\n    }\n\n    const loopElsIndex = this.loopEls.findIndex(\n      ({ blockId }) => blockId === currentLoop.blockId\n    );\n\n    if (loopElsIndex !== -1) this.loopEls.splice(loopElsIndex, 1);\n  }\n\n  delete this.loopList[block.data.loopId];\n  delete this.engine.referenceData.loopData[block.data.loopId];\n  this.engine.addRefDataSnapshot('loopData');\n\n  return {\n    data: prevBlockData,\n    nextBlockId: this.getBlockConnections(block.id),\n  };\n}\n\nexport default loopBreakpoint;\n"
  },
  {
    "path": "src/workflowEngine/blocksHandler/handlerLoopData.js",
    "content": "import objectPath from 'object-path';\nimport { parseJSON, isXPath } from '@/utils/helper';\n\nasync function loopData({ data, id }, { refData }) {\n  try {\n    if (this.loopList[data.loopId]) {\n      const index = this.loopList[data.loopId].index + 1;\n\n      this.loopList[data.loopId].index = index;\n\n      let currentLoopData;\n\n      if (data.loopThrough === 'numbers') {\n        currentLoopData = refData.loopData[data.loopId].data + 1;\n      } else {\n        currentLoopData = this.loopList[data.loopId].data[index];\n      }\n\n      refData.loopData[data.loopId] = {\n        data: currentLoopData,\n        $index: index,\n      };\n    } else {\n      const maxLoop = +data.maxLoop || 0;\n      const getLoopData = {\n        numbers: () => data.fromNumber,\n        table: () => refData.table,\n        'custom-data': () => JSON.parse(data.loopData),\n        'data-columns': () => refData.table,\n        'google-sheets': () => refData.googleSheets[data.referenceKey],\n        variable: () => {\n          let variableVal = objectPath.get(\n            refData.variables,\n            data.variableName\n          );\n\n          if (Array.isArray(variableVal)) return variableVal;\n\n          variableVal = parseJSON(variableVal, variableVal);\n\n          switch (typeof variableVal) {\n            case 'string':\n              variableVal = variableVal.split('');\n              break;\n            case 'number':\n              variableVal = Array.from(\n                { length: variableVal },\n                (_, index) => index + 1\n              );\n              break;\n            default:\n          }\n\n          return variableVal;\n        },\n        elements: async () => {\n          const findBy = isXPath(data.elementSelector)\n            ? 'xpath'\n            : 'cssSelector';\n          const { elements, url, loopId } = await this._sendMessageToTab({\n            id,\n            label: 'loop-data',\n            data: {\n              findBy,\n              max: maxLoop,\n              multiple: true,\n              reverseLoop: data.reverseLoop,\n              selector: data.elementSelector,\n              waitForSelector: data.waitForSelector ?? false,\n              waitSelectorTimeout: data.waitSelectorTimeout ?? 5000,\n            },\n          });\n          this.loopEls.push({\n            url,\n            loopId,\n            findBy,\n            max: maxLoop,\n            blockId: id,\n            selector: data.elementSelector,\n          });\n\n          return elements;\n        },\n      };\n\n      const currLoopData = await getLoopData[data.loopThrough]();\n      let index = 0;\n\n      if (data.loopThrough !== 'numbers') {\n        if (!Array.isArray(currLoopData)) {\n          throw new Error('invalid-loop-data');\n        }\n\n        const startIndex = +data.startIndex;\n\n        if (data.resumeLastWorkflow && this.engine.isPopup) {\n          index = JSON.parse(localStorage.getItem(`index:${id}`)) || 0;\n        } else if (!Number.isNaN(startIndex) && startIndex > 0) {\n          index = startIndex;\n        }\n\n        if (data.reverseLoop && data.loopThrough !== 'elements') {\n          currLoopData.reverse();\n        }\n      }\n\n      this.loopList[data.loopId] = {\n        index,\n        blockId: id,\n        id: data.loopId,\n        data: currLoopData,\n        type: data.loopThrough,\n        maxLoop:\n          data.loopThrough === 'numbers'\n            ? data.toNumber + 1 - data.fromNumber\n            : maxLoop,\n      };\n      /* eslint-disable-next-line */\n      refData.loopData[data.loopId] = {\n        data:\n          data.loopThrough === 'numbers'\n            ? data.fromNumber\n            : currLoopData[index],\n        $index: index,\n      };\n      this.engine.addRefDataSnapshot('loopData');\n    }\n\n    if (this.engine.isPopup) {\n      localStorage.setItem(`index:${id}`, this.loopList[data.loopId].index);\n    }\n\n    return {\n      data: refData.loopData[data.loopId],\n      nextBlockId: this.getBlockConnections(id),\n    };\n  } catch (error) {\n    if (data.loopThrough === 'elements') {\n      error.data = { selector: data.elementSelector };\n    }\n\n    throw error;\n  }\n}\n\nexport default loopData;\n"
  },
  {
    "path": "src/workflowEngine/blocksHandler/handlerLoopElements.js",
    "content": "async function loopElements({ data, id }, { refData }) {\n  try {\n    if (!this.activeTab.id) throw new Error('no-tab');\n\n    if (this.loopList[data.loopId]) {\n      const index = this.loopList[data.loopId].index + 1;\n\n      this.loopList[data.loopId].index = index;\n\n      refData.loopData[data.loopId] = {\n        $index: index,\n        data: this.loopList[data.loopId].data[index],\n      };\n    } else {\n      const maxLoop = +data.maxLoop || 0;\n      const { elements, url, loopId } = await this._sendMessageToTab({\n        id,\n        label: 'loop-data',\n        data: {\n          max: maxLoop,\n          multiple: true,\n          ...data,\n        },\n      });\n      this.loopEls.push({\n        url,\n        loopId,\n        max: maxLoop,\n        blockId: id,\n        findBy: data.findBy,\n        selector: data.selector,\n      });\n\n      const loopPayload = {\n        maxLoop,\n        index: 0,\n        blockId: id,\n        data: elements,\n        id: data.loopId,\n        type: 'elements',\n      };\n\n      if (data.loadMoreAction !== 'none') {\n        loopPayload.loadMoreAction = {\n          maxLoop,\n          loopAttrId: loopId,\n          loopId: data.loopId,\n          findBy: data.findBy,\n          type: data.loadMoreAction,\n          selector: data.selector.trim(),\n          scrollToBottom: data.scrollToBottom,\n          actionElMaxWaitTime: data.actionElMaxWaitTime,\n          actionElSelector: data.actionElSelector.trim(),\n          actionPageMaxWaitTime: data.actionPageMaxWaitTime,\n        };\n      }\n\n      this.loopList[data.loopId] = loopPayload;\n      /* eslint-disable-next-line */\n      refData.loopData[data.loopId] = {\n        $index: 0,\n        data: elements[0],\n      };\n    }\n\n    return {\n      data: refData.loopData[data.loopId],\n      nextBlockId: this.getBlockConnections(id),\n    };\n  } catch (error) {\n    if (error?.message === 'element-not-found') {\n      error.data = { selector: data.selector };\n    }\n\n    throw error;\n  }\n}\n\nexport default loopElements;\n"
  },
  {
    "path": "src/workflowEngine/blocksHandler/handlerNewTab.js",
    "content": "import BrowserAPIService from '@/service/browser-api/BrowserAPIService';\nimport { isWhitespace, sleep } from '@/utils/helper';\nimport Browser from 'webextension-polyfill';\nimport {\n  attachDebugger,\n  injectPreloadScript,\n  sendDebugCommand,\n  waitTabLoaded,\n} from '../helper';\n\nfunction isValidURL(url) {\n  try {\n    // eslint-disable-next-line\n    new URL(url);\n    return true;\n  } catch (error) {\n    return false;\n  }\n}\n\nasync function newTab({ id, data }) {\n  if (this.windowId) {\n    try {\n      // FIXME: ??? query info but do not use it\n      await BrowserAPIService.windows.get(this.windowId);\n    } catch (error) {\n      this.windowId = null;\n    }\n  } else {\n    await Browser.runtime\n      .sendMessage({\n        name: 'background--browser-api',\n        data: {\n          name: 'windows.getCurrent',\n        },\n      })\n      .then((window) => {\n        this.windowId = window.id;\n        return Promise.resolve(window);\n      });\n  }\n\n  if (!isValidURL(data.url)) {\n    const error = new Error(\n      isWhitespace(data.url) ? 'url-empty' : 'invalid-active-tab'\n    );\n    error.data = { url: data.url };\n\n    throw error;\n  }\n\n  let tab = null;\n  const isChrome = BROWSER_TYPE === 'chrome';\n\n  if (data.updatePrevTab && this.activeTab.id) {\n    tab = await BrowserAPIService.tabs.update(this.activeTab.id, {\n      url: data.url,\n      active: data.active,\n    });\n  } else {\n    tab = await BrowserAPIService.tabs.create({\n      url: data.url,\n      active: data.active,\n      windowId: this.windowId,\n    });\n  }\n\n  this.activeTab.url = data.url;\n  if (tab) {\n    if (this.settings.debugMode || data.customUserAgent) {\n      await attachDebugger(tab.id, this.activeTab.id);\n      this.debugAttached = true;\n\n      if (data.customUserAgent && isChrome) {\n        await sendDebugCommand(tab.id, 'Network.setUserAgentOverride', {\n          userAgent: data.userAgent,\n        });\n        await BrowserAPIService.tabs.reload(tab.id);\n        await sleep(1000);\n      }\n    }\n\n    if (data.tabZoom && data.tabZoom !== 1) {\n      await sleep(1000);\n      await BrowserAPIService.tabs.setZoom(tab.id, data.tabZoom);\n    }\n\n    this.activeTab.id = tab.id;\n    this.windowId = tab.windowId;\n  }\n\n  if (data.inGroup && !data.updatePrevTab) {\n    const options = {\n      groupId: this.activeTab.groupId,\n      tabIds: this.activeTab.id,\n    };\n\n    if (!this.activeTab.groupId) {\n      options.createProperties = {\n        windowId: this.windowId,\n      };\n    }\n\n    if (isChrome) {\n      BrowserAPIService.tabs.group(options, (tabGroupId) => {\n        this.activeTab.groupId = tabGroupId;\n      });\n    }\n  }\n\n  this.activeTab.frameId = 0;\n\n  if (isChrome && !this.settings.debugMode && data.customUserAgent) {\n    BrowserAPIService.debugger.detach({ tabId: tab.id });\n  }\n\n  if (this.preloadScripts.length > 0) {\n    if (this.engine.isMV2) {\n      await this._sendMessageToTab({\n        isPreloadScripts: true,\n        label: 'javascript-code',\n        data: { scripts: this.preloadScripts },\n      });\n    } else {\n      await injectPreloadScript({\n        scripts: this.preloadScripts,\n        frameSelector: this.frameSelector,\n        target: {\n          tabId: this.activeTab.id,\n          frameIds: [this.activeTab.frameId || 0],\n        },\n      });\n    }\n  }\n\n  if (data.waitTabLoaded) {\n    await waitTabLoaded({\n      listenError: true,\n      tabId: this.activeTab.id,\n      ms: this.settings?.tabLoadTimeout ?? 30000,\n    });\n  }\n\n  await BrowserAPIService.windows.update(tab.windowId, { focused: true });\n\n  return {\n    data: data.url,\n    nextBlockId: this.getBlockConnections(id),\n  };\n}\n\nexport default newTab;\n"
  },
  {
    "path": "src/workflowEngine/blocksHandler/handlerNewWindow.js",
    "content": "import BrowserAPIService from '@/service/browser-api/BrowserAPIService';\nimport { attachDebugger } from '../helper';\n\nexport async function newWindow({ data, id }) {\n  const windowOptions = {\n    state: data.windowState,\n    incognito: data.incognito,\n    type: data.type || 'normal',\n  };\n\n  if (data.windowState === 'normal') {\n    ['top', 'left', 'height', 'width'].forEach((key) => {\n      if (data[key] <= 0) return;\n\n      windowOptions[key] = data[key];\n    });\n  }\n  if (data.url) windowOptions.url = data.url;\n\n  const newWindowInstance = await BrowserAPIService.windows.create(\n    windowOptions\n  );\n  this.windowId = newWindowInstance.id;\n\n  if (data.url) {\n    const [tab] = newWindowInstance.tabs;\n\n    if (this.settings.debugMode)\n      await attachDebugger(tab.id, this.activeTab.id);\n\n    this.activeTab.id = tab.id;\n    this.activeTab.url = tab.url;\n  }\n\n  return {\n    data: newWindowInstance.id,\n    nextBlockId: this.getBlockConnections(id),\n  };\n}\n\nexport default newWindow;\n"
  },
  {
    "path": "src/workflowEngine/blocksHandler/handlerNotification.js",
    "content": "import BrowserAPIService from '@/service/browser-api/BrowserAPIService';\nimport { nanoid } from 'nanoid';\n\nexport default async function ({ data, id }) {\n  const hasPermission = await BrowserAPIService.permissions.contains({\n    permissions: ['notifications'],\n  });\n\n  if (!hasPermission) {\n    const error = new Error('no-permission');\n    error.data = { permission: 'notifications' };\n\n    throw error;\n  }\n\n  const options = {\n    title: data.title,\n    message: data.message,\n    iconUrl: BrowserAPIService.runtime.getURL('icon-128.png'),\n  };\n\n  ['iconUrl', 'imageUrl'].forEach((key) => {\n    const url = data[key];\n    if (!url || !url.startsWith('http')) return;\n\n    options[key] = url;\n  });\n\n  await BrowserAPIService.notifications.create(nanoid(), {\n    ...options,\n    type: options.imageUrl ? 'image' : 'basic',\n  });\n\n  return {\n    data: '',\n    nextBlockId: this.getBlockConnections(id),\n  };\n}\n"
  },
  {
    "path": "src/workflowEngine/blocksHandler/handlerParameterPrompt.js",
    "content": "import BrowserAPIService from '@/service/browser-api/BrowserAPIService';\nimport { sleep } from '@/utils/helper';\nimport { MessageListener } from '@/utils/message';\nimport { nanoid } from 'nanoid/non-secure';\nimport renderString from '../templating/renderString';\n\nfunction getInputtedParams(promptId, ms = 10000) {\n  return new Promise((resolve, reject) => {\n    const timeout = null;\n\n    const storageListener = (event) => {\n      if (!event[promptId]) return;\n\n      clearTimeout(timeout);\n      BrowserAPIService.storage.onChanged.removeListener(storageListener);\n      BrowserAPIService.storage.local.remove(promptId);\n\n      const { newValue } = event[promptId];\n      if (newValue.$isError) {\n        reject(new Error(newValue.message));\n        return;\n      }\n\n      resolve(newValue);\n    };\n\n    if (ms > 0) {\n      setTimeout(() => {\n        BrowserAPIService.storage.onChanged.removeListener(storageListener);\n        resolve({});\n      }, ms);\n    }\n\n    BrowserAPIService.storage.onChanged.addListener(storageListener);\n  });\n}\n\nasync function renderParamValue(param, refData, isPopup) {\n  const renderedVals = {};\n\n  const keys = ['defaultValue', 'description', 'placeholder'];\n  await Promise.allSettled(\n    keys.map(async (key) => {\n      if (!param[key]) return;\n      renderedVals[key] = (\n        await renderString(param[key], refData, isPopup)\n      ).value;\n    })\n  );\n\n  return { ...param, ...renderedVals };\n}\n\nexport default async function ({ data, id }, { refData }) {\n  const paramURL = BrowserAPIService.runtime.getURL('/params.html');\n\n  let tabs;\n  try {\n    tabs = await BrowserAPIService.tabs.query({});\n  } catch (e) {\n    console.error('Local tabs.query failed, trying background:', e.message);\n  }\n\n  if (!tabs || !Array.isArray(tabs)) {\n    try {\n      tabs = await MessageListener.sendMessage(\n        'browser-api',\n        { name: 'tabs.query', args: [{}] },\n        'background'\n      );\n    } catch (e) {\n      console.error('Background tabs.query also failed:', e.message);\n      tabs = [];\n    }\n  }\n\n  let tab = tabs.find((item) => item.url?.includes(paramURL));\n\n  if (!tab) {\n    const { tabs: newTabs } = await BrowserAPIService.windows.create({\n      type: 'popup',\n      width: 480,\n      height: 600,\n      url: BrowserAPIService.runtime.getURL('/params.html'),\n    });\n    [tab] = newTabs;\n    await sleep(1000);\n  } else {\n    await BrowserAPIService.tabs.update(tab.id, {\n      active: true,\n    });\n    await BrowserAPIService.windows.update(tab.windowId, { focused: true });\n  }\n\n  const promptId = `params-prompt:${nanoid(4)}__${id}`;\n  const { timeout } = data;\n\n  const params = await Promise.all(\n    data.parameters.map((item) =>\n      renderParamValue(item, refData, this.engine.isPopup)\n    )\n  );\n\n  await BrowserAPIService.tabs.sendMessage(tab.id, {\n    name: 'workflow:params-block',\n    data: {\n      params,\n      promptId,\n      blockId: id,\n      timeoutMs: timeout,\n      execId: this.engine.id,\n      timeout: Date.now() + timeout,\n      name: this.engine.workflow.name,\n      icon: this.engine.workflow.icon,\n      description: this.engine.workflow.description,\n    },\n  });\n\n  const result = await getInputtedParams(promptId, timeout);\n\n  await Promise.allSettled(\n    Object.entries(result).map(async ([varName, varValue]) =>\n      this.setVariable(varName, varValue)\n    )\n  );\n\n  return {\n    data: '',\n    nextBlockId: this.getBlockConnections(id),\n  };\n}\n"
  },
  {
    "path": "src/workflowEngine/blocksHandler/handlerProxy.js",
    "content": "import { isWhitespace } from '@/utils/helper';\nimport BrowserAPIService from '@/service/browser-api/BrowserAPIService';\n\nfunction setProxy({ data, id }) {\n  const nextBlockId = this.getBlockConnections(id);\n\n  return new Promise((resolve, reject) => {\n    if (data.clearProxy) {\n      BrowserAPIService.proxy.settings.clear({});\n    }\n\n    const config = {\n      mode: 'fixed_servers',\n      rules: {\n        singleProxy: {\n          scheme: data.scheme,\n        },\n        bypassList: isWhitespace(data.bypassList)\n          ? []\n          : data.bypassList.split(','),\n      },\n    };\n\n    let proxyPort = data.port;\n\n    if (!isWhitespace(data.host)) {\n      let proxyHost = data.host;\n\n      const schemeRegex = /^https?|socks4|socks5/i;\n      if (schemeRegex.test(data.host)) {\n        /* eslint-disable-next-line */\n        let [scheme, host] = data.host.split(/:\\/\\/(.*)/);\n\n        if (host.includes(':')) {\n          [host, proxyPort] = host.split(':');\n        }\n\n        proxyHost = host;\n        config.rules.singleProxy.scheme = scheme;\n      }\n\n      config.rules.singleProxy.host = proxyHost;\n    } else {\n      if (data.clearProxy) {\n        this.engine.isUsingProxy = false;\n\n        resolve({\n          data: '',\n          nextBlockId,\n        });\n\n        return;\n      }\n\n      const error = new Error('invalid-proxy-host');\n      error.nextBlockId = nextBlockId;\n\n      reject(error);\n      return;\n    }\n\n    if (proxyPort && !Number.isNaN(+proxyPort)) {\n      config.rules.singleProxy.port = +proxyPort;\n    }\n    BrowserAPIService.proxy.settings\n      .set({ value: config, scope: 'regular' })\n      .then(() => {\n        this.engine.isUsingProxy = true;\n\n        resolve({\n          data: data.host,\n          nextBlockId,\n        });\n      });\n  });\n}\n\nexport default setProxy;\n"
  },
  {
    "path": "src/workflowEngine/blocksHandler/handlerRegexVariable.js",
    "content": "import objectPath from 'object-path';\n\nexport async function regexVariable({ id, data }) {\n  const refVariables = this.engine.referenceData.variables;\n  const variableExist = objectPath.has(refVariables, data.variableName);\n\n  if (!variableExist) {\n    throw new Error(`Cant find \"${data.variableName}\" variable`);\n  }\n\n  const str = objectPath.get(refVariables, data.variableName);\n  if (typeof str !== 'string') {\n    throw new Error(\n      `The value of the \"${data.variableName}\" variable is not a string/text`\n    );\n  }\n\n  const method = data.method || 'match';\n  const regex = new RegExp(data.expression, data.flag.join(''));\n\n  let newValue = '';\n\n  if (method === 'match') {\n    const matches = str.match(regex);\n    newValue = matches && !data.flag.includes('g') ? matches[0] : matches;\n  } else if (method === 'replace') {\n    newValue = str.replace(regex, data.replaceVal ?? '');\n  }\n\n  objectPath.set(\n    this.engine.referenceData.variables,\n    data.variableName,\n    newValue\n  );\n\n  return {\n    data: newValue,\n    nextBlockId: this.getBlockConnections(id),\n  };\n}\n\nexport default regexVariable;\n"
  },
  {
    "path": "src/workflowEngine/blocksHandler/handlerReloadTab.js",
    "content": "import BrowserAPIService from '@/service/browser-api/BrowserAPIService';\n\nexport async function reloadTab({ id }) {\n  if (!this.activeTab.id) throw new Error('no-tab');\n\n  await BrowserAPIService.tabs.reload(this.activeTab.id);\n\n  return {\n    data: '',\n    nextBlockId: this.getBlockConnections(id),\n  };\n}\n\nexport default reloadTab;\n"
  },
  {
    "path": "src/workflowEngine/blocksHandler/handlerRepeatTask.js",
    "content": "function repeatTask({ data, id }) {\n  return new Promise((resolve) => {\n    const repeat = Number.isNaN(+data.repeatFor) ? 0 : +data.repeatFor;\n\n    if (this.repeatedTasks[id] > repeat || !this.getBlockConnections(id, 2)) {\n      delete this.repeatedTasks[id];\n\n      resolve({\n        data: repeat,\n        nextBlockId: this.getBlockConnections(id),\n      });\n    } else {\n      this.repeatedTasks[id] = (this.repeatedTasks[id] || 1) + 1;\n\n      resolve({\n        data: repeat,\n        nextBlockId: this.getBlockConnections(id, 2),\n      });\n    }\n  });\n}\n\nexport default repeatTask;\n"
  },
  {
    "path": "src/workflowEngine/blocksHandler/handlerSaveAssets.js",
    "content": "import BrowserAPIService from '@/service/browser-api/BrowserAPIService';\n\nfunction getFilename(url) {\n  try {\n    const filename = new URL(url).pathname.split('/').pop();\n    const hasExtension = /\\.[0-9a-z]+$/i.test(filename);\n\n    if (!hasExtension) return null;\n\n    return filename;\n  } catch (e) {\n    return null;\n  }\n}\n\nexport default async function ({ data, id, label }) {\n  const hasPermission = await BrowserAPIService.permissions.contains({\n    permissions: ['downloads'],\n  });\n\n  if (!hasPermission) {\n    const error = new Error('no-permission');\n    error.data = { permission: 'downloads' };\n\n    throw error;\n  }\n\n  let sources = [data.url];\n  let index = 0;\n  const downloadFile = (url) => {\n    const options = { url, conflictAction: data.onConflict };\n    let filename = decodeURIComponent(data.filename || getFilename(url));\n\n    if (filename) {\n      if (data.onConflict === 'overwrite' && index !== 0) {\n        filename = `(${index}) ${filename}`;\n      }\n\n      options.filename = filename;\n      index += 1;\n    }\n\n    return BrowserAPIService.downloads.download(options);\n  };\n\n  let downloadIds = null;\n\n  if (data.type === 'element') {\n    sources = await this._sendMessageToTab({\n      id,\n      data,\n      label,\n      tabId: this.activeTab.id,\n    });\n\n    downloadIds = await Promise.all(sources.map((url) => downloadFile(url)));\n  } else if (data.type === 'url') {\n    downloadIds = [await downloadFile(data.url)];\n  }\n\n  if (data.saveDownloadIds) {\n    if (data.assignVariable) {\n      await this.setVariable(data.variableName, downloadIds);\n    }\n    if (data.saveData) {\n      this.addDataToColumn(data.dataColumn, downloadIds);\n    }\n  }\n\n  return {\n    data: { sources, downloadIds },\n    nextBlockId: this.getBlockConnections(id),\n  };\n}\n"
  },
  {
    "path": "src/workflowEngine/blocksHandler/handlerSliceVariable.js",
    "content": "import objectPath from 'object-path';\n\nexport async function sliceData({ id, data }) {\n  const variable = objectPath.get(\n    this.engine.referenceData.variables,\n    data.variableName\n  );\n  const payload = {\n    data: variable,\n    nextBlockId: this.getBlockConnections(id),\n  };\n\n  if (!variable || !variable?.slice) return payload;\n\n  let startIndex = 0;\n  let endIndex = variable.length;\n\n  if (data.startIdxEnabled) {\n    startIndex = data.startIndex;\n  }\n  if (data.endIdxEnabled) {\n    endIndex = data.endIndex;\n  }\n\n  const slicedVariable = variable.slice(startIndex, endIndex);\n  payload.data = slicedVariable;\n  objectPath.set(\n    this.engine.referenceData.variables,\n    data.variableName,\n    slicedVariable\n  );\n\n  return payload;\n}\n\nexport default sliceData;\n"
  },
  {
    "path": "src/workflowEngine/blocksHandler/handlerSortData.js",
    "content": "import { objectHasKey } from '@/utils/helper';\n\nexport async function sliceData({ id, data }) {\n  let dataToSort = null;\n\n  if (data.dataSource === 'table') {\n    dataToSort = this.engine.referenceData.table;\n  } else if (data.dataSource === 'variable') {\n    const { variables } = this.engine.referenceData;\n\n    if (!objectHasKey(variables, data.varSourceName)) {\n      throw new Error(`Cant find \"${data.varSourceName}\" variable`);\n    }\n\n    dataToSort = variables[data.varSourceName];\n  }\n\n  if (!Array.isArray(dataToSort)) {\n    const dataType = dataToSort === null ? 'null' : typeof dataToSort;\n\n    throw new Error(`Can't sort data with \"${dataType}\" data type`);\n  }\n\n  const getComparisonValue = ({ itemA, itemB, order = 'asc' }) => {\n    let comparison = 0;\n\n    if (itemA > itemB) {\n      comparison = 1;\n    } else if (itemA < itemB) {\n      comparison = -1;\n    }\n\n    return order === 'desc' ? comparison * -1 : comparison;\n  };\n  const sortedArray = dataToSort.sort((a, b) => {\n    let comparison = 0;\n\n    if (data.sortByProperty) {\n      data.itemProperties.forEach(({ name, order }) => {\n        comparison = getComparisonValue({\n          order,\n          itemA: a[name] ?? a,\n          itemB: b[name] ?? b,\n        });\n      });\n    } else {\n      comparison = getComparisonValue({\n        itemA: a,\n        itemB: b,\n      });\n    }\n\n    return comparison;\n  });\n\n  if (data.assignVariable) {\n    await this.setVariable(data.variableName, sortedArray);\n  }\n  if (data.saveData) {\n    this.addDataToColumn(data.dataColumn, sortedArray);\n  }\n\n  return {\n    data: sortedArray,\n    nextBlockId: this.getBlockConnections(id),\n  };\n}\n\nexport default sliceData;\n"
  },
  {
    "path": "src/workflowEngine/blocksHandler/handlerSwitchTab.js",
    "content": "import BrowserAPIService from '@/service/browser-api/BrowserAPIService';\nimport { attachDebugger, injectPreloadScript } from '../helper';\n\nexport default async function ({ data, id }) {\n  const nextBlockId = this.getBlockConnections(id);\n  const generateError = (message, errorData) => {\n    const error = new Error(message);\n    error.nextBlockId = nextBlockId;\n\n    if (errorData) error.data = errorData;\n\n    return error;\n  };\n  this.windowId = null;\n\n  let tab = null;\n  const activeTab = data.activeTab ?? true;\n  const findTabBy = data.findTabBy || 'match-patterns';\n  const isPrevNext = ['next-tab', 'prev-tab'].includes(findTabBy);\n\n  if (!this.activeTab.id && isPrevNext) {\n    throw new Error('no-tab');\n  }\n\n  const isTabsQuery = ['match-patterns', 'tab-title'];\n  const tabs =\n    findTabBy !== 'match-patterns'\n      ? await BrowserAPIService.tabs.query({})\n      : [];\n\n  if (isTabsQuery.includes(findTabBy)) {\n    const query = {};\n\n    if (data.findTabBy === 'match-patterns') query.url = data.matchPattern;\n    else if (data.findTabBy === 'tab-title') query.title = data.tabTitle;\n\n    [tab] = await BrowserAPIService.tabs.query(query);\n\n    if (!tab) {\n      if (data.createIfNoMatch) {\n        if (!data.url.startsWith('http')) {\n          throw generateError('invalid-active-tab', { url: data.url });\n        }\n\n        tab = await BrowserAPIService.tabs.create({\n          url: data.url,\n          active: activeTab,\n          windowId: this.windowId,\n        });\n      } else {\n        throw generateError('no-match-tab', { pattern: data.matchPattern });\n      }\n    }\n  } else if (isPrevNext) {\n    const incrementBy = findTabBy.includes('next') ? 1 : -1;\n    let tabIndex = tabs.findIndex((item) => item.id === this.activeTab.id);\n\n    tabIndex += incrementBy;\n\n    if (tabIndex < 0) tabIndex = tabs.length - 1;\n    else if (tabIndex > tabs.length - 1) tabIndex = 0;\n\n    tab = tabs[tabIndex];\n  } else if (findTabBy === 'tab-index') {\n    tab = tabs[data.tabIndex];\n\n    if (!tab)\n      throw generateError(`Can't find a tab with ${data.tabIndex} index`);\n  }\n\n  await BrowserAPIService.tabs.update(tab.id, { active: activeTab });\n\n  this.activeTab.id = tab.id;\n  this.activeTab.frameId = 0;\n  this.activeTab.url = tab.url;\n  this.windowId = tab.windowId;\n\n  if (this.settings.debugMode) {\n    await attachDebugger(tab.id, this.activeTab.id);\n    this.debugAttached = true;\n  }\n\n  if (this.preloadScripts.length > 0) {\n    if (this.engine.isMV2) {\n      await this._sendMessageToTab({\n        isPreloadScripts: true,\n        label: 'javascript-code',\n        data: { scripts: this.preloadScripts },\n      });\n    } else {\n      await injectPreloadScript({\n        scripts: this.preloadScripts,\n        frameSelector: this.frameSelector,\n        target: {\n          tabId: this.activeTab.id,\n          frameIds: [this.activeTab.frameId || 0],\n        },\n      });\n    }\n  }\n\n  if (activeTab) {\n    await BrowserAPIService.windows.update(tab.windowId, { focused: true });\n  }\n\n  return {\n    nextBlockId,\n    data: tab.url,\n  };\n}\n"
  },
  {
    "path": "src/workflowEngine/blocksHandler/handlerSwitchTo.js",
    "content": "import { sleep } from '@/utils/helper';\nimport { getFrames } from '../helper';\n\nasync function switchTo(block) {\n  const nextBlockId = this.getBlockConnections(block.id);\n\n  try {\n    if (block.data.windowType === 'main-window') {\n      this.activeTab.frameId = 0;\n\n      delete this.frameSelector;\n\n      return {\n        data: '',\n        nextBlockId,\n      };\n    }\n\n    const { url, isSameOrigin } = await this._sendMessageToTab(block, {\n      frameId: 0,\n    });\n\n    if (isSameOrigin) {\n      this.frameSelector = block.data.selector;\n\n      return {\n        data: block.data.selector,\n        nextBlockId,\n      };\n    }\n\n    const frames = await getFrames(this.activeTab.id);\n\n    let frameId = frames[url] ?? null;\n    if (frameId === null) {\n      // Incase the iframe is redirect\n      frameId = Object.entries(frames).find(([frameURL]) => {\n        try {\n          const currFramePathName = new URL(url).pathname;\n          const framePathName = new URL(frameURL).pathname;\n\n          return currFramePathName === framePathName;\n        } catch (error) {\n          return false;\n        }\n      })?.[1];\n    }\n\n    if (frameId !== null) {\n      this.activeTab.frameId = frameId;\n\n      await sleep(1000);\n\n      return {\n        nextBlockId,\n        data: this.activeTab.frameId,\n      };\n    }\n\n    throw new Error('no-iframe-id');\n  } catch (error) {\n    error.data = { selector: block.data.selector };\n    error.nextBlockId = nextBlockId;\n\n    throw error;\n  }\n}\n\nexport default switchTo;\n"
  },
  {
    "path": "src/workflowEngine/blocksHandler/handlerTabUrl.js",
    "content": "import BrowserAPIService from '@/service/browser-api/BrowserAPIService';\n\nexport async function logData({ id, data }) {\n  let urls = [];\n\n  if (data.type === 'active-tab') {\n    if (!this.activeTab.id) throw new Error('no-tab');\n\n    const tab = await BrowserAPIService.tabs.get(this.activeTab.id);\n    urls = tab.url || tab.pendingUrl || '';\n  } else {\n    const query = {};\n\n    if (data.qMatchPatterns) {\n      query.url = data.qMatchPatterns;\n    }\n    if (data.qTitle) {\n      query.title = data.qTitle;\n    }\n\n    const tabs = await BrowserAPIService.tabs.query(query);\n    urls = tabs.map((tab) => tab.url);\n  }\n\n  if (data.assignVariable) {\n    await this.setVariable(data.variableName, urls);\n  }\n  if (data.saveData) {\n    this.addDataToColumn(data.dataColumn, urls);\n  }\n\n  return {\n    data: urls,\n    nextBlockId: this.getBlockConnections(id),\n  };\n}\n\nexport default logData;\n"
  },
  {
    "path": "src/workflowEngine/blocksHandler/handlerTakeScreenshot.js",
    "content": "import { fileSaver } from '@/utils/helper';\nimport BrowserAPIService from '@/service/browser-api/BrowserAPIService';\nimport { IS_FIREFOX } from '@/common/utils/constant';\nimport { waitTabLoaded } from '../helper';\n\nasync function saveImage({ filename, uri, ext }) {\n  const hasDownloadAccess = await BrowserAPIService.permissions.contains({\n    permissions: ['downloads'],\n  });\n  const name = `${filename || 'Screenshot'}.${ext || 'png'}`;\n\n  if (hasDownloadAccess) {\n    await BrowserAPIService.downloads.download({\n      url: uri,\n      filename: name,\n    });\n\n    return;\n  }\n\n  const image = new Image();\n\n  image.onload = () => {\n    const canvas = document.createElement('canvas');\n    canvas.width = image.width;\n    canvas.height = image.height;\n\n    const context = canvas.getContext('2d');\n    context.drawImage(image, 0, 0);\n\n    fileSaver(name, canvas.toDataURL());\n  };\n\n  image.src = uri;\n}\n\nasync function takeScreenshot({ data, id, label }) {\n  const saveToComputer =\n    typeof data.saveToComputer === 'undefined' || data.saveToComputer;\n\n  try {\n    let screenshot = null;\n    const options = {\n      quality: data.quality,\n      format: data.ext || 'png',\n    };\n    const saveScreenshot = async (dataUrl) => {\n      if (data.saveToColumn) this.addDataToColumn(data.dataColumn, dataUrl);\n      if (saveToComputer)\n        await saveImage({\n          filename: data.fileName,\n          uri: dataUrl,\n          ext: data.ext,\n        });\n      if (data.assignVariable)\n        await this.setVariable(data.variableName, dataUrl);\n    };\n\n    if (data.captureActiveTab) {\n      if (!this.activeTab.id) {\n        throw new Error('no-tab');\n      }\n\n      let tab = null;\n      const isChrome = !IS_FIREFOX;\n      const captureTab = async () => {\n        let result = null;\n\n        if (isChrome) {\n          const currentTab = await BrowserAPIService.tabs.get(\n            this.activeTab.id\n          );\n          result = await BrowserAPIService.tabs.captureVisibleTab(\n            currentTab.windowId,\n            options\n          );\n        } else {\n          result = await BrowserAPIService.tabs.captureTab(\n            this.activeTab.id,\n            options\n          );\n        }\n\n        return result;\n      };\n\n      if (isChrome) {\n        [tab] = await BrowserAPIService.tabs.query({\n          active: true,\n          url: '*://*/*',\n        });\n\n        if (this.windowId) {\n          await BrowserAPIService.windows.update(this.windowId, {\n            focused: true,\n          });\n        }\n      }\n\n      await BrowserAPIService.tabs.update(this.activeTab.id, { active: true });\n      await waitTabLoaded({ tabId: this.activeTab.id, listenError: true });\n\n      screenshot = await (data.fullPage ||\n      ['element', 'fullpage'].includes(data.type)\n        ? this._sendMessageToTab({\n            label,\n            options,\n            data: {\n              type: data.type,\n              selector: data.selector,\n            },\n            tabId: this.activeTab.id,\n          })\n        : captureTab());\n\n      if (tab) {\n        await BrowserAPIService.windows.update(tab.windowId, { focused: true });\n        await BrowserAPIService.tabs.update(tab.id, { active: true });\n      }\n\n      await saveScreenshot(screenshot);\n    } else {\n      screenshot = await BrowserAPIService.tabs.captureVisibleTab(options);\n\n      await saveScreenshot(screenshot);\n    }\n\n    return {\n      data: screenshot,\n      nextBlockId: this.getBlockConnections(id),\n    };\n  } catch (error) {\n    if (data.type === 'element') error.data = { selector: data.selector };\n\n    throw error;\n  }\n}\n\nexport default takeScreenshot;\n"
  },
  {
    "path": "src/workflowEngine/blocksHandler/handlerTrigger.js",
    "content": "async function trigger(block) {\n  return new Promise((resolve) => {\n    resolve({\n      data: '',\n      nextBlockId: this.getBlockConnections(block.id),\n    });\n  });\n}\n\nexport default trigger;\n"
  },
  {
    "path": "src/workflowEngine/blocksHandler/handlerWaitConnections.js",
    "content": "async function waitConnections({ data, id }, { prevBlock }) {\n  return new Promise((resolve, reject) => {\n    let timeout;\n    let resolved = false;\n\n    const nextBlockId = this.getBlockConnections(id);\n    const destroyWorker =\n      data.specificFlow && prevBlock?.id !== data.flowBlockId;\n\n    const registerConnections = () => {\n      const connections = this.engine.connectionsMap;\n      Object.keys(connections).forEach((key) => {\n        const isConnected = [...connections[key].values()].some(\n          (connection) => connection.id === id\n        );\n\n        if (!isConnected) return;\n\n        const index = key.indexOf('-output');\n        const prevBlockId = key.slice(0, index === -1 ? key.length : index);\n        this.engine.waitConnections[id][prevBlockId] = {\n          isHere: false,\n          isContinue: false,\n        };\n      });\n    };\n    const checkConnections = () => {\n      if (resolved) return;\n\n      const state = Object.values(this.engine.waitConnections[id]);\n      const isAllHere = state.every((worker) => worker.isHere);\n\n      if (isAllHere) {\n        this.engine.waitConnections[id][prevBlock.id].isContinue = true;\n        const allContinue = state.every((worker) => worker.isContinue);\n\n        if (allContinue) {\n          registerConnections();\n        }\n\n        clearTimeout(timeout);\n\n        if (data.specificFlow && data.flowBlockId) {\n          const connectionExist = Object.keys(\n            this.engine.waitConnections[id]\n          ).includes(data.flowBlockId);\n\n          if (!connectionExist) {\n            reject(new Error(`No specific flow selected`));\n            return;\n          }\n        }\n\n        resolve({\n          data: '',\n          nextBlockId,\n          destroyWorker,\n        });\n      } else {\n        setTimeout(() => {\n          checkConnections();\n        }, 1000);\n      }\n    };\n\n    if (!this.engine.waitConnections[id]) {\n      this.engine.waitConnections[id] = {};\n\n      registerConnections();\n    }\n\n    this.engine.waitConnections[id][prevBlock.id].isHere = true;\n\n    timeout = setTimeout(() => {\n      resolved = true;\n\n      resolve({\n        data: '',\n        nextBlockId,\n        destroyWorker,\n      });\n    }, data.timeout);\n\n    checkConnections();\n  });\n}\n\nexport default waitConnections;\n"
  },
  {
    "path": "src/workflowEngine/blocksHandler/handlerWebhook.js",
    "content": "import objectPath from 'object-path';\nimport { isWhitespace } from '@/utils/helper';\nimport { executeWebhook } from '../utils/webhookUtil';\nimport renderString from '../templating/renderString';\n\nconst ALL_HTTP_RESPONSE_KEYWORD = '$response';\n\nexport async function webhook({ data, id }, { refData }) {\n  const nextBlockId = this.getBlockConnections(id);\n  const fallbackOutput = this.getBlockConnections(id, 'fallback');\n\n  try {\n    if (isWhitespace(data.url)) throw new Error('url-empty');\n    if (!data.url.startsWith('http')) {\n      const error = new Error('invalid-active-tab');\n      error.data = { url: data.url };\n\n      throw error;\n    }\n\n    const newHeaders = [];\n    for (const { value, name } of data.headers) {\n      const newValue = (await renderString(value, refData, this.engine.isPopup))\n        .value;\n\n      newHeaders.push({ name, value: newValue });\n    }\n\n    const response = await executeWebhook({ ...data, headers: newHeaders });\n\n    if (!response.ok) {\n      const { status, statusText } = response;\n      const responseData = await (data.responseType === 'json'\n        ? response.json()\n        : response.text());\n      const ctxData = {\n        ctxData: {\n          request: { status, statusText, data: responseData },\n        },\n      };\n\n      if (fallbackOutput && fallbackOutput.length > 0) {\n        return {\n          ctxData,\n          data: '',\n          nextBlockId: fallbackOutput,\n        };\n      }\n\n      const error = new Error(`(${response.status}) ${response.statusText}`);\n      error.ctxData = ctxData;\n\n      throw error;\n    }\n\n    if (!data.assignVariable && !data.saveData) {\n      return {\n        data: '',\n        nextBlockId,\n      };\n    }\n\n    const includeResponse = data.dataPath.includes(ALL_HTTP_RESPONSE_KEYWORD);\n    let returnData = '';\n\n    if (data.responseType === 'json') {\n      const jsonRes = await response.json();\n\n      if (!includeResponse) {\n        returnData = objectPath.get(jsonRes, data.dataPath);\n      } else {\n        returnData = jsonRes;\n      }\n    } else if (data.responseType === 'base64') {\n      const blob = await response.blob();\n      const base64 = await new Promise((resolve) => {\n        const reader = new FileReader();\n        reader.onload = () => {\n          resolve(reader.result);\n        };\n        reader.readAsDataURL(blob);\n      });\n\n      returnData = base64;\n    } else {\n      returnData = await response.text();\n    }\n\n    if (includeResponse) {\n      const { status, statusText, url, redirected, ok } = response;\n      const responseData = {\n        ok,\n        url,\n        status,\n        statusText,\n        redirected,\n        data: returnData,\n      };\n\n      returnData = objectPath.get({ $response: responseData }, data.dataPath);\n    }\n\n    if (data.assignVariable) {\n      await this.setVariable(data.variableName, returnData);\n    }\n    if (data.saveData) {\n      if (data.dataColumn === '$assignColumns' && Array.isArray(returnData)) {\n        this.addDataToColumn(returnData);\n      } else {\n        this.addDataToColumn(data.dataColumn, returnData);\n      }\n    }\n\n    return {\n      nextBlockId,\n      data: returnData,\n    };\n  } catch (error) {\n    const fallbackErrors = ['Failed to fetch', 'user aborted'];\n    const executeFallback =\n      fallbackOutput &&\n      fallbackErrors.some((message) => error.message.includes(message));\n    if (executeFallback) {\n      return {\n        data: '',\n        nextBlockId: fallbackOutput,\n      };\n    }\n\n    error.nextBlockId = nextBlockId;\n\n    throw error;\n  }\n}\n\nexport default webhook;\n"
  },
  {
    "path": "src/workflowEngine/blocksHandler/handlerWhileLoop.js",
    "content": "import testConditions from '../utils/testConditions';\nimport checkCodeCondition from '../utils/conditionCode';\n\nasync function whileLoop({ data, id }, { refData }) {\n  const { debugMode } = this.engine.workflow?.settings || {};\n  const conditionPayload = {\n    refData,\n    isMV2: this.engine.isMV2,\n    isPopup: this.engine.isPopup,\n    activeTab: this.activeTab.id,\n    checkCodeCondition: (payload) => {\n      payload.debugMode = debugMode;\n      return checkCodeCondition(this.activeTab, payload);\n    },\n    sendMessage: (payload) =>\n      this._sendMessageToTab({ ...payload.data, label: 'conditions', id }),\n  };\n  const result = await testConditions(data.conditions, conditionPayload);\n  const nextBlockId = this.getBlockConnections(\n    id,\n    result.isMatch ? 1 : 'fallback'\n  );\n\n  return {\n    data: '',\n    nextBlockId,\n    replacedValue: result?.replacedValue || {},\n  };\n}\n\nexport default whileLoop;\n"
  },
  {
    "path": "src/workflowEngine/blocksHandler/handlerWorkflowState.js",
    "content": "export default async function ({ data, id }) {\n  try {\n    let stopCurrent = false;\n\n    if (data.type === 'stop-current') {\n      // 如果需要抛出错误\n      if (data.throwError) {\n        throw new Error(data.errorMessage || 'Workflow stopped manually');\n      } else {\n        return {};\n      }\n    }\n    if (['stop-specific', 'stop-all'].includes(data.type)) {\n      const ids = [];\n      const isSpecific = data.type === 'stop-specific';\n      this.engine.states.getAll.forEach((state) => {\n        const workflowNotIncluded =\n          isSpecific && !data.workflowsToStop.includes(state.workflowId);\n        if (workflowNotIncluded) return;\n\n        ids.push(state.id);\n      });\n\n      for (const stateId of ids) {\n        if (stateId === this.engine.id) {\n          stopCurrent = isSpecific ? true : !data.exceptCurrent;\n        } else {\n          await this.engine.states.stop(stateId);\n        }\n      }\n    }\n\n    if (stopCurrent) return {};\n\n    return {\n      data: '',\n      nextBlockId: this.getBlockConnections(id),\n    };\n  } catch (error) {\n    error.data = error.data || {};\n    console.error(error);\n\n    throw error;\n  }\n}\n"
  },
  {
    "path": "src/workflowEngine/blocksHandler.js",
    "content": "import { toCamelCase } from '@/utils/helper';\nimport customHandlers from '@business/blocks/backgroundHandler';\n\nconst blocksHandler = require.context('./blocksHandler', false, /\\.js$/);\nconst handlers = blocksHandler.keys().reduce((acc, key) => {\n  const name = key.replace(/^\\.\\/handler|\\.js/g, '');\n\n  acc[toCamelCase(name)] = blocksHandler(key).default;\n\n  return acc;\n}, {});\n\nexport default function () {\n  return {\n    ...handlers,\n    ...customHandlers(),\n  };\n}\n"
  },
  {
    "path": "src/workflowEngine/helper.js",
    "content": "import BrowserAPIService from '@/service/browser-api/BrowserAPIService';\nimport { MessageListener } from '@/utils/message';\nimport { customAlphabet } from 'nanoid/non-secure';\nimport browser from 'webextension-polyfill';\n\nexport function escapeElementPolicy(script) {\n  if (window?.trustedTypes?.createPolicy) {\n    try {\n      // 尝试使用可能在CSP白名单中的名称\n      const policyNames = ['default', 'dompurify', 'jSecure', 'forceInner'];\n      let escapePolicy = null;\n\n      // 尝试创建策略，如果一个名称失败，尝试下一个\n      for (const policyName of policyNames) {\n        try {\n          escapePolicy = window.trustedTypes.createPolicy(policyName, {\n            createHTML: (to_escape) => to_escape,\n            createScript: (to_escape) => to_escape,\n          });\n          // 如果成功创建，跳出循环\n          break;\n        } catch (e) {\n          // 该名称失败，继续尝试下一个\n          console.debug(`Policy name ${policyName} failed, trying next one`);\n        }\n      }\n\n      // 如果成功创建了策略，使用它\n      if (escapePolicy) {\n        return escapePolicy.createScript(script);\n      }\n      // 如果所有策略名称都失败，返回原始脚本\n      console.debug(\n        'All trusted policy creation attempts failed, falling back to raw script'\n      );\n      return script;\n    } catch (e) {\n      // 捕获任何其他错误并降级\n      console.debug('Error creating trusted policy:', e);\n      return script;\n    }\n  }\n\n  return script;\n}\n\nexport function messageSandbox(type, data = {}) {\n  const nanoid = customAlphabet('1234567890abcdef', 5);\n\n  return new Promise((resolve) => {\n    const messageId = nanoid();\n\n    const iframeEl = document.getElementById('sandbox');\n    iframeEl.contentWindow.postMessage({ id: messageId, type, ...data }, '*');\n\n    const messageListener = ({ data: messageData }) => {\n      if (messageData?.type !== 'sandbox' || messageData?.id !== messageId)\n        return;\n\n      window.removeEventListener('message', messageListener);\n\n      resolve(messageData.result);\n    };\n\n    window.addEventListener('message', messageListener);\n  });\n}\n\nexport async function getFrames(tabId) {\n  try {\n    const frames = await BrowserAPIService.webNavigation.getAllFrames({\n      tabId,\n    });\n    const framesObj = frames.reduce((acc, { frameId, url }) => {\n      const key = url === 'about:blank' ? '' : url;\n\n      acc[key] = frameId;\n\n      return acc;\n    }, {});\n\n    return framesObj;\n  } catch (error) {\n    console.error(error);\n    return {};\n  }\n}\n\nexport function sendDebugCommand(tabId, method, params = {}) {\n  return new Promise((resolve) => {\n    BrowserAPIService.debugger.sendCommand({ tabId }, method, params, resolve);\n  });\n}\n\nexport async function attachDebugger(tabId, prevTab) {\n  try {\n    if (prevTab && tabId !== prevTab) {\n      await BrowserAPIService.debugger.detach({ tabId: prevTab });\n    }\n\n    // first attach\n    await BrowserAPIService.debugger.attach({ tabId }, '1.3');\n\n    // and then Page.enable\n    await BrowserAPIService.debugger.sendCommand({ tabId }, 'Page.enable');\n\n    return true;\n  } catch (error) {\n    console.error('Failed to attach debugger:', error);\n    return false;\n  }\n}\n\nexport function waitTabLoaded({ tabId, listenError = false, ms = 10000 }) {\n  return new Promise((resolve, reject) => {\n    let timeout = null;\n    const excludeErrors = ['net::ERR_BLOCKED_BY_CLIENT', 'net::ERR_ABORTED'];\n\n    const onErrorOccurred = (details) => {\n      if (\n        details.tabId !== tabId ||\n        details.frameId !== 0 ||\n        excludeErrors.includes(details.error)\n      )\n        return;\n\n      clearTimeout(timeout);\n      BrowserAPIService.webNavigation.onErrorOccurred.removeListener(\n        onErrorOccurred\n      );\n      reject(new Error(details.error));\n    };\n\n    if (ms > 0) {\n      timeout = setTimeout(() => {\n        BrowserAPIService.webNavigation.onErrorOccurred.removeListener(\n          onErrorOccurred\n        );\n        reject(new Error('Timeout'));\n      }, ms);\n    }\n    if (listenError && BROWSER_TYPE === 'chrome')\n      BrowserAPIService.webNavigation.onErrorOccurred.addListener(\n        onErrorOccurred\n      );\n\n    const activeTabStatus = () => {\n      BrowserAPIService.tabs.get(tabId).then((tab) => {\n        if (!tab) {\n          reject(new Error('no-tab'));\n          return;\n        }\n\n        if (tab.status === 'loading') {\n          setTimeout(() => {\n            activeTabStatus();\n          }, 1000);\n          return;\n        }\n\n        clearTimeout(timeout);\n\n        BrowserAPIService.webNavigation.onErrorOccurred.removeListener(\n          onErrorOccurred\n        );\n        resolve();\n      });\n    };\n\n    activeTabStatus();\n  });\n}\n\nexport function convertData(data, type) {\n  if (type === 'any') return data;\n\n  let result = data;\n\n  switch (type) {\n    case 'integer':\n      /* eslint-disable-next-line */\n      result = typeof data !== 'number' ? +data?.replace(/\\D+/g, '') : data;\n      break;\n    case 'boolean':\n      result = Boolean(data);\n      break;\n    case 'array':\n      result = Array.from(data);\n      break;\n    case 'string':\n      result = String(data);\n      break;\n    default:\n  }\n\n  return result;\n}\n\nexport function automaRefDataStr(varName) {\n  return `\nfunction findData(obj, path) {\n  const paths = path.split('.');\n  const isWhitespace = paths.length === 1 && !/\\\\S/.test(paths[0]);\n\n  if (path.startsWith('$last') && Array.isArray(obj)) {\n    paths[0] = obj.length - 1;\n  }\n\n  if (paths.length === 0 || isWhitespace) return obj;\n  else if (paths.length === 1) return obj[paths[0]];\n\n  let result = obj;\n\n  for (let i = 0; i < paths.length; i++) {\n    if (result[paths[i]] == undefined) {\n      return undefined;\n    } else {\n      result = result[paths[i]];\n    }\n  }\n\n  return result;\n}\nfunction automaRefData(keyword, path = '') {\n  const data = ${varName}[keyword];\n\n  if (!data) return;\n\n  return findData(data, path);\n}\n  `;\n}\n\nexport function injectPreloadScript({ target, scripts, frameSelector }) {\n  return browser.scripting.executeScript({\n    target,\n    world: 'MAIN',\n    args: [scripts, frameSelector || null],\n    func: (preloadScripts, frame) => {\n      let $documentCtx = document;\n\n      if (frame) {\n        const iframeCtx = document.querySelector(frame)?.contentDocument;\n        if (!iframeCtx) return;\n\n        $documentCtx = iframeCtx;\n      }\n\n      preloadScripts.forEach((script) => {\n        const scriptAttr = `block--${script.id}`;\n\n        const isScriptExists = $documentCtx.querySelector(\n          `.automa-custom-js[${scriptAttr}]`\n        );\n\n        if (isScriptExists) return;\n\n        const scriptEl = $documentCtx.createElement('script');\n        scriptEl.textContent = script.data.code;\n        scriptEl.setAttribute(scriptAttr, '');\n        scriptEl.classList.add('automa-custom-js');\n\n        $documentCtx.documentElement.appendChild(scriptEl);\n      });\n    },\n  });\n}\n\nexport async function checkCSPAndInject(\n  { target, debugMode, options = {}, injectOptions = {} },\n  callback\n) {\n  let _callback = '';\n  if (typeof callback === 'function') {\n    _callback = callback.toString();\n  } else if (typeof callback === 'string') {\n    _callback = callback;\n  }\n\n  try {\n    const result = await MessageListener.sendMessage(\n      'check-csp-and-inject',\n      {\n        target,\n        debugMode,\n        callback: _callback,\n        options,\n        injectOptions,\n      },\n      'background'\n    );\n\n    return result;\n  } catch (error) {\n    console.error('CSP check error:', error);\n    return { isBlocked: false, value: null };\n  }\n}\n\nfunction fallbackCopyTextToClipboard(text) {\n  const textArea = document.createElement('textarea');\n  textArea.value = text;\n\n  // Avoid scrolling to bottom\n  textArea.style.top = '0';\n  textArea.style.left = '0';\n  textArea.style.position = 'fixed';\n\n  document.body.appendChild(textArea);\n  textArea.focus();\n  textArea.select();\n\n  try {\n    document.execCommand('copy');\n  } catch (err) {\n    console.error('Fallback: Oops, unable to copy', err);\n  }\n\n  document.body.removeChild(textArea);\n}\n\nexport function copyTextToClipboard(text) {\n  return new Promise((resolve, reject) => {\n    if (!navigator.clipboard) {\n      fallbackCopyTextToClipboard(text);\n      resolve(true);\n      return;\n    }\n    navigator.clipboard\n      .writeText(text)\n      .then(() => {\n        resolve(true);\n      })\n      .catch((error) => {\n        reject(error);\n      });\n  });\n}\n"
  },
  {
    "path": "src/workflowEngine/injectContentScript.js",
    "content": "import browser from 'webextension-polyfill';\n\nconst isMV2 = browser.runtime.getManifest().manifest_version === 2;\n\nasync function contentScriptExist(tabId, frameId = 0) {\n  try {\n    await browser.tabs.sendMessage(\n      tabId,\n      { type: 'content-script-exists' },\n      { frameId }\n    );\n\n    return true;\n  } catch (error) {\n    return false;\n  }\n}\n\nexport default function (tabId, frameId = 0) {\n  return new Promise((resolve) => {\n    const currentFrameId = typeof frameId !== 'number' ? 0 : frameId;\n    let tryCount = 0;\n\n    (async function tryExecute() {\n      try {\n        if (tryCount > 3) {\n          resolve(false);\n          return;\n        }\n\n        tryCount += 1;\n\n        if (isMV2) {\n          await browser.tabs.executeScript(tabId, {\n            allFrames: true,\n            runAt: 'document_start',\n            file: './contentScript.bundle.js',\n          });\n        } else {\n          await browser.scripting.executeScript({\n            target: {\n              tabId,\n              allFrames: true,\n            },\n            injectImmediately: true,\n            files: ['./contentScript.bundle.js'],\n          });\n        }\n\n        const isScriptExists = await contentScriptExist(tabId, currentFrameId);\n\n        if (isScriptExists) {\n          resolve(true);\n        } else {\n          setTimeout(tryExecute, 1000);\n        }\n      } catch (error) {\n        console.error(error);\n        setTimeout(tryExecute, 1000);\n      }\n    })();\n  });\n}\n"
  },
  {
    "path": "src/workflowEngine/templating/index.js",
    "content": "import objectPath from 'object-path';\nimport cloneDeep from 'lodash.clonedeep';\nimport renderString from './renderString';\n\nexport default async function ({ block, refKeys, data, isPopup }) {\n  if (!refKeys || refKeys.length === 0) return block;\n\n  const copyBlock = cloneDeep(block);\n  const addReplacedValue = (value) => {\n    if (!copyBlock.replacedValue) copyBlock.replacedValue = {};\n    copyBlock.replacedValue = { ...copyBlock.replacedValue, ...value };\n  };\n\n  for (const blockDataKey of refKeys) {\n    const currentData = objectPath.get(copyBlock.data, blockDataKey);\n    /* eslint-disable-next-line */\n    if (!currentData) continue;\n\n    if (Array.isArray(currentData)) {\n      for (let index = 0; index < currentData.length; index += 1) {\n        const value = currentData[index];\n        const renderedValue = await renderString(value, data, isPopup);\n\n        addReplacedValue(renderedValue.list);\n        objectPath.set(\n          copyBlock.data,\n          `${blockDataKey}.${index}`,\n          renderedValue.value\n        );\n      }\n    } else if (typeof currentData === 'string') {\n      const renderedValue = await renderString(currentData, data, isPopup);\n\n      addReplacedValue(renderedValue.list);\n      objectPath.set(copyBlock.data, blockDataKey, renderedValue.value);\n    }\n  }\n\n  return copyBlock;\n}\n"
  },
  {
    "path": "src/workflowEngine/templating/mustacheReplacer.js",
    "content": "import objectPath from 'object-path';\nimport credentialUtil from '@/utils/credentialUtil';\nimport { parseJSON } from '@/utils/helper';\nimport templatingFunctions from './templatingFunctions';\n\nconst refKeys = {\n  table: 'table',\n  dataColumn: 'table',\n  dataColumns: 'table',\n};\n\nexport function extractStrFunction(str) {\n  const extractedStr = /^\\$\\s*(\\w+)\\s*\\((.*)\\)/.exec(\n    str.trim().replace(/\\r?\\n|\\r/g, '')\n  );\n\n  if (!extractedStr) return null;\n  const { 1: name, 2: funcParams } = extractedStr;\n  const params = funcParams\n    .split(/,(?=(?:[^'\"\\\\\"\\\\']*['\"][^'\"]*['\"\\\\\"\\\\'])*[^'\"]*$)/)\n    .map((param) => param.trim().replace(/^['\"]|['\"]$/g, '') || '');\n\n  return {\n    name,\n    params,\n  };\n}\n\nexport function keyParser(key, data) {\n  let [dataKey, path] = key.split(/[@.](.+)/);\n\n  dataKey = refKeys[dataKey] ?? dataKey;\n\n  if (!path) return { dataKey, path: '' };\n\n  if (dataKey !== 'table') {\n    if (dataKey === 'loopData' && !path.endsWith('.$index')) {\n      const pathArr = path.split('.');\n      pathArr.splice(1, 0, 'data');\n\n      path = pathArr.join('.');\n    }\n\n    return { dataKey, path };\n  }\n\n  const [firstPath, restPath] = path.split(/\\.(.+)/);\n\n  if (firstPath === '$last') {\n    const lastIndex = data.table.length - 1;\n\n    path = `${lastIndex}.${restPath || ''}`;\n  } else if (!restPath) {\n    path = `0.${firstPath}`;\n  } else if (typeof +firstPath !== 'number' || Number.isNaN(+firstPath)) {\n    path = `0.${firstPath}.${restPath}`;\n  }\n\n  path = path.replace(/\\.$/, '');\n\n  return { dataKey: 'table', path };\n}\n\nfunction replacer(\n  str,\n  {\n    data,\n    regex,\n    tagLen,\n    modifyPath,\n    checkExistence = false,\n    disableStringify = false,\n  }\n) {\n  const replaceResult = {\n    list: {},\n    value: str,\n  };\n\n  replaceResult.value = str.replace(regex, (match) => {\n    let key = match.slice(tagLen, -tagLen).trim();\n\n    if (!key) return '';\n\n    let result = '';\n    let stringify = false;\n    const isFunction = extractStrFunction(key);\n    const funcRef = isFunction && data.functions[isFunction.name];\n\n    if (modifyPath && !funcRef) {\n      key = modifyPath(key);\n    }\n\n    if (funcRef) {\n      const funcParams = isFunction.params.map((param) => {\n        const { value, list } = replacer(param, {\n          data,\n          tagLen: 1,\n          regex: /\\[(.*?)\\]/,\n        });\n\n        Object.assign(replaceResult.list, list);\n\n        return parseJSON(value, value);\n      });\n\n      result = funcRef.apply({ refData: data }, funcParams);\n    } else {\n      /* eslint-disable-next-line */\n      let { dataKey, path } = keyParser(key, data);\n      if (dataKey.startsWith('!')) {\n        stringify = true;\n        dataKey = dataKey.slice(1);\n      }\n\n      if (checkExistence) return objectPath.has(data[dataKey], path);\n\n      result = objectPath.get(data[dataKey], path);\n      if (typeof result === 'undefined') result = match;\n\n      if (dataKey === 'secrets') {\n        result =\n          typeof result !== 'string' ? {} : credentialUtil.decrypt(result);\n      }\n    }\n\n    const finalResult =\n      disableStringify || (typeof result === 'string' && !stringify)\n        ? result\n        : JSON.stringify(result);\n\n    replaceResult.list[match] = finalResult?.slice(0, 512) ?? finalResult;\n\n    return finalResult;\n  });\n\n  return replaceResult;\n}\n\nexport default function (str, refData, options = {}) {\n  if (!str || typeof str !== 'string') return '';\n\n  const data = { ...refData, functions: templatingFunctions };\n  const replacedList = {};\n\n  const replacedStr = replacer(`${str}`, {\n    data,\n    tagLen: 2,\n    regex: /\\{\\{(.*?)\\}\\}/g,\n    modifyPath: (path) => {\n      const { value, list } = replacer(path, {\n        data,\n        tagLen: 1,\n        regex: /\\[(.*?)\\]/g,\n        ...options,\n        checkExistence: false,\n      });\n      Object.assign(replacedList, list);\n\n      return value;\n    },\n    ...options,\n  });\n\n  Object.assign(replacedStr.list, replacedList);\n\n  return replacedStr;\n}\n"
  },
  {
    "path": "src/workflowEngine/templating/renderString.js",
    "content": "import { messageSandbox } from '../helper';\nimport mustacheReplacer from './mustacheReplacer';\n\nconst isFirefox = BROWSER_TYPE === 'firefox';\n\nexport default async function (str, data, options = {}) {\n  if (!str || typeof str !== 'string') return '';\n\n  const hasMustacheTag = /\\{\\{(.*?)\\}\\}/.test(str);\n  if (!hasMustacheTag) {\n    return {\n      list: {},\n      value: str,\n    };\n  }\n\n  let renderedValue = {};\n  const evaluateJS = str.startsWith('!!');\n\n  if (evaluateJS && !isFirefox) {\n    const refKeysRegex =\n      /(variables|table|secrets|loopData|workflow|googleSheets|globalData)@/g;\n    const strToRender = str.replace(refKeysRegex, '$1.');\n\n    renderedValue = await messageSandbox('blockExpression', {\n      str: strToRender,\n      data,\n    });\n  } else {\n    let copyStr = `${str}`;\n    if (evaluateJS) copyStr = copyStr.slice(2);\n\n    renderedValue = mustacheReplacer(copyStr, data, options);\n  }\n\n  return renderedValue;\n}\n"
  },
  {
    "path": "src/workflowEngine/templating/templatingFunctions.js",
    "content": "/* eslint-disable prefer-destructuring, no-useless-escape */\nimport jsonpath from 'jsonpath';\nimport dayjs from 'dayjs';\nimport relativeTime from 'dayjs/plugin/relativeTime';\n\ndayjs.extend(relativeTime);\n\nconst isAllNums = (...args) => args.every((arg) => !Number.isNaN(+arg));\nconst isObject = (obj) =>\n  typeof obj === 'object' && obj !== null && !Array.isArray(obj);\n\nfunction parseJSON(data, def) {\n  try {\n    const result = JSON.parse(data);\n\n    return result;\n  } catch (error) {\n    return def;\n  }\n}\n\nexport default {\n  date(...args) {\n    let date = new Date();\n    let dateFormat = 'DD-MM-YYYY';\n\n    if (args.length === 1) {\n      dateFormat = args[0];\n    } else if (args.length >= 2) {\n      date = new Date(args[0]);\n      dateFormat = args[1];\n    }\n\n    /* eslint-disable-next-line */\n    const isValidDate = date instanceof Date && !isNaN(date);\n    const dayjsDate = dayjs(isValidDate ? date : Date.now());\n\n    let result = dayjsDate.format(dateFormat);\n\n    if (dateFormat === 'relative') result = dayjsDate.fromNow();\n    else if (dateFormat === 'timestamp') result = dayjsDate.valueOf();\n\n    return result;\n  },\n  randint(min = 0, max = 100) {\n    return Math.round(Math.random() * (+max - +min) + +min);\n  },\n  getLength(str) {\n    const value = parseJSON(str, str);\n\n    return value.length ?? value;\n  },\n  slice(value, start, end) {\n    if (!value || !value.slice) return value;\n\n    const startIndex = Number.isNaN(+start) ? 0 : +start;\n    const endIndex = Number.isNaN(+end) ? value.length : +end;\n\n    return value.slice(startIndex, endIndex);\n  },\n  multiply(value, multiplyBy) {\n    if (!isAllNums(value, multiplyBy)) return value;\n\n    return +value * +multiplyBy;\n  },\n  increment(value, incrementBy) {\n    if (!isAllNums(value, incrementBy)) return value;\n\n    return +value + +incrementBy;\n  },\n  divide(value, divideBy) {\n    if (!isAllNums(value, divideBy)) return value;\n\n    return +value / +divideBy;\n  },\n  subtract(value, subtractBy) {\n    if (!isAllNums(value, subtractBy)) return value;\n\n    return +value - +subtractBy;\n  },\n  randData(str) {\n    if (Array.isArray(str)) {\n      const index = Math.floor(Math.random() * str.length);\n      return str[index];\n    }\n\n    const getRand = (data) => data[Math.floor(Math.random() * data.length)];\n    const lowercase = 'abcdefghijklmnopqrstuvwxyz';\n    const uppercase = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';\n    const digits = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];\n    const symbols = `!@#$%^&*()-_+={}[]|\\;:'\"<>,./?\"`;\n    const mapSamples = {\n      l: () => getRand(lowercase),\n      u: () => getRand(uppercase),\n      d: () => getRand(digits),\n      s: () => getRand(symbols),\n      f() {\n        return this.l() + this.u();\n      },\n      n() {\n        return this.l() + this.d();\n      },\n      m() {\n        return this.u() + this.d();\n      },\n      i() {\n        return this.l() + this.u() + this.d();\n      },\n      a() {\n        return getRand(lowercase + uppercase + digits.join('') + symbols);\n      },\n    };\n\n    return `${str}`.replace(\n      /\\?[a-zA-Z]/g,\n      (char) => mapSamples[char.at(-1)]?.() ?? char\n    );\n  },\n  filter(data, exps) {\n    if (!isObject(data) && !Array.isArray(data)) return data;\n\n    return jsonpath.query(data, exps);\n  },\n  replace(value, search, replace) {\n    if (!value) return value;\n\n    return value.replace(search, replace);\n  },\n  replaceAll(value, search, replace) {\n    if (!value) return value;\n\n    return value.replaceAll(search, replace);\n  },\n  toLowerCase(value) {\n    if (!value) return value;\n\n    return value.toLowerCase();\n  },\n  toUpperCase(value) {\n    if (!value) return value;\n\n    return value.toUpperCase();\n  },\n  modulo(value, divisor) {\n    return +value % +divisor;\n  },\n  stringify(value) {\n    return JSON.stringify(value);\n  },\n};\n"
  },
  {
    "path": "src/workflowEngine/utils/conditionCode.js",
    "content": "import { customAlphabet } from 'nanoid/non-secure';\nimport { automaRefDataStr, checkCSPAndInject, messageSandbox } from '../helper';\n\nconst nanoid = customAlphabet('1234567890abcdef', 5);\n\nexport default async function (activeTab, payload) {\n  const variableId = `automa${nanoid()}`;\n\n  if (\n    !payload.data.context ||\n    payload.data.context === 'website' ||\n    !payload.isPopup\n  ) {\n    if (!activeTab.id) throw new Error('no-tab');\n\n    const refDataScriptStr = automaRefDataStr(variableId);\n\n    // 构建一个完全自包含的函数字符串，其中所有变量都是硬编码的\n    // 这确保在跨环境执行时不依赖闭包变量\n    const callbackFunctionStr = `\n      function() {\n        // 直接返回一个自执行的异步函数字符串\n        // 所有变量值都已内联到字符串中\n        return \\`\n        (async () => {\n          const automa${variableId} = ${JSON.stringify(payload.refData)};\n          ${refDataScriptStr}\n          try {\n            ${payload.data.code}\n          } catch (error) {\n            return {\n              $isError: true,\n              message: error.message,\n            }\n          }\n        })();\n        \\`;\n      }\n      `;\n\n    const result = await checkCSPAndInject(\n      {\n        target: { tabId: activeTab.id },\n        debugMode: payload.debugMode,\n      },\n      callbackFunctionStr\n    );\n\n    return result.value;\n  }\n\n  const result = await messageSandbox('conditionCode', payload);\n  if (result && result.$isError) throw new Error(result.message);\n\n  return result;\n}\n"
  },
  {
    "path": "src/workflowEngine/utils/javascriptBlockUtil.js",
    "content": "import { getDocumentCtx } from '@/content/handleSelector';\n\nexport function automaFetchClient(id, { type, resource }) {\n  return new Promise((resolve, reject) => {\n    const validType = ['text', 'json', 'base64'];\n    if (!type || !validType.includes(type)) {\n      reject(new Error('The \"type\" must be \"text\" or \"json\"'));\n      return;\n    }\n\n    const eventName = `__automa-fetch-response-${id}__`;\n    const eventListener = ({ detail }) => {\n      if (detail.id !== id) return;\n\n      window.removeEventListener(eventName, eventListener);\n\n      if (detail.isError) {\n        reject(new Error(detail.result));\n      } else {\n        resolve(detail.result);\n      }\n    };\n\n    window.addEventListener(eventName, eventListener);\n    window.dispatchEvent(\n      new CustomEvent(`__automa-fetch__`, {\n        detail: {\n          id,\n          type,\n          resource,\n        },\n      })\n    );\n  });\n}\n\nexport function jsContentHandlerEval({\n  blockData,\n  automaScript,\n  preloadScripts,\n}) {\n  const preloadScriptsStr = preloadScripts\n    .map(({ script }) => script)\n    .join('\\n');\n\n  return `(() => {\n    ${preloadScriptsStr}\n\n    return new Promise(($automaResolve) => {\n      const $automaTimeoutMs = ${blockData.data.timeout};\n      let $automaTimeout = setTimeout(() => {\n        $automaResolve();\n      }, $automaTimeoutMs);\n\n      ${automaScript}\n\n      try {\n        ${blockData.data.code}\n\n        ${\n          blockData.data.code.includes('automaNextBlock')\n            ? ''\n            : 'automaNextBlock()'\n        }\n      } catch (error) {\n        return { columns: { data: { $error: true, message: error.message } } };\n      }\n    }).catch((error) => {\n      return { columns: { data: { $error: true, message: error.message } } };\n    });\n  })();`;\n}\n\nexport function jsContentHandler($blockData, $preloadScripts, $automaScript) {\n  return new Promise((resolve, reject) => {\n    try {\n      let $documentCtx = document;\n\n      if ($blockData.frameSelector) {\n        const iframeCtx = getDocumentCtx($blockData.frameSelector);\n        if (!iframeCtx) {\n          reject(new Error('iframe-not-found'));\n          return;\n        }\n\n        $documentCtx = iframeCtx;\n      }\n\n      const scriptAttr = `block--${$blockData.id}`;\n\n      const isScriptExists = $documentCtx.querySelector(\n        `.automa-custom-js[${scriptAttr}]`\n      );\n      if (isScriptExists) {\n        resolve('');\n        return;\n      }\n\n      const script = document.createElement('script');\n      script.setAttribute(scriptAttr, '');\n      script.classList.add('automa-custom-js');\n      script.textContent = `(() => {\n        ${$automaScript}\n\n        try {\n          ${$blockData.data.code}\n          ${\n            $blockData.data.everyNewTab ||\n            $blockData.data.code.includes('automaNextBlock')\n              ? ''\n              : 'automaNextBlock()'\n          }\n        } catch (error) {\n          console.error(error);\n          ${\n            $blockData.data.everyNewTab\n              ? ''\n              : 'automaNextBlock({ $error: true, message: error.message })'\n          }\n        }\n      })()`;\n\n      const preloadScriptsEl = $preloadScripts.map((item) => {\n        const scriptEl = document.createElement('script');\n        scriptEl.id = item.id;\n        scriptEl.textContent = item.script;\n\n        $documentCtx.head.appendChild(scriptEl);\n\n        return { element: scriptEl, removeAfterExec: item.removeAfterExec };\n      });\n\n      if (!$blockData.data.everyNewTab) {\n        let timeout;\n        let onNextBlock;\n        let onResetTimeout;\n\n        /* eslint-disable-next-line */\n        function cleanUp() {\n          script.remove();\n          preloadScriptsEl.forEach((item) => {\n            if (item.removeAfterExec) item.element.remove();\n          });\n\n          clearTimeout(timeout);\n\n          $documentCtx.body.removeEventListener(\n            '__automa-reset-timeout__',\n            onResetTimeout\n          );\n          $documentCtx.body.removeEventListener(\n            '__automa-next-block__',\n            onNextBlock\n          );\n        }\n\n        onNextBlock = ({ detail }) => {\n          cleanUp();\n          if (!detail) {\n            resolve({ columns: {}, variables: {} });\n            return;\n          }\n\n          const payload = {\n            insert: detail.insert,\n            data: detail.data?.$error\n              ? detail.data\n              : JSON.stringify(detail?.data ?? {}),\n          };\n          resolve({\n            columns: payload,\n            variables: detail.refData?.variables,\n          });\n        };\n        onResetTimeout = () => {\n          clearTimeout(timeout);\n          timeout = setTimeout(cleanUp, $blockData.data.timeout);\n        };\n\n        $documentCtx.body.addEventListener(\n          '__automa-next-block__',\n          onNextBlock\n        );\n        $documentCtx.body.addEventListener(\n          '__automa-reset-timeout__',\n          onResetTimeout\n        );\n\n        timeout = setTimeout(cleanUp, $blockData.data.timeout);\n      } else {\n        resolve();\n      }\n\n      $documentCtx.head.appendChild(script);\n    } catch (error) {\n      console.error(error);\n    }\n  });\n}\n"
  },
  {
    "path": "src/workflowEngine/utils/testConditions.js",
    "content": "import { parseJSON } from '@/utils/helper';\nimport { conditionBuilder } from '@/utils/shared';\nimport cloneDeep from 'lodash.clonedeep';\nimport renderString from '../templating/renderString';\n\nconst isBoolStr = (str) => {\n  if (str === 'true') return true;\n  if (str === 'false') return false;\n\n  return str;\n};\nconst isNumStr = (str) => (Number.isNaN(+str) ? str : +str);\nconst comparisons = {\n  eq: (a, b) => a === b,\n  eqi: (a, b) => a?.toLocaleLowerCase() === b?.toLocaleLowerCase(),\n  nq: (a, b) => a !== b,\n  gt: (a, b) => isNumStr(a) > isNumStr(b),\n  gte: (a, b) => isNumStr(a) >= isNumStr(b),\n  lt: (a, b) => isNumStr(a) < isNumStr(b),\n  lte: (a, b) => isNumStr(a) <= isNumStr(b),\n  cnt: (a, b) => a?.includes(b) ?? false,\n  cni: (a, b) =>\n    a?.toLocaleLowerCase().includes(b?.toLocaleLowerCase()) ?? false,\n  nct: (a, b) => !comparisons.cnt(a, b),\n  nci: (a, b) => !comparisons.cni(a, b),\n  stw: (a, b) => a?.startsWith(b) ?? false,\n  enw: (a, b) => a?.endsWith(b) ?? false,\n  rgx: (a, b) => {\n    const match = b.match(/^\\/(.*?)\\/([gimy]*)$/);\n    const regex = match ? new RegExp(match[1], match[2]) : new RegExp(b);\n\n    return regex.test(a);\n  },\n  itr: (a) => Boolean(isBoolStr(a)),\n  ifl: (a) => !isBoolStr(a),\n};\n\nconst convertDataType = {\n  string: (val) => `${val}`,\n  number: (val) => +val,\n  json: (val) => parseJSON(val, null),\n  boolean: (val) => Boolean(isBoolStr(val)),\n};\n\nexport default async function (conditionsArr, workflowData) {\n  const result = {\n    isMatch: false,\n    replacedValue: {},\n  };\n\n  async function getConditionItemValue({ type, data }) {\n    if (type.startsWith('data')) {\n      let dataPath = data.dataPath.trim().replace('@', '.');\n      const isInsideBrackets =\n        dataPath.startsWith('{{') && dataPath.endsWith('}}');\n\n      if (!isInsideBrackets) {\n        dataPath = `{{${dataPath}}}`;\n      }\n\n      let dataExists = await renderString(\n        dataPath,\n        workflowData.refData,\n        workflowData.isPopup,\n        {\n          checkExistence: true,\n        }\n      );\n      // It return string for some reason\n      dataExists = Boolean(parseJSON(dataExists.value, false));\n\n      return dataExists;\n    }\n\n    const copyData = cloneDeep(data);\n\n    for (const key of Object.keys(data)) {\n      const { value, list } = await renderString(\n        copyData[key],\n        workflowData.refData,\n        workflowData.isPopup\n      );\n\n      copyData[key] = value ?? '';\n      Object.assign(result.replacedValue, list);\n    }\n\n    if (type === 'value') {\n      const regex = /^(json|string|number|boolean)::/;\n      if (regex.test(copyData.value)) {\n        const [dataType, value] = copyData.value.split(/::(.*)/s);\n        return convertDataType[dataType](value);\n      }\n\n      return copyData.value;\n    }\n\n    if (type.startsWith('code')) {\n      let conditionValue;\n\n      const newRefData = {};\n      Object.keys(workflowData.refData).forEach((keyword) => {\n        if (!copyData.code.includes(keyword)) return;\n\n        newRefData[keyword] = workflowData.refData[keyword];\n      });\n\n      if (workflowData.isMV2 && data.context !== 'background') {\n        conditionValue = await workflowData.sendMessage({\n          type: 'condition-builder',\n          data: {\n            type,\n            data: copyData,\n            refData: newRefData,\n          },\n        });\n      } else {\n        conditionValue = await workflowData.checkCodeCondition({\n          data: copyData,\n          refData: newRefData,\n          isPopup: workflowData.isPopup,\n        });\n      }\n\n      return conditionValue;\n    }\n\n    if (type.startsWith('element')) {\n      const conditionValue = await workflowData.sendMessage({\n        type: 'condition-builder',\n        data: {\n          type,\n          data: copyData,\n        },\n      });\n\n      return conditionValue;\n    }\n\n    return '';\n  }\n\n  async function checkConditions(items) {\n    let conditionResult = true;\n    const condition = {\n      value: '',\n      operator: '',\n    };\n\n    for (const { category, data, type } of items) {\n      if (!conditionResult) return conditionResult;\n\n      if (category === 'compare') {\n        const typeConfig = conditionBuilder.compareTypes.find(\n          ({ id }) => id === type\n        );\n\n        if (!typeConfig) {\n          return conditionResult;\n        }\n\n        const { needValue } = typeConfig;\n\n        if (!needValue) {\n          conditionResult = comparisons[type](condition.value);\n\n          return conditionResult;\n        }\n\n        condition.operator = type;\n      } else if (category === 'value') {\n        const conditionValue = await getConditionItemValue({ data, type });\n        const valueConfig = conditionBuilder.valueTypes.find(\n          ({ id }) => id === type\n        );\n\n        if (!valueConfig) {\n          conditionResult = conditionValue;\n        } else {\n          const { compareable } = valueConfig;\n          if (!compareable) {\n            conditionResult = conditionValue;\n          } else if (condition.operator) {\n            conditionResult = comparisons[condition.operator](\n              condition.value,\n              conditionValue\n            );\n\n            condition.operator = '';\n          }\n        }\n\n        condition.value = conditionValue;\n      }\n    }\n\n    return conditionResult;\n  }\n\n  for (const { conditions } of conditionsArr) {\n    if (result.isMatch) return result;\n\n    let isAllMatch = false;\n\n    for (const { items } of conditions) {\n      isAllMatch = await checkConditions(items, workflowData);\n\n      if (!isAllMatch) break;\n    }\n\n    result.isMatch = isAllMatch;\n  }\n\n  return result;\n}\n"
  },
  {
    "path": "src/workflowEngine/utils/webhookUtil.js",
    "content": "import { parseJSON, isWhitespace } from '@/utils/helper';\nimport getFile from '@/utils/getFile';\n\nconst renderContent = async (content, contentType) => {\n  if (contentType === 'text') return content;\n\n  const renderedJson = parseJSON(content, new Error('invalid-body'));\n\n  if (renderedJson instanceof Error) throw renderedJson;\n\n  if (contentType === 'form') {\n    return new URLSearchParams(renderedJson);\n  }\n  if (contentType === 'form-data') {\n    if (!Array.isArray(renderedJson) || !Array.isArray(renderedJson[0])) {\n      throw new Error('The body must be 2D Array');\n    }\n\n    const formData = new FormData();\n    for (const data of renderedJson) {\n      const [name, path, customFilename] = data;\n      const isFile = /\\.(.*)/.test(path) && BROWSER_TYPE !== 'firefox';\n      const isURL = path.startsWith('http');\n\n      let formContent = path;\n      let filename = customFilename;\n\n      if (isFile || isURL) {\n        formContent = await getFile(path, { returnValue: true });\n\n        if (!filename) {\n          filename = path.split('/').pop();\n        }\n\n        if (!formContent) throw new Error('File not found');\n      } else {\n        let base64Str = '';\n\n        if (path.includes('base64')) {\n          base64Str = path;\n        } else {\n          base64Str = `data:text/plain;base64,${window.btoa(path)}`;\n        }\n\n        const response = await fetch(base64Str);\n        const result = await response.blob();\n\n        formContent = result;\n      }\n\n      const args = [name, formContent];\n      if (filename) args.push(filename);\n\n      formData.append(...args);\n    }\n\n    return formData;\n  }\n\n  return JSON.stringify(renderedJson);\n};\n\nconst filterHeaders = (headers) => {\n  const filteredHeaders = {};\n\n  if (!headers || !Array.isArray(headers)) return filteredHeaders;\n\n  headers.forEach((item) => {\n    if (item.name && item.value) {\n      filteredHeaders[item.name] = item.value;\n    }\n  });\n  return filteredHeaders;\n};\n\nconst contentTypes = {\n  text: 'text/plain',\n  json: 'application/json',\n  'form-data': 'multipart/form-data',\n  form: 'application/x-www-form-urlencoded',\n};\nconst notHaveBody = ['GET', 'HEAD'];\n\nexport async function executeWebhook({\n  url,\n  contentType,\n  headers,\n  timeout,\n  body,\n  method,\n}) {\n  let timeoutId = null;\n  let controller = null;\n\n  if (timeout > 0) {\n    controller = new AbortController();\n    timeoutId = setTimeout(() => {\n      controller.abort();\n    }, timeout);\n  }\n\n  try {\n    const finalHeaders = filterHeaders(headers);\n    if (contentType !== 'form-data')\n      finalHeaders['Content-Type'] = contentTypes[contentType || 'json'];\n\n    const payload = {\n      headers: finalHeaders,\n      method: method || 'POST',\n    };\n    if (controller) payload.signal = controller.signal;\n\n    if (!notHaveBody.includes(method || 'POST') && !isWhitespace(body)) {\n      payload.body = await renderContent(body, contentType);\n    }\n\n    const response = await fetch(url, payload);\n\n    if (timeoutId) clearTimeout(timeoutId);\n\n    return response;\n  } catch (error) {\n    if (timeoutId) clearTimeout(timeoutId);\n\n    throw error;\n  }\n}\n"
  },
  {
    "path": "src/workflowEngine/workflowEvent.js",
    "content": "import { nanoid } from 'nanoid';\nimport { messageSandbox } from './helper';\nimport renderString from './templating/renderString';\n\nclass WorkflowEvent {\n  static async #httpRequest({ url, method, headers, body }, refData) {\n    if (!url.trim()) return;\n\n    const reqHeaders = {\n      'Content-Type': 'application/json',\n    };\n    headers.forEach((header) => {\n      reqHeaders[header.name] = header.value;\n    });\n\n    const renderedBody =\n      method !== 'GET' ? (await renderString(body, refData)).value : undefined;\n\n    await fetch(url, {\n      method,\n      body: renderedBody,\n      headers: reqHeaders,\n    });\n  }\n\n  static async #javascriptCode(event, refData) {\n    const instanceId = `automa${nanoid()}`;\n\n    await messageSandbox('javascriptBlock', {\n      refData,\n      instanceId,\n      preloadScripts: [],\n      blockData: {\n        code: event.code,\n      },\n    });\n  }\n\n  static async handle(event, refData) {\n    switch (event.type) {\n      case 'http-request':\n        await this.#httpRequest(event, refData);\n        break;\n      case 'js-code':\n        await this.#javascriptCode(event, refData);\n        break;\n      default:\n    }\n  }\n}\n\nexport default WorkflowEvent;\n"
  },
  {
    "path": "tailwind.config.js",
    "content": "/* eslint-disable */\nconst defaultTheme = require('tailwindcss/defaultTheme');\nconst colors = require('tailwindcss/colors');\n\nfunction withOpacityValue(variable) {\n  return ({ opacityValue }) => {\n    if (opacityValue === undefined) {\n      return `rgb(var(${variable}))`;\n    }\n    return `rgb(var(${variable}) / ${opacityValue})`;\n  };\n}\nfunction rem2px(input, fontSize = 16) {\n  if (input == null) {\n    return input;\n  }\n\n  switch (typeof input) {\n    case 'object':\n      if (Array.isArray(input)) {\n        return input.map((val) => rem2px(val, fontSize));\n      }\n      const ret = {};\n      for (const key in input) {\n        ret[key] = rem2px(input[key]);\n      }\n      return ret;\n\n    case 'string':\n      return input.replace(\n        /(\\d*\\.?\\d+)rem$/,\n        (_, val) => `${parseFloat(val) * fontSize}px`\n      );\n    default:\n      return input;\n  }\n}\n\nmodule.exports = {\n  content: ['./src/**/*.{js,jsx,ts,tsx,vue}', './business/**/*.{js,jsx,ts,tsx,vue}'],\n  darkMode: 'class', // or 'media' or 'class'\n  theme: {\n    borderRadius: rem2px(defaultTheme.borderRadius),\n    columns: rem2px(defaultTheme.columns),\n    fontSize: rem2px(defaultTheme.fontSize),\n    lineHeight: rem2px(defaultTheme.lineHeight),\n    maxWidth: ({ theme, breakpoints }) => ({\n      ...rem2px(defaultTheme.maxWidth({ theme, breakpoints })),\n    }),\n    spacing: rem2px(defaultTheme.spacing),\n    extend: {\n      colors: {\n        primary: withOpacityValue('--color-primary'),\n        secondary: withOpacityValue('--color-secondary'),\n        accent: withOpacityValue('--color-accent'),\n        gray: colors.zinc,\n        orange: colors.orange,\n      },\n      fontFamily: {\n        sans: ['Poppins', 'sans-serif'],\n        mono: ['Source Code Pro', 'monospace'],\n      },\n      container: {\n        center: true,\n        padding: {\n          DEFAULT: '1rem',\n          sm: '2rem',\n        },\n      },\n    },\n  },\n  variants: {\n    extend: {},\n  },\n  plugins: [require('@tailwindcss/typography')],\n};\n"
  },
  {
    "path": "utils/build-zip.js",
    "content": "/* eslint-disable no-console */\nconst fs = require('fs');\nconst path = require('path');\nconst archiver = require('archiver');\nconst packageJSON = require('../package.json');\n\nconst browser = process.env.BROWSER || 'chrome';\nconst appVersion = packageJSON.version;\nconst fileName = `${packageJSON.name}-${browser}-v${appVersion}.zip`;\n\nconst destDir = path.join(__dirname, '../build');\nconst zipDir = path.join(__dirname, '../build-zip', appVersion);\n\nif (!fs.existsSync(zipDir)) {\n  fs.mkdirSync(zipDir, { recursive: true });\n}\n\nconst archive = archiver('zip', { zlib: { level: 9 } });\nconst stream = fs.createWriteStream(path.join(zipDir, fileName));\n\narchive\n  .directory(destDir, false)\n  .on('error', (error) => {\n    console.error(error);\n  })\n  .pipe(stream);\n\nstream.on('close', () => console.log('Success'));\narchive.finalize();\n"
  },
  {
    "path": "utils/build.js",
    "content": "// Do this as the first thing so that any code reading it knows the right env.\nprocess.env.BABEL_ENV = 'production';\nprocess.env.NODE_ENV = 'production';\nprocess.env.ASSET_PATH = '/';\n\nconst webpack = require('webpack');\nconst config = require('../webpack.config');\n\ndelete config.chromeExtensionBoilerplate;\n\nconfig.mode = 'production';\n\nwebpack(config, function (err) {\n  if (err) throw err;\n});\n"
  },
  {
    "path": "utils/clean-build-cache.js",
    "content": "/* eslint-disable no-console */\nconst fs = require('fs');\nconst path = require('path');\nconst packageJSON = require('../package.json');\n\nconst appVersion = packageJSON.version;\nconst zipDir = path.join(__dirname, '../build-zip', appVersion);\n\nif (fs.existsSync(zipDir)) {\n  const files = fs.readdirSync(zipDir);\n\n  if (files.length > 0) {\n    console.log(`Cleaning old build cache for version ${appVersion}...`);\n\n    files.forEach((file) => {\n      const filePath = path.join(zipDir, file);\n      const stat = fs.statSync(filePath);\n\n      if (stat.isFile()) {\n        fs.unlinkSync(filePath);\n        console.log(`  Removed: ${file}`);\n      } else if (stat.isDirectory()) {\n        fs.rmSync(filePath, { recursive: true, force: true });\n        console.log(`  Removed directory: ${file}`);\n      }\n    });\n\n    console.log('Build cache cleaned successfully.');\n  } else {\n    console.log(`No cache files found for version ${appVersion}.`);\n  }\n} else {\n  console.log(\n    `Build cache directory for version ${appVersion} does not exist. Skipping cleanup.`\n  );\n}\n"
  },
  {
    "path": "utils/env.js",
    "content": "// tiny wrapper with default env vars\nmodule.exports = {\n  NODE_ENV: process.env.NODE_ENV || 'development',\n  PORT: process.env.PORT || 3001,\n  BROWSER: process.env.BROWSER || 'chrome',\n};\n"
  },
  {
    "path": "utils/webserver.js",
    "content": "// Do this as the first thing so that any code reading it knows the right env.\nprocess.env.BABEL_ENV = 'development';\nprocess.env.NODE_ENV = 'development';\nprocess.env.ASSET_PATH = '/';\n\nconst WebpackDevServer = require('webpack-dev-server');\nconst webpack = require('webpack');\nconst path = require('path');\nconst config = require('../webpack.config');\nconst env = require('./env');\n\nconst options = config.chromeExtensionBoilerplate || {};\nconst excludeEntriesToHotReload = options.notHotReload || [];\n\nfor (const entryName in config.entry) {\n  if (excludeEntriesToHotReload.indexOf(entryName) === -1) {\n    config.entry[entryName] = [\n      'webpack/hot/dev-server',\n      `webpack-dev-server/client?hot=true&hostname=localhost&port=${env.PORT}`,\n    ].concat(config.entry[entryName]);\n  }\n}\n\nconfig.plugins = [new webpack.HotModuleReplacementPlugin()].concat(\n  config.plugins || []\n);\n\ndelete config.chromeExtensionBoilerplate;\n\nconst compiler = webpack(config);\n\nconst server = new WebpackDevServer(\n  {\n    https: false,\n    hot: false,\n    client: false,\n    host: 'localhost',\n    port: env.PORT,\n    static: {\n      directory: path.join(__dirname, '../build'),\n    },\n    devMiddleware: {\n      publicPath: `http://localhost:${env.PORT}/`,\n      writeToDisk: true,\n    },\n    headers: {\n      'Access-Control-Allow-Origin': '*',\n    },\n    allowedHosts: 'all',\n  },\n  compiler\n);\n\nif (process.env.NODE_ENV === 'development' && module.hot) {\n  module.hot.accept();\n}\n\n(async () => {\n  await server.start();\n})();\n"
  },
  {
    "path": "webpack.config.js",
    "content": "const webpack = require('webpack');\nconst path = require('path');\nconst fileSystem = require('fs-extra');\nconst { CleanWebpackPlugin } = require('clean-webpack-plugin');\nconst { VueLoaderPlugin } = require('vue-loader');\nconst CopyWebpackPlugin = require('copy-webpack-plugin');\nconst HtmlWebpackPlugin = require('html-webpack-plugin');\nconst MiniCssExtractPlugin = require('mini-css-extract-plugin');\nconst TerserPlugin = require('terser-webpack-plugin');\nconst env = require('./utils/env');\n\nconst ASSET_PATH = process.env.ASSET_PATH || '/';\n\nconst alias = {\n  '@': path.resolve(__dirname, 'src/'),\n  secrets: path.join(__dirname, 'secrets.blank.js'),\n  '@business': path.resolve(__dirname, 'business/dev'),\n};\n\n// load the secrets\nconst secretsPath = path.join(__dirname, `secrets.${env.NODE_ENV}.js`);\n\nconst fileExtensions = [\n  'jpg',\n  'jpeg',\n  'png',\n  'gif',\n  'eot',\n  'otf',\n  'svg',\n  'ttf',\n  'woff',\n  'woff2',\n];\n\nif (fileSystem.existsSync(secretsPath)) {\n  alias.secrets = secretsPath;\n}\n\nconst options = {\n  mode: process.env.NODE_ENV || 'development',\n  entry: {\n    sandbox: path.join(__dirname, 'src', 'sandbox', 'index.js'),\n    execute: path.join(__dirname, 'src', 'execute', 'index.js'),\n    newtab: path.join(__dirname, 'src', 'newtab', 'index.js'),\n    popup: path.join(__dirname, 'src', 'popup', 'index.js'),\n    params: path.join(__dirname, 'src', 'params', 'index.js'),\n    background: path.join(__dirname, 'src', 'background', 'index.js'),\n    contentScript: path.join(__dirname, 'src', 'content', 'index.js'),\n    offscreen: path.join(__dirname, 'src', 'offscreen', 'index.js'),\n    recordWorkflow: path.join(\n      __dirname,\n      'src',\n      'content',\n      'services',\n      'recordWorkflow',\n      'index.js'\n    ),\n    webService: path.join(\n      __dirname,\n      'src',\n      'content',\n      'services',\n      'webService.js'\n    ),\n    elementSelector: path.join(\n      __dirname,\n      'src',\n      'content',\n      'elementSelector',\n      'index.js'\n    ),\n  },\n  chromeExtensionBoilerplate: {\n    notHotReload: [\n      'background',\n      'webService',\n      'contentScript',\n      'recordWorkflow',\n      'elementSelector',\n    ],\n  },\n  output: {\n    path: path.resolve(__dirname, 'build'),\n    filename: '[name].bundle.js',\n    publicPath: ASSET_PATH,\n  },\n  module: {\n    rules: [\n      {\n        test: /\\.vue$/,\n        loader: 'vue-loader',\n        options: {\n          reactivityTransform: true,\n        },\n      },\n      {\n        test: /\\.css$/,\n        use: [\n          MiniCssExtractPlugin.loader,\n          {\n            loader: 'css-loader',\n          },\n          {\n            loader: 'postcss-loader',\n          },\n        ],\n      },\n      {\n        test: /\\.(json5?|ya?ml)$/, // target json, json5, yaml and yml files\n        type: 'javascript/auto',\n        // Use `Rule.include` to specify the files of locale messages to be pre-compiled\n        include: [path.resolve(__dirname, './src/locales')],\n        loader: '@intlify/vue-i18n-loader',\n      },\n      {\n        test: new RegExp(`.(${fileExtensions.join('|')})$`),\n        type: 'asset/resource',\n        dependency: { not: [/node_modules/] },\n        generator: {\n          filename: '[name][ext]',\n        },\n      },\n      {\n        test: /\\.js$/,\n        use: [\n          {\n            loader: 'source-map-loader',\n          },\n          {\n            loader: 'babel-loader',\n          },\n        ],\n        exclude: /node_modules/,\n      },\n    ],\n  },\n  resolve: {\n    alias,\n    extensions: fileExtensions\n      .map((extension) => `.${extension}`)\n      .concat(['.js', '.vue', '.css']),\n  },\n  plugins: [\n    new MiniCssExtractPlugin(),\n    new VueLoaderPlugin(),\n    new webpack.DefinePlugin({\n      BROWSER_TYPE: JSON.stringify(env.BROWSER),\n    }),\n    new webpack.ProgressPlugin(),\n    // clean the build folder\n    new CleanWebpackPlugin({\n      verbose: false,\n    }),\n    // expose and write the allowed env vars on the compiled bundle\n    new webpack.EnvironmentPlugin(['NODE_ENV']),\n    new CopyWebpackPlugin({\n      patterns: [\n        {\n          from:\n            env.NODE_ENV === 'development' && env.BROWSER === 'chrome'\n              ? `src/manifest.${env.BROWSER}.dev.json`\n              : `src/manifest.${env.BROWSER}.json`,\n          to: path.join(__dirname, 'build', 'manifest.json'),\n          force: true,\n          toType: 'file',\n          transform(content) {\n            const manifestObj = {\n              description: process.env.npm_package_description,\n              version: process.env.npm_package_version,\n              ...JSON.parse(content.toString()),\n            };\n            const isChrome = env.BROWSER === 'chrome';\n\n            if (manifestObj.version.includes('-')) {\n              const [version, preRelease] = manifestObj.version.split('-');\n\n              if (isChrome) {\n                manifestObj.version = version;\n                manifestObj.version_name = `${version} ${preRelease}`;\n              } else {\n                manifestObj.version = `${version}${preRelease}`;\n              }\n            }\n\n            return Buffer.from(JSON.stringify(manifestObj));\n          },\n        },\n        {\n          from: 'src/assets/images/icon-128.png',\n          to: path.join(__dirname, 'build'),\n          force: true,\n        },\n        {\n          from: 'src/assets/images/icon-dev-128.png',\n          to: path.join(__dirname, 'build'),\n          force: true,\n        },\n      ],\n    }),\n    new HtmlWebpackPlugin({\n      template: path.join(__dirname, 'src', 'newtab', 'index.html'),\n      filename: 'newtab.html',\n      chunks: ['newtab'],\n      cache: false,\n    }),\n    new HtmlWebpackPlugin({\n      template: path.join(__dirname, 'src', 'sandbox', 'index.html'),\n      filename: 'sandbox.html',\n      chunks: ['sandbox'],\n      cache: false,\n    }),\n    new HtmlWebpackPlugin({\n      template: path.join(__dirname, 'src', 'execute', 'index.html'),\n      filename: 'execute.html',\n      chunks: ['execute'],\n      cache: false,\n    }),\n    new HtmlWebpackPlugin({\n      template: path.join(__dirname, 'src', 'popup', 'index.html'),\n      filename: 'popup.html',\n      chunks: ['popup'],\n      cache: false,\n    }),\n    new HtmlWebpackPlugin({\n      template: path.join(__dirname, 'src', 'params', 'index.html'),\n      filename: 'params.html',\n      chunks: ['params'],\n      cache: false,\n    }),\n    new HtmlWebpackPlugin({\n      template: path.join(__dirname, 'src', 'offscreen', 'index.html'),\n      filename: 'offscreen.html',\n      chunks: ['offscreen'],\n      cache: false,\n    }),\n    new webpack.DefinePlugin({\n      __VUE_OPTIONS_API__: true,\n      __VUE_PROD_DEVTOOLS__: false,\n    }),\n    // Fix i18n warning\n    new webpack.DefinePlugin({\n      __VUE_I18N_FULL_INSTALL__: JSON.stringify(true),\n      __INTLIFY_PROD_DEVTOOLS__: JSON.stringify(false),\n      __VUE_I18N_LEGACY_API__: JSON.stringify(false),\n    }),\n  ],\n  infrastructureLogging: {\n    level: 'info',\n  },\n};\n\nif (env.NODE_ENV === 'development') {\n  options.devtool = 'cheap-module-source-map';\n} else {\n  options.optimization = {\n    minimize: true,\n    minimizer: [\n      new TerserPlugin({\n        extractComments: false,\n      }),\n    ],\n  };\n}\n\nmodule.exports = options;\n"
  }
]