Browse Source

feat: add pin workflow (#771)

Ahmad Kholid 2 years ago
parent
commit
5fced7f10e

+ 76 - 89
src/components/newtab/workflows/WorkflowsLocal.vue

@@ -11,98 +11,41 @@
     </div>
     </div>
   </div>
   </div>
   <template v-else>
   <template v-else>
+    <div v-if="pinnedWorkflows.length > 0" class="mb-8 pb-8 border-b">
+      <div class="flex items-center">
+        <v-remixicon name="riPushpin2Line" class="mr-2" size="20" />
+        <span>{{ t('workflow.pinWorkflow.pinned') }}</span>
+      </div>
+      <div class="workflows-container mt-4">
+        <workflows-local-card
+          v-for="workflow in pinnedWorkflows"
+          :key="workflow.id"
+          :workflow="workflow"
+          :is-hosted="userStore.hostedWorkflows[workflow.id]"
+          :is-shared="sharedWorkflowStore.getById(workflow.id)"
+          :is-pinned="true"
+          :menu="menu"
+          @dragstart="onDragStart"
+          @execute="executeWorkflow"
+          @toggle-pin="togglePinWorkflow(workflow)"
+          @toggle-disable="toggleDisableWorkflow(workflow)"
+        />
+      </div>
+    </div>
     <div class="workflows-container">
     <div class="workflows-container">
-      <shared-card
+      <workflows-local-card
         v-for="workflow in workflows"
         v-for="workflow in workflows"
         :key="workflow.id"
         :key="workflow.id"
-        :data="workflow"
-        :data-workflow="workflow.id"
-        draggable="true"
-        class="cursor-default select-none ring-accent local-workflow"
+        :workflow="workflow"
+        :is-hosted="userStore.hostedWorkflows[workflow.id]"
+        :is-shared="sharedWorkflowStore.getById(workflow.id)"
+        :is-pinned="state.pinnedWorkflows.includes(workflow.id)"
+        :menu="menu"
         @dragstart="onDragStart"
         @dragstart="onDragStart"
-        @click="$router.push(`/workflows/${$event.id}`)"
-      >
-        <template #header>
-          <div class="flex items-center mb-4">
-            <template v-if="workflow && !workflow.isDisabled">
-              <ui-img
-                v-if="workflow.icon.startsWith('http')"
-                :src="workflow.icon"
-                class="rounded-lg overflow-hidden"
-                style="height: 40px; width: 40px"
-                alt="Can not display"
-              />
-              <span
-                v-else
-                class="p-2 rounded-lg bg-box-transparent inline-block"
-              >
-                <v-remixicon :name="workflow.icon" />
-              </span>
-            </template>
-            <p v-else class="py-2">{{ t('common.disabled') }}</p>
-            <div class="flex-grow"></div>
-            <button
-              v-if="!workflow.isDisabled"
-              class="invisible group-hover:visible"
-              @click="executeWorkflow(workflow)"
-            >
-              <v-remixicon name="riPlayLine" />
-            </button>
-            <ui-popover class="h-6 ml-2">
-              <template #trigger>
-                <button>
-                  <v-remixicon name="riMoreLine" />
-                </button>
-              </template>
-              <ui-list class="space-y-1" style="min-width: 150px">
-                <ui-list-item
-                  class="cursor-pointer"
-                  @click="toggleDisableWorkflow(workflow)"
-                >
-                  <v-remixicon name="riToggleLine" class="mr-2 -ml-1" />
-                  <span class="capitalize">
-                    {{
-                      t(`common.${workflow.isDisabled ? 'enable' : 'disable'}`)
-                    }}
-                  </span>
-                </ui-list-item>
-                <ui-list-item
-                  v-for="item in menu"
-                  :key="item.id"
-                  v-close-popover
-                  class="cursor-pointer"
-                  @click="item.action(workflow)"
-                >
-                  <v-remixicon :name="item.icon" class="mr-2 -ml-1" />
-                  <span class="capitalize">{{ item.name }}</span>
-                </ui-list-item>
-              </ui-list>
-            </ui-popover>
-          </div>
-        </template>
-        <template #footer-content>
-          <v-remixicon
-            v-if="sharedWorkflowStore.getById(workflow.id)"
-            v-tooltip:bottom.group="
-              t('workflow.share.sharedAs', {
-                name: sharedWorkflowStore
-                  .getById(workflow.id)
-                  ?.name.slice(0, 64),
-              })
-            "
-            name="riShareLine"
-            size="20"
-            class="ml-2"
-          />
-          <v-remixicon
-            v-if="userStore.hostedWorkflows[workflow.id]"
-            v-tooltip:bottom.group="t('workflow.host.title')"
-            name="riBaseStationLine"
-            size="20"
-            class="ml-2"
-          />
-        </template>
-      </shared-card>
+        @execute="executeWorkflow"
+        @toggle-pin="togglePinWorkflow(workflow)"
+        @toggle-disable="toggleDisableWorkflow(workflow)"
+      />
     </div>
     </div>
     <div
     <div
       v-if="filteredWorkflows.length > 18"
       v-if="filteredWorkflows.length > 18"
@@ -161,6 +104,7 @@
 import { shallowReactive, computed, onMounted, onBeforeUnmount } from 'vue';
 import { shallowReactive, computed, onMounted, onBeforeUnmount } from 'vue';
 import { useI18n } from 'vue-i18n';
 import { useI18n } from 'vue-i18n';
 import SelectionArea from '@viselect/vanilla';
 import SelectionArea from '@viselect/vanilla';
+import browser from 'webextension-polyfill';
 import { arraySorter } from '@/utils/helper';
 import { arraySorter } from '@/utils/helper';
 import { sendMessage } from '@/utils/message';
 import { sendMessage } from '@/utils/message';
 import { useUserStore } from '@/stores/user';
 import { useUserStore } from '@/stores/user';
@@ -168,7 +112,7 @@ import { useDialog } from '@/composable/dialog';
 import { useWorkflowStore } from '@/stores/workflow';
 import { useWorkflowStore } from '@/stores/workflow';
 import { exportWorkflow } from '@/utils/workflowData';
 import { exportWorkflow } from '@/utils/workflowData';
 import { useSharedWorkflowStore } from '@/stores/sharedWorkflow';
 import { useSharedWorkflowStore } from '@/stores/sharedWorkflow';
-import SharedCard from '@/components/newtab/shared/SharedCard.vue';
+import WorkflowsLocalCard from './WorkflowsLocalCard.vue';
 
 
 const props = defineProps({
 const props = defineProps({
   search: {
   search: {
@@ -199,6 +143,7 @@ const workflowStore = useWorkflowStore();
 const sharedWorkflowStore = useSharedWorkflowStore();
 const sharedWorkflowStore = useSharedWorkflowStore();
 
 
 const state = shallowReactive({
 const state = shallowReactive({
+  pinnedWorkflows: [],
   selectedWorkflows: [],
   selectedWorkflows: [],
 });
 });
 const renameState = shallowReactive({
 const renameState = shallowReactive({
@@ -262,6 +207,27 @@ const workflows = computed(() =>
     pagination.currentPage * pagination.perPage
     pagination.currentPage * pagination.perPage
   )
   )
 );
 );
+const pinnedWorkflows = computed(() => {
+  const list = [];
+  state.pinnedWorkflows.forEach((workflowId) => {
+    const workflow = workflowStore.getById(workflowId);
+    if (
+      !workflow ||
+      !workflow.name
+        .toLocaleLowerCase()
+        .includes(props.search.toLocaleLowerCase())
+    )
+      return;
+
+    list.push(workflow);
+  });
+
+  return arraySorter({
+    data: list,
+    key: props.sort.by,
+    order: props.sort.order,
+  });
+});
 
 
 function executeWorkflow(workflow) {
 function executeWorkflow(workflow) {
   sendMessage('workflow:execute', workflow, 'background');
   sendMessage('workflow:execute', workflow, 'background');
@@ -344,6 +310,8 @@ function duplicateWorkflow(workflow) {
     delete copyWorkflow[key];
     delete copyWorkflow[key];
   });
   });
 
 
+  copyWorkflow.name += ' - copy';
+
   workflowStore.insert(copyWorkflow);
   workflowStore.insert(copyWorkflow);
 }
 }
 function onDragStart({ dataTransfer, target }) {
 function onDragStart({ dataTransfer, target }) {
@@ -362,6 +330,21 @@ function clearSelectedWorkflows() {
   });
   });
   selection.clearSelection();
   selection.clearSelection();
 }
 }
+function togglePinWorkflow(workflow) {
+  const index = state.pinnedWorkflows.indexOf(workflow.id);
+  const copyData = [...state.pinnedWorkflows];
+
+  if (index === -1) {
+    copyData.push(workflow.id);
+  } else {
+    copyData.splice(index, 1);
+  }
+
+  state.pinnedWorkflows = copyData;
+  browser.storage.local.set({
+    pinnedWorkflows: copyData,
+  });
+}
 
 
 const menu = [
 const menu = [
   {
   {
@@ -392,6 +375,10 @@ const menu = [
 
 
 onMounted(() => {
 onMounted(() => {
   window.addEventListener('keydown', deleteSelectedWorkflows);
   window.addEventListener('keydown', deleteSelectedWorkflows);
+
+  browser.storage.local.get('pinnedWorkflows').then((storage) => {
+    state.pinnedWorkflows = storage.pinnedWorkflows || [];
+  });
 });
 });
 onBeforeUnmount(() => {
 onBeforeUnmount(() => {
   window.removeEventListener('keydown', deleteSelectedWorkflows);
   window.removeEventListener('keydown', deleteSelectedWorkflows);

+ 116 - 0
src/components/newtab/workflows/WorkflowsLocalCard.vue

@@ -0,0 +1,116 @@
+<template>
+  <shared-card
+    :data="workflow"
+    :data-workflow="workflow.id"
+    draggable="true"
+    class="cursor-default select-none ring-accent local-workflow"
+    @click="$router.push(`/workflows/${$event.id}`)"
+  >
+    <template #header>
+      <div class="flex items-center mb-4">
+        <template v-if="workflow && !workflow.isDisabled">
+          <ui-img
+            v-if="workflow.icon.startsWith('http')"
+            :src="workflow.icon"
+            class="rounded-lg overflow-hidden"
+            style="height: 40px; width: 40px"
+            alt="Can not display"
+          />
+          <span v-else class="p-2 rounded-lg bg-box-transparent inline-block">
+            <v-remixicon :name="workflow.icon" />
+          </span>
+        </template>
+        <p v-else class="py-2">{{ t('common.disabled') }}</p>
+        <div class="flex-grow"></div>
+        <button
+          v-if="!workflow.isDisabled"
+          class="invisible group-hover:visible"
+          @click="$emit('execute')"
+        >
+          <v-remixicon name="riPlayLine" />
+        </button>
+        <ui-popover class="h-6 ml-2">
+          <template #trigger>
+            <button>
+              <v-remixicon name="riMoreLine" />
+            </button>
+          </template>
+          <ui-list class="space-y-1" style="min-width: 165px">
+            <ui-list-item
+              class="cursor-pointer"
+              @click="$emit('toggleDisable')"
+            >
+              <v-remixicon name="riToggleLine" class="mr-2 -ml-1" />
+              <span class="capitalize">
+                {{ t(`common.${workflow.isDisabled ? 'enable' : 'disable'}`) }}
+              </span>
+            </ui-list-item>
+            <ui-list-item class="cursor-pointer" @click="$emit('togglePin')">
+              <v-remixicon name="riPushpin2Line" class="mr-2 -ml-1" />
+              <span>{{
+                t(`workflow.pinWorkflow.${isPinned ? 'unpin' : 'pin'}`)
+              }}</span>
+            </ui-list-item>
+            <ui-list-item
+              v-for="item in menu"
+              :key="item.id"
+              v-close-popover
+              class="cursor-pointer"
+              @click="item.action(workflow)"
+            >
+              <v-remixicon :name="item.icon" class="mr-2 -ml-1" />
+              <span class="capitalize">{{ item.name }}</span>
+            </ui-list-item>
+          </ui-list>
+        </ui-popover>
+      </div>
+    </template>
+    <template #footer-content>
+      <v-remixicon
+        v-if="isShared"
+        v-tooltip:bottom.group="
+          t('workflow.share.sharedAs', {
+            name: isShared?.name.slice(0, 64),
+          })
+        "
+        name="riShareLine"
+        size="20"
+        class="ml-2"
+      />
+      <v-remixicon
+        v-if="isHosted"
+        v-tooltip:bottom.group="t('workflow.host.title')"
+        name="riBaseStationLine"
+        size="20"
+        class="ml-2"
+      />
+    </template>
+  </shared-card>
+</template>
+<script setup>
+import { useI18n } from 'vue-i18n';
+import SharedCard from '@/components/newtab/shared/SharedCard.vue';
+
+defineProps({
+  workflow: {
+    type: Object,
+    default: () => ({}),
+  },
+  menu: {
+    type: Array,
+    default: () => [],
+  },
+  isShared: {
+    type: Object,
+    default: null,
+  },
+  isHosted: {
+    type: Object,
+    default: null,
+  },
+  isPinned: Boolean,
+});
+defineEmits(['toggleDisable', 'togglePin', 'execute']);
+
+const { t } = useI18n();
+</script>

+ 21 - 12
src/components/popup/home/HomeWorkflowCard.vue

@@ -26,17 +26,25 @@
           <v-remixicon name="riMoreLine" />
           <v-remixicon name="riMoreLine" />
         </button>
         </button>
       </template>
       </template>
-      <ui-list class="w-40 space-y-1">
-        <ui-list-item
-          v-if="tab === 'local'"
-          class="capitalize cursor-pointer"
-          @click="$emit('update', { isDisabled: !workflow.isDisabled })"
-        >
-          <v-remixicon name="riToggleLine" class="mr-2 -ml-1" />
-          <span>{{
-            t(`common.${workflow.isDisabled ? 'enable' : 'disable'}`)
-          }}</span>
-        </ui-list-item>
+      <ui-list class="space-y-1" style="min-width: 160px">
+        <template v-if="tab === 'local'">
+          <ui-list-item
+            class="capitalize cursor-pointer"
+            @click="$emit('update', { isDisabled: !workflow.isDisabled })"
+          >
+            <v-remixicon name="riToggleLine" class="mr-2 -ml-1" />
+            <span>{{
+              t(`common.${workflow.isDisabled ? 'enable' : 'disable'}`)
+            }}</span>
+          </ui-list-item>
+          <ui-list-item
+            class="capitalize cursor-pointer"
+            @click="$emit('togglePin')"
+          >
+            <v-remixicon name="riPushpin2Line" class="mr-2 -ml-1" />
+            <span>{{ pinned ? 'Unpin workflow' : 'Pin workflow' }}</span>
+          </ui-list-item>
+        </template>
         <ui-list-item
         <ui-list-item
           v-for="item in filteredMenu"
           v-for="item in filteredMenu"
           :key="item.name"
           :key="item.name"
@@ -64,8 +72,9 @@ const props = defineProps({
     type: String,
     type: String,
     default: 'local',
     default: 'local',
   },
   },
+  pinned: Boolean,
 });
 });
-defineEmits(['execute', 'rename', 'details', 'delete', 'update']);
+defineEmits(['execute', 'togglePin', 'rename', 'details', 'delete', 'update']);
 
 
 const { t } = useI18n();
 const { t } = useI18n();
 
 

+ 5 - 0
src/locales/en/newtab.json

@@ -185,6 +185,11 @@
       "title": "Preview mode",
       "title": "Preview mode",
       "description": "You're in preview mode, changes you made won't be saved"
       "description": "You're in preview mode, changes you made won't be saved"
     },
     },
+    "pinWorkflow": {
+      "pin": "Pin workflow",
+      "unpin": "Unpin workflow",
+      "pinned": "Pinned workflows"
+    },
     "my": "My workflows",
     "my": "My workflows",
     "import": "Import workflow",
     "import": "Import workflow",
     "new": "New workflow",
     "new": "New workflow",

+ 63 - 1
src/popup/pages/Home.vue

@@ -67,7 +67,10 @@
     v-show="state.activeTab === 'team'"
     v-show="state.activeTab === 'team'"
     :search="state.query"
     :search="state.query"
   />
   />
-  <div v-if="state.activeTab !== 'team'" class="px-5 pb-5 space-y-2">
+  <div
+    v-if="state.activeTab !== 'team'"
+    class="px-5 z-20 relative pb-5 space-y-2"
+  >
     <ui-card v-if="workflowStore.getWorkflows.length === 0" class="text-center">
     <ui-card v-if="workflowStore.getWorkflows.length === 0" class="text-center">
       <img src="@/assets/svg/alien.svg" />
       <img src="@/assets/svg/alien.svg" />
       <p class="font-semibold">{{ t('message.empty') }}</p>
       <p class="font-semibold">{{ t('message.empty') }}</p>
@@ -79,16 +82,37 @@
         {{ t('home.workflow.new') }}
         {{ t('home.workflow.new') }}
       </ui-button>
       </ui-button>
     </ui-card>
     </ui-card>
+    <div v-if="pinnedWorkflows.length > 0" class="mt-1 mb-4 border-b pb-4">
+      <div class="flex items-center text-gray-300 mb-1">
+        <v-remixicon name="riPushpin2Line" size="20" class="mr-2" />
+        <span>Pinned workflows</span>
+      </div>
+      <home-workflow-card
+        v-for="workflow in pinnedWorkflows"
+        :key="workflow.id"
+        :workflow="workflow"
+        :tab="state.activeTab"
+        :pinned="true"
+        @details="openWorkflowPage"
+        @update="updateWorkflow(workflow.id, $event)"
+        @execute="executeWorkflow"
+        @rename="renameWorkflow"
+        @delete="deleteWorkflow"
+        @toggle-pin="togglePinWorkflow(workflow)"
+      />
+    </div>
     <home-workflow-card
     <home-workflow-card
       v-for="workflow in workflows"
       v-for="workflow in workflows"
       :key="workflow.id"
       :key="workflow.id"
       :workflow="workflow"
       :workflow="workflow"
       :tab="state.activeTab"
       :tab="state.activeTab"
+      :pinned="state.pinnedWorkflows.includes(workflow.id)"
       @details="openWorkflowPage"
       @details="openWorkflowPage"
       @update="updateWorkflow(workflow.id, $event)"
       @update="updateWorkflow(workflow.id, $event)"
       @execute="executeWorkflow"
       @execute="executeWorkflow"
       @rename="renameWorkflow"
       @rename="renameWorkflow"
       @delete="deleteWorkflow"
       @delete="deleteWorkflow"
+      @toggle-pin="togglePinWorkflow(workflow)"
     />
     />
   </div>
   </div>
   <ui-modal v-model="state.newRecordingModal" custom-content>
   <ui-modal v-model="state.newRecordingModal" custom-content>
@@ -153,9 +177,29 @@ const state = shallowReactive({
   retrieved: false,
   retrieved: false,
   haveAccess: true,
   haveAccess: true,
   activeTab: 'local',
   activeTab: 'local',
+  pinnedWorkflows: [],
   newRecordingModal: false,
   newRecordingModal: false,
 });
 });
 
 
+const pinnedWorkflows = computed(() => {
+  if (state.activeTab !== 'local') return [];
+
+  const list = [];
+  state.pinnedWorkflows.forEach((workflowId) => {
+    const workflow = workflowStore.getById(workflowId);
+    if (
+      !workflow ||
+      !workflow.name
+        .toLocaleLowerCase()
+        .includes(state.query.toLocaleLowerCase())
+    )
+      return;
+
+    list.push(workflow);
+  });
+
+  return list;
+});
 const hostedWorkflows = computed(() => {
 const hostedWorkflows = computed(() => {
   if (state.activeTab !== 'host') return [];
   if (state.activeTab !== 'host') return [];
 
 
@@ -179,6 +223,21 @@ const showTab = computed(
   () => hostedWorkflowStore.toArray.length > 0 || userStore.user?.teams
   () => hostedWorkflowStore.toArray.length > 0 || userStore.user?.teams
 );
 );
 
 
+function togglePinWorkflow(workflow) {
+  const index = state.pinnedWorkflows.indexOf(workflow.id);
+  const copyData = [...state.pinnedWorkflows];
+
+  if (index === -1) {
+    copyData.push(workflow.id);
+  } else {
+    copyData.splice(index, 1);
+  }
+
+  state.pinnedWorkflows = copyData;
+  browser.storage.local.set({
+    pinnedWorkflows: copyData,
+  });
+}
 function executeWorkflow(workflow) {
 function executeWorkflow(workflow) {
   sendMessage('workflow:execute', workflow, 'background');
   sendMessage('workflow:execute', workflow, 'background');
 }
 }
@@ -308,6 +367,9 @@ onMounted(async () => {
   else if (activeTab === 'host' && hostedWorkflowStore.toArray.length < 0)
   else if (activeTab === 'host' && hostedWorkflowStore.toArray.length < 0)
     activeTab = 'local';
     activeTab = 'local';
 
 
+  const storage = await browser.storage.local.get('pinnedWorkflows');
+  state.pinnedWorkflows = storage.pinnedWorkflows || [];
+
   state.retrieved = true;
   state.retrieved = true;
   state.activeTab = activeTab;
   state.activeTab = activeTab;
 });
 });

+ 10 - 1
src/stores/workflow.js

@@ -56,7 +56,6 @@ const defaultWorkflow = (data = null, options = {}) => {
       insertDefaultColumn: false,
       insertDefaultColumn: false,
       defaultColumnName: 'column',
       defaultColumnName: 'column',
     },
     },
-    runCounts: 0,
     version: browser.runtime.getManifest().version,
     version: browser.runtime.getManifest().version,
     globalData: '{\n\t"key": "value"\n}',
     globalData: '{\n\t"key": "value"\n}',
   };
   };
@@ -258,6 +257,16 @@ export const useWorkflowStore = defineStore('workflow', {
       ]);
       ]);
       await this.saveToStorage('workflows');
       await this.saveToStorage('workflows');
 
 
+      const { pinnedWorkflows } = await browser.storage.local.get(
+        'pinnedWorkflows'
+      );
+      const pinnedWorkflowIndex =
+        pinnedWorkflows && pinnedWorkflows.indexOf(id);
+      if (pinnedWorkflowIndex !== -1) {
+        pinnedWorkflows.splice(pinnedWorkflowIndex, 1);
+        await browser.storage.local.set({ pinnedWorkflows });
+      }
+
       return id;
       return id;
     },
     },
   },
   },