Ahmad Kholid 3 년 전
부모
커밋
488488e892

+ 2 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "automa",
-  "version": "0.14.2",
+  "version": "0.15.0",
   "description": "An extension for automating your browser by connecting blocks",
   "license": "MIT",
   "repository": {
@@ -43,6 +43,7 @@
     "vue": "3.2.19",
     "vue-i18n": "^9.2.0-beta.20",
     "vue-router": "^4.0.11",
+    "vue-toastification": "^2.0.0-rc.5",
     "vuedraggable": "^4.1.0",
     "vuex": "^4.0.2",
     "webextension-polyfill": "^0.8.0"

+ 51 - 7
src/background/workflow-engine/blocks-handler/handler-google-sheets.js

@@ -1,21 +1,60 @@
 import { getBlockConnection } from '../helper';
-import { getGoogleSheetsValue } from '@/utils/api';
-import { convert2DArrayToArrayObj, isWhitespace } from '@/utils/helper';
+import { googleSheets } from '@/utils/api';
+import {
+  convert2DArrayToArrayObj,
+  convertArrObjTo2DArr,
+  isWhitespace,
+  parseJSON,
+} from '@/utils/helper';
 
-async function getSpreadsheetValues(data) {
-  const response = await getGoogleSheetsValue(data.spreadsheetId, data.range);
+async function getSpreadsheetValues({ spreadsheetId, range, firstRowAsKey }) {
+  const response = await googleSheets.getValues({ spreadsheetId, range });
 
   if (response.status !== 200) {
     throw new Error(response.statusText);
   }
 
   const { values } = await response.json();
-  const sheetsData = data.firstRowAsKey
-    ? convert2DArrayToArrayObj(values)
-    : values;
+  const sheetsData = firstRowAsKey ? convert2DArrayToArrayObj(values) : values;
 
   return sheetsData;
 }
+async function updateSpreadsheetValues(
+  {
+    spreadsheetId,
+    range,
+    valueInputOption,
+    keysAsFirstRow,
+    dataFrom,
+    customData,
+  },
+  dataColumns
+) {
+  let values = [];
+
+  if (dataFrom === 'data-columns') {
+    if (keysAsFirstRow) {
+      values = convertArrObjTo2DArr(dataColumns);
+    } else {
+      values = dataColumns.map(Object.values);
+    }
+  } else if (dataFrom === 'custom') {
+    values = parseJSON(customData, customData);
+  }
+
+  const response = await googleSheets.updateValues({
+    range,
+    spreadsheetId,
+    valueInputOption,
+    options: {
+      body: JSON.stringify({ values }),
+    },
+  });
+
+  if (response.status !== 200) {
+    throw new Error(response.statusText);
+  }
+}
 
 export default async function ({ data, outputs }) {
   const nextBlockId = getBlockConnection({ outputs });
@@ -35,6 +74,11 @@ export default async function ({ data, outputs }) {
       if (data.refKey && !isWhitespace(data.refKey)) {
         this.referenceData.googleSheets[data.refKey] = spreadsheetValues;
       }
+    } else if (data.type === 'update') {
+      result = await updateSpreadsheetValues(
+        data,
+        this.referenceData.dataColumns
+      );
     }
 
     return {

+ 5 - 2
src/components/newtab/workflow/edit/EditGetText.vue

@@ -137,12 +137,15 @@ function updateData(value) {
   emit('update:data', { ...props.data, ...value });
 }
 function handleExpCheckbox(id, value) {
+  const copy = [...new Set(regexExp.value)];
+
   if (value) {
-    regexExp.value.push(id);
+    copy.push(id);
   } else {
-    regexExp.value.splice(regexExp.value.indexOf(id), 1);
+    copy.splice(copy.indexOf(id), 1);
   }
 
+  regexExp.value = copy;
   updateData({ regexExp: regexExp.value });
 }
 </script>

+ 77 - 13
src/components/newtab/workflow/edit/EditGoogleSheets.vue

@@ -7,7 +7,6 @@
       @change="updateData({ description: $event })"
     />
     <ui-select
-      v-if="false"
       :model-value="data.type"
       class="w-full mb-2"
       @change="updateData({ type: $event })"
@@ -15,8 +14,8 @@
       <option value="get">
         {{ t('workflow.blocks.google-sheets.select.get') }}
       </option>
-      <option value="write">
-        {{ t('workflow.blocks.google-sheets.select.write') }}
+      <option value="update">
+        {{ t('workflow.blocks.google-sheets.select.update') }}
       </option>
     </ui-select>
     <ui-input
@@ -88,17 +87,75 @@
         class="mt-4 max-h-96"
       />
     </template>
-    <template v-else-if="data.type === 'write'">
-      <pre>
-        halo
-      </pre>
+    <template v-else-if="data.type === 'update'">
+      <ui-select
+        :model-value="data.valueInputOption"
+        class="w-full mt-2"
+        @change="updateData({ valueInputOption: $event })"
+      >
+        <template #label>
+          {{ t('workflow.blocks.google-sheets.valueInputOption') }}
+          <a
+            href="https://developers.google.com/sheets/api/reference/rest/v4/ValueInputOption"
+            target="_blank"
+            rel="noopener"
+          >
+            <v-remixicon name="riInformationLine" size="18" class="inline" />
+          </a>
+        </template>
+        <option
+          v-for="option in valueInputOptions"
+          :key="option"
+          :value="option"
+        >
+          {{ option }}
+        </option>
+      </ui-select>
+      <ui-select
+        :model-value="data.dataFrom"
+        :label="t('workflow.blocks.google-sheets.dataFrom.label')"
+        class="w-full mt-2"
+        @change="updateData({ dataFrom: $event })"
+      >
+        <option v-for="item in dataFrom" :key="item" :value="item">
+          {{ t(`workflow.blocks.google-sheets.dataFrom.options.${item}`) }}
+        </option>
+      </ui-select>
+      <ui-checkbox
+        v-if="data.dataFrom === 'data-columns'"
+        :model-value="data.keysAsFirstRow"
+        class="mt-2"
+        @change="updateData({ keysAsFirstRow: $event })"
+      >
+        {{ t('workflow.blocks.google-sheets.keysAsFirstRow') }}
+      </ui-checkbox>
+      <ui-button
+        v-else
+        class="w-full mt-2"
+        variant="accent"
+        @click="customDataState.showModal = true"
+      >
+        {{ t('workflow.blocks.google-sheets.insertData') }}
+      </ui-button>
     </template>
+    <ui-modal
+      v-model="customDataState.showModal"
+      title="Custom data"
+      content-class="max-w-xl"
+    >
+      <shared-codemirror
+        v-model="customDataState.data"
+        style="height: calc(100vh - 10rem)"
+        lang="json"
+        @change="updateData({ customData: $event })"
+      />
+    </ui-modal>
   </div>
 </template>
 <script setup>
 import { shallowReactive } from 'vue';
 import { useI18n } from 'vue-i18n';
-import { getGoogleSheetsValue } from '@/utils/api';
+import { googleSheets } from '@/utils/api';
 import { convert2DArrayToArrayObj } from '@/utils/helper';
 import SharedCodemirror from '@/components/newtab/shared/SharedCodemirror.vue';
 
@@ -112,10 +169,17 @@ const emit = defineEmits(['update:data']);
 
 const { t } = useI18n();
 
+const dataFrom = ['data-columns', 'custom'];
+const valueInputOptions = ['RAW', 'USER_ENTERED'];
+
 const previewDataState = shallowReactive({
+  data: '',
   status: 'idle',
   errorMessage: '',
-  data: '',
+});
+const customDataState = shallowReactive({
+  showModal: false,
+  data: props.data.customData,
 });
 
 function updateData(value) {
@@ -124,10 +188,10 @@ function updateData(value) {
 async function previewData() {
   try {
     previewDataState.status = 'loading';
-    const response = await getGoogleSheetsValue(
-      props.data.spreadsheetId,
-      props.data.range
-    );
+    const response = await googleSheets.getValues({
+      spreadsheetId: props.data.spreadsheetId,
+      range: props.data.range,
+    });
 
     if (response.status !== 200) {
       throw new Error(response.statusText);

+ 5 - 4
src/components/newtab/workflow/edit/EditLoopData.vue

@@ -119,10 +119,10 @@
   </div>
 </template>
 <script setup>
-/* eslint-disable no-alert */
 import { onMounted, shallowReactive } from 'vue';
 import { nanoid } from 'nanoid';
 import { useI18n } from 'vue-i18n';
+import { useToast } from 'vue-toastification';
 import Papa from 'papaparse';
 import { openFilePicker } from '@/utils/helper';
 import SharedCodemirror from '@/components/newtab/shared/SharedCodemirror.vue';
@@ -140,6 +140,7 @@ const props = defineProps({
 const emit = defineEmits(['update:data']);
 
 const { t } = useI18n();
+const toast = useToast();
 
 const maxFileSize = 1024 * 1024;
 const loopTypes = ['data-columns', 'numbers', 'google-sheets', 'custom-data'];
@@ -163,7 +164,7 @@ function updateData(value) {
 }
 function updateLoopData(value) {
   if (value.length > maxFileSize) {
-    alert(t('message.maxSizeExceeded'));
+    toast.error(t('message.maxSizeExceeded'));
   }
 
   updateData({ loopData: value.slice(0, maxFileSize) });
@@ -181,7 +182,7 @@ function importFile() {
   openFilePicker(['application/json', 'text/csv', 'application/vnd.ms-excel'])
     .then(async (fileObj) => {
       if (fileObj.size > maxFileSize) {
-        alert(t('message.maxSizeExceeded'));
+        toast.error(t('message.maxSizeExceeded'));
         return;
       }
 
@@ -213,7 +214,7 @@ function importFile() {
     })
     .catch((error) => {
       console.error(error);
-      if (error.message.startsWith('invalid')) alert(error.message);
+      if (error.message.startsWith('invalid')) toast.error(error.message);
     });
 }
 

+ 159 - 31
src/components/newtab/workflow/edit/EditTrigger.vue

@@ -62,23 +62,82 @@
           @change="updateData({ time: $event || '00:00' })"
         />
       </div>
-      <div v-else-if="data.type === 'specific-day'" class="mt-2">
-        <ui-input
-          :model-value="data.time"
-          type="time"
-          class="w-full my-2"
-          placeholder="Time"
-          @change="updateData({ time: $event || '00:00' })"
-        />
-        <div class="grid gap-2 grid-cols-2">
-          <ui-checkbox
-            v-for="day in days"
+      <div v-else-if="data.type === 'specific-day'" class="mt-4">
+        <ui-popover
+          :options="{ animation: null }"
+          trigger-width
+          class="w-full mb-2"
+          trigger-class="w-full"
+        >
+          <template #trigger>
+            <ui-button class="w-full">
+              <p class="text-left flex-1 text-overflow mr-2">
+                {{
+                  tempDate.days.length === 0
+                    ? t('workflow.blocks.trigger.selectDay')
+                    : getDaysText(tempDate.days)
+                }}
+              </p>
+              <v-remixicon
+                size="28"
+                name="riArrowDropDownLine"
+                class="text-gray-600 dark:text-gray-200 -mr-2"
+              />
+            </ui-button>
+          </template>
+          <div class="grid gap-2 grid-cols-2">
+            <ui-checkbox
+              v-for="(day, id) in days"
+              :key="id"
+              :model-value="data.days?.includes(id)"
+              @change="onSelectDayChange($event, id)"
+            >
+              {{ t(`workflow.blocks.trigger.days.${id}`) }}
+            </ui-checkbox>
+          </div>
+        </ui-popover>
+        <div class="flex items-center">
+          <ui-input v-model="tempDate.time" type="time" class="flex-1 mr-2" />
+          <ui-button variant="accent" @click="addTime">
+            {{ t('workflow.blocks.trigger.addTime') }}
+          </ui-button>
+        </div>
+        <div class="my-2">
+          <ui-expand
+            v-for="(day, index) in sortedDaysArr"
             :key="day.id"
-            :model-value="data.days?.includes(day.id)"
-            @change="onDayChange($event, day.id)"
+            header-class="focus:ring-0 flex items-center py-2 w-full group text-left"
+            type="time"
+            class="w-full"
           >
-            {{ t(`workflow.blocks.trigger.days.${day.id}`) }}
-          </ui-checkbox>
+            <template #header>
+              <p class="flex-1">
+                {{ t(`workflow.blocks.trigger.days.${day.id}`) }}
+              </p>
+              <span class="text-gray-600 dark:text-gray-200">
+                <v-remixicon
+                  name="riDeleteBin7Line"
+                  class="mr-1 group invisible group-hover:visible inline-block"
+                  @click="daysArr.splice(index, 1)"
+                />
+                {{ day.times.length }}x
+              </span>
+            </template>
+            <div class="grid grid-cols-2 gap-1 mb-1">
+              <div
+                v-for="(time, timeIndex) in day.times"
+                :key="time"
+                class="flex items-center px-4 py-2 border rounded-lg group"
+              >
+                <span class="flex-1"> {{ formatTime(time) }} </span>
+                <v-remixicon
+                  name="riDeleteBin7Line"
+                  class="cursor-pointer"
+                  @click="removeDayTime(index, timeIndex)"
+                />
+              </div>
+            </div>
+          </ui-expand>
         </div>
       </div>
       <div v-else-if="data.type === 'visit-web'" class="mt-2">
@@ -132,9 +191,11 @@
   </div>
 </template>
 <script setup>
-import { shallowReactive, onUnmounted } from 'vue';
+import { reactive, ref, computed, watch, onMounted, onUnmounted } from 'vue';
 import { useI18n } from 'vue-i18n';
+import { useToast } from 'vue-toastification';
 import dayjs from 'dayjs';
+import { isObject } from '@/utils/helper';
 
 const props = defineProps({
   data: {
@@ -145,6 +206,7 @@ const props = defineProps({
 const emit = defineEmits(['update:data']);
 
 const { t } = useI18n();
+const toast = useToast();
 
 const triggers = [
   'manual',
@@ -154,15 +216,15 @@ const triggers = [
   'visit-web',
   'keyboard-shortcut',
 ];
-const days = [
-  { id: 0, name: 'Sunday' },
-  { id: 1, name: 'Monday' },
-  { id: 2, name: 'Tuesday' },
-  { id: 3, name: 'Wednesday' },
-  { id: 4, name: 'Thursday' },
-  { id: 5, name: 'Friday' },
-  { id: 6, name: 'Saturday' },
-];
+const days = {
+  0: 'Sunday',
+  1: 'Monday',
+  2: 'Tuesday',
+  3: 'Wednesday',
+  4: 'Thursday',
+  5: 'Friday',
+  6: 'Saturday',
+};
 const maxDate = dayjs().add(30, 'day').format('YYYY-MM-DD');
 const minDate = dayjs().format('YYYY-MM-DD');
 const allowedKeys = {
@@ -177,21 +239,70 @@ const allowedKeys = {
   Enter: 'enter',
 };
 
-const recordKeys = shallowReactive({
+const recordKeys = reactive({
   isRecording: false,
   keys: props.data.shortcut,
 });
+const tempDate = reactive({
+  days: [],
+  time: '00:00',
+});
+const daysArr = ref(null);
+
+const sortedDaysArr = computed(() =>
+  daysArr.value ? daysArr.value.slice().sort((a, b) => a.id - b.id) : []
+);
 
 function updateData(value) {
   emit('update:data', { ...props.data, ...value });
 }
-function onDayChange(value, id) {
-  const dataDays = [...(props.data?.days || [])];
+function getDaysText(dayIds) {
+  return dayIds
+    .map((day) => t(`workflow.blocks.trigger.days.${day}`))
+    .join(', ');
+}
+function formatTime(time) {
+  const [hour, minute] = time.split(':');
 
-  if (value) dataDays.push(id);
-  else dataDays.splice(dataDays.indexOf(id), 1);
+  return dayjs().hour(hour).minute(minute).format('hh:mm A');
+}
+function removeDayTime(index, timeIndex) {
+  daysArr.value[index].times.splice(timeIndex, 1);
 
-  updateData({ days: dataDays.sort() });
+  if (daysArr.value[index].times.length === 0) {
+    daysArr.value.splice(index, 1);
+  }
+}
+function onSelectDayChange(value, id) {
+  if (value) tempDate.days.push(+id);
+  else tempDate.days.splice(tempDate.days.indexOf(+id), 1);
+}
+function addTime() {
+  tempDate.days.forEach((dayId) => {
+    const dayIndex = daysArr.value.findIndex(({ id }) => id === dayId);
+
+    if (dayIndex === -1) {
+      daysArr.value.push({
+        id: dayId,
+        times: [tempDate.time],
+      });
+    } else {
+      const isTimeExist = daysArr.value[dayIndex].times.includes(tempDate.time);
+
+      if (isTimeExist) {
+        const message = t('workflow.blocks.trigger.timeExist', {
+          time: formatTime(tempDate.time),
+          day: t(`workflow.blocks.trigger.days.${dayId}`),
+        });
+
+        toast.error(message);
+
+        return;
+      }
+
+      daysArr.value[dayIndex].times.push(tempDate.time);
+    }
+  });
 }
 function handleKeydownEvent(event) {
   event.preventDefault();
@@ -251,6 +362,23 @@ function updateDate(value) {
   updateData({ date });
 }
 
+watch(
+  daysArr,
+  (value, oldValue) => {
+    if (!oldValue) return;
+
+    updateData({ days: value });
+  },
+  { deep: true }
+);
+
+onMounted(() => {
+  const isStringDay =
+    props.data.days.length > 0 && !isObject(props.data.days[0]);
+  daysArr.value = isStringDay
+    ? props.data.days.map((day) => ({ id: day, times: [props.data.time] }))
+    : props.data.days;
+});
 onUnmounted(() => {
   window.removeEventListener('keydown', handleKeydownEvent);
 });

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

@@ -1,6 +1,10 @@
 <template>
   <div class="ui-popover inline-block" :class="{ hidden: to }">
-    <div ref="targetEl" class="ui-popover__trigger h-full inline-block">
+    <div
+      ref="targetEl"
+      :class="triggerClass"
+      class="ui-popover__trigger h-full inline-block"
+    >
       <slot name="trigger" v-bind="{ isShow }"></slot>
     </div>
     <div
@@ -42,10 +46,18 @@ export default {
       type: Boolean,
       default: false,
     },
+    triggerWidth: {
+      type: Boolean,
+      default: false,
+    },
     modelValue: {
       type: Boolean,
       default: false,
     },
+    triggerClass: {
+      type: String,
+      default: null,
+    },
   },
   emits: ['show', 'trigger', 'close', 'update:modelValue'],
   setup(props, { emit }) {
@@ -101,6 +113,12 @@ export default {
         interactive: true,
         appendTo: () => document.body,
         onShow: (event) => {
+          if (props.triggerWidth) {
+            event.popper.style.width = `${
+              event.reference.getBoundingClientRect().width
+            }px`;
+          }
+
           emit('show', event);
           isShow.value = true;
         },

+ 4 - 2
src/components/ui/UiSelect.vue

@@ -1,11 +1,13 @@
 <template>
   <div :class="{ 'inline-block': !block }" class="ui-select cursor-pointer">
     <label
-      v-if="label"
+      v-if="label || $slots.label"
       :for="selectId"
       class="text-gray-600 dark:text-gray-200 text-sm ml-2"
     >
-      {{ label }}
+      <slot name="label">
+        {{ label }}
+      </slot>
     </label>
     <div class="ui-select__content flex items-center w-full block relative">
       <v-remixicon

+ 4 - 0
src/lib/vue-toastification.js

@@ -0,0 +1,4 @@
+import Toast from 'vue-toastification';
+import 'vue-toastification/dist/index.css';
+
+export default Toast;

+ 15 - 2
src/locales/en/blocks.json

@@ -36,6 +36,9 @@
       "trigger": {
         "name": "Trigger",
         "description": "Block where the workflow will start executing",
+        "addTime": "Add time",
+        "selectDay": "Select day",
+        "timeExist": "You alread add {time} on {day}",
         "days": [
           "Sunday",
           "Monday",
@@ -78,9 +81,19 @@
       },
       "google-sheets": {
         "name": "Google sheets",
-        "description": "Read Google Sheets data",
+        "description": "Read or update Google Sheets data",
         "previewData": "Preview data",
         "firstRow": "Use the first row as keys",
+        "keysAsFirstRow": "Use keys as the first row",
+        "insertData": "Insert data",
+        "valueInputOption": "Value input option",
+        "dataFrom": {
+          "label": "Data from",
+          "options": {
+            "data-columns": "Data columns",
+            "custom": "Custom"
+          }
+        },
         "refKey": {
           "label": "Reference key",
           "placeholder": "Key name"
@@ -95,7 +108,7 @@
         },
         "select": {
           "get": "Get spreadsheet data",
-          "write": "Write spreadsheet data"
+          "update": "Update spreadsheet data"
         }
       },
       "active-tab": {

+ 1 - 1
src/newtab/App.vue

@@ -6,7 +6,7 @@
     </main>
     <ui-dialog />
     <div
-      v-if="false"
+      v-if="isUpdated"
       class="p-4 shadow-2xl z-50 fixed bottom-8 left-1/2 -translate-x-1/2 rounded-lg bg-accent text-white flex items-center"
     >
       <v-remixicon name="riInformationLine" class="mr-3" />

+ 2 - 0
src/newtab/index.js

@@ -5,6 +5,7 @@ import store from '../store';
 import compsUi from '../lib/comps-ui';
 import vueI18n from '../lib/vue-i18n';
 import vRemixicon, { icons } from '../lib/v-remixicon';
+import vueToastification from '../lib/vue-toastification';
 import '../assets/css/tailwind.css';
 import '../assets/css/fonts.css';
 import '../assets/css/style.css';
@@ -14,6 +15,7 @@ createApp(App)
   .use(store)
   .use(compsUi)
   .use(vueI18n)
+  .use(vueToastification)
   .use(vRemixicon, icons)
   .mount('#app');
 

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

@@ -161,6 +161,7 @@ import {
   toRaw,
 } from 'vue';
 import { useStore } from 'vuex';
+import { useToast } from 'vue-toastification';
 import { useRoute, useRouter, onBeforeRouteLeave } from 'vue-router';
 import { useI18n } from 'vue-i18n';
 import defu from 'defu';
@@ -187,6 +188,7 @@ import SharedWorkflowState from '@/components/newtab/shared/SharedWorkflowState.
 const { t } = useI18n();
 const store = useStore();
 const route = useRoute();
+const toast = useToast();
 const router = useRouter();
 const dialog = useDialog();
 const shortcut = useShortcut('editor:toggle-sidebar', toggleSidebar);
@@ -325,7 +327,7 @@ function editBlock(data) {
 function executeWorkflow() {
   if (editor.value.getNodesFromName('trigger').length === 0) {
     /* eslint-disable-next-line */
-    alert(t('message.noTriggerBlock'));
+    toast.error(t('message.noTriggerBlock'));
     return;
   }
 

+ 19 - 4
src/utils/api.js

@@ -6,8 +6,23 @@ export function fetchApi(path, options) {
   return fetch(`${secrets.baseApiUrl}${urlPath}`, options);
 }
 
-export function getGoogleSheetsValue(spreadsheetId, range) {
-  const url = `/services/google-sheets?spreadsheetId=${spreadsheetId}&range=${range}`;
+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);
-}
+    return fetchApi(url);
+  },
+  updateValues({ spreadsheetId, range, valueInputOption, options = {} }) {
+    const url = `${this.getUrl(spreadsheetId, range)}&valueInputOption=${
+      valueInputOption || 'RAW'
+    }`;
+
+    return fetchApi(url, {
+      ...options,
+      method: 'PUT',
+    });
+  },
+};

+ 24 - 0
src/utils/helper.js

@@ -1,3 +1,27 @@
+export function convertArrObjTo2DArr(arr) {
+  const keyIndex = new Map();
+  const values = [[]];
+
+  arr.forEach((obj) => {
+    const keys = Object.keys(obj);
+    const row = [];
+
+    keys.forEach((key) => {
+      if (!keyIndex.has(key)) {
+        keyIndex.set(key, keyIndex.size);
+        values[0].push(key);
+      }
+
+      const rowIndex = keyIndex.get(key);
+      row[rowIndex] = obj[key];
+    });
+
+    values.push([...row]);
+  });
+
+  return values;
+}
+
 export function convert2DArrayToArrayObj(values) {
   let keyIndex = 0;
   const keys = values.shift();

+ 11 - 2
src/utils/reference-data/index.js

@@ -1,5 +1,5 @@
 import { set as setObjectPath } from 'object-path-immutable';
-import dayjs from 'dayjs';
+import dayjs from '@/lib/dayjs';
 import { objectHasKey } from '@/utils/helper';
 import mustacheReplacer from './mustache-replacer';
 
@@ -20,10 +20,18 @@ export const funcs = {
 
     /* eslint-disable-next-line */
     const isValidDate = date instanceof Date && !isNaN(date);
-    const result = dayjs(isValidDate ? date : Date.now()).format(dateFormat);
+    const dayjsDate = dayjs(isValidDate ? date : Date.now());
+
+    const result =
+      dateFormat === 'relative'
+        ? dayjsDate.fromNow()
+        : dayjsDate.format(dateFormat);
 
     return result;
   },
+  randint(min = 0, max = 100) {
+    return Math.round(Math.random() * (+max - +min) + +min);
+  },
 };
 
 export default function ({ block, data: refData }) {
@@ -35,6 +43,7 @@ export default function ({ block, data: refData }) {
     'fileName',
     'selector',
     'prefixText',
+    'customData',
     'globalData',
     'suffixText',
     'extraRowValue',

+ 7 - 3
src/utils/shared.js

@@ -431,12 +431,16 @@ export const tasks = {
     allowedInputs: true,
     maxConnection: 1,
     data: {
+      range: '',
+      refKey: '',
+      type: 'get',
+      customData: '',
       description: '',
       spreadsheetId: '',
-      type: 'get',
-      range: '',
       firstRowAsKey: false,
-      refKey: '',
+      keysAsFirstRow: false,
+      valueInputOption: 'RAW',
+      dataFrom: 'data-columns',
     },
   },
   conditions: {

+ 25 - 6
src/utils/workflow-trigger.js

@@ -1,5 +1,6 @@
 import browser from 'webextension-polyfill';
 import dayjs from 'dayjs';
+import { isObject } from './helper';
 
 export async function cleanWorkflowTriggers(workflowId) {
   try {
@@ -32,15 +33,33 @@ export async function cleanWorkflowTriggers(workflowId) {
 export function registerSpecificDay(workflowId, data) {
   if (data.days.length === 0) return null;
 
-  const [hour, minute] = data.time.split(':');
-  const dates = data.days.map((id) =>
-    dayjs().day(id).hour(hour).minute(minute)
-  );
+  const getDate = (dayId, time) => {
+    const [hour, minute] = time.split(':');
+    const date = dayjs().day(dayId).hour(hour).minute(minute).second(0);
+
+    return date.valueOf();
+  };
+
+  const dates = data.days
+    .reduce((acc, item) => {
+      if (isObject(item)) {
+        item.times.forEach((time) => {
+          acc.push(getDate(item.id, time));
+        });
+      } else {
+        acc.push(getDate(item, data.time));
+      }
+
+      return acc;
+    }, [])
+    .sort();
+
   const findDate =
-    dates.find((date) => date.valueOf() > Date.now()) || dates[0].add(7, 'day');
+    dates.find((date) => date > Date.now()) ||
+    dayjs(dates[0]).add(7, 'day').valueOf();
 
   return browser.alarms.create(workflowId, {
-    when: findDate.valueOf(),
+    when: findDate,
   });
 }
 

+ 5 - 0
yarn.lock

@@ -6913,6 +6913,11 @@ vue-router@^4.0.11:
   dependencies:
     "@vue/devtools-api" "^6.0.0-beta.18"
 
+vue-toastification@^2.0.0-rc.5:
+  version "2.0.0-rc.5"
+  resolved "https://registry.yarnpkg.com/vue-toastification/-/vue-toastification-2.0.0-rc.5.tgz#92798604d806ae473cfb76ed776fae294280f8f8"
+  integrity sha512-q73e5jy6gucEO/U+P48hqX+/qyXDozAGmaGgLFm5tXX4wJBcVsnGp4e/iJqlm9xzHETYOilUuwOUje2Qg1JdwA==
+
 vue@3.2.19:
   version "3.2.19"
   resolved "https://registry.yarnpkg.com/vue/-/vue-3.2.19.tgz#da2c80a6a0271c7097fee9e31692adfd9d569c8f"