Browse Source

feat: improve editor autocomplete

Ahmad Kholid 3 years ago
parent
commit
77dc1ef008
25 changed files with 207 additions and 318 deletions
  1. 37 29
      src/components/newtab/workflow/WorkflowEditBlock.vue
  2. 4 12
      src/components/newtab/workflow/edit/EditAttributeValue.vue
  3. 65 0
      src/components/newtab/workflow/edit/EditAutocomplete.vue
  4. 3 12
      src/components/newtab/workflow/edit/EditCloseTab.vue
  5. 0 5
      src/components/newtab/workflow/edit/EditConditions.vue
  6. 0 4
      src/components/newtab/workflow/edit/EditDeleteData.vue
  7. 3 12
      src/components/newtab/workflow/edit/EditElementExists.vue
  8. 3 12
      src/components/newtab/workflow/edit/EditExportData.vue
  9. 4 16
      src/components/newtab/workflow/edit/EditForms.vue
  10. 6 21
      src/components/newtab/workflow/edit/EditGetText.vue
  11. 5 18
      src/components/newtab/workflow/edit/EditGoogleSheets.vue
  12. 3 13
      src/components/newtab/workflow/edit/EditHandleDialog.vue
  13. 3 13
      src/components/newtab/workflow/edit/EditInteractionBase.vue
  14. 3 13
      src/components/newtab/workflow/edit/EditLoopData.vue
  15. 3 13
      src/components/newtab/workflow/edit/EditNewTab.vue
  16. 5 21
      src/components/newtab/workflow/edit/EditSaveAssets.vue
  17. 1 8
      src/components/newtab/workflow/edit/EditScrollElement.vue
  18. 5 21
      src/components/newtab/workflow/edit/EditSwitchTab.vue
  19. 3 6
      src/components/newtab/workflow/edit/EditSwitchTo.vue
  20. 3 12
      src/components/newtab/workflow/edit/EditTakeScreenshot.vue
  21. 1 8
      src/components/newtab/workflow/edit/EditTriggerEvent.vue
  22. 4 16
      src/components/newtab/workflow/edit/EditUploadFile.vue
  23. 3 12
      src/components/newtab/workflow/edit/EditWebhook.vue
  24. 0 5
      src/components/newtab/workflow/edit/EditWhileLoop.vue
  25. 40 16
      src/components/ui/UiAutocomplete.vue

+ 37 - 29
src/components/newtab/workflow/WorkflowEditBlock.vue

@@ -24,7 +24,6 @@
       :key="data.blockId"
       v-model:data="blockData"
       :block-id="data.blockId"
-      :autocomplete="autocompleteList"
     />
     <on-block-error
       v-if="!excludeOnError.includes(data.id)"
@@ -36,9 +35,10 @@
   </div>
 </template>
 <script>
-import { computed, ref, watch } from 'vue';
+import { computed, provide, ref, watch } from 'vue';
 import { useI18n } from 'vue-i18n';
 import { tasks } from '@/utils/shared';
+import { parseJSON } from '@/utils/helper';
 import OnBlockError from './edit/OnBlockError.vue';
 
 const editComponents = require.context(
@@ -80,13 +80,6 @@ export default {
   },
   emits: ['close', 'update', 'update:autocomplete'],
   setup(props, { emit }) {
-    const defaultAutocomplete = [
-      'activeTabUrl',
-      '$date',
-      '$randint',
-      '$getLength',
-      'globalData',
-    ];
     const excludeOnError = [
       'webhook',
       'while-loop',
@@ -96,7 +89,16 @@ export default {
     ];
 
     const { t } = useI18n();
-    const autocompleteData = ref({});
+    const autocompleteData = ref({
+      common: {
+        table: {},
+        globalData: [],
+        activeTabUrl: '',
+        $date: '',
+        $randint: '',
+        $getLength: '',
+      },
+    });
 
     const blockData = computed({
       get() {
@@ -106,16 +108,12 @@ export default {
         emit('update', value);
       },
     });
-    const autocompleteList = computed(() => {
-      const blockId = props.data.itemId || props.data.blockId;
-      const arr = [
-        defaultAutocomplete,
-        autocompleteData.value.table,
-        autocompleteData.value[blockId],
-      ];
-
-      return arr.flatMap((items) => [...(items || [])]);
-    });
+    const autocompleteList = computed(() => ({
+      ...autocompleteData.value.common,
+      ...autocompleteData.value[props.data.itemId || props.data.blockId],
+    }));
+    console.log(autocompleteList.value);
+    provide('autocompleteData', autocompleteList);
 
     const dataKeywords = {
       loopId: 'loopData',
@@ -123,7 +121,7 @@ export default {
       variableName: 'variables',
     };
     function addAutocompleteData(id, name, data) {
-      if (!autocompleteData.value[id]) autocompleteData.value[id] = new Set();
+      if (!autocompleteData.value[id]) autocompleteData.value[id] = {};
 
       if (!tasks[name].autocomplete) return;
 
@@ -132,7 +130,12 @@ export default {
           key === 'variableName' && !data.assignVariable;
         if (!data[key] || variableNotAssigned) return;
 
-        autocompleteData.value[id].add(`${dataKeywords[key]}@${data[key]}`);
+        const keyword = dataKeywords[key];
+        if (!autocompleteData.value[id][keyword]) {
+          autocompleteData.value[id][keyword] = {};
+        }
+
+        autocompleteData.value[id][keyword][data[key]] = '';
       });
     }
     function getGroupBlockData(blocks, currentItemId) {
@@ -192,18 +195,24 @@ export default {
           traceBlockData(props.data.blockId, currentBlock, blocks);
         }
 
-        if (!autocompleteData.value.table) {
-          autocompleteData.value.table = new Set();
-          props.workflow.table?.forEach((column) => {
-            autocompleteData.value.table.add(`table@${column.name}`);
-          });
-        }
+        props.workflow.table?.forEach((column) => {
+          autocompleteData.value.common.table[column.name] = '';
+        });
+
+        const workflowGlobalData = props.workflow.globalData;
+        autocompleteData.value.common.globalData = parseJSON(
+          workflowGlobalData,
+          workflowGlobalData
+        );
+
+        console.log(autocompleteData.value);
       },
       { immediate: true }
     );
     watch(
       autocompleteData,
       () => {
+        console.log(autocompleteData.value);
         emit('update:autocomplete', autocompleteData.value);
       },
       { deep: true }
@@ -213,7 +222,6 @@ export default {
       t,
       blockData,
       excludeOnError,
-      autocompleteList,
     };
   },
 };

+ 4 - 12
src/components/newtab/workflow/edit/EditAttributeValue.vue

@@ -1,12 +1,7 @@
 <template>
-  <edit-interaction-base v-bind="{ data, autocomplete }" @change="updateData">
+  <edit-interaction-base v-bind="{ data }" @change="updateData">
     <hr />
-    <ui-autocomplete
-      :items="autocomplete"
-      :trigger-char="['{{', '}}']"
-      block
-      hide-empty
-    >
+    <edit-autocomplete>
       <ui-input
         :model-value="data.attributeName"
         :label="t('workflow.blocks.attribute-value.forms.name')"
@@ -15,7 +10,7 @@
         class="w-full"
         @change="updateData({ attributeName: $event })"
       />
-    </ui-autocomplete>
+    </edit-autocomplete>
     <insert-workflow-data
       :data="data"
       extra-row
@@ -28,16 +23,13 @@
 import { useI18n } from 'vue-i18n';
 import EditInteractionBase from './EditInteractionBase.vue';
 import InsertWorkflowData from './InsertWorkflowData.vue';
+import EditAutocomplete from './EditAutocomplete.vue';
 
 const props = defineProps({
   data: {
     type: Object,
     default: () => ({}),
   },
-  autocomplete: {
-    type: Array,
-    default: () => [],
-  },
 });
 const emit = defineEmits(['update:data']);
 

+ 65 - 0
src/components/newtab/workflow/edit/EditAutocomplete.vue

@@ -0,0 +1,65 @@
+<template>
+  <ui-autocomplete
+    :items="autocompleteList"
+    :trigger-char="['{{', '}}']"
+    :custom-filter="autocompleteFilter"
+    :replace-after="['@', '.']"
+    block
+    @search="onSearch"
+  >
+    <slot />
+  </ui-autocomplete>
+</template>
+<script setup>
+import { inject, shallowReactive, computed } from 'vue';
+import objectPath from 'object-path';
+
+const autocompleteData = inject('autocompleteData', {});
+const state = shallowReactive({
+  path: '',
+  pathLen: -1,
+});
+
+const cache = new Map();
+
+function autocompleteFilter({ text, item }) {
+  const query = text.replace('@', '.').split('.').pop();
+
+  return item.toLocaleLowerCase().includes(query);
+}
+function onSearch(value) {
+  const path = (value ?? '').replace('@', '.');
+  const pathArr = path.split('.');
+
+  if (pathArr.length < 1) {
+    state.path = '';
+    state.pathLen = 0;
+
+    return;
+  }
+
+  if (pathArr.length !== state.pathLen) {
+    state.path = path.endsWith('.') ? path.slice(0, -1) : path;
+    state.pathLen = pathArr.length;
+  }
+}
+
+const autocompleteList = computed(() => {
+  if (cache.has(state.path)) {
+    return cache.get(state.path);
+  }
+
+  const data =
+    !state.path || state.pathLen < 1
+      ? autocompleteData.value
+      : objectPath.get(autocompleteData.value, state.path);
+
+  const list = typeof data === 'string' ? [] : Object.keys(data || {});
+
+  console.log(cache);
+
+  cache.set(state.path, list);
+
+  return list;
+});
+</script>

+ 3 - 12
src/components/newtab/workflow/edit/EditCloseTab.vue

@@ -30,13 +30,7 @@
           {{ t('workflow.blocks.close-tab.activeTab') }}
         </ui-checkbox>
       </div>
-      <ui-autocomplete
-        v-if="!data.activeTab"
-        :items="autocomplete"
-        :trigger-char="['{{', '}}']"
-        block
-        hide-empty
-      >
+      <edit-autocomplete v-if="!data.activeTab">
         <ui-input
           :model-value="data.url"
           class="w-full mt-1"
@@ -59,7 +53,7 @@
             </a>
           </template>
         </ui-input>
-      </ui-autocomplete>
+      </edit-autocomplete>
     </template>
     <ui-checkbox
       v-else
@@ -73,16 +67,13 @@
 </template>
 <script setup>
 import { useI18n } from 'vue-i18n';
+import EditAutocomplete from './EditAutocomplete.vue';
 
 const props = defineProps({
   data: {
     type: Object,
     default: () => ({}),
   },
-  autocomplete: {
-    type: Array,
-    default: () => [],
-  },
 });
 const emit = defineEmits(['update:data']);
 

+ 0 - 5
src/components/newtab/workflow/edit/EditConditions.vue

@@ -56,7 +56,6 @@
             class="text-xl font-semibold mb-4 bg-transparent focus:ring-0"
           />
           <shared-condition-builder
-            :autocomplete="autocomplete"
             :model-value="conditions[state.conditionsIndex].conditions"
             @change="conditions[state.conditionsIndex].conditions = $event"
           />
@@ -82,10 +81,6 @@ const props = defineProps({
     type: String,
     default: '',
   },
-  autocomplete: {
-    type: Array,
-    default: () => [],
-  },
 });
 const emit = defineEmits(['update:data']);
 

+ 0 - 4
src/components/newtab/workflow/edit/EditDeleteData.vue

@@ -69,10 +69,6 @@ const props = defineProps({
     type: Object,
     default: () => ({}),
   },
-  autocomplete: {
-    type: Array,
-    default: () => [],
-  },
 });
 const emit = defineEmits(['update:data']);
 

+ 3 - 12
src/components/newtab/workflow/edit/EditElementExists.vue

@@ -10,13 +10,7 @@
         {{ t(`workflow.blocks.base.findElement.options.${type}`) }}
       </option>
     </ui-select>
-    <ui-autocomplete
-      :items="autocomplete"
-      :trigger-char="['{{', '}}']"
-      block
-      hide-empty
-      class="mb-1"
-    >
+    <edit-autocomplete class="mb-1">
       <ui-input
         :model-value="data.selector"
         :label="t('workflow.blocks.element-exists.selector')"
@@ -25,7 +19,7 @@
         class="w-full"
         @change="updateData({ selector: $event })"
       />
-    </ui-autocomplete>
+    </edit-autocomplete>
     <ui-input
       :model-value="data.tryCount"
       :title="t('workflow.blocks.element-exists.tryFor.title')"
@@ -57,16 +51,13 @@
 <script setup>
 import { onMounted } from 'vue';
 import { useI18n } from 'vue-i18n';
+import EditAutocomplete from './EditAutocomplete.vue';
 
 const props = defineProps({
   data: {
     type: Object,
     default: () => ({}),
   },
-  autocomplete: {
-    type: Array,
-    default: () => [],
-  },
 });
 const emit = defineEmits(['update:data']);
 

+ 3 - 12
src/components/newtab/workflow/edit/EditExportData.vue

@@ -32,13 +32,7 @@
       class="w-full mt-2"
       @change="updateData({ variableName: $event })"
     />
-    <ui-autocomplete
-      :items="autocomplete"
-      :trigger-char="['{{', '}}']"
-      block
-      hide-empty
-      class="mt-2"
-    >
+    <edit-autocomplete class="mt-2">
       <ui-input
         :model-value="data.name"
         autocomplete="off"
@@ -47,7 +41,7 @@
         placeholder="unnamed"
         @change="updateData({ name: $event })"
       />
-    </ui-autocomplete>
+    </edit-autocomplete>
     <ui-select
       v-if="permission.has.downloads"
       :model-value="data.onConflict"
@@ -83,16 +77,13 @@
 import { useI18n } from 'vue-i18n';
 import { dataExportTypes } from '@/utils/shared';
 import { useHasPermissions } from '@/composable/hasPermissions';
+import EditAutocomplete from './EditAutocomplete.vue';
 
 const props = defineProps({
   data: {
     type: Object,
     default: () => ({}),
   },
-  autocomplete: {
-    type: Array,
-    default: () => [],
-  },
 });
 const emit = defineEmits(['update:data']);
 

+ 4 - 16
src/components/newtab/workflow/edit/EditForms.vue

@@ -1,8 +1,5 @@
 <template>
-  <edit-interaction-base
-    v-bind="{ data, hide: hideBase, autocomplete }"
-    @change="updateData"
-  >
+  <edit-interaction-base v-bind="{ data, hide: hideBase }" @change="updateData">
     <hr />
     <ui-checkbox
       :model-value="data.getValue"
@@ -35,20 +32,14 @@
         {{ t('workflow.blocks.forms.selected') }}
       </ui-checkbox>
       <template v-if="data.type === 'text-field' || data.type === 'select'">
-        <ui-autocomplete
-          :items="autocomplete"
-          :trigger-char="['{{', '}}']"
-          block
-          hide-empty
-          class="w-full mb-1"
-        >
+        <edit-autocomplete class="w-full mb-1">
           <ui-textarea
             :model-value="data.value"
             :placeholder="t('workflow.blocks.forms.text-field.value')"
             class="w-full"
             @change="updateData({ value: $event })"
           />
-        </ui-autocomplete>
+        </edit-autocomplete>
         <ui-checkbox
           :model-value="data.clearValue"
           @change="updateData({ clearValue: $event })"
@@ -73,6 +64,7 @@
 import { useI18n } from 'vue-i18n';
 import InsertWorkflowData from './InsertWorkflowData.vue';
 import EditInteractionBase from './EditInteractionBase.vue';
+import EditAutocomplete from './EditAutocomplete.vue';
 
 const props = defineProps({
   data: {
@@ -83,10 +75,6 @@ const props = defineProps({
     type: Boolean,
     default: false,
   },
-  autocomplete: {
-    type: Array,
-    default: () => [],
-  },
 });
 const emit = defineEmits(['update:data']);
 

+ 6 - 21
src/components/newtab/workflow/edit/EditGetText.vue

@@ -1,5 +1,5 @@
 <template>
-  <edit-interaction-base v-bind="{ data, autocomplete }" @change="updateData">
+  <edit-interaction-base v-bind="{ data }" @change="updateData">
     <hr />
     <div class="flex rounded-lg bg-input px-4 items-center transition">
       <span>/</span>
@@ -27,13 +27,7 @@
       </ui-popover>
     </div>
     <div class="mt-2 flex space-x-2">
-      <ui-autocomplete
-        :items="autocomplete"
-        :trigger-char="['{{', '}}']"
-        block
-        hide-empty
-        class="w-full"
-      >
+      <edit-autocomplete class="w-full">
         <ui-input
           :model-value="data.prefixText"
           :title="t('workflow.blocks.get-text.prefixText.title')"
@@ -43,14 +37,8 @@
           class="w-full"
           @change="updateData({ prefixText: $event })"
         />
-      </ui-autocomplete>
-      <ui-autocomplete
-        :items="autocomplete"
-        :trigger-char="['{{', '}}']"
-        block
-        hide-empty
-        class="w-full"
-      >
+      </edit-autocomplete>
+      <edit-autocomplete class="w-full">
         <ui-input
           :model-value="data.suffixText"
           :title="t('workflow.blocks.get-text.suffixText.title')"
@@ -60,7 +48,7 @@
           class="w-full"
           @change="updateData({ suffixText: $event })"
         />
-      </ui-autocomplete>
+      </edit-autocomplete>
     </div>
     <ui-checkbox
       :model-value="data.includeTags"
@@ -83,16 +71,13 @@ import { ref } from 'vue';
 import { useI18n } from 'vue-i18n';
 import InsertWorkflowData from './InsertWorkflowData.vue';
 import EditInteractionBase from './EditInteractionBase.vue';
+import EditAutocomplete from './EditAutocomplete.vue';
 
 const props = defineProps({
   data: {
     type: Object,
     default: () => ({}),
   },
-  autocomplete: {
-    type: Array,
-    default: () => [],
-  },
 });
 const emit = defineEmits(['update:data']);
 

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

@@ -18,12 +18,7 @@
         {{ t('workflow.blocks.google-sheets.select.update') }}
       </option>
     </ui-select>
-    <ui-autocomplete
-      :items="autocomplete"
-      :trigger-char="['{{', '}}']"
-      block
-      hide-empty
-    >
+    <edit-autocomplete>
       <ui-input
         :model-value="data.spreadsheetId"
         class="w-full"
@@ -42,13 +37,8 @@
           </a>
         </template>
       </ui-input>
-    </ui-autocomplete>
-    <ui-autocomplete
-      :items="autocomplete"
-      :trigger-char="['{{', '}}']"
-      block
-      hide-empty
-    >
+    </edit-autocomplete>
+    <edit-autocomplete>
       <ui-input
         :model-value="data.range"
         class="w-full mt-1"
@@ -67,7 +57,7 @@
           </a>
         </template>
       </ui-input>
-    </ui-autocomplete>
+    </edit-autocomplete>
     <template v-if="data.type === 'get'">
       <ui-input
         :model-value="data.refKey"
@@ -172,6 +162,7 @@ import { shallowReactive, defineAsyncComponent } from 'vue';
 import { useI18n } from 'vue-i18n';
 import { googleSheets } from '@/utils/api';
 import { convert2DArrayToArrayObj } from '@/utils/helper';
+import EditAutocomplete from './EditAutocomplete.vue';
 
 const SharedCodemirror = defineAsyncComponent(() =>
   import('@/components/newtab/shared/SharedCodemirror.vue')
@@ -182,10 +173,6 @@ const props = defineProps({
     type: Object,
     default: () => ({}),
   },
-  autocomplete: {
-    type: Array,
-    default: () => [],
-  },
 });
 const emit = defineEmits(['update:data']);
 

+ 3 - 13
src/components/newtab/workflow/edit/EditHandleDialog.vue

@@ -14,14 +14,7 @@
     >
       {{ t('workflow.blocks.handle-dialog.accept') }}
     </ui-checkbox>
-    <ui-autocomplete
-      v-if="data.accept"
-      :items="autocomplete"
-      :trigger-char="['{{', '}}']"
-      block
-      hide-empty
-      class="mt-1"
-    >
+    <edit-autocomplete v-if="data.accept" class="mt-1">
       <ui-input
         :model-value="data.promptText"
         :label="t('workflow.blocks.handle-dialog.promptText.label')"
@@ -31,21 +24,18 @@
         class="w-full"
         @change="updateData({ promptText: $event })"
       />
-    </ui-autocomplete>
+    </edit-autocomplete>
   </div>
 </template>
 <script setup>
 import { useI18n } from 'vue-i18n';
+import EditAutocomplete from './EditAutocomplete.vue';
 
 const props = defineProps({
   data: {
     type: Object,
     default: () => ({}),
   },
-  autocomplete: {
-    type: Array,
-    default: () => [],
-  },
 });
 const emit = defineEmits(['update:data']);
 

+ 3 - 13
src/components/newtab/workflow/edit/EditInteractionBase.vue

@@ -20,14 +20,7 @@
           {{ t(`workflow.blocks.base.findElement.options.${type}`) }}
         </option>
       </ui-select>
-      <ui-autocomplete
-        v-if="!hideSelector"
-        :items="autocomplete"
-        :trigger-char="['{{', '}}']"
-        block
-        hide-empty
-        class="mb-1"
-      >
+      <edit-autocomplete v-if="!hideSelector" class="mb-1">
         <ui-input
           v-if="!hideSelector"
           :model-value="data.selector"
@@ -36,7 +29,7 @@
           class="w-full"
           @change="updateData({ selector: $event })"
         />
-      </ui-autocomplete>
+      </edit-autocomplete>
       <ui-expand
         v-if="!hideSelector"
         hide-header-icon
@@ -94,6 +87,7 @@
 <script setup>
 import { onMounted } from 'vue';
 import { useI18n } from 'vue-i18n';
+import EditAutocomplete from './EditAutocomplete.vue';
 
 const props = defineProps({
   data: {
@@ -112,10 +106,6 @@ const props = defineProps({
     type: Boolean,
     default: false,
   },
-  autocomplete: {
-    type: Array,
-    default: () => [],
-  },
 });
 const emit = defineEmits(['update:data', 'change']);
 

+ 3 - 13
src/components/newtab/workflow/edit/EditLoopData.vue

@@ -44,14 +44,7 @@
       class="w-full mt-2"
       @change="updateData({ variableName: $event })"
     />
-    <ui-autocomplete
-      v-else-if="data.loopThrough === 'elements'"
-      :items="autocomplete"
-      :trigger-char="['{{', '}}']"
-      block
-      hide-empty
-      class="mt-2"
-    >
+    <edit-autocomplete v-else-if="data.loopThrough === 'elements'" class="mt-2">
       <ui-input
         :model-value="data.elementSelector"
         :label="t('workflow.blocks.base.selector')"
@@ -60,7 +53,7 @@
         class="w-full"
         @change="updateData({ elementSelector: $event })"
       />
-    </ui-autocomplete>
+    </edit-autocomplete>
     <ui-button
       v-else-if="data.loopThrough === 'custom-data'"
       class="w-full mt-4"
@@ -169,6 +162,7 @@ import { useI18n } from 'vue-i18n';
 import { useToast } from 'vue-toastification';
 import Papa from 'papaparse';
 import { openFilePicker } from '@/utils/helper';
+import EditAutocomplete from './EditAutocomplete.vue';
 
 const SharedCodemirror = defineAsyncComponent(() =>
   import('@/components/newtab/shared/SharedCodemirror.vue')
@@ -183,10 +177,6 @@ const props = defineProps({
     type: Object,
     default: () => ({}),
   },
-  autocomplete: {
-    type: Array,
-    default: () => [],
-  },
 });
 const emit = defineEmits(['update:data']);
 

+ 3 - 13
src/components/newtab/workflow/edit/EditNewTab.vue

@@ -6,14 +6,7 @@
       class="w-full"
       @change="updateData({ description: $event })"
     />
-    <ui-autocomplete
-      v-if="!data.activeTab"
-      :items="autocomplete"
-      :trigger-char="['{{', '}}']"
-      block
-      hide-empty
-      class="mt-2"
-    >
+    <edit-autocomplete v-if="!data.activeTab" class="mt-2">
       <ui-input
         :model-value="data.url"
         :label="t('workflow.blocks.new-tab.url')"
@@ -22,7 +15,7 @@
         placeholder="http://example.com/"
         @change="updateData({ url: $event })"
       />
-    </ui-autocomplete>
+    </edit-autocomplete>
     <ui-checkbox
       :model-value="data.updatePrevTab"
       class="leading-tight mt-2"
@@ -63,16 +56,13 @@
 </template>
 <script setup>
 import { useI18n } from 'vue-i18n';
+import EditAutocomplete from './EditAutocomplete.vue';
 
 const props = defineProps({
   data: {
     type: Object,
     default: () => ({}),
   },
-  autocomplete: {
-    type: Array,
-    default: () => [],
-  },
 });
 const emit = defineEmits(['update:data']);
 

+ 5 - 21
src/components/newtab/workflow/edit/EditSaveAssets.vue

@@ -2,7 +2,6 @@
   <edit-interaction-base
     :data="data"
     :hide="!permission.has.downloads"
-    :autocomplete="autocomplete"
     :hide-selector="data.type !== 'element'"
     @change="updateData"
   >
@@ -28,13 +27,7 @@
         </ui-button>
       </template>
     </template>
-    <ui-autocomplete
-      v-if="data.type === 'url'"
-      :items="autocomplete"
-      :trigger-char="['{{', '}}']"
-      block
-      hide-empty
-    >
+    <edit-autocomplete v-if="data.type === 'url'">
       <ui-input
         :model-value="data.url"
         label="URL"
@@ -43,15 +36,9 @@
         placeholder="https://example.com/picture.png"
         @change="updateData({ url: $event })"
       />
-    </ui-autocomplete>
+    </edit-autocomplete>
     <template v-if="permission.has.downloads">
-      <ui-autocomplete
-        :items="autocomplete"
-        :trigger-char="['{{', '}}']"
-        block
-        hide-empty
-        class="mt-4"
-      >
+      <edit-autocomplete class="mt-4">
         <ui-input
           :model-value="data.filename"
           :label="t('workflow.blocks.save-assets.filename')"
@@ -60,7 +47,7 @@
           placeholder="image.jpeg"
           @change="updateData({ filename: $event })"
         />
-      </ui-autocomplete>
+      </edit-autocomplete>
       <ui-select
         :model-value="data.onConflict"
         :label="t('workflow.blocks.handle-download.onConflict')"
@@ -78,16 +65,13 @@
 import { useI18n } from 'vue-i18n';
 import { useHasPermissions } from '@/composable/hasPermissions';
 import EditInteractionBase from './EditInteractionBase.vue';
+import EditAutocomplete from './EditAutocomplete.vue';
 
 const props = defineProps({
   data: {
     type: Object,
     default: () => ({}),
   },
-  autocomplete: {
-    type: Array,
-    default: () => [],
-  },
 });
 const emit = defineEmits(['update:data']);
 

+ 1 - 8
src/components/newtab/workflow/edit/EditScrollElement.vue

@@ -1,8 +1,5 @@
 <template>
-  <edit-interaction-base
-    v-bind="{ data, autocomplete, hide: hideBase }"
-    @change="updateData"
-  >
+  <edit-interaction-base v-bind="{ data, hide: hideBase }" @change="updateData">
     <div v-if="!data.scrollIntoView" class="flex items-center mt-3 space-x-2">
       <ui-input
         :model-value="data.scrollX || 0"
@@ -61,10 +58,6 @@ const props = defineProps({
     type: Boolean,
     default: false,
   },
-  autocomplete: {
-    type: Array,
-    default: () => [],
-  },
 });
 const emit = defineEmits(['update:data']);
 

+ 5 - 21
src/components/newtab/workflow/edit/EditSwitchTab.vue

@@ -1,11 +1,6 @@
 <template>
   <div>
-    <ui-autocomplete
-      :items="autocomplete"
-      :trigger-char="['{{', '}}']"
-      block
-      hide-empty
-    >
+    <edit-autocomplete>
       <ui-input
         :model-value="data.matchPattern"
         placeholder="https://example.com/*"
@@ -29,7 +24,7 @@
           </a>
         </template>
       </ui-input>
-    </ui-autocomplete>
+    </edit-autocomplete>
     <ui-checkbox
       :model-value="data.createIfNoMatch"
       class="mt-1"
@@ -37,15 +32,7 @@
     >
       {{ t('workflow.blocks.switch-tab.createIfNoMatch') }}
     </ui-checkbox>
-    <ui-autocomplete
-      v-if="data.createIfNoMatch"
-      :items="autocomplete"
-      :trigger-char="['{{', '}}']"
-      block
-      hide-empty
-      class="mt-2"
-      @change="updateData({ url: $event })"
-    >
+    <edit-autocomplete v-if="data.createIfNoMatch" class="mt-2">
       <ui-input
         :model-value="data.url"
         :label="t('workflow.blocks.switch-tab.url')"
@@ -53,21 +40,18 @@
         class="w-full"
         @change="updateData({ url: $event })"
       />
-    </ui-autocomplete>
+    </edit-autocomplete>
   </div>
 </template>
 <script setup>
 import { useI18n } from 'vue-i18n';
+import EditAutocomplete from './EditAutocomplete.vue';
 
 const props = defineProps({
   data: {
     type: Object,
     default: () => ({}),
   },
-  autocomplete: {
-    type: Array,
-    default: () => [],
-  },
 });
 const emit = defineEmits(['update:data']);
 

+ 3 - 6
src/components/newtab/workflow/edit/EditSwitchTo.vue

@@ -19,7 +19,7 @@
         {{ t('workflow.blocks.switch-to.windowTypes.iframe') }}
       </option>
     </ui-select>
-    <ui-autocomplete
+    <edit-autocomplete
       v-if="data.windowType === 'iframe'"
       :items="autocomplete"
       :trigger-char="['{{', '}}']"
@@ -34,21 +34,18 @@
         class="mb-1 w-full"
         @change="updateData({ selector: $event })"
       />
-    </ui-autocomplete>
+    </edit-autocomplete>
   </div>
 </template>
 <script setup>
 import { useI18n } from 'vue-i18n';
+import EditAutocomplete from './EditAutocomplete.vue';
 
 const props = defineProps({
   data: {
     type: Object,
     default: () => ({}),
   },
-  autocomplete: {
-    type: Array,
-    default: () => [],
-  },
 });
 const emit = defineEmits(['update:data']);
 

+ 3 - 12
src/components/newtab/workflow/edit/EditTakeScreenshot.vue

@@ -29,13 +29,7 @@
       {{ t('workflow.blocks.take-screenshot.saveToComputer') }}
     </ui-checkbox>
     <div v-if="data.saveToComputer" class="flex items-center mt-1">
-      <ui-autocomplete
-        :items="autocomplete"
-        :trigger-char="['{{', '}}']"
-        block
-        hide-empty
-        class="flex-1 mr-2"
-      >
+      <edit-autocomplete class="flex-1 mr-2">
         <ui-input
           :model-value="data.fileName"
           :placeholder="t('common.fileName')"
@@ -44,7 +38,7 @@
           title="File name"
           @change="updateData({ fileName: $event })"
         />
-      </ui-autocomplete>
+      </edit-autocomplete>
       <ui-select
         :model-value="data.ext || 'png'"
         placeholder="Type"
@@ -98,16 +92,13 @@
 import { inject, onMounted } from 'vue';
 import { useI18n } from 'vue-i18n';
 import { objectHasKey } from '@/utils/helper';
+import EditAutocomplete from './EditAutocomplete.vue';
 
 const props = defineProps({
   data: {
     type: Object,
     default: () => ({}),
   },
-  autocomplete: {
-    type: Array,
-    default: () => [],
-  },
 });
 const emit = defineEmits(['update:data']);
 

+ 1 - 8
src/components/newtab/workflow/edit/EditTriggerEvent.vue

@@ -1,8 +1,5 @@
 <template>
-  <edit-interaction-base
-    v-bind="{ data, autocomplete, hide: hideBase }"
-    @change="updateData"
-  >
+  <edit-interaction-base v-bind="{ data, hide: hideBase }" @change="updateData">
     <ui-select
       :model-value="data.eventName"
       :placeholder="t('workflow.blocks.trigger-event.selectEvent')"
@@ -81,10 +78,6 @@ const props = defineProps({
     type: Boolean,
     default: false,
   },
-  autocomplete: {
-    type: Array,
-    default: () => [],
-  },
 });
 const emit = defineEmits(['update:data']);
 

+ 4 - 16
src/components/newtab/workflow/edit/EditUploadFile.vue

@@ -1,8 +1,5 @@
 <template>
-  <edit-interaction-base
-    v-bind="{ data, autocomplete, hide: hideBase }"
-    @change="updateData"
-  >
+  <edit-interaction-base v-bind="{ data, hide: hideBase }" @change="updateData">
     <template v-if="hasFileAccess">
       <div class="mt-4 space-y-2">
         <div
@@ -10,20 +7,14 @@
           :key="index"
           class="flex items-center group"
         >
-          <ui-autocomplete
-            :items="autocomplete"
-            :trigger-char="['{{', '}}']"
-            block
-            hide-empty
-            class="mr-2"
-          >
+          <edit-autocomplete class="mr-2">
             <ui-input
               v-model="filePaths[index]"
               :placeholder="t('workflow.blocks.upload-file.filePath')"
               autocomplete="off"
               class="w-full"
             />
-          </ui-autocomplete>
+          </edit-autocomplete>
           <v-remixicon
             name="riDeleteBin7Line"
             class="invisible cursor-pointer group-hover:visible"
@@ -57,6 +48,7 @@
 import { useI18n } from 'vue-i18n';
 import { ref, watch } from 'vue';
 import EditInteractionBase from './EditInteractionBase.vue';
+import EditAutocomplete from './EditAutocomplete.vue';
 
 const props = defineProps({
   data: {
@@ -67,10 +59,6 @@ const props = defineProps({
     type: Boolean,
     default: false,
   },
-  autocomplete: {
-    type: Array,
-    default: () => [],
-  },
 });
 const emit = defineEmits(['update:data']);
 

+ 3 - 12
src/components/newtab/workflow/edit/EditWebhook.vue

@@ -16,13 +16,7 @@
         {{ method }}
       </option>
     </ui-select>
-    <ui-autocomplete
-      :items="autocomplete"
-      :trigger-char="['{{', '}}']"
-      block
-      hide-empty
-      class="mb-2"
-    >
+    <edit-autocomplete class="mb-2">
       <ui-input
         :model-value="data.url"
         :label="`${t('workflow.blocks.webhook.url')}*`"
@@ -33,7 +27,7 @@
         type="url"
         @change="updateData({ url: $event })"
       />
-    </ui-autocomplete>
+    </edit-autocomplete>
     <ui-select
       :model-value="data.contentType"
       :label="t('workflow.blocks.webhook.contentType')"
@@ -157,6 +151,7 @@ import { ref, watch, defineAsyncComponent } from 'vue';
 import { useI18n } from 'vue-i18n';
 import { contentTypes } from '@/utils/shared';
 import InsertWorkflowData from './InsertWorkflowData.vue';
+import EditAutocomplete from './EditAutocomplete.vue';
 
 const SharedCodemirror = defineAsyncComponent(() =>
   import('@/components/newtab/shared/SharedCodemirror.vue')
@@ -167,10 +162,6 @@ const props = defineProps({
     type: Object,
     default: () => ({}),
   },
-  autocomplete: {
-    type: Array,
-    default: () => [],
-  },
 });
 const emit = defineEmits(['update:data']);
 

+ 0 - 5
src/components/newtab/workflow/edit/EditWhileLoop.vue

@@ -27,7 +27,6 @@
         </div>
         <shared-condition-builder
           :model-value="data.conditions"
-          :autocomplete="autocomplete"
           class="overflow-auto p-4 mt-4 scroll"
           style="height: calc(100vh - 8rem)"
           @change="updateData({ conditions: $event })"
@@ -47,10 +46,6 @@ const props = defineProps({
     type: Object,
     default: () => ({}),
   },
-  autocomplete: {
-    type: Array,
-    default: () => [],
-  },
 });
 const emit = defineEmits(['update:data']);
 

+ 40 - 16
src/components/ui/UiAutocomplete.vue

@@ -46,7 +46,7 @@ const props = defineProps({
     default: '',
   },
   items: {
-    type: Array,
+    type: [Array, Object],
     default: () => [],
   },
   itemKey: {
@@ -65,8 +65,16 @@ const props = defineProps({
     type: Boolean,
     default: false,
   },
+  customFilter: {
+    type: Function,
+    default: null,
+  },
+  replaceAfter: {
+    type: [String, Array],
+    default: null,
+  },
 });
-const emit = defineEmits(['update:modelValue', 'change']);
+const emit = defineEmits(['update:modelValue', 'change', 'search']);
 
 let input = null;
 const componentId = useComponentId('autocomplete');
@@ -89,10 +97,14 @@ const filteredItems = computed(() => {
     triggerChar ? state.searchText : props.modelValue
   ).toLocaleLowerCase();
 
+  const defaultFilter = ({ item, text }) => {
+    return getItem(item)?.toLocaleLowerCase().includes(text);
+  };
+  const filterFunction = props.customFilter || defaultFilter;
+
   return props.items.filter(
-    (item) =>
-      !state.inputChanged ||
-      getItem(item)?.toLocaleLowerCase().includes(searchText)
+    (item, index) =>
+      !state.inputChanged || filterFunction({ item, index, text: searchText })
   );
 });
 
@@ -133,6 +145,8 @@ function showPopover() {
     const charIndex = getLastKeyBeforeCaret(selectionStart);
     const text = getSearchText(selectionStart, charIndex);
 
+    emit('search', text);
+
     if (charIndex >= 0 && text) {
       state.inputChanged = true;
       state.showPopover = true;
@@ -178,6 +192,7 @@ function selectItem(itemIndex) {
   selectedItem = getItem(selectedItem);
 
   let caretPosition;
+  let charLastIndex = 0;
   const isTriggerChar = state.charIndex >= 0 && state.searchText;
 
   if (isTriggerChar) {
@@ -185,9 +200,20 @@ function selectItem(itemIndex) {
     const index = state.charIndex;
     const charLength = props.triggerChar[0].length;
 
-    caretPosition = index + charLength + selectedItem.length;
+    if (props.replaceAfter) {
+      const lastChars = Array.isArray(props.replaceAfter)
+        ? props.replaceAfter
+        : [props.replaceAfter];
+      lastChars.forEach((char) => {
+        const lastIndex = val.lastIndexOf(char);
+
+        if (lastIndex > charLastIndex) charLastIndex = lastIndex - 1;
+      });
+    }
+
+    caretPosition = index + charLength + selectedItem.length + charLastIndex;
     selectedItem =
-      val.slice(0, index + charLength) +
+      val.slice(0, index + charLength + charLastIndex) +
       selectedItem +
       val.slice(state.searchText.length + index + charLength, val.length);
   }
@@ -195,15 +221,13 @@ function selectItem(itemIndex) {
   updateValue(selectedItem);
 
   if (isTriggerChar) {
-    setTimeout(() => {
-      input.selectionEnd = caretPosition;
-      const isNotTextarea = input.tagName !== 'TEXTAREA';
-
-      if (isNotTextarea) {
-        input.blur();
-        input.focus();
-      }
-    }, 300);
+    input.selectionEnd = caretPosition;
+    const isNotTextarea = input.tagName !== 'TEXTAREA';
+
+    if (isNotTextarea) {
+      input.blur();
+      input.focus();
+    }
   }
 }
 function handleKeydown(event) {