Browse Source

feat(newtab): add rename and delete functionality to workflow

Ahmad Kholid 3 years ago
parent
commit
640401f075

+ 12 - 0
src/assets/css/tailwind.css

@@ -29,6 +29,18 @@ select:focus,
   overflow: hidden;
   text-overflow: ellipsis;
 }
+.scroll::-webkit-scrollbar {
+  width: 7px;
+  height: 9px;
+}
+.scroll::-webkit-scrollbar-thumb {
+  @apply bg-gray-300 dark:bg-gray-700;
+  border-radius: 8px;
+}
+.scroll::-webkit-scrollbar-track {
+  background: transparent;
+}
+
 
 @layer utilities {
   .hoverable {

File diff suppressed because it is too large
+ 0 - 0
src/assets/svg/alien.svg


+ 8 - 2
src/background/index.js

@@ -1,2 +1,8 @@
-console.log('This is the background page.');
-console.log('Put the background scripts here.');
+chrome.runtime.onInstalled.addListener((details) => {
+  if (details.reason === chrome.runtime.OnInstalledReason.INSTALL) {
+    chrome.storage.set({
+      workflows: [],
+      tasks: [],
+    });
+  }
+});

+ 41 - 7
src/components/newtab/workflow/WorkflowCard.vue

@@ -8,24 +8,58 @@
       <button>
         <v-remixicon name="riPlayLine" />
       </button>
-      <button v-if="showDetails" class="ml-3">
-        <v-remixicon name="riMoreLine" />
-      </button>
+      <ui-popover v-if="showDetails" class="ml-3 h-6">
+        <template #trigger>
+          <button>
+            <v-remixicon name="riMoreLine" />
+          </button>
+        </template>
+        <ui-list class="w-44 space-y-1">
+          <ui-list-item
+            v-close-popover
+            class="cursor-pointer"
+            @click="$emit('rename', workflow)"
+          >
+            <v-remixicon name="riPencilLine" class="mr-3 -ml-1" />
+            <span>Rename</span>
+          </ui-list-item>
+          <ui-list-item
+            v-close-popover
+            class="text-red-500 cursor-pointer"
+            @click="$emit('delete', workflow)"
+          >
+            <v-remixicon name="riDeleteBin7Line" class="mr-3 -ml-1" />
+            <span>Delete</span>
+          </ui-list-item>
+        </ui-list>
+      </ui-popover>
     </div>
     <router-link to="/workflows/anId">
-      <p class="line-clamp leading-tight font-semibold">Workflow name</p>
+      <p class="line-clamp leading-tight font-semibold" :title="workflow.name">
+        {{ workflow.name }}
+      </p>
       <p class="text-gray-600 dark:text-gray-200 leading-tight text-overflow">
-        3 Days ago
+        {{ formatDate() }}
       </p>
     </router-link>
   </ui-card>
 </template>
 <script setup>
-/* eslint-disable-next-line */
-defineProps({
+/* eslint-disable no-undef */
+
+import dayjs from '@/lib/dayjs';
+
+const props = defineProps({
+  workflow: {
+    type: Object,
+    default: () => ({}),
+  },
   showDetails: {
     type: Boolean,
     default: true,
   },
 });
+defineEmits(['delete']);
+
+const formatDate = () => dayjs(props.workflow.createdAt).fromNow();
 </script>

+ 17 - 7
src/components/newtab/workflow/WorkflowDetailsCard.vue

@@ -19,16 +19,16 @@
         <v-remixicon name="riDeleteBin7Line" />
       </ui-button>
     </div>
-    <ui-tabs v-model="state.activeTab" fill class="mx-4 mt-5 mb-4">
+    <ui-tabs v-model="state.activeTab" fill class="m-4">
       <ui-tab value="tasks">Tasks</ui-tab>
       <ui-tab value="data-schema">Data Schema</ui-tab>
     </ui-tabs>
-    <ui-tab-panels v-model="state.activeTab">
-      <ui-tab-panel
-        value="tasks"
-        class="px-4 grid grid-cols-2 gap-2 pb-4 overflow-auto pt-1"
-        style="max-height: calc(100vh - 200px)"
-      >
+    <ui-tab-panels
+      v-model="state.activeTab"
+      class="scroll bg-scroll overflow-auto pb-4 px-4"
+      style="max-height: calc(100vh - 240px); overflow: overlay"
+    >
+      <ui-tab-panel value="tasks" class="grid grid-cols-2 gap-2">
         <div
           v-for="task in taskList"
           :key="task.id"
@@ -64,6 +64,16 @@ const taskList = [
     name: 'Click element',
     icon: 'riCursorLine',
   },
+  {
+    id: 'get-text',
+    name: 'Get text',
+    icon: 'riParagraph',
+  },
+  {
+    id: 'save-assets',
+    name: 'Save assets',
+    icon: 'riImageLine',
+  },
   {
     id: 'export-data',
     name: 'Export data',

+ 4 - 3
src/components/ui/UiDialog.vue

@@ -1,5 +1,5 @@
 <template>
-  <ui-modal :model-value="state.show" content-class="max-w-sm" persist>
+  <ui-modal :model-value="state.show" content-class="max-w-sm">
     <template #header>
       <h3 class="font-semibold text-lg">{{ state.options.title }}</h3>
     </template>
@@ -12,7 +12,7 @@
       autofocus
       :placeholder="state.options.placeholder"
       :label="state.options.label"
-      class="w-full mt-4"
+      class="w-full"
     ></ui-input>
     <div class="mt-8 flex space-x-2">
       <ui-button class="w-6/12" @click="fireCallback('onCancel')">
@@ -39,7 +39,7 @@ const defaultOptions = {
   placeholder: '',
   label: '',
   okText: 'Confirm',
-  okVariant: 'primary',
+  okVariant: 'accent',
   cancelText: 'Cancel',
   onConfirm: null,
   onCancel: null,
@@ -56,6 +56,7 @@ export default {
 
     emitter.on('show-dialog', (type, options) => {
       state.type = type;
+      state.input = options?.inputValue ?? '';
       state.options = {
         ...defaultOptions,
         ...options,

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

@@ -3,7 +3,7 @@
     :aria-selected="uiTabs.modelValue.value === value"
     :class="[
       uiTabs.modelValue.value === value
-        ? 'border-accent'
+        ? 'border-accent text-gray-800 dark:text-white'
         : 'border-transparent',
       { 'flex-1': uiTabs.fill.value },
     ]"

+ 3 - 1
src/components/ui/UiTabPanels.vue

@@ -1,5 +1,7 @@
 <template>
-  <slot></slot>
+  <div class="ui-tab-panels">
+    <slot></slot>
+  </div>
 </template>
 <script setup>
 import { toRefs, provide } from 'vue';

+ 9 - 1
src/components/ui/UiTabs.vue

@@ -1,7 +1,15 @@
 <template>
   <div
     aria-role="tablist"
-    class="ui-tabs border-b flex items-center relative"
+    class="
+      ui-tabs
+      text-gray-600
+      dark:text-gray-200
+      border-b
+      flex
+      items-center
+      relative
+    "
     @mouseleave="showHoverIndicator = false"
   >
     <div

+ 16 - 0
src/composable/dialog.js

@@ -0,0 +1,16 @@
+import emitter from 'tiny-emitter/instance';
+
+export function useDialog() {
+  function confirm(options) {
+    emitter.emit('show-dialog', 'confirm', options);
+  }
+
+  function prompt(options) {
+    emitter.emit('show-dialog', 'prompt', options);
+  }
+
+  return {
+    prompt,
+    confirm,
+  };
+}

+ 10 - 0
src/directives/VClosePopover.js

@@ -0,0 +1,10 @@
+import { hideAll } from 'tippy.js';
+
+export default {
+  mounted(el) {
+    el.addEventListener('click', hideAll);
+  },
+  beforeUnmount(el) {
+    el.removeEventListener('click', hideAll);
+  },
+};

+ 2 - 1
src/lib/comps-ui.js

@@ -1,4 +1,5 @@
 import VAutofocus from '../directives/VAutofocus';
+import VClosePopover from '../directives/VClosePopover';
 
 const uiComponents = require.context('../components/ui', false, /\.vue$/);
 
@@ -12,8 +13,8 @@ function componentsExtractor(app, components) {
 }
 
 export default function (app) {
-  console.log(app, 'anana');
   app.directive('autofocus', VAutofocus);
+  app.directive('close-popover', VClosePopover);
 
   componentsExtractor(app, uiComponents);
 }

+ 6 - 0
src/lib/dayjs.js

@@ -0,0 +1,6 @@
+import dayjs from 'dayjs';
+import relativeTime from 'dayjs/plugin/relativeTime';
+
+dayjs.extend(relativeTime);
+
+export default dayjs;

+ 6 - 0
src/lib/v-remixicon.js

@@ -28,6 +28,9 @@ import {
   riEqualizerLine,
   riCursorLine,
   riDownloadLine,
+  riParagraph,
+  riImageLine,
+  riCloseLine,
 } from 'v-remixicon/icons';
 
 export const icons = {
@@ -59,6 +62,9 @@ export const icons = {
   riEqualizerLine,
   riCursorLine,
   riDownloadLine,
+  riParagraph,
+  riImageLine,
+  riCloseLine,
   mdiDrag:
     'M7,19V17H9V19H7M11,19V17H13V19H11M15,19V17H17V19H15M7,15V13H9V15H7M11,15V13H13V15H11M15,15V13H17V15H15M7,11V9H9V11H7M11,11V9H13V11H11M15,11V9H17V11H15M7,7V5H9V7H7M11,7V5H13V7H11M15,7V5H17V7H15Z',
 };

+ 1 - 2
src/lib/vuex-orm.js

@@ -1,8 +1,7 @@
 import VuexORM, { Query } from '@vuex-orm/core';
 
 function callback(model, param, entity) {
-  console.log('halo?', model, param, entity);
-  // this.store.dispatch('saveToStorage', entity);
+  this.store.dispatch('saveToStorage', entity);
 }
 
 Query.on('afterUpdate', callback);

+ 1 - 1
src/manifest.json

@@ -11,7 +11,7 @@
   "icons": {
     "128": "icon-128.png"
   },
-  "permissions": ["scripting", "storage", "tabs"],
+  "permissions": ["scripting", "storage", "unlimitedStorage", "tabs"],
   "web_accessible_resources": [
     {
       "resources": ["content.styles.css", "icon-128.png", "icon-34.png"],

+ 11 - 0
src/models/workflow.js

@@ -11,10 +11,21 @@ class Workflow extends Model {
     return {
       id: this.uid(() => nanoid()),
       name: this.string(''),
+      icon: this.string('riGlobalLine'),
       data: this.attr(null),
+      lastRunAt: this.number(),
+      createdAt: this.number(),
       tasks: this.hasMany(Task, 'workflowId'),
     };
   }
+
+  static async insert(payload) {
+    const res = await super.insert(payload);
+
+    await this.store().dispatch('saveToStorage', 'workflows');
+
+    return res;
+  }
 }
 
 export default Workflow;

+ 22 - 0
src/newtab/App.vue

@@ -3,7 +3,29 @@
   <main class="pl-16 container mx-auto pr-2 py-6">
     <router-view />
   </main>
+  <ui-dialog />
 </template>
 <script setup>
+import { onMounted } from 'vue';
+import { useStore } from 'vuex';
+import browser from 'webextension-polyfill';
 import AppSidebar from '@/components/newtab/app/AppSidebar.vue';
+
+const store = useStore();
+
+onMounted(async () => {
+  console.log(browser, 'browser');
+  try {
+    const data = await browser.storage.local.get(['workflows', 'tasks']);
+
+    Object.keys(data).forEach((entity) => {
+      store.dispatch('entities/create', {
+        entity,
+        data: data[entity],
+      });
+    });
+  } catch (error) {
+    console.error(error);
+  }
+});
 </script>

+ 14 - 3
src/newtab/pages/Home.vue

@@ -3,13 +3,18 @@
   <div class="flex items-start">
     <div class="w-7/12 mr-8">
       <div class="grid gap-3 mb-8 grid-cols-3">
-        <workflow-card v-for="i in 3" :key="i" :show-details="false" />
+        <workflow-card
+          v-for="workflow in workflows"
+          :key="workflow.id"
+          v-bind="{ workflow }"
+          :show-details="false"
+        />
       </div>
       <div>
-        <div class="mb-4 flex items-center justify-between">
+        <div class="mb-2 flex items-center justify-between">
           <p class="font-semibold inline-block text-lg">Logs</p>
           <router-link to="/logs" class="text-gray-600 dark:text-gray-200">
-            See all
+            View all
           </router-link>
         </div>
         <table class="w-full table-fixed">
@@ -62,6 +67,8 @@
   </div>
 </template>
 <script setup>
+import { computed } from 'vue';
+import Workflow from '@/models/workflow';
 import SharedTaskList from '@/components/shared/SharedTaskList.vue';
 import WorkflowCard from '@/components/newtab/workflow/WorkflowCard.vue';
 
@@ -70,4 +77,8 @@ const tasks = [
   { name: 'Get data', status: 'success' },
   { name: 'Close web', status: 'running' },
 ];
+
+const workflows = computed(() =>
+  Workflow.query().orderBy('createdAt', 'desc').limit(3).get()
+);
 </script>

+ 89 - 6
src/newtab/pages/Workflows.vue

@@ -2,34 +2,117 @@
   <h1 class="text-2xl font-semibold mb-8">Workflows</h1>
   <div class="flex items-center mb-6 space-x-4">
     <ui-input
+      v-model="state.query"
       prepend-icon="riSearch2Line"
       placeholder="Search..."
       class="flex-1"
     />
     <div class="flex items-center workflow-sort">
-      <ui-button icon class="rounded-r-none border-gray-300 border-r">
-        <v-remixicon name="riSortAsc" />
+      <ui-button
+        icon
+        class="rounded-r-none border-gray-300 border-r"
+        @click="state.sortOrder = state.sortOrder === 'asc' ? 'desc' : 'asc'"
+      >
+        <v-remixicon
+          :name="state.sortOrder === 'asc' ? 'riSortAsc' : 'riSortDesc'"
+        />
       </ui-button>
-      <ui-select placeholder="Sort by">
+      <ui-select v-model="state.sortBy" placeholder="Sort by">
         <option v-for="sort in sorts" :key="sort.id" :value="sort.id">
           {{ sort.name }}
         </option>
       </ui-select>
     </div>
-    <ui-button variant="accent"> New Workflow </ui-button>
+    <ui-button variant="accent" @click="newWorkflow"> New workflow </ui-button>
   </div>
-  <div class="grid gap-4 grid-cols-4">
-    <workflow-card v-for="i in 13" :key="i" />
+  <div v-if="Workflow.all().length === 0" class="py-12 flex items-center">
+    <img src="@/assets/svg/alien.svg" class="w-96" />
+    <div class="ml-4">
+      <h1 class="text-2xl font-semibold max-w-md mb-6">
+        Oppss... It's looks like you don't have any workflows.
+      </h1>
+      <ui-button variant="accent" @click="newWorkflow">New workflow</ui-button>
+    </div>
+  </div>
+  <div v-else class="grid gap-4 grid-cols-4">
+    <workflow-card
+      v-for="workflow in workflows"
+      :key="workflow.id"
+      v-bind="{ workflow }"
+      @delete="deleteWorkflow"
+      @rename="renameWorkflow"
+    />
   </div>
 </template>
 <script setup>
+import { computed, shallowReactive } from 'vue';
+import { useDialog } from '@/composable/dialog';
 import WorkflowCard from '@/components/newtab/workflow/WorkflowCard.vue';
+import Workflow from '@/models/workflow';
+
+const dialog = useDialog();
 
 const sorts = [
   { name: 'Name', id: 'name' },
   { name: 'Created date', id: 'createdAt' },
   { name: 'Running date', id: 'lastRunAt' },
 ];
+const state = shallowReactive({
+  query: '',
+  sortBy: 'createdAt',
+  sortOrder: 'desc',
+});
+
+const workflows = computed(() =>
+  Workflow.query()
+    .where(({ name }) =>
+      name.toLocaleLowerCase().includes(state.query.toLocaleLowerCase())
+    )
+    .orderBy(state.sortBy, state.sortOrder)
+    .get()
+);
+
+function newWorkflow() {
+  dialog.prompt({
+    title: 'New workflow',
+    placeholder: 'Workflow name',
+    okText: 'Add workflow',
+    onConfirm: (name) => {
+      Workflow.insert({
+        data: {
+          name,
+          createdAt: Date.now(),
+        },
+      });
+    },
+  });
+}
+function deleteWorkflow({ name, id }) {
+  dialog.confirm({
+    title: 'Delete workflow',
+    okVariant: 'danger',
+    body: `Are you sure you want to delete "${name}" workflow?`,
+    onConfirm: () => {
+      Workflow.delete(id);
+    },
+  });
+}
+function renameWorkflow({ id, name }) {
+  dialog.prompt({
+    title: 'Rename workflow',
+    placeholder: 'Workflow name',
+    okText: 'Rename',
+    inputValue: name,
+    onConfirm: (newName) => {
+      Workflow.update({
+        where: id,
+        data: {
+          name: newName,
+        },
+      });
+    },
+  });
+}
 </script>
 <style>
 .workflow-sort select {

+ 5 - 2
src/newtab/pages/workflows/[id].vue

@@ -1,6 +1,9 @@
 <template>
-  <div class="flex items-center">
-    <workflow-details-card />
+  <div class="flex items-start">
+    <workflow-details-card class="mr-6" />
+    <div class="flex-1">
+      <h1 class="text-xl font-semibold">Tasks</h1>
+    </div>
   </div>
 </template>
 <script setup>

+ 16 - 0
src/store/index.js

@@ -1,9 +1,25 @@
 import { createStore } from 'vuex';
+import browser from 'webextension-polyfill';
 import vuexORM from '@/lib/vuex-orm';
 import * as models from '@/models';
 
 const store = createStore({
   plugins: [vuexORM(models)],
+  actions: {
+    saveToStorage({ getters }, key) {
+      return new Promise((resolve, reject) => {
+        if (!key) {
+          reject(new Error('You need to pass the entity name'));
+          return;
+        }
+        const data = getters[`entities/${key}/all`]();
+
+        browser.storage.local.set({ [key]: data }).then(() => {
+          resolve();
+        });
+      });
+    },
+  },
 });
 
 export default store;

Some files were not shown because too many files changed in this diff