Bläddra i källkod

feat: add Google Sheets (GDrive) block

Ahmad Kholid 2 år sedan
förälder
incheckning
f0d674fe83

+ 2 - 2
src/components/block/BlockBase.vue

@@ -4,10 +4,10 @@
       class="block-menu-container absolute top-0 hidden w-full"
       style="transform: translateY(-100%)"
     >
-      <div>
+      <div class="pointer-events-none">
         <p
           title="Block id (click to copy)"
-          class="block-menu text-overflow inline-block px-1 dark:text-gray-300"
+          class="block-menu pointer-events-auto text-overflow inline-block px-1 dark:text-gray-300"
           style="max-width: 96px; margin-bottom: 0"
           @click="insertToClipboard"
         >

+ 48 - 12
src/components/newtab/workflow/edit/EditGoogleSheets.vue

@@ -15,7 +15,10 @@
         {{ t(`workflow.blocks.google-sheets.select.${action}`) }}
       </option>
     </ui-select>
-    <edit-autocomplete>
+    <slot />
+    <edit-autocomplete
+      v-if="!googleDrive || data.inputSpreadsheetId === 'manually'"
+    >
       <ui-input
         :model-value="data.spreadsheetId"
         class="w-full"
@@ -45,7 +48,7 @@
       Automa doesn't have access to the spreadsheet
       <v-remixicon name="riInformationLine" size="18" class="inline" />
     </a>
-    <edit-autocomplete>
+    <edit-autocomplete v-if="data.type !== 'create'">
       <ui-input
         :model-value="data.range"
         class="mt-1 w-full"
@@ -92,9 +95,13 @@
         {{ previewDataState.errorMessage }}
       </p>
     </template>
-    <template v-else-if="data.type === 'getRange'">
+    <template v-else-if="['getRange', 'create'].includes(data.type)">
+      <p class="mt-4">
+        {{ t('workflow.blocks.google-sheets.spreadsheetId.label') }}
+      </p>
       <insert-workflow-data :data="data" variables @update="updateData" />
       <ui-button
+        v-if="data.type === 'getRange'"
         :loading="previewDataState.status === 'loading'"
         variant="accent"
         class="mt-4"
@@ -207,8 +214,10 @@
 <script setup>
 import { shallowReactive, defineAsyncComponent } from 'vue';
 import { useI18n } from 'vue-i18n';
-import { googleSheets, fetchApi } from '@/utils/api';
+import { useToast } from 'vue-toastification';
+import { fetchApi } from '@/utils/api';
 import { convert2DArrayToArrayObj, debounce } from '@/utils/helper';
+import googleSheetsApi from '@/utils/googleSheetsApi';
 import EditAutocomplete from './EditAutocomplete.vue';
 import InsertWorkflowData from './InsertWorkflowData.vue';
 
@@ -221,12 +230,25 @@ const props = defineProps({
     type: Object,
     default: () => ({}),
   },
+  googleDrive: Boolean,
+  additionalActions: {
+    type: Array,
+    default: () => [],
+  },
 });
 const emit = defineEmits(['update:data']);
 
 const { t } = useI18n();
+const toast = useToast();
 
-const actions = ['get', 'getRange', 'update', 'append', 'clear'];
+const actions = [
+  'get',
+  'getRange',
+  'update',
+  'append',
+  'clear',
+  ...props.additionalActions,
+];
 const dataFrom = ['data-columns', 'custom'];
 const valueInputOptions = ['RAW', 'USER_ENTERED'];
 const insertDataOptions = ['OVERWRITE', 'INSERT_ROWS'];
@@ -272,18 +294,32 @@ async function previewData() {
       range: props.data.range,
       spreadsheetId: props.data.spreadsheetId,
     };
+
+    if (!props.data.spreadsheetId) {
+      toast.error(
+        props.googleDrive
+          ? 'No spreadsheet is selected'
+          : 'Spreadsheet id is empty'
+      );
+      previewDataState.status = 'idle';
+      return;
+    }
+    if (!props.data.range) {
+      toast.error('Spreadsheet range is empty');
+      previewDataState.status = 'idle';
+      return;
+    }
+
     const response = await (isGetValues
-      ? googleSheets.getValues(params)
-      : googleSheets.getRange(params));
+      ? googleSheetsApi(props.googleDrive).getValues(params)
+      : googleSheetsApi(props.googleDrive).getRange(params));
 
-    if (!response.ok) {
-      const error = await response.json();
+    let result = props.googleDrive ? response : await response.json();
 
-      throw new Error(error.message || response.statusText);
+    if (!response.ok && !props.googleDrive) {
+      throw new Error(result.message || response.statusText);
     }
 
-    let result = await response.json();
-
     if (isGetValues) {
       const values = result?.values ?? [];
       result = props.data.firstRowAsKey

+ 103 - 0
src/components/newtab/workflow/edit/EditGoogleSheetsDrive.vue

@@ -0,0 +1,103 @@
+<template>
+  <div v-if="!store.isGDriveConnected">
+    <p>
+      You haven't
+      <a href="https://google.com" target="_blank" class="underline"
+        >connected Automa to Google Drive</a
+      >.
+    </p>
+  </div>
+  <edit-google-sheets
+    v-else
+    google-drive
+    :data="data"
+    :additional-actions="['create']"
+    @update:data="updateData"
+  >
+    <ui-tabs
+      v-if="data.type !== 'create'"
+      small
+      :model-value="data.inputSpreadsheetId"
+      fill
+      class="w-full my-2"
+      type="fill"
+      @change="updateData({ inputSpreadsheetId: $event })"
+    >
+      <ui-tab value="connected"> Connected </ui-tab>
+      <ui-tab value="manually"> Manually </ui-tab>
+    </ui-tabs>
+    <div
+      v-if="data.type !== 'create' && data.inputSpreadsheetId === 'connected'"
+      class="flex items-end"
+    >
+      <ui-select
+        :model-value="data.spreadsheetId"
+        :label="t('workflow.blocks.google-sheets-drive.connected')"
+        :placeholder="t('workflow.blocks.google-sheets-drive.select')"
+        class="w-full"
+        @change="updateData({ spreadsheetId: $event })"
+      >
+        <option
+          v-for="sheet in store.connectedSheets"
+          :key="sheet.id"
+          :value="sheet.id"
+        >
+          {{ sheet.name }}
+        </option>
+      </ui-select>
+      <ui-button
+        v-tooltip="t('workflow.blocks.google-sheets-drive.connect')"
+        icon
+        class="ml-2"
+        @click="connectSheet"
+      >
+        <v-remixicon name="riLink" />
+      </ui-button>
+    </div>
+    <ui-input
+      v-if="data.type === 'create'"
+      :model-value="data.sheetName"
+      label="Sheet name"
+      placeholder="A Spreadsheet"
+      class="w-full"
+      @change="updateData({ sheetName: $event })"
+    />
+  </edit-google-sheets>
+</template>
+<script setup>
+import { useI18n } from 'vue-i18n';
+import { useToast } from 'vue-toastification';
+import { useStore } from '@/stores/main';
+import openGDriveFilePicker from '@/utils/openGDriveFilePicker';
+import EditGoogleSheets from './EditGoogleSheets.vue';
+
+const props = defineProps({
+  data: {
+    type: Object,
+    default: () => ({}),
+  },
+});
+const emit = defineEmits(['update:data']);
+
+const { t } = useI18n();
+const toast = useToast();
+const store = useStore();
+store.getConnectedSheets();
+
+function updateData(value) {
+  emit('update:data', { ...props.data, ...value });
+}
+function connectSheet() {
+  openGDriveFilePicker().then(({ name, id, mimeType }) => {
+    if (mimeType !== 'application/vnd.google-apps.spreadsheet') {
+      toast.error('File is not a google spreadsheet');
+      return;
+    }
+
+    const sheetExists = store.connectedSheets.some((sheet) => sheet.id === id);
+    if (sheetExists) return;
+
+    store.connectedSheets.push({ name, id });
+  });
+}
+</script>

+ 54 - 5
src/content/services/webService.js

@@ -46,11 +46,6 @@ window.addEventListener('DOMContentLoaded', async () => {
 
     await db.put('store', workflows, 'workflows');
 
-    const session =
-      parseJSON(localStorage.getItem('supabase.auth.token'), null)
-        ?.currentSession ?? null;
-    await browser.storage.local.set({ session });
-
     const webListener = initWebListener();
     webListener.on('open-dashboard', ({ path }) => {
       if (!path) return;
@@ -200,3 +195,57 @@ window.addEventListener('DOMContentLoaded', async () => {
     console.error(error);
   }
 });
+
+window.addEventListener('user-logout', () => {
+  browser.storage.local.remove(['session', 'sessionToken']);
+});
+
+window.addEventListener('app-mounted', async () => {
+  try {
+    const STORAGE_KEY = 'supabase.auth.token';
+    const session = parseJSON(localStorage.getItem(STORAGE_KEY), null);
+    const storage = await browser.storage.local.get([
+      'session',
+      'sessionToken',
+    ]);
+
+    const setUserSession = async () => {
+      const saveToStorage = { session };
+
+      const isGoogleProvider =
+        session?.user?.user_metadata?.iss.includes('googleapis.com');
+      const { session: currSession, sessionToken: currSessionToken } =
+        await browser.storage.local.get(['session', 'sessionToken']);
+      if (
+        isGoogleProvider &&
+        ((session && session.user.id === currSession?.user.id) ||
+          !currSessionToken)
+      ) {
+        saveToStorage.sessionToken = {
+          access: session.provider_token,
+          refresh: session.provider_refresh_token,
+        };
+      }
+
+      await browser.storage.local.set(saveToStorage);
+    };
+
+    if (session && !storage.session) {
+      await setUserSession();
+    } else if (session && storage.session) {
+      if (session.user.id !== storage.session.id) {
+        await setUserSession();
+      } else {
+        const currentSession = { ...storage.session };
+        if (storage.sessionToken) {
+          currentSession.provider_token = storage.sessionToken.access;
+          currentSession.provider_refresh_token = storage.sessionToken.refresh;
+        }
+
+        localStorage.setItem(STORAGE_KEY, JSON.stringify(currentSession));
+      }
+    }
+  } catch (error) {
+    console.error(error);
+  }
+});

+ 2 - 0
src/lib/vRemixicon.js

@@ -93,6 +93,7 @@ import {
   riDownloadLine,
   riFileListLine,
   riDragDropLine,
+  riDriveLine,
   riClipboardLine,
   riCheckDoubleLine,
   riDoubleQuotesL,
@@ -229,6 +230,7 @@ export const icons = {
   riDownloadLine,
   riFileListLine,
   riDragDropLine,
+  riDriveLine,
   riClipboardLine,
   riCheckDoubleLine,
   riDoubleQuotesL,

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

@@ -395,6 +395,13 @@
         "insertVars": "Insert current workflow variables",
         "useCommas": "Use commas to separate the variable name"
       },
+      "google-sheets-drive": {
+        "name": "@:workflow.blocks.google-sheets.name (GDrive)",
+        "description": "@:workflow.blocks.google-sheets.description",
+        "connected": "Connected sheets",
+        "select": "Select sheet",
+        "connect": "Connect sheet"
+      },
       "google-sheets": {
         "name": "Google Sheets",
         "description": "Read or update Google Sheets data",
@@ -429,7 +436,8 @@
           "getRange": "Get spreadsheet range",
           "update": "Update spreadsheet cell values",
           "append": "Append spreadsheet cell values",
-          "clear": "Clear spreadsheet cell values"
+          "clear": "Clear spreadsheet cell values",
+          "create": "Create a spreadsheet"
         }
       },
       "active-tab": {

+ 1 - 0
src/locales/en/common.json

@@ -26,6 +26,7 @@
     "save": "Save",
     "data": "data",
     "stop": "Stop",
+    "sheet": "Sheet",
     "action": "Action | Actions",
     "packages": "Packages",
     "storage": "Storage",

+ 1 - 1
src/manifest.chrome.json

@@ -73,5 +73,5 @@
   "sandbox": {
     "pages": ["/sandbox.html"]
   },
-  "content_security_policy": "script-src 'self' 'unsafe-inline' 'unsafe-eval' https:; object-src 'self'"
+  "content_security_policy": "script-src 'self' https://apis.google.com/ https://accounts.google.com/ https://www.googleapis.com https://ajax.googleapis.com; object-src 'self'"
 }

+ 28 - 0
src/stores/main.js

@@ -2,6 +2,7 @@ import { defineStore } from 'pinia';
 import defu from 'defu';
 import browser from 'webextension-polyfill';
 import deepmerge from 'lodash.merge';
+import { fetchGapi } from '@/utils/api';
 
 export const useStore = defineStore('main', {
   storageMap: {
@@ -28,6 +29,9 @@ export const useStore = defineStore('main', {
       },
     },
     retrieved: true,
+    connectedSheets: [],
+    isGDriveConnected: false,
+    connectedSheetsRetrieved: false,
   }),
   actions: {
     loadSettings() {
@@ -40,5 +44,29 @@ export const useStore = defineStore('main', {
       this.settings = deepmerge(this.settings, settings);
       await this.saveToStorage('settings');
     },
+    async getConnectedSheets() {
+      try {
+        if (this.connectedSheetsRetrieved) return;
+
+        // const query = encodeURIComponent('mimeType="application/vnd.google-apps.spreadsheet"');
+        const result = await fetchGapi(
+          `https://www.googleapis.com/drive/v3/files?q=${''}`
+        );
+
+        this.isGDriveConnected = true;
+        this.connectedSheets = result.files.filter(
+          (file) => file.mimeType === 'application/vnd.google-apps.spreadsheet'
+        );
+      } catch (error) {
+        if (
+          error.message === 'no-scope' ||
+          error.message.includes('insufficient authentication')
+        ) {
+          this.isGDriveConnected = false;
+        }
+
+        console.error(error);
+      }
+    },
   },
 });

+ 82 - 55
src/utils/api.js

@@ -2,70 +2,39 @@ import secrets from 'secrets';
 import browser from 'webextension-polyfill';
 import { parseJSON, isObject } from './helper';
 
-function queryBuilder(obj) {
-  let str = '';
-
-  Object.entries(obj).forEach(([key, value], index) => {
-    if (index !== 0) str += `&`;
+export async function fetchApi(path, options) {
+  const urlPath = path.startsWith('/') ? path : `/${path}`;
+  const headers = {
+    'Content-Type': 'application/json',
+    ...(options?.headers || {}),
+  };
 
-    str += `${key}=${value}`;
-  });
+  const { session } = await browser.storage.local.get('session');
+  if (session) {
+    let token = session.access_token;
+
+    if (Date.now() > session.expires_at * 1000) {
+      const response = await fetch(
+        `${secrets.baseApiUrl}/me/refresh-auth-session`
+      );
+      const result = await response.json();
+      if (!response.ok) {
+        throw new Error(result.message);
+      }
 
-  return str;
-}
+      await browser.storage.local.set({ session: result });
+      token = result.access_token;
+    }
 
-export async function fetchApi(path, options) {
-  const urlPath = path.startsWith('/') ? path : `/${path}`;
+    headers.Authorization = `Bearer ${token}`;
+  }
 
   return fetch(`${secrets.baseApiUrl}${urlPath}`, {
-    headers: {
-      'Content-Type': 'application/json',
-    },
     ...options,
+    headers,
   });
 }
 
-export const googleSheets = {
-  getUrl(spreadsheetId, range) {
-    return `/services/google-sheets?spreadsheetId=${spreadsheetId}&range=${range}`;
-  },
-  getValues({ spreadsheetId, range }) {
-    const url = this.getUrl(spreadsheetId, range);
-
-    return fetchApi(url);
-  },
-  getRange({ spreadsheetId, range }) {
-    return googleSheets.updateValues({
-      range,
-      append: true,
-      spreadsheetId,
-      options: {
-        body: JSON.stringify({ values: [] }),
-        queries: {
-          valueInputOption: 'RAW',
-          includeValuesInResponse: false,
-          insertDataOption: 'INSERT_ROWS',
-        },
-      },
-    });
-  },
-  clearValues({ spreadsheetId, range }) {
-    return fetchApi(this.getUrl(spreadsheetId, range), {
-      method: 'DELETE',
-    });
-  },
-  updateValues({ spreadsheetId, range, options = {}, append }) {
-    const url = `${this.getUrl(spreadsheetId, range)}&${queryBuilder(
-      options?.queries || {}
-    )}`;
-
-    return fetchApi(url, {
-      ...options,
-      method: append ? 'POST' : 'PUT',
-    });
-  },
-};
-
 export async function cacheApi(key, callback, useCache = true) {
   const isBoolOpts = typeof useCache === 'boolean';
   const options = {
@@ -176,3 +145,61 @@ export async function getUserWorkflows(useCache = true) {
     useCache
   );
 }
+
+export async function fetchGapi(url, resource = {}, options = {}) {
+  const { sessionToken } = await browser.storage.local.get('sessionToken');
+  if (!sessionToken) throw new Error('unauthorized');
+
+  const { search, origin, pathname } = new URL(url);
+  const searchParams = new URLSearchParams(search);
+  searchParams.set('access_token', sessionToken.access);
+
+  let tryCount = 0;
+  const maxTry = options?.tryCount || 3;
+
+  const startFetch = async () => {
+    const response = await fetch(
+      `${origin}${pathname}?${searchParams.toString()}`,
+      resource
+    );
+    const result = await response.json();
+    const insufficientScope =
+      response.status === 403 &&
+      result.error?.message.includes('insufficient authentication scopes');
+    if (
+      (response.status === 401 || insufficientScope) &&
+      sessionToken.refresh
+    ) {
+      const refreshResponse = await fetchApi(
+        `/me/refresh-session?token=${sessionToken.refresh}`
+      );
+      const refreshResult = await refreshResponse.json();
+
+      if (!refreshResponse.ok) {
+        throw new Error(refreshResult.message);
+      }
+
+      searchParams.set('access_token', refreshResult.token);
+      sessionToken.access = refreshResult.token;
+
+      await browser.storage.local.set({ sessionToken });
+
+      if (tryCount < maxTry) {
+        tryCount += 1;
+        const awaitResult = await startFetch();
+
+        return awaitResult;
+      }
+
+      throw new Error('unauthorized');
+    }
+    if (!response.ok) {
+      throw new Error(result?.error?.message, { cause: result });
+    }
+
+    return result;
+  };
+
+  const result = await startFetch();
+  return result;
+}

+ 116 - 0
src/utils/googleSheetsApi.js

@@ -0,0 +1,116 @@
+import { fetchGapi, fetchApi } from './api';
+
+function queryBuilder(obj) {
+  let str = '';
+
+  Object.entries(obj).forEach(([key, value], index) => {
+    if (index !== 0) str += `&`;
+
+    str += `${key}=${value}`;
+  });
+
+  return str;
+}
+
+export const googleSheetNative = {
+  getUrl(path) {
+    return `https://sheets.googleapis.com/v4/spreadsheets${path}`;
+  },
+  getValues({ spreadsheetId, range }) {
+    const url = googleSheetNative.getUrl(`/${spreadsheetId}/values/${range}`);
+
+    return fetchGapi(url);
+  },
+  getRange({ spreadsheetId, range }) {
+    const url = googleSheetNative.getUrl(
+      `/${spreadsheetId}/values/${range}:append?valueInputOption=RAW&includeValuesInResponse=false&insertDataOption=INSERT_ROWS`
+    );
+
+    return fetchGapi(url, {
+      method: 'POST',
+    });
+  },
+  clearValues({ spreadsheetId, range }) {
+    const url = googleSheetNative.getUrl(
+      `/${spreadsheetId}/values/${range}:clear`
+    );
+
+    return fetchGapi(url, { method: 'POST' });
+  },
+  updateValues({ spreadsheetId, range, options, append }) {
+    let url = '';
+    let method = '';
+
+    if (append) {
+      url = googleSheetNative.getUrl(
+        `/${spreadsheetId}/values/${range}:append`
+      );
+      method = 'POST';
+    } else {
+      url = googleSheetNative.getUrl(`/${spreadsheetId}/values/${range}`);
+      method = 'PUT';
+    }
+
+    const payload = { method };
+    if (options.body) payload.body = options.body;
+
+    return fetchGapi(`${url}?${queryBuilder(options?.queries || {})}`, payload);
+  },
+  create(name) {
+    const url = googleSheetNative.getUrl('');
+
+    return fetchGapi(url, {
+      method: 'POST',
+      body: JSON.stringify({
+        properties: {
+          title: name,
+        },
+      }),
+    });
+  },
+};
+
+export const googleSheets = {
+  getUrl(spreadsheetId, range) {
+    return `/services/google-sheets?spreadsheetId=${spreadsheetId}&range=${range}`;
+  },
+  getValues({ spreadsheetId, range }) {
+    const url = this.getUrl(spreadsheetId, range);
+
+    return fetchApi(url);
+  },
+  getRange({ spreadsheetId, range }) {
+    return googleSheets.updateValues({
+      range,
+      append: true,
+      spreadsheetId,
+      options: {
+        body: JSON.stringify({ values: [] }),
+        queries: {
+          valueInputOption: 'RAW',
+          includeValuesInResponse: false,
+          insertDataOption: 'INSERT_ROWS',
+        },
+      },
+    });
+  },
+  clearValues({ spreadsheetId, range }) {
+    return fetchApi(this.getUrl(spreadsheetId, range), {
+      method: 'DELETE',
+    });
+  },
+  updateValues({ spreadsheetId, range, options = {}, append }) {
+    const url = `${this.getUrl(spreadsheetId, range)}&${queryBuilder(
+      options?.queries || {}
+    )}`;
+
+    return fetchApi(url, {
+      ...options,
+      method: append ? 'POST' : 'PUT',
+    });
+  },
+};
+
+export default function (isDriveSheet = false) {
+  return isDriveSheet ? googleSheetNative : googleSheets;
+}

+ 95 - 0
src/utils/openGDriveFilePicker.js

@@ -0,0 +1,95 @@
+import secrets from 'secrets';
+import browser from 'webextension-polyfill';
+
+function injectFilePicker() {
+  return new Promise((resolve, reject) => {
+    const scriptExist = document.querySelector('#google-api');
+    if (scriptExist) {
+      resolve();
+      return;
+    }
+
+    let gisLoaded = false;
+    let pickerLoaded = false;
+
+    const scriptApi = document.createElement('script');
+    scriptApi.onload = () => {
+      window.gapi.load('picker', () => {
+        pickerLoaded = true;
+      });
+    };
+    scriptApi.id = 'google-api';
+    scriptApi.src = 'https://apis.google.com/js/api.js';
+
+    const scriptGis = document.createElement('script');
+    scriptGis.onload = () => {
+      gisLoaded = true;
+    };
+    scriptGis.src = 'https://accounts.google.com/gsi/client';
+
+    document.body.appendChild(scriptApi);
+    document.body.appendChild(scriptGis);
+
+    let count = 0;
+    const checkIfLoaded = () => {
+      if (count > 10) {
+        reject(new Error('Timeout'));
+        return;
+      }
+
+      if (gisLoaded && pickerLoaded) {
+        resolve();
+        return;
+      }
+
+      count += 1;
+      setTimeout(checkIfLoaded, 1500);
+    };
+
+    checkIfLoaded();
+  });
+}
+
+function selectFile(accessToken) {
+  return new Promise((resolve) => {
+    const callback = (event) => {
+      if (!event || event?.action !== 'picked') return;
+
+      const [doc] = event.docs;
+      resolve(doc);
+    };
+
+    const picker = new window.google.picker.PickerBuilder()
+      .addView(
+        new window.google.picker.DocsView(
+          window.google.picker.ViewId.SPREADSHEETS
+        ).setMode(window.google.picker.DocsViewMode.LIST)
+      )
+      .setAppId(secrets.googleProjectId)
+      .setOAuthToken(accessToken)
+      .setDeveloperKey(secrets.googleApiKey)
+      .setCallback(callback)
+      .build();
+    picker.setVisible(true);
+
+    window.gDrivePicker = picker;
+  });
+}
+
+export default async function () {
+  await injectFilePicker();
+
+  const { sessionToken, session } = await browser.storage.local.get([
+    'sessionToken',
+    'session',
+  ]);
+  if (!sessionToken || !sessionToken.access) return null;
+
+  const isGoogleProvider =
+    session?.user?.user_metadata?.iss.includes('googleapis.com');
+  if (!isGoogleProvider) return null;
+
+  const result = await selectFile(sessionToken.access);
+
+  return result;
+}

+ 34 - 0
src/utils/shared.js

@@ -598,6 +598,40 @@ export const tasks = {
       dataFrom: 'data-columns',
     },
   },
+  'google-sheets-drive': {
+    name: 'Google sheets (GDrive)',
+    description: 'Read Google Sheets data',
+    icon: 'riDriveLine',
+    component: 'BlockBasic',
+    editComponent: 'EditGoogleSheetsDrive',
+    category: 'onlineServices',
+    inputs: 1,
+    outputs: 1,
+    allowedInputs: true,
+    maxConnection: 1,
+    refDataKeys: ['customData', 'range', 'spreadsheetId'],
+    autocomplete: ['refKey'],
+    data: {
+      disableBlock: false,
+      range: '',
+      refKey: '',
+      type: 'get',
+      customData: '',
+      description: '',
+      spreadsheetId: '',
+      dataColumn: '',
+      inputSpreadsheetId: 'connected',
+      saveData: true,
+      sheetName: '',
+      assignVariable: false,
+      variableName: '',
+      firstRowAsKey: false,
+      keysAsFirstRow: true,
+      valueInputOption: 'RAW',
+      InsertDataOption: 'INSERT_ROWS',
+      dataFrom: 'data-columns',
+    },
+  },
   conditions: {
     name: 'Conditions',
     description: 'Conditional block',

+ 75 - 40
src/workflowEngine/blocksHandler/handlerGoogleSheets.js

@@ -1,4 +1,4 @@
-import { googleSheets } from '@/utils/api';
+import googleSheetsApi from '@/utils/googleSheetsApi';
 import {
   convert2DArrayToArrayObj,
   convertArrObjTo2DArr,
@@ -6,11 +6,19 @@ import {
   parseJSON,
 } from '@/utils/helper';
 
-async function getSpreadsheetValues({ spreadsheetId, range, firstRowAsKey }) {
-  const response = await googleSheets.getValues({ spreadsheetId, range });
-  const result = await response.json();
+async function getSpreadsheetValues({
+  spreadsheetId,
+  range,
+  firstRowAsKey,
+  isDriveSheet,
+}) {
+  const response = await googleSheetsApi(isDriveSheet).getValues({
+    spreadsheetId,
+    range,
+  });
 
-  if (!response.ok) {
+  const result = isDriveSheet ? response : await response.json();
+  if (!isDriveSheet && !response.ok) {
     throw new Error(result.message);
   }
 
@@ -20,11 +28,14 @@ async function getSpreadsheetValues({ spreadsheetId, range, firstRowAsKey }) {
 
   return sheetsData;
 }
-async function getSpreadsheetRange({ spreadsheetId, range }) {
-  const response = await googleSheets.getRange({ spreadsheetId, range });
-  const result = await response.json();
+async function getSpreadsheetRange({ spreadsheetId, range, isDriveSheet }) {
+  const response = await googleSheetsApi(isDriveSheet).getRange({
+    spreadsheetId,
+    range,
+  });
 
-  if (!response.ok) {
+  const result = isDriveSheet ? response : await response.json();
+  if (!isDriveSheet && !response.ok) {
     throw new Error(result.message);
   }
 
@@ -35,11 +46,14 @@ async function getSpreadsheetRange({ spreadsheetId, range }) {
 
   return data;
 }
-async function clearSpreadsheetValues({ spreadsheetId, range }) {
-  const response = await googleSheets.clearValues({ spreadsheetId, range });
-  const result = await response.json();
+async function clearSpreadsheetValues({ spreadsheetId, range, isDriveSheet }) {
+  const response = await googleSheetsApi(isDriveSheet).clearValues({
+    spreadsheetId,
+    range,
+  });
 
-  if (!response.ok) {
+  const result = isDriveSheet ? response : await response.json();
+  if (!isDriveSheet && !response.ok) {
     throw new Error(result.message);
   }
 
@@ -51,6 +65,7 @@ async function updateSpreadsheetValues(
     append,
     dataFrom,
     customData,
+    isDriveSheet,
     spreadsheetId,
     keysAsFirstRow,
     insertDataOption,
@@ -91,7 +106,7 @@ async function updateSpreadsheetValues(
     });
   }
 
-  const response = await googleSheets.updateValues({
+  const response = await googleSheetsApi(isDriveSheet).updateValues({
     range,
     append,
     spreadsheetId,
@@ -101,10 +116,9 @@ async function updateSpreadsheetValues(
     },
   });
 
-  if (!response.ok) {
-    const error = await response.json();
-
-    throw new Error(error.message);
+  const result = isDriveSheet ? response : await response.json();
+  if (!isDriveSheet && !response.ok) {
+    throw new Error(result.message);
   }
 }
 
@@ -113,34 +127,55 @@ export default async function ({ data, id }, { refData }) {
   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)) {
-      refData.googleSheets[data.refKey] = spreadsheetValues;
-    }
-  } else if (data.type === 'getRange') {
-    result = await getSpreadsheetRange(data);
-
-    if (data.assignVariable) {
-      this.setVariable(data.variableName, result);
-    }
-    if (data.saveData) {
-      this.addDataToColumn(data.dataColumn, result);
-    }
-  } else if (['update', 'append'].includes(data.type)) {
+  const handleUpdate = async (append = false) => {
     result = await updateSpreadsheetValues(
       {
         ...data,
-        append: data.type === 'append',
+        append,
       },
       refData.table
     );
-  } else if (data.type === 'clear') {
-    result = await clearSpreadsheetValues(data);
-  }
+  };
+  const actionHandlers = {
+    get: async () => {
+      const spreadsheetValues = await getSpreadsheetValues(data);
+
+      result = spreadsheetValues;
+
+      if (data.refKey && !isWhitespace(data.refKey)) {
+        refData.googleSheets[data.refKey] = spreadsheetValues;
+      }
+    },
+    getRange: async () => {
+      result = await getSpreadsheetRange(data);
+
+      if (data.assignVariable) {
+        this.setVariable(data.variableName, result);
+      }
+      if (data.saveData) {
+        this.addDataToColumn(data.dataColumn, result);
+      }
+    },
+    update: () => handleUpdate(),
+    append: () => handleUpdate(true),
+    clear: async () => {
+      result = await clearSpreadsheetValues(data);
+    },
+    create: async () => {
+      const { spreadsheetId } = await googleSheetsApi(true).create(
+        data.sheetName
+      );
+      result = spreadsheetId;
+
+      if (data.assignVariable) {
+        this.setVariable(data.variableName, result);
+      }
+      if (data.saveData) {
+        this.addDataToColumn(data.dataColumn, result);
+      }
+    },
+  };
+  await actionHandlers[data.type]();
 
   return {
     data: result,

+ 6 - 0
src/workflowEngine/blocksHandler/handlerGoogleSheetsDrive.js

@@ -0,0 +1,6 @@
+import handlerGoogleSheets from './handlerGoogleSheets';
+
+export default function (blockData, additionalData) {
+  blockData.data.isDriveSheet = true;
+  return handlerGoogleSheets.call(this, blockData, additionalData);
+}