Browse Source

feat: add google sheets block

Ahmad Kholid 3 years ago
parent
commit
11b262fe6f
34 changed files with 314 additions and 349 deletions
  1. 50 0
      src/background/workflow-engine/blocks-handler/handler-google-sheets.js
  2. 8 1
      src/background/workflow-engine/engine.js
  3. 1 10
      src/components/block/BlockConditions.vue
  4. 1 10
      src/components/block/BlockElementExists.vue
  5. 1 9
      src/components/block/BlockGroup.vue
  6. 1 9
      src/components/block/BlockRepeatTask.vue
  7. 3 32
      src/components/newtab/app/AppSidebar.vue
  8. 1 1
      src/components/newtab/shared/SharedCodemirror.vue
  9. 1 10
      src/components/newtab/workflow/WorkflowActions.vue
  10. 2 18
      src/components/newtab/workflow/WorkflowDetailsCard.vue
  11. 2 25
      src/components/newtab/workflow/edit/EditConditions.vue
  12. 144 0
      src/components/newtab/workflow/edit/EditGoogleSheets.vue
  13. 1 8
      src/components/newtab/workflow/edit/EditWebhook.vue
  14. 1 8
      src/components/ui/UiCard.vue
  15. 2 18
      src/components/ui/UiCheckbox.vue
  16. 1 9
      src/components/ui/UiListItem.vue
  17. 1 12
      src/components/ui/UiModal.vue
  18. 1 9
      src/components/ui/UiPagination.vue
  19. 1 8
      src/components/ui/UiPopover.vue
  20. 2 18
      src/components/ui/UiRadio.vue
  21. 1 13
      src/components/ui/UiSelect.vue
  22. 2 19
      src/components/ui/UiTabs.vue
  23. 1 10
      src/components/ui/UiTextarea.vue
  24. 2 2
      src/content/blocks-handler/handler-link.js
  25. 3 30
      src/content/element-selector/App.vue
  26. 2 1
      src/content/index.js
  27. 19 1
      src/locales/en/blocks.json
  28. 2 0
      src/locales/en/newtab.json
  29. 1 10
      src/newtab/pages/Workflows.vue
  30. 4 38
      src/newtab/pages/collections/[id].vue
  31. 2 7
      src/newtab/pages/logs/[id].vue
  32. 13 0
      src/utils/api.js
  33. 30 0
      src/utils/helper.js
  34. 7 3
      src/utils/shared.js

+ 50 - 0
src/background/workflow-engine/blocks-handler/handler-google-sheets.js

@@ -0,0 +1,50 @@
+import { getBlockConnection } from '../helper';
+import { getGoogleSheetsValue } from '@/utils/api';
+import { convert2DArrayToArrayObj, isWhitespace } from '@/utils/helper';
+
+async function getSpreadsheetValues(data) {
+  const response = await getGoogleSheetsValue(data.spreadsheetId, data.range);
+
+  if (response.status !== 200) {
+    throw new Error(response.statusText);
+  }
+
+  const { values } = await response.json();
+  const sheetsData = data.firstRowAsKey
+    ? convert2DArrayToArrayObj(values)
+    : values;
+
+  return sheetsData;
+}
+
+export default async function ({ data, outputs }) {
+  const nextBlockId = getBlockConnection({ outputs });
+
+  try {
+    if (isWhitespace(data.spreadsheetId))
+      throw new Error('empty-spreadsheet-id');
+    if (isWhitespace(data.range)) throw new Error('empty-spreadsheet-range');
+
+    let result = '';
+
+    if (data.type === 'get') {
+      const spreadsheetValues = await getSpreadsheetValues(data);
+
+      result = spreadsheetValues;
+
+      if (data.refKey && !isWhitespace(data.refKey)) {
+        this.googleSheets[data.refKey] = spreadsheetValues;
+      }
+    }
+
+    return {
+      nextBlockId,
+      data: result,
+    };
+  } catch (error) {
+    error.nextBlockId = nextBlockId;
+    console.log(error.message, 'halo');
+
+    throw error;
+  }
+}

+ 8 - 1
src/background/workflow-engine/engine.js

@@ -103,6 +103,8 @@ class WorkflowEngine {
     this.childWorkflow = null;
     this.workflowTimeout = null;
 
+    this.googleSheets = {};
+
     this.tabUpdatedListeners = {};
     this.tabUpdatedHandler = tabUpdatedHandler.bind(this);
     this.tabRemovedHandler = tabRemovedHandler.bind(this);
@@ -183,7 +185,11 @@ class WorkflowEngine {
   }
 
   addLog(detail) {
-    if (this.logs.length >= 1001 || detail.name === 'blocks-group') return;
+    if (
+      (this.logs.length >= 1001 || detail.name === 'blocks-group') &&
+      detail.type !== 'error'
+    )
+      return;
 
     this.logs.push(detail);
   }
@@ -338,6 +344,7 @@ class WorkflowEngine {
         dataColumns: this.data,
         loopData: this.loopData,
         globalData: this.globalData,
+        googleSheets: this.googleSheets,
         activeTabUrl: this.activeTabUrl,
       };
       const replacedBlock = referenceData(block, refData);

+ 1 - 10
src/components/block/BlockConditions.vue

@@ -35,16 +35,7 @@
           @click="deleteCondition(index)"
         />
         <div
-          class="
-            flex
-            items-center
-            flex-1
-            p-2
-            bg-box-transparent
-            rounded-lg
-            overflow-hidden
-            w-44
-          "
+          class="flex items-center flex-1 p-2 bg-box-transparent rounded-lg overflow-hidden w-44"
         >
           <p class="w-5/12 text-overflow text-right">
             {{ item.compareValue || '_____' }}

+ 1 - 10
src/components/block/BlockElementExists.vue

@@ -14,16 +14,7 @@
     </div>
     <p
       :title="t('workflow.blocks.element-exists.selector')"
-      class="
-        text-overflow
-        p-2
-        rounded-lg
-        bg-box-transparent
-        text-sm
-        font-mono
-        text-right
-        mb-2
-      "
+      class="text-overflow p-2 rounded-lg bg-box-transparent text-sm font-mono text-right mb-2"
       style="max-width: 200px"
     >
       {{ block.data.selector || t('workflow.blocks.element-exists.selector') }}

+ 1 - 9
src/components/block/BlockGroup.vue

@@ -74,15 +74,7 @@
       </template>
       <template #footer>
         <div
-          class="
-            p-2
-            rounded-lg
-            text-gray-600
-            dark:text-gray-200
-            border
-            text-center
-            border-dashed
-          "
+          class="p-2 rounded-lg text-gray-600 dark:text-gray-200 border text-center border-dashed"
         >
           {{ t('workflow.blocks.blocks-group.dropText') }}
         </div>

+ 1 - 9
src/components/block/BlockRepeatTask.vue

@@ -16,15 +16,7 @@
       />
     </div>
     <label
-      class="
-        mb-2
-        block
-        bg-input
-        focus-within:bg-input
-        pr-4
-        transition
-        rounded-lg
-      "
+      class="mb-2 block bg-input focus-within:bg-input pr-4 transition rounded-lg"
     >
       <input
         :value="block.data.repeatFor || 0"

+ 3 - 32
src/components/newtab/app/AppSidebar.vue

@@ -1,17 +1,6 @@
 <template>
   <aside
-    class="
-      fixed
-      flex flex-col
-      items-center
-      h-screen
-      left-0
-      top-0
-      w-16
-      py-6
-      bg-white
-      z-50
-    "
+    class="fixed flex flex-col items-center h-screen left-0 top-0 w-16 py-6 bg-white z-50"
   >
     <img src="@/assets/svg/logo.svg" class="w-10 mb-4 mx-auto" />
     <div
@@ -21,16 +10,7 @@
       <div
         v-show="showHoverIndicator"
         ref="hoverIndicator"
-        class="
-          rounded-lg
-          h-10
-          w-10
-          absolute
-          left-1/2
-          bg-box-transparent
-          transition-transform
-          duration-200
-        "
+        class="rounded-lg h-10 w-10 absolute left-1/2 bg-box-transparent transition-transform duration-200"
         style="transform: translate(-50%, 0)"
       ></div>
       <router-link
@@ -44,16 +24,7 @@
           v-tooltip:right.group="t(`common.${tab.id}`, 2)"
           :class="{ 'is-active': isActive }"
           :href="href"
-          class="
-            z-10
-            relative
-            w-full
-            flex
-            items-center
-            justify-center
-            tab
-            relative
-          "
+          class="z-10 relative w-full flex items-center justify-center tab relative"
           @click="navigate"
           @mouseenter="hoverHandler"
         >

+ 1 - 1
src/components/newtab/shared/SharedCodemirror.vue

@@ -2,7 +2,7 @@
   <div
     ref="containerEl"
     :class="{ 'hide-gutters': !lineNumbers }"
-    class="codemirror relative"
+    class="codemirror relative overflow-auto"
   ></div>
 </template>
 <script setup>

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

@@ -62,16 +62,7 @@
         class="flex h-3 w-3 absolute top-0 left-0 -ml-1 -mt-1"
       >
         <span
-          class="
-            animate-ping
-            absolute
-            inline-flex
-            h-full
-            w-full
-            rounded-full
-            bg-primary
-            opacity-75
-          "
+          class="animate-ping absolute inline-flex h-full w-full rounded-full bg-primary opacity-75"
         ></span>
         <span
           class="relative inline-flex rounded-full h-3 w-3 bg-blue-600"

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

@@ -20,14 +20,7 @@
           <span
             v-for="icon in icons"
             :key="icon"
-            class="
-              cursor-pointer
-              rounded-lg
-              inline-block
-              text-center
-              p-2
-              hoverable
-            "
+            class="cursor-pointer rounded-lg inline-block text-center p-2 hoverable"
             @click="$emit('update', { icon })"
           >
             <v-remixicon :name="icon" />
@@ -78,16 +71,7 @@
             )
           "
           draggable="true"
-          class="
-            transform
-            select-none
-            cursor-move
-            relative
-            p-4
-            rounded-lg
-            bg-input
-            transition
-          "
+          class="transform select-none cursor-move relative p-4 rounded-lg bg-input transition"
           @dragstart="
             $event.dataTransfer.setData('block', JSON.stringify(block))
           "

+ 2 - 25
src/components/newtab/workflow/edit/EditConditions.vue

@@ -21,16 +21,7 @@
           class="py-2 px-4 w-full transition rounded-lg bg-transparent"
         />
         <button
-          class="
-            bg-white
-            absolute
-            top-1/2
-            right-4
-            p-2
-            rounded-lg
-            -translate-y-1/2
-            group-hover:right-14
-          "
+          class="bg-white absolute top-1/2 right-4 p-2 rounded-lg -translate-y-1/2 group-hover:right-14"
           @click="deleteCondition(index)"
         >
           <v-remixicon size="20" name="riDeleteBin7Line" />
@@ -38,21 +29,7 @@
         <select
           v-model="condition.type"
           :title="getTitle(index)"
-          class="
-            bg-white
-            absolute
-            right-4
-            font-mono
-            z-10
-            p-2
-            top-1/2
-            leading-tight
-            -translate-y-1/2
-            text-center
-            transition
-            rounded-lg
-            appearance-none
-          "
+          class="bg-white absolute right-4 font-mono z-10 p-2 top-1/2 leading-tight -translate-y-1/2 text-center transition rounded-lg appearance-none"
         >
           <option
             v-for="(name, type) in conditionTypes"

+ 144 - 0
src/components/newtab/workflow/edit/EditGoogleSheets.vue

@@ -0,0 +1,144 @@
+<template>
+  <div>
+    <ui-select
+      v-if="false"
+      :model-value="data.type"
+      class="w-full mb-2"
+      @change="updateData({ type: $event })"
+    >
+      <option value="get">
+        {{ t('workflow.blocks.google-sheets.select.get') }}
+      </option>
+      <option value="write">
+        {{ t('workflow.blocks.google-sheets.select.write') }}
+      </option>
+    </ui-select>
+    <ui-input
+      :model-value="data.spreadsheetId"
+      class="w-full"
+      placeholder="abcd123"
+      @change="updateData({ spreadsheetId: $event })"
+    >
+      <template #label>
+        {{ t('workflow.blocks.google-sheets.spreadsheetId.label') }}
+        <a
+          href="https://developers.google.com/sheets/api/guides/concepts#:~:text=the%20Sheets%20API%3A-,Spreadsheet,-The%20primary%20object"
+          target="_blank"
+          rel="noopener"
+          :title="t('workflow.blocks.google-sheets.spreadsheetId.link')"
+        >
+          <v-remixicon name="riInformationLine" size="18" class="inline" />
+        </a>
+      </template>
+    </ui-input>
+    <ui-input
+      :model-value="data.range"
+      class="w-full mt-1"
+      placeholder="Sheet1!A1:B2"
+      @change="updateData({ range: $event })"
+    >
+      <template #label>
+        {{ t('workflow.blocks.google-sheets.range.label') }}
+        <a
+          href="https://developers.google.com/sheets/api/guides/concepts#expandable-1"
+          target="_blank"
+          rel="noopener"
+          :title="t('workflow.blocks.google-sheets.range.link')"
+        >
+          <v-remixicon name="riInformationLine" size="18" class="inline" />
+        </a>
+      </template>
+    </ui-input>
+    <template v-if="data.type === 'get'">
+      <ui-input
+        :model-value="data.refKey"
+        :label="t('workflow.blocks.google-sheets.refKey.label')"
+        :placeholder="t('workflow.blocks.google-sheets.refKey.placeholder')"
+        class="mt-1 w-full"
+        @change="updateData({ refKey: $event })"
+      />
+      <ui-checkbox
+        :model-value="data.firstRowAsKey"
+        class="mt-3"
+        @change="updateData({ firstRowAsKey: $event })"
+      >
+        {{ t('workflow.blocks.google-sheets.firstRow') }}
+      </ui-checkbox>
+      <ui-button
+        :loading="previewDataState.status === 'loading'"
+        variant="accent"
+        class="mt-3"
+        @click="previewData"
+      >
+        {{ t('workflow.blocks.google-sheets.previewData') }}
+      </ui-button>
+      <p v-if="previewDataState.status === 'error'" class="text-red-500">
+        {{ previewDataState.errorMessage }}
+      </p>
+      <shared-codemirror
+        v-if="previewDataState.data"
+        :model-value="previewDataState.data"
+        readonly
+        class="mt-4 max-h-96"
+      />
+    </template>
+    <template v-else-if="data.type === 'write'">
+      <pre>
+        halo
+      </pre>
+    </template>
+  </div>
+</template>
+<script setup>
+import { shallowReactive } from 'vue';
+import { useI18n } from 'vue-i18n';
+import { getGoogleSheetsValue } from '@/utils/api';
+import { convert2DArrayToArrayObj } from '@/utils/helper';
+import SharedCodemirror from '@/components/newtab/shared/SharedCodemirror.vue';
+
+const props = defineProps({
+  data: {
+    type: Object,
+    default: () => ({}),
+  },
+});
+const emit = defineEmits(['update:data']);
+
+const { t } = useI18n();
+
+const previewDataState = shallowReactive({
+  status: 'idle',
+  errorMessage: '',
+  data: '',
+});
+
+function updateData(value) {
+  emit('update:data', { ...props.data, ...value });
+}
+async function previewData() {
+  try {
+    previewDataState.status = 'loading';
+    const response = await getGoogleSheetsValue(
+      props.data.spreadsheetId,
+      props.data.range
+    );
+
+    if (response.status !== 200) {
+      throw new Error(response.statusText);
+    }
+
+    const { values } = await response.json();
+    const sheetsData = props.data.firstRowAsKey
+      ? convert2DArrayToArrayObj(values)
+      : values;
+
+    previewDataState.data = JSON.stringify(sheetsData, null, 2);
+
+    previewDataState.status = 'idle';
+  } catch (error) {
+    console.error(error);
+    previewDataState.status = 'error';
+    previewDataState.errorMessage = error.message;
+  }
+}
+</script>

+ 1 - 8
src/components/newtab/workflow/edit/EditWebhook.vue

@@ -76,14 +76,7 @@
       <ui-tab-panel value="body">
         <pre
           v-if="!showContentModalRef"
-          class="
-            rounded-lg
-            text-gray-200
-            p-4
-            max-h-80
-            bg-gray-900
-            overflow-auto
-          "
+          class="rounded-lg text-gray-200 p-4 max-h-80 bg-gray-900 overflow-auto"
           @click="showContentModalRef = true"
           v-text="data.body"
         />

+ 1 - 8
src/components/ui/UiCard.vue

@@ -2,14 +2,7 @@
   <component
     :is="tag"
     v-bind="$attrs"
-    class="
-      bg-white
-      dark:bg-gray-800
-      transform
-      rounded-lg
-      transition-transform
-      ui-card
-    "
+    class="bg-white dark:bg-gray-800 transform rounded-lg transition-transform ui-card"
     :class="[padding, { 'hover:shadow-xl hover:-translate-y-1': hover }]"
   >
     <slot></slot>

+ 2 - 18
src/components/ui/UiCheckbox.vue

@@ -2,14 +2,7 @@
   <label class="checkbox-ui inline-flex items-center">
     <div
       :class="{ 'pointer-events-none opacity-75': disabled }"
-      class="
-        relative
-        h-5
-        w-5
-        inline-block
-        focus-within:ring-2 focus-within:ring-accent
-        rounded
-      "
+      class="relative h-5 w-5 inline-block focus-within:ring-2 focus-within:ring-accent rounded"
     >
       <input
         type="checkbox"
@@ -19,16 +12,7 @@
         @change="changeHandler"
       />
       <div
-        class="
-          border
-          rounded
-          absolute
-          top-0
-          left-0
-          bg-input
-          checkbox-ui__mark
-          cursor-pointer
-        "
+        class="border rounded absolute top-0 left-0 bg-input checkbox-ui__mark cursor-pointer"
       >
         <v-remixicon
           name="riCheckLine"

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

@@ -1,15 +1,7 @@
 <template>
   <component
     :is="tag"
-    class="
-      ui-list-item
-      rounded-lg
-      flex
-      items-center
-      transition
-      w-full
-      focus:outline-none
-    "
+    class="ui-list-item rounded-lg flex items-center transition w-full focus:outline-none"
     role="listitem"
     :class="[
       active ? color : 'hoverable',

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

@@ -7,18 +7,7 @@
       <transition name="modal" mode="out-in">
         <div
           v-if="show"
-          class="
-            bg-black
-            p-5
-            overflow-y-auto
-            bg-opacity-20
-            modal-ui__content-container
-            z-50
-            flex
-            justify-center
-            items-end
-            md:items-center
-          "
+          class="bg-black p-5 overflow-y-auto bg-opacity-20 modal-ui__content-container z-50 flex justify-center items-end md:items-center"
           :style="{ 'backdrop-filter': blur && 'blur(2px)' }"
           @click.self="closeModal"
         >

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

@@ -15,15 +15,7 @@
         :value="modelValue"
         :max="maxPage"
         min="0"
-        class="
-          p-2
-          text-center
-          transition
-          w-10
-          appearance-none
-          bg-input
-          rounded-lg
-        "
+        class="p-2 text-center transition w-10 appearance-none bg-input rounded-lg"
         type="number"
         @click="$event.target.select()"
         @input="updatePage(+$event.target.value, $event.target)"

+ 1 - 8
src/components/ui/UiPopover.vue

@@ -5,14 +5,7 @@
     </div>
     <div
       ref="content"
-      class="
-        ui-popover__content
-        bg-white
-        dark:bg-gray-800
-        rounded-lg
-        shadow-xl
-        border
-      "
+      class="ui-popover__content bg-white dark:bg-gray-800 rounded-lg shadow-xl border"
       :class="[padding]"
     >
       <slot v-bind="{ isShow }"></slot>

+ 2 - 18
src/components/ui/UiRadio.vue

@@ -1,14 +1,7 @@
 <template>
   <label class="radio-ui inline-flex items-center">
     <div
-      class="
-        relative
-        h-5
-        w-5
-        inline-block
-        focus-within:ring-2 focus-within:ring-accent
-        rounded-full
-      "
+      class="relative h-5 w-5 inline-block focus-within:ring-2 focus-within:ring-accent rounded-full"
     >
       <input
         type="radio"
@@ -18,16 +11,7 @@
         @change="changeHandler"
       />
       <div
-        class="
-          border
-          rounded-full
-          absolute
-          top-0
-          left-0
-          bg-input
-          radio-ui__mark
-          cursor-pointer
-        "
+        class="border rounded-full absolute top-0 left-0 bg-input radio-ui__mark cursor-pointer"
       ></div>
     </div>
     <span v-if="$slots.default" class="ml-2 inline-block">

+ 1 - 13
src/components/ui/UiSelect.vue

@@ -18,19 +18,7 @@
         :id="selectId"
         :class="{ 'pl-8': prependIcon }"
         :value="modelValue"
-        class="
-          px-4
-          pr-10
-          transition
-          rounded-lg
-          bg-input bg-transparent
-          py-2
-          z-10
-          appearance-none
-          w-full
-          h-full
-          appearance-none
-        "
+        class="px-4 pr-10 transition rounded-lg bg-input bg-transparent py-2 z-10 appearance-none w-full h-full appearance-none"
         @change="emitValue"
       >
         <option v-if="placeholder" value="" disabled selected>

+ 2 - 19
src/components/ui/UiTabs.vue

@@ -1,30 +1,13 @@
 <template>
   <div
     aria-role="tablist"
-    class="
-      ui-tabs
-      text-gray-600
-      dark:text-gray-200
-      border-b
-      flex
-      space-x-1
-      items-center
-      relative
-    "
+    class="ui-tabs text-gray-600 dark:text-gray-200 border-b flex space-x-1 items-center relative"
     @mouseleave="showHoverIndicator = false"
   >
     <div
       v-show="showHoverIndicator"
       ref="hoverIndicator"
-      class="
-        ui-tabs__indicator
-        z-0
-        top-[5px]
-        absolute
-        left-0
-        rounded-lg
-        bg-box-transparent
-      "
+      class="ui-tabs__indicator z-0 top-[5px] absolute left-0 rounded-lg bg-box-transparent"
     ></div>
     <slot></slot>
   </div>

+ 1 - 10
src/components/ui/UiTextarea.vue

@@ -6,16 +6,7 @@
     <textarea
       ref="textarea"
       v-bind="{ value: modelValue, placeholder, maxlength: max }"
-      class="
-        ui-textarea
-        w-full
-        ui-input
-        rounded-lg
-        px-4
-        py-2
-        transition
-        bg-input
-      "
+      class="ui-textarea w-full ui-input rounded-lg px-4 py-2 transition bg-input"
       :class="{ 'overflow-hidden resize-none': autoresize }"
       :style="{ height }"
       @input="emitValue"

+ 2 - 2
src/content/blocks-handler/handler-link.js

@@ -1,8 +1,8 @@
-import { markElement } from '../helper';
+import { handleElement, markElement } from '../helper';
 
 function link(block) {
   return new Promise((resolve, reject) => {
-    const element = document.querySelector(block.data.selector);
+    const element = handleElement(block, { returnElement: true });
 
     if (!element) {
       reject(new Error('element-not-found'));

+ 3 - 30
src/content/element-selector/App.vue

@@ -4,44 +4,17 @@
       'select-none': state.isDragging,
       'bg-black bg-opacity-30': !state.hide,
     }"
-    class="
-      root
-      fixed
-      h-full
-      w-full
-      pointer-events-none
-      top-0
-      text-gray-900
-      left-0
-    "
+    class="root fixed h-full w-full pointer-events-none top-0 text-gray-900 left-0"
     style="z-index: 9999999999; font-family: Inter, sans-serif; font-size: 16px"
   >
     <div
       ref="cardEl"
       :style="{ transform: `translate(${cardRect.x}px, ${cardRect.y}px)` }"
       style="width: 320px"
-      class="
-        absolute
-        root-card
-        bg-white
-        shadow-xl
-        z-50
-        p-4
-        pointer-events-auto
-        rounded-lg
-      "
+      class="absolute root-card bg-white shadow-xl z-50 p-4 pointer-events-auto rounded-lg"
     >
       <div
-        class="
-          absolute
-          p-2
-          drag-button
-          shadow-xl
-          bg-white
-          p-1
-          cursor-move
-          rounded-lg
-        "
+        class="absolute p-2 drag-button shadow-xl bg-white p-1 cursor-move rounded-lg"
         style="top: -15px; left: -15px"
       >
         <v-remixicon

+ 2 - 1
src/content/index.js

@@ -6,12 +6,13 @@ import blocksHandler from './blocks-handler';
 (() => {
   if (window.isAutomaInjected) return;
 
+  console.log(window.isAutomaInjected);
   window.isAutomaInjected = true;
 
   browser.runtime.onMessage.addListener((data) => {
     if (data.isBlock) {
       const handler = blocksHandler[toCamelCase(data.name)];
-
+      console.log(data);
       if (handler) {
         return handler(data);
       }

+ 19 - 1
src/locales/en/blocks.json

@@ -78,7 +78,25 @@
       },
       "google-sheets": {
         "name": "Google sheets",
-        "description": "Read or write google sheet data"
+        "description": "Read Google Sheets data",
+        "previewData": "Preview data",
+        "firstRow": "Use the first row as keys",
+        "refKey": {
+          "label": "Reference key",
+          "placeholder": "Key name"
+        },
+        "spreadsheetId": {
+          "label": "Spreadsheet Id",
+          "link": "See how to get spreadsheet Id"
+        },
+        "range": {
+          "label": "Range",
+          "link": "Click to see more example"
+        },
+        "select": {
+          "get": "Get spreadsheet data",
+          "write": "Write spreadsheet data"
+        }
       },
       "active-tab": {
         "name": "Active tab",

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

@@ -91,9 +91,11 @@
       "invalid-proxy-host": "Invalid proxy host",
       "workflow-disabled": "Workflow is disabled",
       "selector-empty": "Element selector is empty",
+      "empty-spreadsheet-id": "Spreadsheet Id is empty",
       "invalid-body": "Content body is not valid JSON",
       "invalid-active-tab": "\"{url}\" is invalid URL",
       "empty-workflow": "You must select a workflow first",
+      "empty-spreadsheet-range": "Spreadsheet range is empty",
       "active-tab-removed": "Workflow active tab is removed",
       "stop-timeout": "Workflow is stopped because of timeout",
       "no-workflow": "Can't find workflow with \"{workflowId}\" ID",

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

@@ -38,16 +38,7 @@
           class="flex h-3 w-3 absolute top-0 right-0 -mr-1 -mt-1"
         >
           <span
-            class="
-              animate-ping
-              absolute
-              inline-flex
-              h-full
-              w-full
-              rounded-full
-              bg-primary
-              opacity-75
-            "
+            class="animate-ping absolute inline-flex h-full w-full rounded-full bg-primary opacity-75"
           ></span>
           <span
             class="relative inline-flex rounded-full h-3 w-3 bg-blue-600"

+ 4 - 38
src/newtab/pages/collections/[id].vue

@@ -4,12 +4,7 @@
       <input
         :value="collection.name"
         placeholder="Collection name"
-        class="
-          text-2xl
-          hover:ring-2 hover:ring-accent
-          font-semibold
-          bg-transparent
-        "
+        class="text-2xl hover:ring-2 hover:ring-accent font-semibold bg-transparent"
         @blur="updateCollection({ name: $event.target.value || 'Unnamed' })"
       />
       <div class="flex-grow"></div>
@@ -67,16 +62,7 @@
                 {{ t('common.running') }}
                 <span
                   v-if="runningCollection.length > 0"
-                  class="
-                    ml-2
-                    p-1
-                    text-center
-                    inline-block
-                    text-xs
-                    rounded-full
-                    bg-black
-                    text-white
-                  "
+                  class="ml-2 p-1 text-center inline-block text-xs rounded-full bg-black text-white"
                   style="min-width: 25px"
                 >
                   {{ runningCollection.length }}
@@ -98,19 +84,7 @@
           <ui-tab-panel class="relative" value="flow">
             <div
               v-if="collection.flow.length === 0"
-              class="
-                border
-                text-gray-600
-                absolute
-                top-0
-                w-full
-                z-0
-                dark:text-gray-200
-                rounded-lg
-                border-dashed
-                text-center
-                p-4
-              "
+              class="border text-gray-600 absolute top-0 w-full z-0 dark:text-gray-200 rounded-lg border-dashed text-center p-4"
             >
               {{ t('collection.dragDropText') }}
             </div>
@@ -123,15 +97,7 @@
             >
               <template #item="{ element, index }">
                 <ui-card
-                  class="
-                    group
-                    flex
-                    cursor-move
-                    mb-2
-                    items-center
-                    relative
-                    overflow-hidden
-                  "
+                  class="group flex cursor-move mb-2 items-center relative overflow-hidden"
                 >
                   <span
                     :class="[

+ 2 - 7
src/newtab/pages/logs/[id].vue

@@ -46,12 +46,7 @@
               <p
                 v-if="item.message"
                 :title="item.message"
-                class="
-                  text-sm
-                  leading-tight
-                  text-overflow text-gray-600
-                  dark:text-gray-200
-                "
+                class="text-sm leading-tight text-overflow text-gray-600 dark:text-gray-200"
               >
                 {{ item.message }}
               </p>
@@ -166,7 +161,7 @@ function translateLog(log) {
 
   copyLog.message = getTranslatation(
     { path: `log.messages.${log.message}`, params: log },
-    ''
+    log.message
   );
 
   return copyLog;

+ 13 - 0
src/utils/api.js

@@ -0,0 +1,13 @@
+import secrets from 'secrets';
+
+export function fetchApi(path, options) {
+  const urlPath = path.startsWith('/') ? path : `/${path}`;
+
+  return fetch(`${secrets.baseApiUrl}${urlPath}`, options);
+}
+
+export function getGoogleSheetsValue(spreadsheetId, range) {
+  const url = `/services/google-sheets?spreadsheetId=${spreadsheetId}&range=${range}`;
+
+  return fetchApi(url);
+}

+ 30 - 0
src/utils/helper.js

@@ -1,3 +1,33 @@
+export function convert2DArrayToArrayObj(values) {
+  let keyIndex = 0;
+  const keys = values.shift();
+  const result = [];
+
+  for (let columnIndex = 0; columnIndex < values.length; columnIndex += 1) {
+    const currentColumn = {};
+
+    for (
+      let rowIndex = 0;
+      rowIndex < values[columnIndex].length;
+      rowIndex += 1
+    ) {
+      let key = keys[rowIndex];
+
+      if (!key) {
+        keyIndex += 1;
+        key = `_row${keyIndex}`;
+        keys.push(key);
+      }
+
+      currentColumn[key] = values[columnIndex][rowIndex];
+
+      result.push(currentColumn);
+    }
+  }
+
+  return result;
+}
+
 export function parseJSON(data, def) {
   try {
     const result = JSON.parse(data);

+ 7 - 3
src/utils/shared.js

@@ -415,17 +415,21 @@ export const tasks = {
   },
   'google-sheets': {
     name: 'Google sheets',
-    description: 'Read or write google sheet data',
+    description: 'Read Google Sheets data',
     icon: 'mdiGoogleSheet',
     component: 'BlockBasic',
+    editComponent: 'EditGoogleSheets',
     category: 'onlineServices',
     inputs: 1,
     outputs: 1,
     allowedInputs: true,
     maxConnection: 1,
     data: {
-      url: '',
+      spreadsheetId: '',
       type: 'get',
+      range: '',
+      firstRowAsKey: false,
+      refKey: '',
     },
   },
   conditions: {
@@ -569,7 +573,7 @@ export const categories = {
   },
   onlineServices: {
     name: 'Online services',
-    color: 'bg-purple-200',
+    color: 'bg-red-200',
   },
   conditions: {
     name: 'Conditions',