diff --git a/app/javascript/dashboard/components/buttons/ResolveAction.vue b/app/javascript/dashboard/components/buttons/ResolveAction.vue index 4c18f248c..163328e76 100644 --- a/app/javascript/dashboard/components/buttons/ResolveAction.vue +++ b/app/javascript/dashboard/components/buttons/ResolveAction.vue @@ -86,6 +86,7 @@ import { hasPressedAltAndMKey, } from 'shared/helpers/KeyboardHelpers'; +import { findSnoozeTime } from 'dashboard/helper/snoozeHelpers'; import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem.vue'; import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu.vue'; @@ -181,7 +182,7 @@ export default { onCmdSnoozeConversation(snoozeType) { this.toggleStatus( this.STATUS_TYPE.SNOOZED, - this.snoozeTimes[snoozeType] || null + findSnoozeTime(snoozeType) || null ); }, onCmdOpenConversation() { diff --git a/app/javascript/dashboard/components/widgets/conversation/ConversationHeader.vue b/app/javascript/dashboard/components/widgets/conversation/ConversationHeader.vue index f9ac4fb2d..98e71c6ec 100644 --- a/app/javascript/dashboard/components/widgets/conversation/ConversationHeader.vue +++ b/app/javascript/dashboard/components/widgets/conversation/ConversationHeader.vue @@ -57,7 +57,6 @@ import { hasPressedAltAndOKey } from 'shared/helpers/KeyboardHelpers'; import { mapGetters } from 'vuex'; import agentMixin from '../../../mixins/agentMixin.js'; import BackButton from '../BackButton'; -import differenceInHours from 'date-fns/differenceInHours'; import eventListenerMixins from 'shared/mixins/eventListenerMixins'; import inboxMixin from 'shared/mixins/inboxMixin'; import InboxName from '../InboxName'; @@ -65,6 +64,8 @@ import MoreActions from './MoreActions'; import Thumbnail from '../Thumbnail'; import wootConstants from 'dashboard/constants/globals'; import { conversationListPageURL } from 'dashboard/helper/URLHelper'; +import { conversationReopenTime } from 'dashboard/helper/snoozeHelpers'; + export default { components: { BackButton, @@ -125,17 +126,9 @@ export default { snoozedDisplayText() { const { snoozed_until: snoozedUntil } = this.currentChat; if (snoozedUntil) { - // When the snooze is applied, it schedules the unsnooze event to next day/week 9AM. - // By that logic if the time difference is less than or equal to 24 + 9 hours we can consider it tomorrow. - const MAX_TIME_DIFFERENCE = 33; - const isSnoozedUntilTomorrow = - differenceInHours(new Date(snoozedUntil), new Date()) <= - MAX_TIME_DIFFERENCE; - return this.$t( - isSnoozedUntilTomorrow - ? 'CONVERSATION.HEADER.SNOOZED_UNTIL_TOMORROW' - : 'CONVERSATION.HEADER.SNOOZED_UNTIL_NEXT_WEEK' - ); + return `${this.$t( + 'CONVERSATION.HEADER.SNOOZED_UNTIL' + )} ${conversationReopenTime(snoozedUntil)}`; } return this.$t('CONVERSATION.HEADER.SNOOZED_UNTIL_NEXT_REPLY'); }, diff --git a/app/javascript/dashboard/constants/globals.js b/app/javascript/dashboard/constants/globals.js index e5aaf7814..749774476 100644 --- a/app/javascript/dashboard/constants/globals.js +++ b/app/javascript/dashboard/constants/globals.js @@ -30,5 +30,12 @@ export default { TESTIMONIAL_URL: 'https://testimonials.cdn.chatwoot.com/content.json', SMALL_SCREEN_BREAKPOINT: 1024, AVAILABILITY_STATUS_KEYS: ['online', 'busy', 'offline'], + SNOOZE_OPTIONS: { + UNTIL_NEXT_REPLY: 'until_next_reply', + AN_HOUR_FROM_NOW: 'an_hour_from_now', + UNTIL_TOMORROW: 'until_tomorrow', + UNTIL_NEXT_WEEK: 'until_next_week', + UNTIL_NEXT_MONTH: 'until_next_month', + }, }; export const DEFAULT_REDIRECT_URL = '/app/'; diff --git a/app/javascript/dashboard/helper/snoozeHelpers.js b/app/javascript/dashboard/helper/snoozeHelpers.js new file mode 100644 index 000000000..07cb3a20f --- /dev/null +++ b/app/javascript/dashboard/helper/snoozeHelpers.js @@ -0,0 +1,66 @@ +import { + getUnixTime, + format, + add, + startOfWeek, + addWeeks, + startOfMonth, + isMonday, + isToday, + setHours, +} from 'date-fns'; +import wootConstants from 'dashboard/constants/globals'; + +const SNOOZE_OPTIONS = wootConstants.SNOOZE_OPTIONS; + +export const findStartOfNextWeek = currentDate => { + const startOfNextWeek = startOfWeek(addWeeks(currentDate, 1)); + return isMonday(startOfNextWeek) + ? startOfNextWeek + : add(startOfNextWeek, { + days: (8 - startOfNextWeek.getDay()) % 7, + }); +}; + +export const findStartOfNextMonth = currentDate => { + const startOfNextMonth = startOfMonth(add(currentDate, { months: 1 })); + return isMonday(startOfNextMonth) + ? startOfNextMonth + : add(startOfNextMonth, { + days: (8 - startOfNextMonth.getDay()) % 7, + }); +}; + +export const findNextDay = currentDate => { + return add(currentDate, { days: 1 }); +}; + +export const setHoursToNine = date => { + return setHours(date, 9, 0, 0); +}; + +export const findSnoozeTime = (snoozeType, currentDate = new Date()) => { + let parsedDate = null; + if (snoozeType === SNOOZE_OPTIONS.AN_HOUR_FROM_NOW) { + parsedDate = add(currentDate, { hours: 1 }); + } else if (snoozeType === SNOOZE_OPTIONS.UNTIL_TOMORROW) { + parsedDate = setHoursToNine(findNextDay(currentDate)); + } else if (snoozeType === SNOOZE_OPTIONS.UNTIL_NEXT_WEEK) { + parsedDate = setHoursToNine(findStartOfNextWeek(currentDate)); + } else if (snoozeType === SNOOZE_OPTIONS.UNTIL_NEXT_MONTH) { + parsedDate = setHoursToNine(findStartOfNextMonth(currentDate)); + } + + return parsedDate ? getUnixTime(parsedDate) : null; +}; +export const conversationReopenTime = snoozedUntil => { + if (!snoozedUntil) { + return null; + } + const date = new Date(snoozedUntil); + + if (isToday(date)) { + return format(date, 'h.mmaaa'); + } + return snoozedUntil ? format(date, 'd MMM, h.mmaaa') : null; +}; diff --git a/app/javascript/dashboard/helper/specs/snoozeHelpers.spec.js b/app/javascript/dashboard/helper/specs/snoozeHelpers.spec.js new file mode 100644 index 000000000..6da35d02c --- /dev/null +++ b/app/javascript/dashboard/helper/specs/snoozeHelpers.spec.js @@ -0,0 +1,105 @@ +import { + findSnoozeTime, + conversationReopenTime, + findStartOfNextWeek, + findStartOfNextMonth, + findNextDay, + setHoursToNine, +} from '../snoozeHelpers'; + +describe('#Snooze Helpers', () => { + describe('findStartOfNextWeek', () => { + it('should return first working day of next week if a date is passed', () => { + const today = new Date('06/16/2023'); + const startOfNextWeek = new Date('06/19/2023'); + expect(findStartOfNextWeek(today)).toEqual(startOfNextWeek); + }); + it('should return first working day of next week if a date is passed', () => { + const today = new Date('06/03/2023'); + const startOfNextWeek = new Date('06/05/2023'); + expect(findStartOfNextWeek(today)).toEqual(startOfNextWeek); + }); + }); + + describe('findStartOfNextMonth', () => { + it('should return first working day of next month if a valid date is passed', () => { + const today = new Date('06/21/2023'); + const startOfNextMonth = new Date('07/03/2023'); + expect(findStartOfNextMonth(today)).toEqual(startOfNextMonth); + }); + it('should return first working day of next month if a valid date is passed', () => { + const today = new Date('02/28/2023'); + const startOfNextMonth = new Date('03/06/2023'); + expect(findStartOfNextMonth(today)).toEqual(startOfNextMonth); + }); + }); + + describe('setHoursToNine', () => { + it('should return date with 9.00AM time', () => { + const nextDay = new Date('06/17/2023'); + nextDay.setHours(9, 0, 0, 0); + expect(setHoursToNine(nextDay)).toEqual(nextDay); + }); + }); + + describe('findSnoozeTime', () => { + it('should return nil if until_next_reply is passed', () => { + expect(findSnoozeTime('until_next_reply')).toEqual(null); + }); + + it('should return next hour time stamp if an_hour_from_now is passed', () => { + const nextHour = new Date(); + nextHour.setHours(nextHour.getHours() + 1); + expect(findSnoozeTime('an_hour_from_now')).toBeCloseTo( + Math.floor(nextHour.getTime() / 1000) + ); + }); + + it('should return next day 9.00AM time stamp until_tomorrow is passed', () => { + const today = new Date('06/16/2023'); + const nextDay = new Date('06/17/2023'); + nextDay.setHours(9, 0, 0, 0); + expect(findSnoozeTime('until_tomorrow', today)).toBeCloseTo( + nextDay.getTime() / 1000 + ); + }); + + it('should return next week monday 9.00AM time stamp if until_next_week is passed', () => { + const today = new Date('06/16/2023'); + const startOfNextWeek = new Date('06/19/2023'); + startOfNextWeek.setHours(9, 0, 0, 0); + expect(findSnoozeTime('until_next_week', today)).toBeCloseTo( + startOfNextWeek.getTime() / 1000 + ); + }); + + it('should return next month 9.00AM time stamp if until_next_month is passed', () => { + const today = new Date('06/21/2023'); + const startOfNextMonth = new Date('07/03/2023'); + startOfNextMonth.setHours(9, 0, 0, 0); + expect(findSnoozeTime('until_next_month', today)).toBeCloseTo( + startOfNextMonth.getTime() / 1000 + ); + }); + }); + + describe('conversationReopenTime', () => { + it('should return nil if snoozedUntil is nil', () => { + expect(conversationReopenTime(null)).toEqual(null); + }); + + it('should return formatted date if snoozedUntil is not nil', () => { + expect(conversationReopenTime('2023-06-07T09:00:00.000Z')).toEqual( + '7 Jun, 9.00am' + ); + }); + }); + + describe('findNextDay', () => { + it('should return next day', () => { + const today = new Date('06/16/2023'); + const nextDay = new Date('06/17/2023'); + expect(findNextDay(today)).toEqual(nextDay); + }); + }); +}); diff --git a/app/javascript/dashboard/i18n/locale/en/generalSettings.json b/app/javascript/dashboard/i18n/locale/en/generalSettings.json index f5af885a9..de41110c0 100644 --- a/app/javascript/dashboard/i18n/locale/en/generalSettings.json +++ b/app/javascript/dashboard/i18n/locale/en/generalSettings.json @@ -106,6 +106,7 @@ "CHANGE_ASSIGNEE": "Change Assignee", "CHANGE_PRIORITY": "Change Priority", "CHANGE_TEAM": "Change Team", + "SNOOZE_CONVERSATION": "Snooze Conversation", "ADD_LABEL": "Add label to the conversation", "REMOVE_LABEL": "Remove label from the conversation", "SETTINGS": "Settings" @@ -141,7 +142,10 @@ "SNOOZE_CONVERSATION": "Snooze Conversation", "UNTIL_NEXT_REPLY": "Until next reply", "UNTIL_NEXT_WEEK": "Until next week", - "UNTIL_TOMORROW": "Until tomorrow" + "UNTIL_TOMORROW": "Until tomorrow", + "UNTIL_NEXT_MONTH": "Until next month", + "AN_HOUR_FROM_NOW": "Until an hour from now", + "CUSTOM": "Custom..." } }, "DASHBOARD_APPS": { diff --git a/app/javascript/dashboard/routes/dashboard/commands/CommandBarIcons.js b/app/javascript/dashboard/routes/dashboard/commands/CommandBarIcons.js index 4a4bd9cf0..97831ae53 100644 --- a/app/javascript/dashboard/routes/dashboard/commands/CommandBarIcons.js +++ b/app/javascript/dashboard/routes/dashboard/commands/CommandBarIcons.js @@ -6,8 +6,7 @@ export const ICON_REMOVE_LABEL = ``; export const ICON_RESOLVE_CONVERSATION = ``; export const ICON_SEND_TRANSCRIPT = ``; -export const ICON_SNOOZE_CONVERSATION = ``; -export const ICON_SNOOZE_UNTIL_NEXT_REPLY = ``; +export const ICON_SNOOZE_CONVERSATION = ``; export const ICON_SNOOZE_UNTIL_NEXT_WEEK = ``; export const ICON_SNOOZE_UNTIL_TOMORRROW = ``; export const ICON_CONVERSATION_DASHBOARD = ``; diff --git a/app/javascript/dashboard/routes/dashboard/commands/conversationHotKeys.js b/app/javascript/dashboard/routes/dashboard/commands/conversationHotKeys.js index 01bd6a2f5..ea1ca868d 100644 --- a/app/javascript/dashboard/routes/dashboard/commands/conversationHotKeys.js +++ b/app/javascript/dashboard/routes/dashboard/commands/conversationHotKeys.js @@ -20,9 +20,6 @@ import { ICON_RESOLVE_CONVERSATION, ICON_SEND_TRANSCRIPT, ICON_SNOOZE_CONVERSATION, - ICON_SNOOZE_UNTIL_NEXT_REPLY, - ICON_SNOOZE_UNTIL_NEXT_WEEK, - ICON_SNOOZE_UNTIL_TOMORRROW, ICON_UNMUTE_CONVERSATION, ICON_PRIORITY_URGENT, ICON_PRIORITY_HIGH, @@ -31,6 +28,8 @@ import { ICON_PRIORITY_NONE, } from './CommandBarIcons'; +const SNOOZE_OPTIONS = wootConstants.SNOOZE_OPTIONS; + const OPEN_CONVERSATION_ACTIONS = [ { id: 'resolve_conversation', @@ -39,32 +38,60 @@ const OPEN_CONVERSATION_ACTIONS = [ icon: ICON_RESOLVE_CONVERSATION, handler: () => bus.$emit(CMD_RESOLVE_CONVERSATION), }, +]; + +const SNOOZE_CONVERSATION_ACTIONS = [ { id: 'snooze_conversation', title: 'COMMAND_BAR.COMMANDS.SNOOZE_CONVERSATION', icon: ICON_SNOOZE_CONVERSATION, - children: ['until_next_reply', 'until_tomorrow', 'until_next_week'], + children: Object.values(SNOOZE_OPTIONS), }, + { - id: 'until_next_reply', + id: SNOOZE_OPTIONS.UNTIL_NEXT_REPLY, title: 'COMMAND_BAR.COMMANDS.UNTIL_NEXT_REPLY', parent: 'snooze_conversation', - icon: ICON_SNOOZE_UNTIL_NEXT_REPLY, - handler: () => bus.$emit(CMD_SNOOZE_CONVERSATION, 'nextReply'), + section: 'COMMAND_BAR.SECTIONS.SNOOZE_CONVERSATION', + icon: ICON_SNOOZE_CONVERSATION, + handler: () => + bus.$emit(CMD_SNOOZE_CONVERSATION, SNOOZE_OPTIONS.UNTIL_NEXT_REPLY), }, { - id: 'until_tomorrow', + id: SNOOZE_OPTIONS.AN_HOUR_FROM_NOW, + title: 'COMMAND_BAR.COMMANDS.AN_HOUR_FROM_NOW', + parent: 'snooze_conversation', + section: 'COMMAND_BAR.SECTIONS.SNOOZE_CONVERSATION', + icon: ICON_SNOOZE_CONVERSATION, + handler: () => + bus.$emit(CMD_SNOOZE_CONVERSATION, SNOOZE_OPTIONS.AN_HOUR_FROM_NOW), + }, + { + id: SNOOZE_OPTIONS.UNTIL_TOMORROW, title: 'COMMAND_BAR.COMMANDS.UNTIL_TOMORROW', + section: 'COMMAND_BAR.SECTIONS.SNOOZE_CONVERSATION', parent: 'snooze_conversation', - icon: ICON_SNOOZE_UNTIL_TOMORRROW, - handler: () => bus.$emit(CMD_SNOOZE_CONVERSATION, 'tomorrow'), + icon: ICON_SNOOZE_CONVERSATION, + handler: () => + bus.$emit(CMD_SNOOZE_CONVERSATION, SNOOZE_OPTIONS.UNTIL_TOMORROW), }, { - id: 'until_next_week', + id: SNOOZE_OPTIONS.UNTIL_NEXT_WEEK, title: 'COMMAND_BAR.COMMANDS.UNTIL_NEXT_WEEK', + section: 'COMMAND_BAR.SECTIONS.SNOOZE_CONVERSATION', parent: 'snooze_conversation', - icon: ICON_SNOOZE_UNTIL_NEXT_WEEK, - handler: () => bus.$emit(CMD_SNOOZE_CONVERSATION, 'nextWeek'), + icon: ICON_SNOOZE_CONVERSATION, + handler: () => + bus.$emit(CMD_SNOOZE_CONVERSATION, SNOOZE_OPTIONS.UNTIL_NEXT_WEEK), + }, + { + id: SNOOZE_OPTIONS.UNTIL_NEXT_MONTH, + title: 'COMMAND_BAR.COMMANDS.UNTIL_NEXT_MONTH', + section: 'COMMAND_BAR.SECTIONS.SNOOZE_CONVERSATION', + parent: 'snooze_conversation', + icon: ICON_SNOOZE_CONVERSATION, + handler: () => + bus.$emit(CMD_SNOOZE_CONVERSATION, SNOOZE_OPTIONS.UNTIL_NEXT_MONTH), }, ]; @@ -135,6 +162,7 @@ export default { conversationId() { return this.currentChat?.id; }, + statusActions() { const isOpen = this.currentChat?.status === wootConstants.STATUS_TYPE.OPEN; @@ -145,7 +173,10 @@ export default { let actions = []; if (isOpen) { - actions = OPEN_CONVERSATION_ACTIONS; + actions = [ + ...OPEN_CONVERSATION_ACTIONS, + ...SNOOZE_CONVERSATION_ACTIONS, + ]; } else if (isResolved || isSnoozed) { actions = RESOLVED_CONVERSATION_ACTIONS; } @@ -296,6 +327,7 @@ export default { SEND_TRANSCRIPT_ACTION, ]); }, + conversationHotKeys() { if (isAConversationRoute(this.$route.name)) { return [