From 3e07320d226acc07573c5e2a94d4e1193912698f Mon Sep 17 00:00:00 2001 From: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Date: Wed, 27 Mar 2024 13:19:51 +0530 Subject: [PATCH] feat: SLA threshold card component (#9163) - Component to display SLA timer in the conversation card and header --- .../conversation/components/SLACardLabel.vue | 103 ++++++++++++++++++ .../dashboard/helper/directives/resize.js | 41 +++++++ .../helper/specs/directives/resize.spec.js | 78 +++++++++++++ .../i18n/locale/en/conversation.json | 9 +- app/javascript/packs/application.js | 2 + 5 files changed, 232 insertions(+), 1 deletion(-) create mode 100644 app/javascript/dashboard/components/widgets/conversation/components/SLACardLabel.vue create mode 100644 app/javascript/dashboard/helper/directives/resize.js create mode 100644 app/javascript/dashboard/helper/specs/directives/resize.spec.js diff --git a/app/javascript/dashboard/components/widgets/conversation/components/SLACardLabel.vue b/app/javascript/dashboard/components/widgets/conversation/components/SLACardLabel.vue new file mode 100644 index 000000000..381ca53b1 --- /dev/null +++ b/app/javascript/dashboard/components/widgets/conversation/components/SLACardLabel.vue @@ -0,0 +1,103 @@ + + + + + + {{ slaStatusText }} + + + + {{ slaStatus.threshold }} + + + + + diff --git a/app/javascript/dashboard/helper/directives/resize.js b/app/javascript/dashboard/helper/directives/resize.js new file mode 100644 index 000000000..35e5315b0 --- /dev/null +++ b/app/javascript/dashboard/helper/directives/resize.js @@ -0,0 +1,41 @@ +import { debounce } from '@chatwoot/utils'; + +const RESIZE_OBSERVER_DEBOUNCE_TIME = 100; + +function createResizeObserver(el, binding) { + const { value } = binding; + const observer = new ResizeObserver( + debounce(entries => { + const entry = entries[0]; + if (entry && value && typeof value === 'function') { + value(entry); + } + }, RESIZE_OBSERVER_DEBOUNCE_TIME) + ); + + el.cwResizeObserver = observer; + observer.observe(el); +} + +function destroyResizeObserver(el) { + if (el.cwResizeObserver) { + el.cwResizeObserver.unobserve(el); + el.cwResizeObserver.disconnect(); + delete el.cwResizeObserver; + } +} + +export default { + bind(el, binding) { + createResizeObserver(el, binding); + }, + update(el, binding) { + if (binding.oldValue !== binding.value) { + destroyResizeObserver(el); + createResizeObserver(el, binding); + } + }, + unbind(el) { + destroyResizeObserver(el); + }, +}; diff --git a/app/javascript/dashboard/helper/specs/directives/resize.spec.js b/app/javascript/dashboard/helper/specs/directives/resize.spec.js new file mode 100644 index 000000000..fa099f40a --- /dev/null +++ b/app/javascript/dashboard/helper/specs/directives/resize.spec.js @@ -0,0 +1,78 @@ +import resize from '../../directives/resize'; + +class ResizeObserverMock { + // eslint-disable-next-line class-methods-use-this + observe() {} + + // eslint-disable-next-line class-methods-use-this + unobserve() {} + + // eslint-disable-next-line class-methods-use-this + disconnect() {} +} + +describe('resize directive', () => { + let el; + let binding; + let observer; + + beforeEach(() => { + el = document.createElement('div'); + binding = { + value: jest.fn(), + }; + observer = { + observe: jest.fn(), + unobserve: jest.fn(), + disconnect: jest.fn(), + }; + window.ResizeObserver = ResizeObserverMock; + jest.spyOn(window, 'ResizeObserver').mockImplementation(() => observer); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should create ResizeObserver on bind', () => { + resize.bind(el, binding); + + expect(ResizeObserver).toHaveBeenCalled(); + expect(observer.observe).toHaveBeenCalledWith(el); + }); + + it('should call callback on observer callback', () => { + el = document.createElement('div'); + binding = { + value: jest.fn(), + }; + + resize.bind(el, binding); + + const entries = [{ contentRect: { width: 100, height: 100 } }]; + const callback = binding.value; + callback(entries[0]); + + expect(binding.value).toHaveBeenCalledWith(entries[0]); + }); + + it('should destroy and recreate observer on update', () => { + resize.bind(el, binding); + + resize.update(el, { ...binding, oldValue: 'old' }); + + expect(observer.unobserve).toHaveBeenCalledWith(el); + expect(observer.disconnect).toHaveBeenCalled(); + expect(ResizeObserver).toHaveBeenCalledTimes(2); + expect(observer.observe).toHaveBeenCalledTimes(2); + }); + + it('should destroy observer on unbind', () => { + resize.bind(el, binding); + + resize.unbind(el); + + expect(observer.unobserve).toHaveBeenCalledWith(el); + expect(observer.disconnect).toHaveBeenCalled(); + }); +}); diff --git a/app/javascript/dashboard/i18n/locale/en/conversation.json b/app/javascript/dashboard/i18n/locale/en/conversation.json index 227c802d6..2bdf2af7a 100644 --- a/app/javascript/dashboard/i18n/locale/en/conversation.json +++ b/app/javascript/dashboard/i18n/locale/en/conversation.json @@ -64,7 +64,14 @@ "SNOOZED_UNTIL": "Snoozed until", "SNOOZED_UNTIL_TOMORROW": "Snoozed until tomorrow", "SNOOZED_UNTIL_NEXT_WEEK": "Snoozed until next week", - "SNOOZED_UNTIL_NEXT_REPLY": "Snoozed until next reply" + "SNOOZED_UNTIL_NEXT_REPLY": "Snoozed until next reply", + "SLA_STATUS": { + "FRT": "FRT {status}", + "NRT": "NRT {status}", + "RT": "RT {status}", + "BREACH": "breach", + "DUE": "due" + } }, "RESOLVE_DROPDOWN": { "MARK_PENDING": "Mark as pending", diff --git a/app/javascript/packs/application.js b/app/javascript/packs/application.js index 18354bc62..61d6dec8d 100644 --- a/app/javascript/packs/application.js +++ b/app/javascript/packs/application.js @@ -30,6 +30,7 @@ import FluentIcon from 'shared/components/FluentIcon/DashboardIcon'; import VueDOMPurifyHTML from 'vue-dompurify-html'; import { domPurifyConfig } from '../shared/helpers/HTMLSanitizer'; import AnalyticsPlugin from '../dashboard/helper/AnalyticsHelper/plugin'; +import resizeDirective from '../dashboard/helper/directives/resize.js'; Vue.config.env = process.env; @@ -78,6 +79,7 @@ Vue.component('woot-switch', WootSwitch); Vue.component('woot-wizard', WootWizard); Vue.component('fluent-icon', FluentIcon); +Vue.directive('resize', resizeDirective); const i18nConfig = new VueI18n({ locale: 'en', messages: i18n,