Browse Source

feat: add workflow permissions modal

Ahmad Kholid 3 years ago
parent
commit
219723b0c0

+ 24 - 0
src/background/index.js

@@ -522,5 +522,29 @@ message.on('workflow:execute', (workflowData, sender) => {
   workflow.execute(workflowData, workflowData?.options || {});
 });
 message.on('workflow:stop', (id) => workflow.states.stop(id));
+message.on('workflow:added', (workflowId) => {
+  console.log(browser.runtime.getURL('/newtab.html'));
+  browser.tabs
+    .query({ url: browser.runtime.getURL('/newtab.html') })
+    .then((tabs) => {
+      console.log(tabs, tabs.length);
+      if (tabs.length >= 1) {
+        const lastTab = tabs.at(-1);
+
+        tabs.forEach((tab) => {
+          browser.tabs.sendMessage(tab.id, {
+            data: { workflowId },
+            type: 'workflow:added',
+          });
+        });
+
+        browser.tabs.update(lastTab.id, {
+          active: true,
+        });
+      } else {
+        openDashboard(`/workflows/${workflowId}?permission=true`);
+      }
+    });
+});
 
 browser.runtime.onMessage.addListener(message.listener());

+ 64 - 0
src/components/newtab/shared/SharedPermissionsModal.vue

@@ -0,0 +1,64 @@
+<template>
+  <ui-modal :title="t('workflowPermissions.title')" persist>
+    <p class="font-semibold">
+      {{ t('workflowPermissions.description') }}
+    </p>
+    <ui-list class="mt-2 space-y-1">
+      <ui-list-item
+        v-for="permission in permissions"
+        :key="permission"
+        small
+        style="align-items: flex-start"
+      >
+        <v-remixicon :name="icons[permission]" class="mt-1" />
+        <div class="ml-4 flex-1 overflow-hidden">
+          <p class="leading-tight">
+            {{ t(`workflowPermissions.${permission}.title`) }}
+          </p>
+          <p class="text-gray-600 dark:text-gray-200 leading-tight">
+            {{ t(`workflowPermissions.${permission}.description`) }}
+          </p>
+        </div>
+      </ui-list-item>
+    </ui-list>
+    <div class="text-right mt-8">
+      <ui-button class="mr-2" @click="emit('update:modelValue', false)">
+        {{ t('common.cancel') }}
+      </ui-button>
+      <ui-button variant="accent" @click="requestPermission">
+        {{ t('workflow.blocks.clipboard.grantPermission') }}
+      </ui-button>
+    </div>
+  </ui-modal>
+</template>
+<script setup>
+import { toRaw } from 'vue';
+import { useI18n } from 'vue-i18n';
+import browser from 'webextension-polyfill';
+
+const props = defineProps({
+  permissions: {
+    type: Array,
+    default: () => [],
+  },
+});
+const emit = defineEmits(['update:modelValue']);
+
+const { t } = useI18n();
+
+const icons = {
+  downloads: 'riDownloadLine',
+  cliboards: 'riClipboardLine',
+  contextMenus: 'riFileListLine',
+  notifications: 'riNotification3Line',
+};
+
+function requestPermission() {
+  console.log(props.permissions);
+  browser.permissions
+    .request({ permissions: toRaw(props.permissions) })
+    .then(() => {
+      emit('update:modelValue', false);
+    });
+}
+</script>

+ 1 - 1
src/components/ui/UiModal.vue

@@ -32,7 +32,7 @@
                 ></v-remixicon>
               </div>
             </div>
-            <slot></slot>
+            <slot :close="closeModal"></slot>
           </ui-card>
         </div>
       </transition>

+ 1 - 1
src/composable/shortcut.js

@@ -34,7 +34,7 @@ const defaultShortcut = {
   },
   'action:search': {
     id: 'action:search',
-    combo: 'mod+shift+f',
+    combo: 'mod+f',
   },
   'action:new': {
     id: 'action:new',

+ 1 - 0
src/content/services/webService.js

@@ -66,6 +66,7 @@ function initWebListener() {
         }
 
         await browser.storage.local.set({ workflows: workflowsStorage });
+        sendMessage('workflow:added', workflowId, 'background');
       } catch (error) {
         console.error(error);
       }

+ 2 - 0
src/lib/vRemixicon.js

@@ -79,6 +79,7 @@ import {
   riSubtractLine,
   riBracketsLine,
   riDownloadLine,
+  riFileListLine,
   riDragDropLine,
   riClipboardLine,
   riDoubleQuotesL,
@@ -199,6 +200,7 @@ export const icons = {
   riSubtractLine,
   riBracketsLine,
   riDownloadLine,
+  riFileListLine,
   riDragDropLine,
   riClipboardLine,
   riDoubleQuotesL,

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

@@ -22,6 +22,26 @@
       }
     }
   },
+  "workflowPermissions": {
+    "title": "Workflow permissions",
+    "description": "This workflow requires these permissions to run properly",
+    "contextMenus": {
+      "title": "Context menu",
+      "description": "To execute the workflow via the context menu"
+    },
+    "clipboard": {
+      "title": "Clipboard",
+      "description": "For accessing the clipboard data"
+    },
+    "notifications": {
+      "title": "Notification",
+      "description": "For displaying a notification"
+    },
+    "downloads": {
+      "title": "Download",
+      "description": "Saving the page assets and renaming the downloaded file"
+    }
+  },
   "updateMessage": {
     "text1": "Automa has been updated to v{version},",
     "text2": "see what's new."

+ 9 - 0
src/newtab/App.vue

@@ -56,6 +56,7 @@ import iconFirefox from '@/assets/svg/logoFirefox.svg';
 import iconChrome from '@/assets/svg/logo.svg';
 import { ref } from 'vue';
 import { useI18n } from 'vue-i18n';
+import { useRouter } from 'vue-router';
 import { compare } from 'compare-versions';
 import browser from 'webextension-polyfill';
 import { useStore } from '@/stores/main';
@@ -89,6 +90,7 @@ document.head.appendChild(iconElement);
 const { t } = useI18n();
 const store = useStore();
 const theme = useTheme();
+const router = useRouter();
 const userStore = useUserStore();
 const folderStore = useFolderStore();
 const workflowStore = useWorkflowStore();
@@ -180,6 +182,13 @@ window.addEventListener('storage', ({ key, newValue }) => {
     ({ isDestroyed }) => !isDestroyed
   );
 });
+browser.runtime.onMessage.addListener(({ type, data }) => {
+  if (type === 'workflow:added') {
+    workflowStore.loadData().then(() => {
+      router.push(`/workflows/${data.workflowId}?permission=true`);
+    });
+  }
+});
 
 (async () => {
   try {

+ 33 - 2
src/newtab/pages/Workflows.vue

@@ -24,7 +24,7 @@
               <ui-list-item
                 v-close-popover
                 class="cursor-pointer"
-                @click="importWorkflow({ multiple: true })"
+                @click="openImportDialog"
               >
                 {{ t('workflow.import') }}
               </ui-list-item>
@@ -194,6 +194,10 @@
         </ui-button>
       </div>
     </ui-modal>
+    <shared-permissions-modal
+      v-model="permissionState.showModal"
+      :permissions="permissionState.items"
+    />
   </div>
 </template>
 <script setup>
@@ -203,15 +207,16 @@ import { useToast } from 'vue-toastification';
 import { useDialog } from '@/composable/dialog';
 import { useShortcut } from '@/composable/shortcut';
 import { useGroupTooltip } from '@/composable/groupTooltip';
-import { importWorkflow } from '@/utils/workflowData';
 import { isWhitespace } from '@/utils/helper';
 import { useUserStore } from '@/stores/user';
 import { useWorkflowStore } from '@/stores/workflow';
 import { useHostedWorkflowStore } from '@/stores/hostedWorkflow';
+import { importWorkflow, getWorkflowPermissions } from '@/utils/workflowData';
 import WorkflowsLocal from '@/components/newtab/workflows/WorkflowsLocal.vue';
 import WorkflowsShared from '@/components/newtab/workflows/WorkflowsShared.vue';
 import WorkflowsHosted from '@/components/newtab/workflows/WorkflowsHosted.vue';
 import WorkflowsFolder from '@/components/newtab/workflows/WorkflowsFolder.vue';
+import SharedPermissionsModal from '@/components/newtab/shared/SharedPermissionsModal.vue';
 
 useGroupTooltip();
 const { t } = useI18n();
@@ -237,6 +242,10 @@ const addWorkflowModal = shallowReactive({
   show: false,
   description: '',
 });
+const permissionState = shallowReactive({
+  items: [],
+  showModal: false,
+});
 
 const hostedWorkflows = computed(() => hostedWorkflowStore.toArray);
 
@@ -285,6 +294,28 @@ function addHostedWorkflow() {
     },
   });
 }
+async function openImportDialog() {
+  try {
+    const workflows = await importWorkflow({ multiple: true });
+    const insertedWorkflows = Object.values(workflows);
+    let requiredPermissions = [];
+
+    for (const workflow of insertedWorkflows) {
+      if (workflow.drawflow) {
+        const permissions = await getWorkflowPermissions(workflow.drawflow);
+        requiredPermissions.push(...permissions);
+      }
+    }
+
+    requiredPermissions = Array.from(new Set(requiredPermissions));
+    if (requiredPermissions.length === 0) return;
+
+    permissionState.items = requiredPermissions;
+    permissionState.showModal = true;
+  } catch (error) {
+    console.error(error);
+  }
+}
 
 const shortcut = useShortcut(['action:search', 'action:new'], ({ id }) => {
   if (id === 'action:search') {

+ 20 - 1
src/newtab/pages/workflows/[id].vue

@@ -133,6 +133,10 @@
       @close="modalState.show = false"
     />
   </ui-modal>
+  <shared-permissions-modal
+    v-model="permissionState.showModal"
+    :permissions="permissionState.items"
+  />
 </template>
 <script setup>
 import {
@@ -153,9 +157,10 @@ import { useStore } from '@/stores/main';
 import { useUserStore } from '@/stores/user';
 import { useWorkflowStore } from '@/stores/workflow';
 import { useShortcut, getShortcut } from '@/composable/shortcut';
+import { getWorkflowPermissions } from '@/utils/workflowData';
 import { tasks } from '@/utils/shared';
-import { debounce, parseJSON, throttle } from '@/utils/helper';
 import { fetchApi } from '@/utils/api';
+import { debounce, parseJSON, throttle } from '@/utils/helper';
 import browser from 'webextension-polyfill';
 import DroppedNode from '@/utils/editor/DroppedNode';
 import convertWorkflowData from '@/utils/convertWorkflowData';
@@ -167,6 +172,7 @@ import WorkflowDataTable from '@/components/newtab/workflow/WorkflowDataTable.vu
 import WorkflowGlobalData from '@/components/newtab/workflow/WorkflowGlobalData.vue';
 import WorkflowDetailsCard from '@/components/newtab/workflow/WorkflowDetailsCard.vue';
 import EditorLogs from '@/components/newtab/workflow/editor/EditorLogs.vue';
+import SharedPermissionsModal from '@/components/newtab/shared/SharedPermissionsModal.vue';
 import EditorLocalCtxMenu from '@/components/newtab/workflow/editor/EditorLocalCtxMenu.vue';
 import EditorLocalActions from '@/components/newtab/workflow/editor/EditorLocalActions.vue';
 
@@ -188,6 +194,10 @@ const state = reactive({
   workflowConverted: false,
   activeTab: route.query.tab || 'editor',
 });
+const permissionState = reactive({
+  permissions: [],
+  showModal: false,
+});
 const modalState = reactive({
   name: '',
   show: false,
@@ -731,6 +741,15 @@ onMounted(() => {
     state.workflowConverted = true;
   });
 
+  if (route.query.permission) {
+    getWorkflowPermissions(workflow.value.drawflow).then((permissions) => {
+      if (permissions.length === 0) return;
+
+      permissionState.items = permissions;
+      permissionState.showModal = true;
+    });
+  }
+
   window.onbeforeunload = () => {
     updateHostedWorkflow();
 

+ 110 - 41
src/utils/workflowData.js

@@ -2,55 +2,124 @@ import browser from 'webextension-polyfill';
 import { useWorkflowStore } from '@/stores/workflow';
 import { parseJSON, fileSaver, openFilePicker } from './helper';
 
+const contextMenuPermission =
+  BROWSER_TYPE === 'firefox' ? 'menus' : 'contextMenus';
+const checkPermission = (permissions) =>
+  browser.permissions.contains({ permissions });
+const requiredPermissions = {
+  trigger: {
+    name: contextMenuPermission,
+    hasPermission({ data }) {
+      if (data.type !== 'context-menu') return true;
+
+      return checkPermission([contextMenuPermission]);
+    },
+  },
+  clipboard: {
+    name: 'clipboardRead',
+    hasPermission() {
+      return checkPermission(['clipboardRead']);
+    },
+  },
+  notification: {
+    name: 'notifications',
+    hasPermission() {
+      return checkPermission(['notifications']);
+    },
+  },
+  'handle-download': {
+    name: 'downloads',
+    hasPermission() {
+      return checkPermission(['downloads']);
+    },
+  },
+  'save-assets': {
+    name: 'downloads',
+    hasPermission() {
+      return checkPermission(['downloads']);
+    },
+  },
+};
+
+export async function getWorkflowPermissions(drawflow) {
+  let blocks = [];
+  const permissions = [];
+  const drawflowData =
+    typeof drawflow === 'string' ? parseJSON(drawflow) : drawflow;
+
+  if (drawflowData.nodes) {
+    blocks = drawflowData.nodes;
+  } else {
+    blocks = Object.values(drawflowData.drawflow?.Home?.data || {});
+  }
+
+  for (const block of blocks) {
+    const name = block.label || block.name;
+    const permission = requiredPermissions[name];
+
+    if (permission && !permissions.includes(permission.name)) {
+      const hasPermission = await permission.hasPermission(block);
+      if (!hasPermission) permissions.push(permission.name);
+    }
+  }
+
+  return permissions;
+}
+
 export function importWorkflow(attrs = {}) {
-  openFilePicker(['application/json'], attrs)
-    .then((files) => {
-      const handleOnLoadReader = ({ target }) => {
-        const workflow = JSON.parse(target.result);
-        const workflowStore = useWorkflowStore();
-
-        if (workflow.includedWorkflows) {
-          Object.keys(workflow.includedWorkflows).forEach((workflowId) => {
-            const isWorkflowExists = Boolean(
-              workflowStore.workflows[workflowId]
-            );
-
-            if (isWorkflowExists) return;
-
-            const currentWorkflow = workflow.includedWorkflows[workflowId];
-            currentWorkflow.table =
-              currentWorkflow.table || currentWorkflow.dataColumns;
-            delete currentWorkflow.dataColumns;
-
-            workflowStore.insert({
-              ...currentWorkflow,
-              id: workflowId,
-              createdAt: Date.now(),
+  return new Promise((resolve, reject) => {
+    openFilePicker(['application/json'], attrs)
+      .then((files) => {
+        const handleOnLoadReader = ({ target }) => {
+          const workflow = JSON.parse(target.result);
+          const workflowStore = useWorkflowStore();
+
+          if (workflow.includedWorkflows) {
+            Object.keys(workflow.includedWorkflows).forEach((workflowId) => {
+              const isWorkflowExists = Boolean(
+                workflowStore.workflows[workflowId]
+              );
+
+              if (isWorkflowExists) return;
+
+              const currentWorkflow = workflow.includedWorkflows[workflowId];
+              currentWorkflow.table =
+                currentWorkflow.table || currentWorkflow.dataColumns;
+              delete currentWorkflow.dataColumns;
+
+              workflowStore.insert({
+                ...currentWorkflow,
+                id: workflowId,
+                createdAt: Date.now(),
+              });
             });
-          });
 
-          delete workflow.includedWorkflows;
-        }
+            delete workflow.includedWorkflows;
+          }
 
-        workflow.table = workflow.table || workflow.dataColumns;
-        delete workflow.dataColumns;
+          workflow.table = workflow.table || workflow.dataColumns;
+          delete workflow.dataColumns;
 
-        workflowStore.insert({
-          ...workflow,
-          createdAt: Date.now(),
-        });
-      };
+          workflowStore
+            .insert({
+              ...workflow,
+              createdAt: Date.now(),
+            })
+            .then(resolve);
+        };
 
-      files.forEach((file) => {
-        const reader = new FileReader();
+        files.forEach((file) => {
+          const reader = new FileReader();
 
-        reader.onload = handleOnLoadReader;
-        reader.readAsText(file);
+          reader.onload = handleOnLoadReader;
+          reader.readAsText(file);
+        });
+      })
+      .catch((error) => {
+        console.error(error);
+        reject(error);
       });
-    })
-    .catch((error) => {
-      console.error(error);
-    });
+  });
 }
 
 export function convertWorkflow(workflow, additionalKeys = []) {