Переглянути джерело

feat: add keyboard shortcut

Ahmad Kholid 3 роки тому
батько
коміт
b48858631a

+ 20 - 1
src/components/newtab/app/AppSidebar.vue

@@ -21,7 +21,9 @@
         custom
       >
         <a
-          v-tooltip:right.group="t(`common.${tab.id}`, 2)"
+          v-tooltip:right.group="
+            `${t(`common.${tab.id}`, 2)} (${tab.shortcut.readable})`
+          "
           :class="{ 'is-active': isActive }"
           :href="href"
           class="z-10 relative w-full flex items-center justify-center tab relative"
@@ -58,10 +60,13 @@
 <script setup>
 import { ref } from 'vue';
 import { useI18n } from 'vue-i18n';
+import { useRouter } from 'vue-router';
+import { useShortcut, getShortcut } from '@/composable/shortcut';
 import { useGroupTooltip } from '@/composable/groupTooltip';
 
 useGroupTooltip();
 const { t } = useI18n();
+const router = useRouter();
 
 const links = [
   {
@@ -85,31 +90,45 @@ const tabs = [
     id: 'dashboard',
     icon: 'riHome5Line',
     path: '/',
+    shortcut: getShortcut('page:dashboard', '/'),
   },
   {
     id: 'workflow',
     icon: 'riFlowChart',
     path: '/workflows',
+    shortcut: getShortcut('page:workflows', '/workflows'),
   },
   {
     id: 'collection',
     icon: 'riFolderLine',
     path: '/collections',
+    shortcut: getShortcut('page:collections', '/collections'),
   },
   {
     id: 'log',
     icon: 'riHistoryLine',
     path: '/logs',
+    shortcut: getShortcut('page:logs', '/logs'),
   },
   {
     id: 'settings',
     icon: 'riSettings3Line',
     path: '/settings',
+    shortcut: getShortcut('page:settings', '/settings'),
   },
 ];
 const hoverIndicator = ref(null);
 const showHoverIndicator = ref(false);
 
+useShortcut(
+  tabs.map(({ shortcut }) => shortcut),
+  ({ data }) => {
+    if (!data) return;
+
+    router.push(data);
+  }
+);
+
 function hoverHandler({ target }) {
   showHoverIndicator.value = true;
   hoverIndicator.value.style.transform = `translate(-50%, ${target.offsetTop}px)`;

+ 10 - 1
src/components/newtab/logs/LogsFilters.vue

@@ -1,8 +1,11 @@
 <template>
   <div class="flex items-center mb-6 space-x-4">
     <ui-input
+      id="search-input"
       :model-value="filters.query"
-      :placeholder="`${t('common.search')}...`"
+      :placeholder="`${t('common.search')}... (${
+        shortcut['action:search'].readable
+      })`"
       prepend-icon="riSearch2Line"
       class="flex-1"
       @change="updateFilters('query', $event)"
@@ -67,6 +70,7 @@
 </template>
 <script setup>
 import { useI18n } from 'vue-i18n';
+import { useShortcut } from '@/composable/shortcut';
 
 defineProps({
   filters: {
@@ -81,6 +85,11 @@ defineProps({
 const emit = defineEmits(['updateSorts', 'updateFilters']);
 
 const { t } = useI18n();
+const shortcut = useShortcut('action:search', () => {
+  const searchInput = document.querySelector('#search-input input');
+
+  searchInput?.focus();
+});
 
 const filterByStatus = [
   { id: 'all', name: t('common.all') },

+ 18 - 2
src/components/newtab/workflow/WorkflowActions.vue

@@ -14,6 +14,7 @@
     <button
       v-if="!workflow.isDisabled"
       v-tooltip.group="t('common.execute')"
+      :title="shortcuts['editor:execute-workflow'].readable"
       icon
       class="hoverable p-2 rounded-lg"
       @click="$emit('execute')"
@@ -56,7 +57,12 @@
         </ui-list-item>
       </ui-list>
     </ui-popover>
-    <ui-button variant="accent" class="relative" @click="$emit('save')">
+    <ui-button
+      :title="shortcuts['editor:save'].readable"
+      variant="accent"
+      class="relative"
+      @click="$emit('save')"
+    >
       <span
         v-if="isDataChanged"
         class="flex h-3 w-3 absolute top-0 left-0 -ml-1 -mt-1"
@@ -76,6 +82,7 @@
 <script setup>
 import { useI18n } from 'vue-i18n';
 import { useGroupTooltip } from '@/composable/groupTooltip';
+import { useShortcut, getShortcut } from '@/composable/shortcut';
 
 defineProps({
   isDataChanged: {
@@ -87,7 +94,7 @@ defineProps({
     default: () => ({}),
   },
 });
-defineEmits([
+const emit = defineEmits([
   'showModal',
   'execute',
   'rename',
@@ -99,6 +106,15 @@ defineEmits([
 
 useGroupTooltip();
 const { t } = useI18n();
+const shortcuts = useShortcut(
+  [
+    getShortcut('editor:save', 'save'),
+    getShortcut('editor:execute-workflow', 'execute'),
+  ],
+  ({ data }) => {
+    emit(data);
+  }
+);
 
 const modalActions = [
   {

+ 25 - 7
src/components/newtab/workflow/WorkflowBuilder.vue

@@ -37,16 +37,23 @@
       :options="contextMenu.position"
       padding="p-3"
     >
-      <ui-list class="w-36 space-y-1">
+      <ui-list class="space-y-1 w-52">
         <ui-list-item
           v-for="item in contextMenu.items"
           :key="item.id"
           v-close-popover
-          class="cursor-pointer"
+          class="cursor-pointer justify-between"
           @click="contextMenuHandler[item.event]"
         >
-          <v-remixicon :name="item.icon" class="mr-2 -ml-1" />
-          <span>{{ item.name }}</span>
+          <span>
+            {{ item.name }}
+          </span>
+          <span
+            v-if="item.shortcut"
+            class="text-sm capitalize text-gray-600 dark:text-gray-200"
+          >
+            {{ item.shortcut }}
+          </span>
         </ui-list-item>
       </ui-list>
     </ui-popover>
@@ -58,6 +65,7 @@ import { onMounted, shallowRef, reactive, getCurrentInstance } from 'vue';
 import { useI18n } from 'vue-i18n';
 import { compare } from 'compare-versions';
 import emitter from 'tiny-emitter/instance';
+import { useShortcut, getShortcut } from '@/composable/shortcut';
 import { tasks } from '@/utils/shared';
 import { parseJSON } from '@/utils/helper';
 import { useGroupTooltip } from '@/composable/groupTooltip';
@@ -86,12 +94,14 @@ export default {
           name: t('workflow.editor.duplicate'),
           icon: 'riFileCopyLine',
           event: 'duplicateBlock',
+          shortcut: getShortcut('editor:duplicate-block').readable,
         },
         {
           id: 'delete',
           name: t('common.delete'),
           icon: 'riDeleteBin7Line',
           event: 'deleteBlock',
+          shortcut: 'Del',
         },
       ],
     };
@@ -157,9 +167,9 @@ export default {
     function deleteBlock() {
       editor.value.removeNodeId(contextMenu.data);
     }
-    function duplicateBlock() {
+    function duplicateBlock(id) {
       const { name, pos_x, pos_y, data, html } = editor.value.getNodeFromId(
-        contextMenu.data.substr(5)
+        id || contextMenu.data.substr(5)
       );
 
       if (name === 'trigger') return;
@@ -179,6 +189,14 @@ export default {
       );
     }
 
+    useShortcut('editor:duplicate-block', () => {
+      const selectedElement = document.querySelector('.drawflow-node.selected');
+
+      if (!selectedElement) return;
+
+      duplicateBlock(selectedElement.id.substr(5));
+    });
+
     onMounted(() => {
       const context = getCurrentInstance().appContext.app._context;
       const element = document.querySelector('#drawflow');
@@ -310,7 +328,7 @@ export default {
       dropHandler,
       contextMenuHandler: {
         deleteBlock,
-        duplicateBlock,
+        duplicateBlock: () => duplicateBlock(),
       },
     };
   },

+ 10 - 1
src/components/newtab/workflow/WorkflowDetailsCard.vue

@@ -45,8 +45,11 @@
     </div>
   </div>
   <ui-input
+    id="search-input"
     v-model="query"
-    :placeholder="`${t('common.search')}...`"
+    :placeholder="`${t('common.search')}... (${
+      shortcut['action:search'].readable
+    })`"
     prepend-icon="riSearch2Line"
     class="px-4 mt-4 mb-2"
   />
@@ -98,6 +101,7 @@
 <script setup>
 import { computed, ref } from 'vue';
 import { useI18n } from 'vue-i18n';
+import { useShortcut } from '@/composable/shortcut';
 import { tasks, categories } from '@/utils/shared';
 
 defineProps({
@@ -113,6 +117,11 @@ defineProps({
 const emit = defineEmits(['update']);
 
 const { t } = useI18n();
+const shortcut = useShortcut('action:search', () => {
+  const searchInput = document.querySelector('#search-input input');
+
+  searchInput?.focus();
+});
 
 const icons = [
   'riGlobalLine',

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

@@ -95,10 +95,8 @@ import { watch, reactive } from 'vue';
 import { useI18n } from 'vue-i18n';
 import { syntaxTree } from '@codemirror/language';
 import { autocompletion, snippet } from '@codemirror/autocomplete';
-import * as anu from '@codemirror/autocomplete';
 import SharedCodemirror from '@/components/newtab/shared/SharedCodemirror.vue';
 
-console.log(anu);
 const props = defineProps({
   data: {
     type: Object,

+ 1 - 1
src/components/newtab/workflow/edit/EditTrigger.vue

@@ -203,7 +203,7 @@ function handleKeydownEvent(event) {
   const { ctrlKey, altKey, metaKey, shiftKey, key } = event;
 
   if (ctrlKey || metaKey) keys.push('mod');
-  if (altKey) keys.push('alt');
+  if (altKey) keys.push('option');
   if (shiftKey) keys.push('shift');
 
   const isValidKey = !!allowedKeys[key] || /^[a-z0-9,./;'[\]\-=`]$/i.test(key);

+ 123 - 0
src/composable/shortcut.js

@@ -0,0 +1,123 @@
+import { onUnmounted, onMounted } from 'vue';
+import Mousetrap from 'mousetrap';
+import { isObject } from '@/utils/helper';
+
+export const mapShortcuts = {
+  'page:dashboard': {
+    id: 'page:dashboard',
+    combo: 'option+1',
+  },
+  'page:workflows': {
+    id: 'page:workflows',
+    combo: 'option+2',
+  },
+  'page:collections': {
+    id: 'page:collections',
+    combo: 'option+3',
+  },
+  'page:logs': {
+    id: 'page:logs',
+    combo: 'option+4',
+  },
+  'page:settings': {
+    id: 'page:settings',
+    combo: 'option+5',
+  },
+  'action:search': {
+    id: 'action:search',
+    combo: 'mod+shift+f',
+  },
+  'editor:duplicate-block': {
+    id: 'editor:duplicate-block',
+    combo: 'mod+option+d',
+  },
+  'editor:save': {
+    id: 'editor:save',
+    combo: 'mod+shift+s',
+  },
+  'editor:execute-workflow': {
+    id: 'editor:execute-workflow',
+    combo: 'option+enter',
+  },
+};
+
+const os = navigator.appVersion.indexOf('Win') !== -1 ? 'win' : 'mac';
+export function getReadableShortcut(str) {
+  const list = {
+    option: {
+      win: 'alt',
+      mac: 'option',
+    },
+    mod: {
+      win: 'ctrl',
+      mac: '⌘',
+    },
+  };
+  const regex = new RegExp(Object.keys(list).join('|'), 'g');
+  const replacedStr = str.replace(regex, (match) => {
+    return list[match][os];
+  });
+
+  return replacedStr;
+}
+
+export function getShortcut(id, data) {
+  const shortcut = mapShortcuts[id] || {};
+
+  if (data) shortcut.data = data;
+  if (!shortcut.readable) {
+    shortcut.readable = getReadableShortcut(shortcut.combo);
+  }
+
+  return shortcut;
+}
+
+export function useShortcut(shortcuts, handler) {
+  Mousetrap.prototype.stopCallback = () => false;
+
+  const extractedShortcuts = {
+    ids: {},
+    keys: [],
+    data: {},
+  };
+  const handleShortcut = (event, combo) => {
+    const shortcutId = extractedShortcuts.ids[combo];
+    const params = {
+      event,
+      ...extractedShortcuts.data[shortcutId],
+    };
+
+    if (typeof params.data === 'function') {
+      params.data(params);
+    } else {
+      handler?.(params);
+    }
+  };
+  const addShortcutData = ({ combo, id, readable, ...rest }) => {
+    extractedShortcuts.ids[combo] = id;
+    extractedShortcuts.keys.push(combo);
+    extractedShortcuts.data[id] = { combo, id, readable, ...rest };
+  };
+
+  if (isObject(shortcuts)) {
+    addShortcutData(getShortcut(shortcuts.id, shortcuts.data));
+  } else if (typeof shortcuts === 'string') {
+    addShortcutData(getShortcut(shortcuts));
+  } else {
+    shortcuts.forEach((item) => {
+      const currentShortcut =
+        typeof item === 'string' ? getShortcut(item) : item;
+
+      addShortcutData(currentShortcut);
+    });
+  }
+
+  onMounted(() => {
+    Mousetrap.bind(extractedShortcuts.keys, handleShortcut);
+  });
+  onUnmounted(() => {
+    Mousetrap.unbind(extractedShortcuts.keys);
+  });
+
+  return extractedShortcuts.data;
+}

+ 10 - 1
src/newtab/pages/Collections.vue

@@ -6,8 +6,11 @@
     </p>
     <div class="flex items-center my-6 space-x-4">
       <ui-input
+        id="search-input"
         v-model="query"
-        :placeholder="`${t('common.search')}...`"
+        :placeholder="`${t('common.search')}... (${
+          shortcut['action:search'].readable
+        })`"
         prepend-icon="riSearch2Line"
         class="flex-1"
       />
@@ -48,11 +51,17 @@ import { ref, computed } from 'vue';
 import { useI18n } from 'vue-i18n';
 import { sendMessage } from '@/utils/message';
 import { useDialog } from '@/composable/dialog';
+import { useShortcut } from '@/composable/shortcut';
 import Collection from '@/models/collection';
 import SharedCard from '@/components/newtab/shared/SharedCard.vue';
 
 const dialog = useDialog();
 const { t } = useI18n();
+const shortcut = useShortcut('action:search', () => {
+  const searchInput = document.querySelector('#search-input input');
+
+  searchInput?.focus();
+});
 
 const collectionCardMenu = [
   { id: 'rename', name: t('common.rename'), icon: 'riPencilLine' },

+ 10 - 1
src/newtab/pages/Workflows.vue

@@ -5,9 +5,12 @@
     </h1>
     <div class="flex items-center mb-6 space-x-4">
       <ui-input
+        id="search-input"
         v-model="state.query"
+        :placeholder="`${t(`common.search`)}... (${
+          shortcut['action:search'].readable
+        })`"
         prepend-icon="riSearch2Line"
-        :placeholder="`${t(`common.search`)}...`"
         class="flex-1"
       />
       <div class="flex items-center workflow-sort">
@@ -173,11 +176,17 @@ import { useI18n } from 'vue-i18n';
 import { useDialog } from '@/composable/dialog';
 import { sendMessage } from '@/utils/message';
 import { exportWorkflow, importWorkflow } from '@/utils/workflow-data';
+import { useShortcut } from '@/composable/shortcut';
 import SharedCard from '@/components/newtab/shared/SharedCard.vue';
 import Workflow from '@/models/workflow';
 
 const dialog = useDialog();
 const { t } = useI18n();
+const shortcut = useShortcut('action:search', () => {
+  const searchInput = document.querySelector('#search-input input');
+
+  searchInput?.focus();
+});
 
 const sorts = ['name', 'createdAt'];
 const menu = [

+ 0 - 1
src/utils/reference-data/index.js

@@ -17,7 +17,6 @@ export const funcs = {
       date = new Date(args[0]);
       dateFormat = getDateFormat(args[1]);
     }
-    console.log(this, 'anu');
 
     /* eslint-disable-next-line */
     const isValidDate = date instanceof Date && !isNaN(date);

+ 1 - 1
src/utils/webhookUtil.js

@@ -50,7 +50,7 @@ export async function executeWebhook({
   const id = setTimeout(() => {
     controller.abort();
   }, timeout);
-  console.log(body);
+
   try {
     const finalHeaders = filterHeaders(headers);
     const finalContent = renderContent(body, contentType);