Parcourir la source

feat: add popup

Ahmad Kholid il y a 2 ans
Parent
commit
446cccb419

+ 4 - 1
src/manifest.chrome.json

@@ -1,7 +1,10 @@
 {
   "manifest_version": 3,
   "name": "Automa",
-  "action": {},
+  "action": {
+    "default_popup": "popup.html",
+    "default_icon": "icon-128.png"
+  },
   "background": {
     "service_worker": "background.bundle.js",
     "type": "module"

+ 4 - 1
src/manifest.firefox.json

@@ -10,7 +10,10 @@
     "scripts": ["background.bundle.js"],
     "persistent": false
   },
-  "browser_action": {},
+  "browser_action": {
+    "default_popup": "popup.html",
+    "default_icon": "icon-128.png"
+  },
   "icons": {
     "128": "icon-128.png"
   },

+ 52 - 0
src/popup/App.vue

@@ -0,0 +1,52 @@
+<template>
+  <template v-if="retrieved">
+    <router-view />
+    <ui-dialog />
+  </template>
+</template>
+<script setup>
+import { ref, onMounted } from 'vue';
+import browser from 'webextension-polyfill';
+import { useStore } from '@/stores/main';
+import { sendMessage } from '@/utils/message';
+import { useWorkflowStore } from '@/stores/workflow';
+import { useHostedWorkflowStore } from '@/stores/hostedWorkflow';
+import { loadLocaleMessages, setI18nLanguage } from '@/lib/vueI18n';
+
+const store = useStore();
+const workflowStore = useWorkflowStore();
+const hostedWorkflowStore = useHostedWorkflowStore();
+
+const retrieved = ref(false);
+
+browser.storage.local.get('isRecording').then(({ isRecording }) => {
+  if (!isRecording) return;
+
+  sendMessage('open:dashboard', '/recording', 'background').then(() => {
+    window.close();
+  });
+});
+
+onMounted(async () => {
+  try {
+    await store.loadSettings();
+    await loadLocaleMessages(store.settings.locale, 'popup');
+    await setI18nLanguage(store.settings.locale);
+
+    await workflowStore.loadData();
+    await hostedWorkflowStore.loadData();
+
+    retrieved.value = true;
+  } catch (error) {
+    console.error(error);
+    retrieved.value = true;
+  }
+});
+</script>
+<style>
+body {
+  height: 500px;
+  width: 350px;
+  font-size: 16px;
+}
+</style>

+ 11 - 0
src/popup/index.html

@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <title></title>
+  </head>
+
+  <body>
+    <div id="app" class="scroll"></div>
+  </body>
+</html>

+ 20 - 0
src/popup/index.js

@@ -0,0 +1,20 @@
+import { createApp } from 'vue';
+import App from './App.vue';
+import router from './router';
+import pinia from '../lib/pinia';
+import compsUi from '../lib/compsUi';
+import vueI18n from '../lib/vueI18n';
+import vRemixicon, { icons } from '../lib/vRemixicon';
+import '../assets/css/tailwind.css';
+import '../assets/css/fonts.css';
+import '../assets/css/flow.css';
+
+createApp(App)
+  .use(router)
+  .use(compsUi)
+  .use(vueI18n)
+  .use(pinia)
+  .use(vRemixicon, icons)
+  .mount('#app');
+
+if (module.hot) module.hot.accept();

+ 285 - 0
src/popup/pages/Home.vue

@@ -0,0 +1,285 @@
+<template>
+  <div
+    :class="[!showTab ? 'h-48' : 'h-56']"
+    class="bg-accent rounded-b-2xl absolute top-0 left-0 w-full"
+  ></div>
+  <div
+    :class="[!showTab ? 'mb-6' : 'mb-2']"
+    class="dark placeholder-black relative z-10 text-white px-5 pt-8"
+  >
+    <div class="flex items-center mb-4">
+      <h1 class="text-xl font-semibold text-white">Automa</h1>
+      <div class="flex-grow"></div>
+      <ui-button
+        v-tooltip.group="
+          t(`home.elementSelector.${state.haveAccess ? 'name' : 'noAccess'}`)
+        "
+        icon
+        class="mr-2"
+        @click="initElementSelector"
+      >
+        <v-remixicon name="riFocus3Line" />
+      </ui-button>
+      <ui-button
+        v-tooltip.group="t('common.dashboard')"
+        icon
+        :title="t('common.dashboard')"
+        @click="openDashboard('')"
+      >
+        <v-remixicon name="riHome5Line" />
+      </ui-button>
+    </div>
+    <div class="flex">
+      <ui-input
+        v-model="state.query"
+        :placeholder="`${t('common.search')}...`"
+        autocomplete="off"
+        prepend-icon="riSearch2Line"
+        class="w-full search-input"
+      />
+    </div>
+    <ui-tabs
+      v-if="showTab"
+      v-model="state.activeTab"
+      fill
+      class="mt-1"
+      @change="onTabChange"
+    >
+      <ui-tab value="local">
+        {{ t(`home.workflow.type.local`) }}
+      </ui-tab>
+      <ui-tab v-if="hostedWorkflowStore.toArray.length > 0" value="host">
+        {{ t(`home.workflow.type.host`) }}
+      </ui-tab>
+      <ui-tab v-if="userStore.user?.teams" value="team"> Teams </ui-tab>
+    </ui-tabs>
+  </div>
+  <home-team-workflows
+    v-if="state.retrieved"
+    v-show="state.activeTab === 'team'"
+    :search="state.query"
+  />
+  <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">
+      <img src="@/assets/svg/alien.svg" />
+      <p class="font-semibold">{{ t('message.empty') }}</p>
+      <ui-button
+        variant="accent"
+        class="mt-6"
+        @click="openDashboard('/workflows')"
+      >
+        {{ t('home.workflow.new') }}
+      </ui-button>
+    </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"
+        class="mb-2"
+        @details="openWorkflowPage"
+        @update="updateWorkflow(workflow.id, $event)"
+        @execute="executeWorkflow"
+        @rename="renameWorkflow"
+        @delete="deleteWorkflow"
+        @toggle-pin="togglePinWorkflow(workflow)"
+      />
+    </div>
+    <home-workflow-card
+      v-for="workflow in workflows"
+      :key="workflow.id"
+      :workflow="workflow"
+      :tab="state.activeTab"
+      :pinned="state.pinnedWorkflows.includes(workflow.id)"
+      @details="openWorkflowPage"
+      @update="updateWorkflow(workflow.id, $event)"
+      @execute="executeWorkflow"
+      @rename="renameWorkflow"
+      @delete="deleteWorkflow"
+      @toggle-pin="togglePinWorkflow(workflow)"
+    />
+  </div>
+</template>
+<script setup>
+import { computed, onMounted, shallowReactive } from 'vue';
+import { useI18n } from 'vue-i18n';
+import browser from 'webextension-polyfill';
+import { useUserStore } from '@/stores/user';
+import { useDialog } from '@/composable/dialog';
+import { sendMessage } from '@/utils/message';
+import { useWorkflowStore } from '@/stores/workflow';
+import { useGroupTooltip } from '@/composable/groupTooltip';
+import { useTeamWorkflowStore } from '@/stores/teamWorkflow';
+import { useHostedWorkflowStore } from '@/stores/hostedWorkflow';
+import { initElementSelector as initElementSelectorFunc } from '@/newtab/utils/elementSelector';
+import automa from '@business';
+import HomeWorkflowCard from '@/components/popup/home/HomeWorkflowCard.vue';
+import HomeTeamWorkflows from '@/components/popup/home/HomeTeamWorkflows.vue';
+
+const { t } = useI18n();
+const dialog = useDialog();
+const userStore = useUserStore();
+const workflowStore = useWorkflowStore();
+const teamWorkflowStore = useTeamWorkflowStore();
+const hostedWorkflowStore = useHostedWorkflowStore();
+
+useGroupTooltip();
+
+const state = shallowReactive({
+  query: '',
+  teams: [],
+  cardHeight: 255,
+  retrieved: false,
+  haveAccess: true,
+  activeTab: 'local',
+  pinnedWorkflows: [],
+});
+
+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(() => {
+  if (state.activeTab !== 'host') return [];
+
+  return hostedWorkflowStore.toArray.filter((workflow) =>
+    workflow.name.toLocaleLowerCase().includes(state.query.toLocaleLowerCase())
+  );
+});
+const localWorkflows = computed(() => {
+  if (state.activeTab !== 'local') return [];
+
+  return workflowStore.getWorkflows
+    .filter(({ name }) =>
+      name.toLocaleLowerCase().includes(state.query.toLocaleLowerCase())
+    )
+    .sort((a, b) => (a.createdAt > b.createdAt ? -1 : 1));
+});
+const workflows = computed(() =>
+  state.activeTab === 'local' ? localWorkflows.value : hostedWorkflows.value
+);
+const showTab = computed(
+  () => 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) {
+  sendMessage('workflow:execute', workflow, 'background');
+}
+function updateWorkflow(id, data) {
+  return workflowStore.update({
+    id,
+    data,
+  });
+}
+function renameWorkflow({ id, name }) {
+  dialog.prompt({
+    title: t('home.workflow.rename'),
+    placeholder: t('common.name'),
+    okText: t('common.rename'),
+    inputValue: name,
+    onConfirm: (newName) => {
+      updateWorkflow(id, { name: newName });
+    },
+  });
+}
+function deleteWorkflow({ id, name }) {
+  dialog.confirm({
+    title: t('home.workflow.delete'),
+    okVariant: 'danger',
+    body: t('message.delete', { name }),
+    onConfirm: () => {
+      if (state.activeTab === 'local') {
+        workflowStore.delete(id);
+      } else {
+        hostedWorkflowStore.delete(id);
+      }
+    },
+  });
+}
+function openDashboard(url) {
+  sendMessage('open:dashboard', url, 'background');
+}
+function initElementSelector() {
+  initElementSelectorFunc().then(() => {
+    window.close();
+  });
+}
+function openWorkflowPage({ id, hostId }) {
+  let url = `/workflows/${id}`;
+
+  if (state.activeTab === 'host') {
+    url = `/workflows/${hostId}/host`;
+  }
+
+  openDashboard(url);
+}
+function onTabChange(value) {
+  localStorage.setItem('popup-tab', value);
+}
+
+onMounted(async () => {
+  const [tab] = await browser.tabs.query({ active: true, currentWindow: true });
+  state.haveAccess = /^(https?)/.test(tab.url);
+
+  const storage = await browser.storage.local.get('pinnedWorkflows');
+  state.pinnedWorkflows = storage.pinnedWorkflows || [];
+
+  await userStore.loadUser({ storage: localStorage, ttl: 1000 * 60 * 5 });
+  await teamWorkflowStore.loadData();
+
+  let activeTab = localStorage.getItem('popup-tab') || 'local';
+
+  await automa('app');
+
+  if (activeTab === 'team' && !userStore.user?.teams) activeTab = 'local';
+  else if (activeTab === 'host' && hostedWorkflowStore.toArray.length < 0)
+    activeTab = 'local';
+
+  state.retrieved = true;
+  state.activeTab = activeTab;
+});
+</script>
+<style>
+.recording-card {
+  transition: height 300ms cubic-bezier(0.4, 0, 0.2, 1) !important;
+}
+</style>

+ 15 - 0
src/popup/router.js

@@ -0,0 +1,15 @@
+import { createRouter, createWebHashHistory } from 'vue-router';
+import Home from './pages/Home.vue';
+
+const routes = [
+  {
+    path: '/',
+    name: 'home',
+    component: Home,
+  },
+];
+
+export default createRouter({
+  routes,
+  history: createWebHashHistory(),
+});

+ 8 - 1
webpack.config.js

@@ -14,7 +14,7 @@ const ASSET_PATH = process.env.ASSET_PATH || '/';
 const alias = {
   '@': path.resolve(__dirname, 'src/'),
   secrets: path.join(__dirname, 'secrets.blank.js'),
-  '@business': path.resolve(__dirname, 'business/prod'),
+  '@business': path.resolve(__dirname, 'business/dev'),
 };
 
 // load the secrets
@@ -42,6 +42,7 @@ const options = {
   entry: {
     sandbox: path.join(__dirname, 'src', 'sandbox', 'index.js'),
     newtab: path.join(__dirname, 'src', 'newtab', 'index.js'),
+    popup: path.join(__dirname, 'src', 'popup', 'index.js'),
     params: path.join(__dirname, 'src', 'params', 'index.js'),
     background: path.join(__dirname, 'src', 'background', 'index.js'),
     contentScript: path.join(__dirname, 'src', 'content', 'index.js'),
@@ -199,6 +200,12 @@ const options = {
       chunks: ['sandbox'],
       cache: false,
     }),
+    new HtmlWebpackPlugin({
+      template: path.join(__dirname, 'src', 'popup', 'index.html'),
+      filename: 'popup.html',
+      chunks: ['popup'],
+      cache: false,
+    }),
     new HtmlWebpackPlugin({
       template: path.join(__dirname, 'src', 'params', 'index.html'),
       filename: 'params.html',