Ahmad Kholid 3 年 前
コミット
62e88785d8
33 ファイル変更429 行追加170 行削除
  1. 2 2
      package.json
  2. 2 5
      src/background/workflow-engine/blocks-handler/handler-conditions.js
  3. 12 0
      src/background/workflow-engine/blocks-handler/handler-new-tab.js
  4. 10 4
      src/background/workflow-engine/blocks-handler/handler-new-window.js
  5. 1 0
      src/components/block/BlockBasic.vue
  6. 9 36
      src/components/newtab/app/AppSidebar.vue
  7. 5 2
      src/components/newtab/logs/LogsDataViewer.vue
  8. 1 1
      src/components/newtab/workflow/WorkflowBuilder.vue
  9. 5 2
      src/components/newtab/workflow/WorkflowGlobalData.vue
  10. 5 2
      src/components/newtab/workflow/edit/EditExecuteWorkflow.vue
  11. 5 2
      src/components/newtab/workflow/edit/EditGoogleSheets.vue
  12. 5 2
      src/components/newtab/workflow/edit/EditJavascriptCode.vue
  13. 5 2
      src/components/newtab/workflow/edit/EditLoopData.vue
  14. 30 0
      src/components/newtab/workflow/edit/EditNewWindow.vue
  15. 43 20
      src/components/newtab/workflow/edit/EditUploadFile.vue
  16. 5 2
      src/components/newtab/workflow/edit/EditWebhook.vue
  17. 6 2
      src/content/blocks-handler/handler-take-screenshot.js
  18. 1 1
      src/content/blocks-handler/handler-upload-file.js
  19. 79 8
      src/content/element-selector/App.vue
  20. 4 8
      src/content/element-selector/AppBlocks.vue
  21. 0 39
      src/content/element-selector/AppElementAttributes.vue
  22. 34 0
      src/content/element-selector/AppElementList.vue
  23. 8 1
      src/locales/en/blocks.json
  24. 2 1
      src/locales/en/newtab.json
  25. 2 0
      src/newtab/App.vue
  26. 6 3
      src/newtab/pages/Settings.vue
  27. 11 2
      src/newtab/pages/collections/[id].vue
  28. 109 0
      src/newtab/pages/settings/About.vue
  29. 5 1
      src/newtab/router.js
  30. 1 0
      src/store/index.js
  31. 2 0
      src/utils/reference-data/mustache-replacer.js
  32. 4 0
      src/utils/shared.js
  33. 10 22
      yarn.lock

+ 2 - 2
package.json

@@ -1,6 +1,6 @@
 {
   "name": "automa",
-  "version": "0.17.0",
+  "version": "0.17.2",
   "description": "An extension for automating your browser by connecting blocks",
   "license": "MIT",
   "repository": {
@@ -23,7 +23,7 @@
   },
   "dependencies": {
     "@codemirror/basic-setup": "^0.19.1",
-    "@codemirror/lang-javascript": "^0.19.3",
+    "@codemirror/lang-javascript": "0.19.1",
     "@codemirror/lang-json": "^0.19.1",
     "@codemirror/theme-one-dark": "^0.19.1",
     "@medv/finder": "^2.1.0",

+ 2 - 5
src/background/workflow-engine/blocks-handler/handler-conditions.js

@@ -19,11 +19,8 @@ function conditions({ data, outputs }, { prevBlockData, refData }) {
     data.conditions.forEach(({ type, value, compareValue }, index) => {
       if (isConditionMatch) return;
 
-      const firstValue = mustacheReplacer({
-        str: compareValue ?? prevData,
-        data: refData,
-      });
-      const secondValue = mustacheReplacer({ str: value, data: refData });
+      const firstValue = mustacheReplacer(compareValue ?? prevData, refData);
+      const secondValue = mustacheReplacer(value, refData);
 
       const isMatch = compareBlockValue(type, firstValue, secondValue);
 

+ 12 - 0
src/background/workflow-engine/blocks-handler/handler-new-tab.js

@@ -1,5 +1,6 @@
 import browser from 'webextension-polyfill';
 import { getBlockConnection } from '../helper';
+import { isWhitespace } from '@/utils/helper';
 
 async function newTab(block) {
   if (this.windowId) {
@@ -14,6 +15,16 @@ async function newTab(block) {
 
   try {
     const { updatePrevTab, url, active, inGroup } = block.data;
+    const isInvalidUrl = !/^https?/.test(url);
+
+    if (isInvalidUrl) {
+      const error = new Error(
+        isWhitespace(url) ? 'url-empty' : 'invalid-active-tab'
+      );
+      error.data = { url };
+
+      throw error;
+    }
 
     if (updatePrevTab && this.activeTab.id) {
       await browser.tabs.update(this.activeTab.id, { url, active });
@@ -54,6 +65,7 @@ async function newTab(block) {
     };
   } catch (error) {
     console.error(error);
+    console.dir(error);
     error.nextBlockId = nextBlockId;
 
     throw error;

+ 10 - 4
src/background/workflow-engine/blocks-handler/handler-new-window.js

@@ -6,11 +6,17 @@ export async function newWindow(block) {
 
   try {
     const { incognito, windowState } = block.data;
-    const { id } = await browser.windows.create({
-      incognito,
-      state: windowState,
-    });
+    const windowOptions = { incognito, state: windowState };
 
+    if (windowState === 'normal') {
+      ['top', 'left', 'height', 'width'].forEach((key) => {
+        if (block.data[key] <= 0) return;
+
+        windowOptions[key] = block.data[key];
+      });
+    }
+
+    const { id } = await browser.windows.create(windowOptions);
     this.windowId = id;
 
     return {

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

@@ -80,6 +80,7 @@ function handleStartDrag(event) {
     data: block.data,
     id: block.details.id,
     blockId: block.id,
+    fromBlockBasic: true,
   };
 
   event.dataTransfer.setData('block', JSON.stringify(payload));

+ 9 - 36
src/components/newtab/app/AppSidebar.vue

@@ -2,7 +2,11 @@
   <aside
     class="fixed flex flex-col items-center h-screen left-0 top-0 w-16 py-6 bg-white z-50"
   >
-    <img src="@/assets/svg/logo.svg" class="w-10 mb-4 mx-auto" />
+    <img
+      :title="`v${extensionVersion}`"
+      src="@/assets/svg/logo.svg"
+      class="w-10 mb-4 mx-auto"
+    />
     <div
       class="space-y-2 w-full relative text-center"
       @mouseleave="showHoverIndicator = false"
@@ -37,24 +41,9 @@
       </router-link>
     </div>
     <div class="flex-grow"></div>
-    <ui-popover placement="right" trigger="mouseenter click">
-      <template #trigger>
-        <v-remixicon class="cursor-pointer" name="riInformationLine" />
-      </template>
-      <ui-list class="space-y-1">
-        <ui-list-item
-          v-for="item in links"
-          :key="item.name"
-          :href="item.url"
-          tag="a"
-          rel="noopener"
-          target="_blank"
-        >
-          <v-remixicon :name="item.icon" class="-ml-1 mr-2" />
-          <span>{{ item.name }}</span>
-        </ui-list-item>
-      </ui-list>
-    </ui-popover>
+    <router-link v-tooltip:right.group="t('settings.menu.about')" to="/about">
+      <v-remixicon class="cursor-pointer" name="riInformationLine" />
+    </router-link>
   </aside>
 </template>
 <script setup>
@@ -68,23 +57,7 @@ useGroupTooltip();
 const { t } = useI18n();
 const router = useRouter();
 
-const links = [
-  {
-    name: 'Donate',
-    icon: 'riHandHeartLine',
-    url: 'https://paypal.me/akholid060',
-  },
-  {
-    name: t('common.docs', 2),
-    icon: 'riBook3Line',
-    url: 'https://docs.automa.site',
-  },
-  {
-    name: 'GitHub',
-    icon: 'riGithubFill',
-    url: 'https://github.com/kholid060/automa',
-  },
-];
+const extensionVersion = chrome.runtime.getManifest().version;
 const tabs = [
   {
     id: 'dashboard',

+ 5 - 2
src/components/newtab/logs/LogsDataViewer.vue

@@ -35,11 +35,14 @@
   />
 </template>
 <script setup>
-import { ref, computed } from 'vue';
+import { ref, computed, defineAsyncComponent } from 'vue';
 import { useI18n } from 'vue-i18n';
 import { dataExportTypes } from '@/utils/shared';
 import dataExporter, { generateJSON } from '@/utils/data-exporter';
-import SharedCodemirror from '@/components/newtab/shared/SharedCodemirror.vue';
+
+const SharedCodemirror = defineAsyncComponent(() =>
+  import('@/components/newtab/shared/SharedCodemirror.vue')
+);
 
 const props = defineProps({
   log: {

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

@@ -154,7 +154,7 @@ export default {
     function dropHandler({ dataTransfer, clientX, clientY, target }) {
       const block = JSON.parse(dataTransfer.getData('block') || null);
 
-      if (!block) return;
+      if (!block || block.fromBlockBasic) return;
 
       const isTriggerExists =
         block.id === 'trigger' &&

+ 5 - 2
src/components/newtab/workflow/WorkflowGlobalData.vue

@@ -19,10 +19,13 @@
   </div>
 </template>
 <script setup>
-import { ref, watch } from 'vue';
+import { ref, watch, defineAsyncComponent } from 'vue';
 import { useI18n } from 'vue-i18n';
 import { debounce } from '@/utils/helper';
-import SharedCodemirror from '@/components/newtab/shared/SharedCodemirror.vue';
+
+const SharedCodemirror = defineAsyncComponent(() =>
+  import('@/components/newtab/shared/SharedCodemirror.vue')
+);
 
 const props = defineProps({
   workflow: {

+ 5 - 2
src/components/newtab/workflow/edit/EditExecuteWorkflow.vue

@@ -45,11 +45,14 @@
   </div>
 </template>
 <script setup>
-import { computed, shallowReactive } from 'vue';
+import { computed, shallowReactive, defineAsyncComponent } from 'vue';
 import { useI18n } from 'vue-i18n';
 import { useRoute } from 'vue-router';
 import Workflow from '@/models/workflow';
-import SharedCodemirror from '@/components/newtab/shared/SharedCodemirror.vue';
+
+const SharedCodemirror = defineAsyncComponent(() =>
+  import('@/components/newtab/shared/SharedCodemirror.vue')
+);
 
 const props = defineProps({
   data: {

+ 5 - 2
src/components/newtab/workflow/edit/EditGoogleSheets.vue

@@ -153,11 +153,14 @@
   </div>
 </template>
 <script setup>
-import { shallowReactive } from 'vue';
+import { shallowReactive, defineAsyncComponent } from 'vue';
 import { useI18n } from 'vue-i18n';
 import { googleSheets } from '@/utils/api';
 import { convert2DArrayToArrayObj } from '@/utils/helper';
-import SharedCodemirror from '@/components/newtab/shared/SharedCodemirror.vue';
+
+const SharedCodemirror = defineAsyncComponent(() =>
+  import('@/components/newtab/shared/SharedCodemirror.vue')
+);
 
 const props = defineProps({
   data: {

+ 5 - 2
src/components/newtab/workflow/edit/EditJavascriptCode.vue

@@ -91,11 +91,14 @@
   </div>
 </template>
 <script setup>
-import { watch, reactive } from 'vue';
+import { watch, reactive, defineAsyncComponent } from 'vue';
 import { useI18n } from 'vue-i18n';
 import { syntaxTree } from '@codemirror/language';
 import { autocompletion, snippet } from '@codemirror/autocomplete';
-import SharedCodemirror from '@/components/newtab/shared/SharedCodemirror.vue';
+
+const SharedCodemirror = defineAsyncComponent(() =>
+  import('@/components/newtab/shared/SharedCodemirror.vue')
+);
 
 const props = defineProps({
   data: {

+ 5 - 2
src/components/newtab/workflow/edit/EditLoopData.vue

@@ -119,13 +119,16 @@
   </div>
 </template>
 <script setup>
-import { onMounted, shallowReactive } from 'vue';
+import { onMounted, shallowReactive, defineAsyncComponent } from 'vue';
 import { nanoid } from 'nanoid';
 import { useI18n } from 'vue-i18n';
 import { useToast } from 'vue-toastification';
 import Papa from 'papaparse';
 import { openFilePicker } from '@/utils/helper';
-import SharedCodemirror from '@/components/newtab/shared/SharedCodemirror.vue';
+
+const SharedCodemirror = defineAsyncComponent(() =>
+  import('@/components/newtab/shared/SharedCodemirror.vue')
+);
 
 const props = defineProps({
   blockId: {

+ 30 - 0
src/components/newtab/workflow/edit/EditNewWindow.vue

@@ -19,6 +19,7 @@
     <ui-checkbox
       :model-value="data.incognito"
       :disabled="!allowInIncognito"
+      class="mb-4"
       @change="updateData({ incognito: $event })"
     >
       {{ t('workflow.blocks.new-window.incognito.text') }}
@@ -26,6 +27,35 @@
         &#128712;
       </span>
     </ui-checkbox>
+    <template v-if="data.windowState === 'normal'">
+      <div class="flex items-center space-x-2">
+        <ui-input
+          :model-value="data.top"
+          :label="t('workflow.blocks.new-window.top')"
+          @change="updateData({ top: +$event })"
+        />
+        <ui-input
+          :model-value="data.left"
+          :label="t('workflow.blocks.new-window.left')"
+          @change="updateData({ left: +$event })"
+        />
+      </div>
+      <div class="flex items-center space-x-2">
+        <ui-input
+          :model-value="data.height"
+          :label="t('workflow.blocks.new-window.height')"
+          @change="updateData({ height: +$event })"
+        />
+        <ui-input
+          :model-value="data.width"
+          :label="t('workflow.blocks.new-window.width')"
+          @change="updateData({ width: +$event })"
+        />
+      </div>
+      <p class="mt-4 text-gray-600">
+        {{ t('workflow.blocks.new-window.note') }}
+      </p>
+    </template>
   </div>
 </template>
 <script setup>

+ 43 - 20
src/components/newtab/workflow/edit/EditUploadFile.vue

@@ -1,26 +1,44 @@
 <template>
   <edit-interaction-base v-bind="{ data, hide: hideBase }" @change="updateData">
-    <div class="mt-4 space-y-2">
-      <div
-        v-for="(path, index) in filePaths"
-        :key="index"
-        class="flex items-center group"
-      >
-        <ui-input
-          v-model="filePaths[index]"
-          :placeholder="t('workflow.blocks.upload-file.filePath')"
-          class="mr-2"
-        />
-        <v-remixicon
-          name="riDeleteBin7Line"
-          class="invisible cursor-pointer group-hover:visible"
-          @click="filePaths.splice(index, 1)"
-        />
+    <template v-if="hasFileAccess">
+      <div class="mt-4 space-y-2">
+        <div
+          v-for="(path, index) in filePaths"
+          :key="index"
+          class="flex items-center group"
+        >
+          <ui-input
+            v-model="filePaths[index]"
+            :placeholder="t('workflow.blocks.upload-file.filePath')"
+            class="mr-2"
+          />
+          <v-remixicon
+            name="riDeleteBin7Line"
+            class="invisible cursor-pointer group-hover:visible"
+            @click="filePaths.splice(index, 1)"
+          />
+        </div>
+      </div>
+      <ui-button variant="accent" class="mt-2" @click="filePaths.push('')">
+        {{ t('workflow.blocks.upload-file.addFile') }}
+      </ui-button>
+    </template>
+    <template v-else>
+      <div class="mt-4 p-2 rounded-lg bg-red-200 flex items-start">
+        <v-remixicon name="riErrorWarningLine" />
+        <div class="ml-2 flex-1 leading-tight">
+          <p>{{ t('workflow.blocks.upload-file.noFileAccess') }}</p>
+        </div>
       </div>
-    </div>
-    <ui-button variant="accent" class="mt-2" @click="filePaths.push('')">
-      {{ t('workflow.blocks.upload-file.addFile') }}
-    </ui-button>
+      <a
+        href="https://docs.automa.site/blocks/upload-file.html#requirements"
+        target="_blank"
+        rel="noopener"
+        class="leading-tight inline-block text-primary mt-2"
+      >
+        {{ t('workflow.blocks.upload-file.requirement') }}
+      </a>
+    </template>
   </edit-interaction-base>
 </template>
 <script setup>
@@ -43,11 +61,16 @@ const emit = defineEmits(['update:data']);
 const { t } = useI18n();
 
 const filePaths = ref([...props.data.filePaths]);
+const hasFileAccess = ref(false);
 
 function updateData(value) {
   emit('update:data', { ...props.data, ...value });
 }
 
+chrome.extension.isAllowedFileSchemeAccess((value) => {
+  hasFileAccess.value = value;
+});
+
 watch(
   filePaths,
   (paths) => {

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

@@ -107,10 +107,13 @@
   </div>
 </template>
 <script setup>
-import { ref, watch } from 'vue';
+import { ref, watch, defineAsyncComponent } from 'vue';
 import { useI18n } from 'vue-i18n';
 import { contentTypes } from '@/utils/shared';
-import SharedCodemirror from '@/components/newtab/shared/SharedCodemirror.vue';
+
+const SharedCodemirror = defineAsyncComponent(() =>
+  import('@/components/newtab/shared/SharedCodemirror.vue')
+);
 
 const props = defineProps({
   data: {

+ 6 - 2
src/content/blocks-handler/handler-take-screenshot.js

@@ -56,7 +56,9 @@ export default async function ({ tabId, options }) {
   const context = canvas.getContext('2d');
   const maxCanvasSize = 32767;
 
-  const scrollableElement = findScrollableElement();
+  const scrollElement = document.querySelector('.automa-scrollable-el');
+  let scrollableElement = scrollElement || findScrollableElement();
+
   const takeScreenshot = async () => {
     await sendMessage('set:active-tab', tabId, 'background');
     const imageUrl = await sendMessage(
@@ -74,7 +76,7 @@ export default async function ({ tabId, options }) {
     return imageUrl;
   }
 
-  scrollableElement.classList.add('automa-scrollable-el');
+  scrollableElement.classList?.add('automa-scrollable-el');
 
   const style = injectStyle();
   const originalYPosition = window.scrollY;
@@ -97,6 +99,8 @@ export default async function ({ tabId, options }) {
 
   let scrollPosition = 0;
 
+  if (scrollableElement.tagName === 'HTML') scrollableElement = window;
+
   while (scrollPosition <= originalScrollHeight) {
     const imageUrl = await takeScreenshot();
 

+ 1 - 1
src/content/blocks-handler/handler-upload-file.js

@@ -18,7 +18,7 @@ export default async function (block) {
 
   const getFile = async (path) => {
     const file = await sendMessage('get:file', path, 'background');
-    const name = file.path.replace(/^.*[\\/]/, '');
+    const name = file.path?.replace(/^.*[\\/]/, '') || '';
     const blob = await fetch(file.objUrl).then((response) => response.blob());
 
     URL.revokeObjectURL(file.objUrl);

+ 79 - 8
src/content/element-selector/App.vue

@@ -42,6 +42,9 @@
       <template v-if="!state.hide && state.selectedElements.length > 0">
         <ui-tabs v-model="state.activeTab" class="mt-2" fill>
           <ui-tab value="attributes"> Attributes </ui-tab>
+          <ui-tab v-if="state.selectElements.length > 0" value="options">
+            Options
+          </ui-tab>
           <ui-tab value="blocks"> Blocks </ui-tab>
         </ui-tabs>
         <ui-tab-panels
@@ -50,13 +53,62 @@
           style="max-height: calc(100vh - 15rem)"
         >
           <ui-tab-panel value="attributes">
-            <app-element-attributes
+            <app-element-list
               :elements="state.selectedElements"
+              @highlight="toggleHighlightElement"
+            >
+              <template #item="{ element }">
+                <div
+                  v-for="attribute in element.attributes"
+                  :key="attribute.name"
+                  class="bg-box-transparent mb-1 rounded-lg py-2 px-3"
+                >
+                  <p
+                    class="text-sm text-overflow leading-tight text-gray-600"
+                    title="Attribute name"
+                  >
+                    {{ attribute.name }}
+                  </p>
+                  <p title="Attribute value" class="text-overflow">
+                    {{ attribute.value }}
+                  </p>
+                </div>
+              </template>
+            </app-element-list>
+          </ui-tab-panel>
+          <ui-tab-panel value="options">
+            <app-element-list
+              :elements="state.selectElements"
+              element-name="Select element options"
               @highlight="
-                state.selectedElements[$event.index].highlight =
-                  $event.highlight
+                toggleHighlightElement({
+                  index: $event.element.index,
+                  highlight: $event.highlight,
+                })
               "
-            />
+            >
+              <template #item="{ element }">
+                <div
+                  v-for="option in element.options"
+                  :key="option.name"
+                  class="bg-box-transparent mb-1 rounded-lg py-2 px-3"
+                >
+                  <p
+                    class="text-sm text-overflow leading-tight text-gray-600"
+                    title="Option name"
+                  >
+                    {{ option.name }}
+                  </p>
+                  <input
+                    :value="option.value"
+                    title="Option value"
+                    class="text-overflow focus:ring-0 w-full bg-transparent"
+                    readonly
+                    @click="$event.target.select()"
+                  />
+                </div>
+              </template>
+            </app-element-list>
           </ui-tab-panel>
           <ui-tab-panel value="blocks">
             <app-blocks
@@ -98,7 +150,7 @@ import { finder } from '@medv/finder';
 import { debounce } from '@/utils/helper';
 import AppBlocks from './AppBlocks.vue';
 import AppSelector from './AppSelector.vue';
-import AppElementAttributes from './AppElementAttributes.vue';
+import AppElementList from './AppElementList.vue';
 
 const selectedElement = {
   path: [],
@@ -115,6 +167,7 @@ const state = reactive({
   elSelector: '',
   isDragging: false,
   isExecuting: false,
+  selectElements: [],
   selectedElements: [],
   hide: window.self !== window.top,
 });
@@ -131,6 +184,9 @@ const cardRect = reactive({
   width: 0,
 });
 
+function toggleHighlightElement({ index, highlight }) {
+  state.selectedElements[index].highlight = highlight;
+}
 function getElementRect(target) {
   if (!target) return {};
 
@@ -148,20 +204,35 @@ function updateSelectedElements(selector) {
 
   try {
     const elements = document.querySelectorAll(selector);
+    const selectElements = [];
 
-    state.selectedElements = Array.from(elements).map((element) => {
+    state.selectedElements = Array.from(elements).map((element, index) => {
       const attributes = Array.from(element.attributes).map(
         ({ name, value }) => ({ name, value })
       );
-
-      return {
+      const elementProps = {
         element,
         attributes,
         highlight: false,
         ...getElementRect(element),
       };
+
+      if (element.tagName === 'SELECT') {
+        const options = Array.from(element.querySelectorAll('option')).map(
+          (option) => ({
+            name: option.innerText,
+            value: option.value,
+          })
+        );
+
+        selectElements.push({ ...elementProps, options, index });
+      }
+
+      return elementProps;
     });
+    state.selectElements = selectElements;
   } catch (error) {
+    state.selectElements = [];
     state.selectedElements = [];
   }
 }

+ 4 - 8
src/content/element-selector/AppBlocks.vue

@@ -26,14 +26,11 @@
       :hide-base="true"
       @update:data="updateParams"
     />
-    <shared-codemirror
+    <pre
       v-if="state.blockResult"
-      v-model="state.blockResult"
-      :line-numbers="false"
-      readonly
-      lang="json"
-      class="h-full mt-2"
-    />
+      class="p-2 rounded-lg text-gray-100 bg-accent h-full mt-2 overflow-auto text-sm"
+      >{{ state.blockResult }}</pre
+    >
   </div>
 </template>
 <script setup>
@@ -45,7 +42,6 @@ import handleEventClick from '../blocks-handler/handler-event-click';
 import handelTriggerEvent from '../blocks-handler/handler-trigger-event';
 import handleElementScroll from '../blocks-handler/handler-element-scroll';
 import EditForms from '@/components/newtab/workflow/edit/EditForms.vue';
-import SharedCodemirror from '@/components/newtab/shared/SharedCodemirror.vue';
 import EditTriggerEvent from '@/components/newtab/workflow/edit/EditTriggerEvent.vue';
 import EditScrollElement from '@/components/newtab/workflow/edit/EditScrollElement.vue';
 

+ 0 - 39
src/content/element-selector/AppElementAttributes.vue

@@ -1,39 +0,0 @@
-<template>
-  <ul class="space-y-4 mt-2">
-    <li
-      v-for="(element, index) in elements"
-      :key="index"
-      @mouseenter="$emit('highlight', { highlight: true, index })"
-      @mouseleave="$emit('highlight', { highlight: false, index })"
-    >
-      <p
-        class="mb-1 cursor-pointer"
-        title="Scroll into view"
-        @click="
-          element.element.scrollIntoView({ block: 'center', inline: 'center' })
-        "
-      >
-        #{{ index + 1 }} Element
-      </p>
-      <div
-        v-for="attribute in element.attributes"
-        :key="attribute.name"
-        class="bg-box-transparent mb-1 rounded-lg py-2 px-3"
-      >
-        <p class="text-sm text-overflow leading-tight text-gray-600">
-          {{ attribute.name }}
-        </p>
-        <p class="text-overflow">{{ attribute.value }}</p>
-      </div>
-    </li>
-  </ul>
-</template>
-<script setup>
-defineProps({
-  elements: {
-    type: Array,
-    default: () => [],
-  },
-});
-defineEmits(['highlight']);
-</script>

+ 34 - 0
src/content/element-selector/AppElementList.vue

@@ -0,0 +1,34 @@
+<template>
+  <ul class="space-y-4 mt-2">
+    <li
+      v-for="(element, index) in elements"
+      :key="index"
+      @mouseenter="$emit('highlight', { highlight: true, index, element })"
+      @mouseleave="$emit('highlight', { highlight: false, index, element })"
+    >
+      <p
+        class="mb-1 cursor-pointer"
+        title="Scroll into view"
+        @click="
+          element.element.scrollIntoView({ block: 'center', inline: 'center' })
+        "
+      >
+        #{{ index + 1 }} {{ elementName }}
+      </p>
+      <slot name="item" v-bind="{ element }" />
+    </li>
+  </ul>
+</template>
+<script setup>
+defineProps({
+  elements: {
+    type: Array,
+    default: () => [],
+  },
+  elementName: {
+    type: String,
+    default: 'Element',
+  },
+});
+defineEmits(['highlight']);
+</script>

+ 8 - 1
src/locales/en/blocks.json

@@ -32,7 +32,9 @@
         "name": "Upload file",
         "description": "Upload file into <input type=\"file\"> element",
         "filePath": "File path",
-        "addFile": "Add file"
+        "addFile": "Add file",
+        "requirement": "See the requirement before using this block",
+        "noFileAccess": "Automa doesn't have file access"
       },
       "browser-event": {
         "name": "Browser event",
@@ -146,6 +148,11 @@
       "new-window": {
         "name": "New window",
         "description": "Create a new window",
+        "top": "Top",
+        "left": "Left",
+        "height": "Height",
+        "width": "Width",
+        "note": "Note: use 0 to disable",
         "windowState": {
           "placeholder": "Window state",
           "options": {

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

@@ -18,7 +18,8 @@
       "reloadPage": "Reload the page to take effect"
     },
     "menu": {
-      "general": "General"
+      "general": "General",
+      "about": "About"
     },
   },
   "workflow": {

+ 2 - 0
src/newtab/App.vue

@@ -15,6 +15,8 @@
       </p>
       <a
         :href="`https://github.com/Kholid060/automa/releases/tag/v${currentVersion}`"
+        target="_blank"
+        rel="noopener"
         class="underline ml-1"
       >
         {{ t('updateMessage.text2') }}

+ 6 - 3
src/newtab/pages/Settings.vue

@@ -6,14 +6,14 @@
         <router-link
           v-for="menu in menus"
           :key="menu.id"
-          v-slot="{ href, navigate, isActive }"
+          v-slot="{ href, navigate, isExactActive }"
           custom
           :to="menu.path"
         >
           <ui-list-item
             :href="href"
             :class="[
-              isActive
+              isExactActive
                 ? 'bg-box-transparent'
                 : 'text-gray-600 dark:text-gray-600',
             ]"
@@ -36,5 +36,8 @@ import { useI18n } from 'vue-i18n';
 
 const { t } = useI18n();
 
-const menus = [{ id: 'general', path: '/settings', icon: 'riSettings3Line' }];
+const menus = [
+  { id: 'general', path: '/settings', icon: 'riSettings3Line' },
+  { id: 'about', path: '/about', icon: 'riInformationLine' },
+];
 </script>

+ 11 - 2
src/newtab/pages/collections/[id].vue

@@ -194,7 +194,13 @@
   </ui-modal>
 </template>
 <script setup>
-import { computed, shallowReactive, onMounted, watch } from 'vue';
+import {
+  computed,
+  shallowReactive,
+  onMounted,
+  watch,
+  defineAsyncComponent,
+} from 'vue';
 import { nanoid } from 'nanoid';
 import { useStore } from 'vuex';
 import { useRoute, useRouter } from 'vue-router';
@@ -206,9 +212,12 @@ import Log from '@/models/log';
 import Workflow from '@/models/workflow';
 import Collection from '@/models/collection';
 import SharedLogsTable from '@/components/newtab/shared/SharedLogsTable.vue';
-import SharedCodemirror from '@/components/newtab/shared/SharedCodemirror.vue';
 import SharedWorkflowState from '@/components/newtab/shared/SharedWorkflowState.vue';
 
+const SharedCodemirror = defineAsyncComponent(() =>
+  import('@/components/newtab/shared/SharedCodemirror.vue')
+);
+
 const { t } = useI18n();
 const store = useStore();
 const route = useRoute();

+ 109 - 0
src/newtab/pages/settings/About.vue

@@ -0,0 +1,109 @@
+<template>
+  <div class="max-w-lg">
+    <div class="p-3 mb-2 bg-box-transparent rounded-full inline-block">
+      <img src="@/assets/svg/logo.svg" class="w-14" />
+    </div>
+    <p class="text-2xl font-semibold">Automa</p>
+    <p class="mb-2 mt-1">Version: {{ extensionVersion }}</p>
+    <p class="text-gray-600">
+      Automa is a chrome extension for browser automation. From auto-fill forms,
+      doing a repetitive task, taking a screenshot, to scraping data of the
+      website, it's up to you what you want to do with this extension.
+    </p>
+    <div class="mt-4 space-x-2">
+      <a
+        v-for="link in links"
+        :key="link.name"
+        v-tooltip.group="link.name"
+        :href="link.url"
+        target="_blank"
+        class="inline-block p-2 rounded-lg transition hoverable"
+      >
+        <v-remixicon :name="link.icon" />
+        <p class="ml-1 hidden">{{ link.name }}</p>
+      </a>
+    </div>
+    <div class="border-b my-8"></div>
+    <h2 class="text-xl font-semibold">Contributors</h2>
+    <p class="mt-1 text-gray-600">
+      Thanks to everyone who has submitted issues, made suggestions, and
+      generally helped make this a better project.
+    </p>
+    <div class="mt-4 gap-2 mb-12 grid grid-cols-7">
+      <a
+        v-for="contributor in store.state.contributors"
+        :key="contributor.username"
+        v-tooltip.group="contributor.username"
+        :href="contributor.url"
+        target="_blank"
+        rel="noopener"
+      >
+        <img
+          :src="contributor.avatar"
+          :alt="`${contributor.username} avatar`"
+          class="w-16 rounded-lg"
+        />
+      </a>
+    </div>
+  </div>
+</template>
+<script setup>
+/* eslint-disable camelcase */
+import { onMounted } from 'vue';
+import { useStore } from 'vuex';
+import { useGroupTooltip } from '@/composable/groupTooltip';
+
+useGroupTooltip();
+const store = useStore();
+
+const extensionVersion = chrome.runtime.getManifest().version;
+const links = [
+  {
+    name: 'GitHub',
+    icon: 'riGithubFill',
+    url: 'https://github.com/kholid060/automa',
+  },
+  { name: 'Website', icon: 'riGlobalLine', url: 'https://www.automa.site' },
+  {
+    name: 'Documentation',
+    icon: 'riBook3Line',
+    url: 'https://docs.automa.site',
+  },
+  {
+    name: 'Donate',
+    icon: 'riHandHeartLine',
+    url: 'https://paypal.me/akholid060',
+  },
+];
+
+onMounted(async () => {
+  if (store.contributors) return;
+
+  try {
+    const response = await fetch(
+      'https://api.github.com/repos/Kholid060/automa/contributors'
+    );
+    const contributors = (await response.json()).reduce(
+      (acc, { type, avatar_url, login, html_url }) => {
+        if (type !== 'Bot') {
+          acc.push({
+            username: login,
+            url: html_url,
+            avatar: avatar_url,
+          });
+        }
+
+        return acc;
+      },
+      []
+    );
+
+    store.commit('updateState', {
+      key: 'contributors',
+      value: contributors,
+    });
+  } catch (error) {
+    console.error(error);
+  }
+});
+</script>

+ 5 - 1
src/newtab/router.js

@@ -9,6 +9,7 @@ import Logs from './pages/Logs.vue';
 import LogsDetails from './pages/logs/[id].vue';
 import Settings from './pages/Settings.vue';
 import SettingsIndex from './pages/settings/index.vue';
+import SettingsAbout from './pages/settings/About.vue';
 
 const routes = [
   {
@@ -54,7 +55,10 @@ const routes = [
   {
     path: '/settings',
     component: Settings,
-    children: [{ path: '', component: SettingsIndex }],
+    children: [
+      { path: '', component: SettingsIndex },
+      { path: '/about', component: SettingsAbout },
+    ],
   },
 ];
 

+ 1 - 0
src/store/index.js

@@ -7,6 +7,7 @@ import { firstWorkflows } from '@/utils/shared';
 const store = createStore({
   plugins: [vuexORM(models)],
   state: () => ({
+    contributors: null,
     workflowState: [],
     settings: {
       locale: 'en',

+ 2 - 0
src/utils/reference-data/mustache-replacer.js

@@ -18,6 +18,8 @@ export function extractStrFunction(str) {
 }
 
 export default function (str, data) {
+  if (!str || typeof str !== 'string') return '';
+
   const replacedStr = str.replace(/\{\{(.*?)\}\}/g, (match) => {
     const key = match.slice(2, -2).trim();
 

+ 4 - 0
src/utils/shared.js

@@ -93,6 +93,10 @@ export const tasks = {
     maxConnection: 1,
     data: {
       description: '',
+      top: 0,
+      left: 0,
+      width: 0,
+      height: 0,
       incognito: false,
       windowState: 'normal',
     },

+ 10 - 22
yarn.lock

@@ -1016,18 +1016,6 @@
     "@lezer/common" "^0.15.0"
     style-mod "^4.0.0"
 
-"@codemirror/highlight@^0.19.7":
-  version "0.19.7"
-  resolved "https://registry.yarnpkg.com/@codemirror/highlight/-/highlight-0.19.7.tgz#91a0c9994c759f5f153861e3aae74ff9e7c7c35b"
-  integrity sha512-3W32hBCY0pbbv/xidismw+RDMKuIag+fo4kZIbD7WoRj+Ttcaxjf+vP6RttRHXLaaqbWh031lTeON8kMlDhMYw==
-  dependencies:
-    "@codemirror/language" "^0.19.0"
-    "@codemirror/rangeset" "^0.19.0"
-    "@codemirror/state" "^0.19.3"
-    "@codemirror/view" "^0.19.0"
-    "@lezer/common" "^0.15.0"
-    style-mod "^4.0.0"
-
 "@codemirror/history@^0.19.0":
   version "0.19.0"
   resolved "https://registry.yarnpkg.com/@codemirror/history/-/history-0.19.0.tgz#cc8095c927c9566f7b69fa404074edde4c54d39c"
@@ -1036,18 +1024,18 @@
     "@codemirror/state" "^0.19.0"
     "@codemirror/view" "^0.19.0"
 
-"@codemirror/lang-javascript@^0.19.3":
-  version "0.19.6"
-  resolved "https://registry.yarnpkg.com/@codemirror/lang-javascript/-/lang-javascript-0.19.6.tgz#bab4ea9ba65189e4bd77c9496275c82e2b6814e7"
-  integrity sha512-NgkoCIc3hdTNTBRIRuPqfUJ0WB798qEgwAgtjwYy6yoiK5CzbDS2z5CFW17h9RmaAx6t1m64iY2CZ3tC7r15Gw==
+"@codemirror/lang-javascript@0.19.1":
+  version "0.19.1"
+  resolved "https://registry.yarnpkg.com/@codemirror/lang-javascript/-/lang-javascript-0.19.1.tgz#157a145a3413c9774573956555c111e353640113"
+  integrity sha512-fFAP4nkfU36c14K8f6ytVYYeuX/2E7dJ+bD7UCZPWavXYjwdHAgDCJGH84MjhBSh0lMc908We80vyYg1j3MnRQ==
   dependencies:
     "@codemirror/autocomplete" "^0.19.0"
-    "@codemirror/highlight" "^0.19.7"
+    "@codemirror/highlight" "^0.19.0"
     "@codemirror/language" "^0.19.0"
     "@codemirror/lint" "^0.19.0"
     "@codemirror/state" "^0.19.0"
     "@codemirror/view" "^0.19.0"
-    "@lezer/javascript" "^0.15.1"
+    "@lezer/javascript" "^0.15.0"
 
 "@codemirror/lang-json@^0.19.1":
   version "0.19.1"
@@ -1296,10 +1284,10 @@
   resolved "https://registry.yarnpkg.com/@lezer/common/-/common-0.15.11.tgz#965b5067036305f12e8a3efc344076850be1d3a8"
   integrity sha512-vv0nSdIaVCRcJ8rPuDdsrNVfBOYe/4Szr/LhF929XyDmBndLDuWiCCHooGlGlJfzELyO608AyDhVsuX/ZG36NA==
 
-"@lezer/javascript@^0.15.1":
-  version "0.15.2"
-  resolved "https://registry.yarnpkg.com/@lezer/javascript/-/javascript-0.15.2.tgz#50b70a02561b047947e050e0619b1aea7131dc5f"
-  integrity sha512-ytWvdJ1NAc0pfrNipGQs8otJVfjVibpIiFKH0fl99rKSA6cVlyQN/XTj/dEAQCfBfCBPAFdc30cuUe5CGZ0odA==
+"@lezer/javascript@^0.15.0":
+  version "0.15.3"
+  resolved "https://registry.yarnpkg.com/@lezer/javascript/-/javascript-0.15.3.tgz#833a4c5650bae07805b9af88de6706368844dc55"
+  integrity sha512-8jA2NpOfpWwSPZxRhd9BxK2ZPvGd7nLE3LFTJ5AbMhXAzMHeMjneV6GEVd7dAIee85dtap0jdb6bgOSO0+lfwA==
   dependencies:
     "@lezer/lr" "^0.15.0"