Browse Source

feat: add option to add time on a specific day trigger

Ahmad Kholid 3 years ago
parent
commit
e0cc9200bf

+ 1 - 0
package.json

@@ -43,6 +43,7 @@
     "vue": "3.2.19",
     "vue": "3.2.19",
     "vue-i18n": "^9.2.0-beta.20",
     "vue-i18n": "^9.2.0-beta.20",
     "vue-router": "^4.0.11",
     "vue-router": "^4.0.11",
+    "vue-toastification": "^2.0.0-rc.5",
     "vuedraggable": "^4.1.0",
     "vuedraggable": "^4.1.0",
     "vuex": "^4.0.2",
     "vuex": "^4.0.2",
     "webextension-polyfill": "^0.8.0"
     "webextension-polyfill": "^0.8.0"

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

@@ -62,23 +62,84 @@
           @change="updateData({ time: $event || '00:00' })"
           @change="updateData({ time: $event || '00:00' })"
         />
         />
       </div>
       </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>
+        <form @submit="addTime">
+          <div class="flex items-center">
+            <ui-input v-model="tempDate.time" type="time" class="flex-1 mr-2" />
+            <ui-button variant="accent">
+              {{ t('workflow.blocks.trigger.addTime') }}
+            </ui-button>
+          </div>
+        </form>
+        <div class="my-2">
+          <ui-expand
+            v-for="(day, index) in sortedDaysArr"
             :key="day.id"
             :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>
       </div>
       <div v-else-if="data.type === 'visit-web'" class="mt-2">
       <div v-else-if="data.type === 'visit-web'" class="mt-2">
@@ -132,9 +193,11 @@
   </div>
   </div>
 </template>
 </template>
 <script setup>
 <script setup>
-import { shallowReactive, onUnmounted } from 'vue';
+import { reactive, ref, computed, watch, onMounted, onUnmounted } from 'vue';
 import { useI18n } from 'vue-i18n';
 import { useI18n } from 'vue-i18n';
+import { useToast } from 'vue-toastification';
 import dayjs from 'dayjs';
 import dayjs from 'dayjs';
+import { isObject } from '@/utils/helper';
 
 
 const props = defineProps({
 const props = defineProps({
   data: {
   data: {
@@ -145,6 +208,7 @@ const props = defineProps({
 const emit = defineEmits(['update:data']);
 const emit = defineEmits(['update:data']);
 
 
 const { t } = useI18n();
 const { t } = useI18n();
+const toast = useToast();
 
 
 const triggers = [
 const triggers = [
   'manual',
   'manual',
@@ -154,15 +218,15 @@ const triggers = [
   'visit-web',
   'visit-web',
   'keyboard-shortcut',
   '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 maxDate = dayjs().add(30, 'day').format('YYYY-MM-DD');
 const minDate = dayjs().format('YYYY-MM-DD');
 const minDate = dayjs().format('YYYY-MM-DD');
 const allowedKeys = {
 const allowedKeys = {
@@ -177,21 +241,70 @@ const allowedKeys = {
   Enter: 'enter',
   Enter: 'enter',
 };
 };
 
 
-const recordKeys = shallowReactive({
+const recordKeys = reactive({
   isRecording: false,
   isRecording: false,
   keys: props.data.shortcut,
   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) {
 function updateData(value) {
   emit('update:data', { ...props.data, ...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(':');
+
+  return dayjs().hour(hour).minute(minute).format('hh:mm A');
+}
+function removeDayTime(index, timeIndex) {
+  daysArr.value[index].times.splice(timeIndex, 1);
+
+  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 (value) dataDays.push(id);
-  else dataDays.splice(dataDays.indexOf(id), 1);
+      if (isTimeExist) {
+        const message = t('workflow.blocks.trigger.timeExist', {
+          time: formatTime(tempDate.time),
+          day: t(`workflow.blocks.trigger.days.${dayId}`),
+        });
 
 
-  updateData({ days: dataDays.sort() });
+        toast.error(message);
+
+        return;
+      }
+
+      daysArr.value[dayIndex].times.push(tempDate.time);
+    }
+  });
 }
 }
 function handleKeydownEvent(event) {
 function handleKeydownEvent(event) {
   event.preventDefault();
   event.preventDefault();
@@ -251,6 +364,23 @@ function updateDate(value) {
   updateData({ date });
   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(() => {
 onUnmounted(() => {
   window.removeEventListener('keydown', handleKeydownEvent);
   window.removeEventListener('keydown', handleKeydownEvent);
 });
 });

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

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

+ 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;

+ 3 - 0
src/locales/en/blocks.json

@@ -36,6 +36,9 @@
       "trigger": {
       "trigger": {
         "name": "Trigger",
         "name": "Trigger",
         "description": "Block where the workflow will start executing",
         "description": "Block where the workflow will start executing",
+        "addTime": "Add time",
+        "selectDay": "Select day",
+        "timeExist": "You alread add {time} on {day}",
         "days": [
         "days": [
           "Sunday",
           "Sunday",
           "Monday",
           "Monday",

+ 2 - 0
src/newtab/index.js

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

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

@@ -1,5 +1,6 @@
 import browser from 'webextension-polyfill';
 import browser from 'webextension-polyfill';
 import dayjs from 'dayjs';
 import dayjs from 'dayjs';
+import { isObject } from './helper';
 
 
 export async function cleanWorkflowTriggers(workflowId) {
 export async function cleanWorkflowTriggers(workflowId) {
   try {
   try {
@@ -32,15 +33,33 @@ export async function cleanWorkflowTriggers(workflowId) {
 export function registerSpecificDay(workflowId, data) {
 export function registerSpecificDay(workflowId, data) {
   if (data.days.length === 0) return null;
   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 =
   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, {
   return browser.alarms.create(workflowId, {
-    when: findDate.valueOf(),
+    when: findDate,
   });
   });
 }
 }
 
 

+ 5 - 0
yarn.lock

@@ -6913,6 +6913,11 @@ vue-router@^4.0.11:
   dependencies:
   dependencies:
     "@vue/devtools-api" "^6.0.0-beta.18"
     "@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:
 vue@3.2.19:
   version "3.2.19"
   version "3.2.19"
   resolved "https://registry.yarnpkg.com/vue/-/vue-3.2.19.tgz#da2c80a6a0271c7097fee9e31692adfd9d569c8f"
   resolved "https://registry.yarnpkg.com/vue/-/vue-3.2.19.tgz#da2c80a6a0271c7097fee9e31692adfd9d569c8f"