From 0bd62cf33f4f27b2e3b154e84a14bad973c9318b Mon Sep 17 00:00:00 2001 From: Fayaz Ahmed Date: Mon, 5 Aug 2024 15:41:45 +0530 Subject: [PATCH] chore: Replace multiple mixins to simgle use composables --- .../dashboard/composables/useAvailability.js | 116 ++++++++++++++++++ .../dashboard/composables/useCampaign.js | 32 +++++ .../dashboard/composables/useDarkMode.js | 35 ++++++ .../dashboard/composables/useHook.js | 16 +++ .../composables/useKeyboardEventListener.js | 67 ++++++++++ .../useMentionSelectionKeyboard.js | 89 ++++++++++++++ .../dashboard/composables/useMessage.js | 22 ++++ .../composables/useMessageFormatter.js | 48 ++++++++ .../dashboard/composables/usePortal.js | 36 ++++++ .../dashboard/composables/useRouterHelper.js | 27 ++++ 10 files changed, 488 insertions(+) create mode 100644 app/javascript/dashboard/composables/useAvailability.js create mode 100644 app/javascript/dashboard/composables/useCampaign.js create mode 100644 app/javascript/dashboard/composables/useDarkMode.js create mode 100644 app/javascript/dashboard/composables/useHook.js create mode 100644 app/javascript/dashboard/composables/useKeyboardEventListener.js create mode 100644 app/javascript/dashboard/composables/useMentionSelectionKeyboard.js create mode 100644 app/javascript/dashboard/composables/useMessage.js create mode 100644 app/javascript/dashboard/composables/useMessageFormatter.js create mode 100644 app/javascript/dashboard/composables/usePortal.js create mode 100644 app/javascript/dashboard/composables/useRouterHelper.js diff --git a/app/javascript/dashboard/composables/useAvailability.js b/app/javascript/dashboard/composables/useAvailability.js new file mode 100644 index 000000000..705df68ee --- /dev/null +++ b/app/javascript/dashboard/composables/useAvailability.js @@ -0,0 +1,116 @@ +import { computed } from 'vue'; +import { utcToZonedTime } from 'date-fns-tz'; +import { isTimeAfter } from 'shared/helpers/DateHelper'; + +/** + * Composable for handling availability-related computations. + * @param {Object} i18n - Vue i18n instance for translations. + * @returns {Object} An object containing computed properties and methods for availability management. + */ +export function useAvailability(i18n) { + const channelConfig = computed(() => window.chatwootWebChannel); + const replyTime = computed(() => window.chatwootWebChannel.replyTime); + + const replyTimeStatus = computed(() => { + switch (replyTime.value) { + case 'in_a_few_minutes': + return i18n.t('REPLY_TIME.IN_A_FEW_MINUTES'); + case 'in_a_few_hours': + return i18n.t('REPLY_TIME.IN_A_FEW_HOURS'); + case 'in_a_day': + return i18n.t('REPLY_TIME.IN_A_DAY'); + default: + return i18n.t('REPLY_TIME.IN_A_FEW_HOURS'); + } + }); + + const replyWaitMessage = computed(() => { + const { workingHoursEnabled } = channelConfig.value; + if (workingHoursEnabled) { + return isOnline.value + ? replyTimeStatus.value + : `${i18n.t('REPLY_TIME.BACK_IN')} ${timeLeftToBackInOnline.value}`; + } + return isOnline.value + ? replyTimeStatus.value + : i18n.t('TEAM_AVAILABILITY.OFFLINE'); + }); + + const outOfOfficeMessage = computed( + () => channelConfig.value.outOfOfficeMessage + ); + + const currentDayAvailability = computed(() => { + const { utcOffset } = channelConfig.value; + const dayOfTheWeek = getDateWithOffset(utcOffset).getDay(); + const [workingHourConfig = {}] = channelConfig.value.workingHours.filter( + workingHour => workingHour.day_of_week === dayOfTheWeek + ); + return { + closedAllDay: workingHourConfig.closed_all_day, + openHour: workingHourConfig.open_hour, + openMinute: workingHourConfig.open_minutes, + closeHour: workingHourConfig.close_hour, + closeMinute: workingHourConfig.close_minutes, + openAllDay: workingHourConfig.open_all_day, + }; + }); + + const isInBetweenTheWorkingHours = computed(() => { + const { + openHour, + openMinute, + closeHour, + closeMinute, + closedAllDay, + openAllDay, + } = currentDayAvailability.value; + + if (openAllDay) { + return true; + } + + if (closedAllDay) { + return false; + } + + const { utcOffset } = channelConfig.value; + const today = getDateWithOffset(utcOffset); + const currentHours = today.getHours(); + const currentMinutes = today.getMinutes(); + const isAfterStartTime = isTimeAfter( + currentHours, + currentMinutes, + openHour, + openMinute + ); + const isBeforeEndTime = isTimeAfter( + closeHour, + closeMinute, + currentHours, + currentMinutes + ); + return isAfterStartTime && isBeforeEndTime; + }); + + const isInBusinessHours = computed(() => { + const { workingHoursEnabled } = channelConfig.value; + return workingHoursEnabled ? isInBetweenTheWorkingHours.value : true; + }); + + const getDateWithOffset = utcOffset => { + return utcToZonedTime(new Date().toISOString(), utcOffset); + }; + + return { + channelConfig, + replyTime, + replyTimeStatus, + replyWaitMessage, + outOfOfficeMessage, + currentDayAvailability, + isInBetweenTheWorkingHours, + isInBusinessHours, + getDateWithOffset, + }; +} diff --git a/app/javascript/dashboard/composables/useCampaign.js b/app/javascript/dashboard/composables/useCampaign.js new file mode 100644 index 000000000..cad09fbc0 --- /dev/null +++ b/app/javascript/dashboard/composables/useCampaign.js @@ -0,0 +1,32 @@ +import { computed } from 'vue'; +import { useRoute } from 'vue-router'; +import { CAMPAIGN_TYPES } from '../constants/campaign'; + +/** + * Composable for handling campaign-related computations. + * @returns {Object} An object containing computed properties for campaign types. + */ +export function useCampaign() { + const route = useRoute(); + + const campaignType = computed(() => { + const campaignTypeMap = { + ongoing_campaigns: CAMPAIGN_TYPES.ONGOING, + one_off: CAMPAIGN_TYPES.ONE_OFF, + }; + return campaignTypeMap[route.name]; + }); + + const isOngoingType = computed( + () => campaignType.value === CAMPAIGN_TYPES.ONGOING + ); + const isOneOffType = computed( + () => campaignType.value === CAMPAIGN_TYPES.ONE_OFF + ); + + return { + campaignType, + isOngoingType, + isOneOffType, + }; +} diff --git a/app/javascript/dashboard/composables/useDarkMode.js b/app/javascript/dashboard/composables/useDarkMode.js new file mode 100644 index 000000000..021fac8b8 --- /dev/null +++ b/app/javascript/dashboard/composables/useDarkMode.js @@ -0,0 +1,35 @@ +import { computed } from 'vue'; +import { useStore } from 'vuex'; + +/** + * Composable for handling dark mode preferences and utility functions. + * @returns {Object} An object containing computed properties and methods for dark mode management. + */ +export function useDarkMode() { + const store = useStore(); + + const darkMode = computed(() => store.getters['appConfig/darkMode']); + + const prefersDarkMode = computed(() => { + const isOSOnDarkMode = + darkMode.value === 'auto' && + window.matchMedia('(prefers-color-scheme: dark)').matches; + return isOSOnDarkMode || darkMode.value === 'dark'; + }); + + const dm = (light, dark) => { + if (darkMode.value === 'light') { + return light; + } + if (darkMode.value === 'dark') { + return dark; + } + return `${light} ${dark}`; + }; + + return { + darkMode, + prefersDarkMode, + dm, + }; +} diff --git a/app/javascript/dashboard/composables/useHook.js b/app/javascript/dashboard/composables/useHook.js new file mode 100644 index 000000000..e21794201 --- /dev/null +++ b/app/javascript/dashboard/composables/useHook.js @@ -0,0 +1,16 @@ +import { computed } from 'vue'; + +/** + * Composable for handling hook-related computations. + * @param {Object} integration - The integration object containing hook information. + * @returns {Object} An object containing computed properties for hook types and connections. + */ +export function useHook(integration) { + const isHookTypeInbox = computed(() => integration.hook_type === 'inbox'); + const hasConnectedHooks = computed(() => !!integration.hooks.length); + + return { + isHookTypeInbox, + hasConnectedHooks, + }; +} diff --git a/app/javascript/dashboard/composables/useKeyboardEventListener.js b/app/javascript/dashboard/composables/useKeyboardEventListener.js new file mode 100644 index 000000000..96da4c7ba --- /dev/null +++ b/app/javascript/dashboard/composables/useKeyboardEventListener.js @@ -0,0 +1,67 @@ +import { onMounted, onBeforeUnmount } from 'vue'; +import { isActiveElementTypeable, isEscape } from '../helpers/KeyboardHelpers'; +import { createKeybindingsHandler } from 'tinykeys'; + +// Store that stores the handler globally, and only gets reset on reload +const taggedHandlers = []; + +/** + * Composable for handling keyboard event listeners. + * @returns {Object} An object containing methods for managing keyboard event listeners. + */ +export function useKeyboardEventListener() { + let handlerIndex = -1; + + const wrapEventsInKeybindingsHandler = events => { + const wrappedEvents = {}; + Object.keys(events).forEach(eventName => { + wrappedEvents[eventName] = keydownWrapper(events[eventName]); + }); + return wrappedEvents; + }; + + const keydownWrapper = handler => { + return e => { + const actionToPerform = + typeof handler === 'function' ? handler : handler.action; + const allowOnFocusedInput = + typeof handler === 'function' ? false : handler.allowOnFocusedInput; + + const isTypeable = isActiveElementTypeable(e); + + if (isTypeable) { + if (isEscape(e)) { + e.target.blur(); + } + + if (!allowOnFocusedInput) return; + } + + actionToPerform(e); + }; + }; + + const addEventHandler = events => { + const wrappedEvents = wrapEventsInKeybindingsHandler(events); + const keydownHandler = createKeybindingsHandler(wrappedEvents); + handlerIndex = taggedHandlers.push(keydownHandler) - 1; + document.addEventListener('keydown', keydownHandler); + }; + + const removeEventHandler = () => { + if (handlerIndex !== -1) { + const handlerToRemove = taggedHandlers[handlerIndex]; + document.removeEventListener('keydown', handlerToRemove); + handlerIndex = -1; + } + }; + + onBeforeUnmount(() => { + removeEventHandler(); + }); + + return { + addEventHandler, + removeEventHandler, + }; +} diff --git a/app/javascript/dashboard/composables/useMentionSelectionKeyboard.js b/app/javascript/dashboard/composables/useMentionSelectionKeyboard.js new file mode 100644 index 000000000..3634860a0 --- /dev/null +++ b/app/javascript/dashboard/composables/useMentionSelectionKeyboard.js @@ -0,0 +1,89 @@ +import { ref, onMounted, onBeforeUnmount } from 'vue'; +import { useKeyboardEventListener } from './useKeyboardEventListener'; + +/** + * Composable for handling keyboard-based mention selection. + * @param {Object} options - Configuration options. + * @param {Array} options.items - The list of items to select from. + * @param {Function} options.onSelect - Callback function when an item is selected. + * @param {Function} options.adjustScroll - Function to adjust scroll position. + * @returns {Object} An object containing methods and state for mention selection. + */ +export function useMentionSelectionKeyboard({ items, onSelect, adjustScroll }) { + const selectedIndex = ref(0); + + const moveSelectionUp = () => { + if (!selectedIndex.value) { + selectedIndex.value = items.length - 1; + } else { + selectedIndex.value -= 1; + } + adjustScroll(); + }; + + const moveSelectionDown = () => { + if (selectedIndex.value === items.length - 1) { + selectedIndex.value = 0; + } else { + selectedIndex.value += 1; + } + adjustScroll(); + }; + + const getKeyboardEvents = () => ({ + ArrowUp: { + action: e => { + moveSelectionUp(); + e.preventDefault(); + }, + allowOnFocusedInput: true, + }, + 'Control+KeyP': { + action: e => { + moveSelectionUp(); + e.preventDefault(); + }, + allowOnFocusedInput: true, + }, + ArrowDown: { + action: e => { + moveSelectionDown(); + e.preventDefault(); + }, + allowOnFocusedInput: true, + }, + 'Control+KeyN': { + action: e => { + moveSelectionDown(); + e.preventDefault(); + }, + allowOnFocusedInput: true, + }, + Enter: { + action: e => { + onSelect(); + e.preventDefault(); + }, + allowOnFocusedInput: true, + }, + }); + + const { addEventHandler, removeEventHandler } = useKeyboardEventListener(); + + onMounted(() => { + const events = getKeyboardEvents(); + if (events) { + addEventHandler(events); + } + }); + + onBeforeUnmount(() => { + removeEventHandler(); + }); + + return { + selectedIndex, + moveSelectionUp, + moveSelectionDown, + }; +} diff --git a/app/javascript/dashboard/composables/useMessage.js b/app/javascript/dashboard/composables/useMessage.js new file mode 100644 index 000000000..97b7a7800 --- /dev/null +++ b/app/javascript/dashboard/composables/useMessage.js @@ -0,0 +1,22 @@ +import { computed } from 'vue'; + +/** + * Composable for handling message-related computations. + * @param {Object} message - The message object to be processed. + * @returns {Object} An object containing computed properties for message content and attachments. + */ +export function useMessage(message) { + const messageContentAttributes = computed(() => { + const { content_attributes: attribute = {} } = message; + return attribute; + }); + + const hasAttachments = computed(() => { + return !!(message.attachments && message.attachments.length > 0); + }); + + return { + messageContentAttributes, + hasAttachments, + }; +} diff --git a/app/javascript/dashboard/composables/useMessageFormatter.js b/app/javascript/dashboard/composables/useMessageFormatter.js new file mode 100644 index 000000000..137cd4b69 --- /dev/null +++ b/app/javascript/dashboard/composables/useMessageFormatter.js @@ -0,0 +1,48 @@ +import MessageFormatter from '../helpers/MessageFormatter'; + +/** + * Composable for handling message formatting operations. + * @returns {Object} An object containing methods for formatting and manipulating messages. + */ +export function useMessageFormatter() { + const formatMessage = (message, isATweet, isAPrivateNote) => { + const messageFormatter = new MessageFormatter( + message, + isATweet, + isAPrivateNote + ); + return messageFormatter.formattedMessage; + }; + + const getPlainText = (message, isATweet) => { + const messageFormatter = new MessageFormatter(message, isATweet); + return messageFormatter.plainText; + }; + + const truncateMessage = (description = '') => { + if (description.length < 100) { + return description; + } + return `${description.slice(0, 97)}...`; + }; + + const highlightContent = ( + content = '', + searchTerm = '', + highlightClass = '' + ) => { + const plainTextContent = getPlainText(content); + const escapedSearchTerm = searchTerm.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + return plainTextContent.replace( + new RegExp(`(${escapedSearchTerm})`, 'ig'), + `$1` + ); + }; + + return { + formatMessage, + getPlainText, + truncateMessage, + highlightContent, + }; +} diff --git a/app/javascript/dashboard/composables/usePortal.js b/app/javascript/dashboard/composables/usePortal.js new file mode 100644 index 000000000..724ce4eb6 --- /dev/null +++ b/app/javascript/dashboard/composables/usePortal.js @@ -0,0 +1,36 @@ +import { computed } from 'vue'; +import { useRoute } from 'vue-router'; +import { useStore } from 'vuex'; +import { frontendURL } from 'dashboard/helper/URLHelper'; +import allLocales from 'shared/constants/locales.js'; + +/** + * Composable for handling portal-related computations and methods. + * @returns {Object} An object containing computed properties and methods for portal management. + */ +export function usePortal() { + const route = useRoute(); + const store = useStore(); + + const accountId = computed(() => store.getters.getCurrentAccountId); + const portalSlug = computed(() => route.params.portalSlug); + const locale = computed(() => route.params.locale); + + const articleUrl = id => { + return frontendURL( + `accounts/${accountId.value}/portals/${portalSlug.value}/${locale.value}/articles/${id}` + ); + }; + + const localeName = code => { + return allLocales[code]; + }; + + return { + accountId, + portalSlug, + locale, + articleUrl, + localeName, + }; +} diff --git a/app/javascript/dashboard/composables/useRouterHelper.js b/app/javascript/dashboard/composables/useRouterHelper.js new file mode 100644 index 000000000..b81695d79 --- /dev/null +++ b/app/javascript/dashboard/composables/useRouterHelper.js @@ -0,0 +1,27 @@ +import { useRouter, useRoute } from 'vue-router'; + +/** + * Composable for handling router-related operations. + * @returns {Object} An object containing methods for router manipulation. + */ +export function useRouterHelper() { + const router = useRouter(); + const route = useRoute(); + + /** + * Replaces the current route with a new one if it's different. + * @param {string} name - The name of the route to replace with. + * @param {Object} params - The params to pass to the new route. + * @returns {Promise|undefined} A promise that resolves when the navigation is complete, or undefined if no navigation occurs. + */ + const replaceRoute = async (name, params = {}) => { + if (route.name !== name) { + return router.replace({ name, params }); + } + return undefined; + }; + + return { + replaceRoute, + }; +}