mirror of
				https://github.com/lingble/chatwoot.git
				synced 2025-11-03 20:48:07 +00:00 
			
		
		
		
	feat: UI to show the SLA threshold in chat screen (#9146)
- UI will show the breach in the conversation list. - UI will show the breach in the conversation header. Fixes: https://linear.app/chatwoot/issue/CW-3146/update-the-ui-to-show-the-breach-in-the-conversation-list Fixes: https://linear.app/chatwoot/issue/CW-3144/ui-update-to-show-the-breachgoing-to-breach
This commit is contained in:
		@@ -1,14 +1,14 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div
 | 
			
		||||
    class="conversations-list-wrap flex-basis-clamp flex-shrink-0 overflow-hidden flex flex-col border-r rtl:border-r-0 rtl:border-l border-slate-50 dark:border-slate-800/50"
 | 
			
		||||
    :class="{
 | 
			
		||||
      hide: !showConversationList,
 | 
			
		||||
      'list--full-width': isOnExpandedLayout,
 | 
			
		||||
    }"
 | 
			
		||||
    class="flex flex-col flex-shrink-0 overflow-hidden border-r conversations-list-wrap rtl:border-r-0 rtl:border-l border-slate-50 dark:border-slate-800/50"
 | 
			
		||||
    :class="[
 | 
			
		||||
      { hidden: !showConversationList },
 | 
			
		||||
      isOnExpandedLayout ? 'basis-full' : 'flex-basis-clamp',
 | 
			
		||||
    ]"
 | 
			
		||||
  >
 | 
			
		||||
    <slot />
 | 
			
		||||
    <div
 | 
			
		||||
      class="flex items-center justify-between py-0 px-4"
 | 
			
		||||
      class="flex items-center justify-between px-4 py-0"
 | 
			
		||||
      :class="{
 | 
			
		||||
        'pb-3 border-b border-slate-75 dark:border-slate-700':
 | 
			
		||||
          hasAppliedFiltersOrActiveFolders,
 | 
			
		||||
@@ -16,7 +16,7 @@
 | 
			
		||||
    >
 | 
			
		||||
      <div class="flex max-w-[85%] justify-center items-center">
 | 
			
		||||
        <h1
 | 
			
		||||
          class="text-xl break-words overflow-hidden whitespace-nowrap font-medium text-ellipsis text-black-900 dark:text-slate-100 mb-0"
 | 
			
		||||
          class="text-xl font-medium break-words truncate text-black-900 dark:text-slate-100"
 | 
			
		||||
          :title="pageTitle"
 | 
			
		||||
        >
 | 
			
		||||
          {{ pageTitle }}
 | 
			
		||||
@@ -107,7 +107,7 @@
 | 
			
		||||
 | 
			
		||||
    <p
 | 
			
		||||
      v-if="!chatListLoading && !conversationList.length"
 | 
			
		||||
      class="overflow-auto p-4 flex justify-center items-center"
 | 
			
		||||
      class="flex items-center justify-center p-4 overflow-auto"
 | 
			
		||||
    >
 | 
			
		||||
      {{ $t('CHAT_LIST.LIST.404') }}
 | 
			
		||||
    </p>
 | 
			
		||||
@@ -127,7 +127,7 @@
 | 
			
		||||
    />
 | 
			
		||||
    <div
 | 
			
		||||
      ref="conversationList"
 | 
			
		||||
      class="conversations-list flex-1"
 | 
			
		||||
      class="flex-1 conversations-list"
 | 
			
		||||
      :class="{ 'overflow-hidden': isContextMenuOpen }"
 | 
			
		||||
    >
 | 
			
		||||
      <virtual-list
 | 
			
		||||
@@ -136,16 +136,16 @@
 | 
			
		||||
        :data-sources="conversationList"
 | 
			
		||||
        :data-component="itemComponent"
 | 
			
		||||
        :extra-props="virtualListExtraProps"
 | 
			
		||||
        class="w-full overflow-auto h-full"
 | 
			
		||||
        class="w-full h-full overflow-auto"
 | 
			
		||||
        footer-tag="div"
 | 
			
		||||
      >
 | 
			
		||||
        <template #footer>
 | 
			
		||||
          <div v-if="chatListLoading" class="text-center">
 | 
			
		||||
            <span class="spinner mt-4 mb-4" />
 | 
			
		||||
            <span class="mt-4 mb-4 spinner" />
 | 
			
		||||
          </div>
 | 
			
		||||
          <p
 | 
			
		||||
            v-if="showEndOfListMessage"
 | 
			
		||||
            class="text-center text-slate-400 dark:text-slate-300 p-4"
 | 
			
		||||
            class="p-4 text-center text-slate-400 dark:text-slate-300"
 | 
			
		||||
          >
 | 
			
		||||
            {{ $t('CHAT_LIST.EOF') }}
 | 
			
		||||
          </p>
 | 
			
		||||
@@ -1034,24 +1034,10 @@ export default {
 | 
			
		||||
</style>
 | 
			
		||||
 | 
			
		||||
<style scoped lang="scss">
 | 
			
		||||
.conversations-list-wrap {
 | 
			
		||||
  &.hide {
 | 
			
		||||
    @apply hidden;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &.list--full-width {
 | 
			
		||||
    @apply basis-full;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.conversations-list {
 | 
			
		||||
  @apply overflow-hidden hover:overflow-y-auto;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.load-more--button {
 | 
			
		||||
  @apply text-center rounded-none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.tab--chat-type {
 | 
			
		||||
  @apply py-0 px-4;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div
 | 
			
		||||
    class="ltr:mr-1 rtl:ml-1 mb-1"
 | 
			
		||||
    class="inline-flex ltr:mr-1 rtl:ml-1 mb-1"
 | 
			
		||||
    :class="labelClass"
 | 
			
		||||
    :style="labelStyle"
 | 
			
		||||
    :title="description"
 | 
			
		||||
@@ -111,7 +111,7 @@ export default {
 | 
			
		||||
 | 
			
		||||
<style scoped lang="scss">
 | 
			
		||||
.label {
 | 
			
		||||
  @apply inline-flex items-center font-medium text-xs rounded-[4px] gap-1 p-1 bg-slate-50 dark:bg-slate-700 text-slate-800 dark:text-slate-100 border border-solid border-slate-75 dark:border-slate-600 h-6;
 | 
			
		||||
  @apply items-center font-medium text-xs rounded-[4px] gap-1 p-1 bg-slate-50 dark:bg-slate-700 text-slate-800 dark:text-slate-100 border border-solid border-slate-75 dark:border-slate-600 h-6;
 | 
			
		||||
 | 
			
		||||
  &.small {
 | 
			
		||||
    @apply text-xs py-0.5 px-1 leading-tight h-5;
 | 
			
		||||
 
 | 
			
		||||
@@ -86,7 +86,11 @@
 | 
			
		||||
          {{ unreadCount > 9 ? '9+' : unreadCount }}
 | 
			
		||||
        </span>
 | 
			
		||||
      </div>
 | 
			
		||||
      <card-labels :conversation-id="chat.id" />
 | 
			
		||||
      <card-labels :conversation-id="chat.id" class="mt-0.5 mx-2 mb-0">
 | 
			
		||||
        <template v-if="hasSlaPolicyId" #before>
 | 
			
		||||
          <SLA-card-label :chat="chat" class="ltr:mr-1 rtl:ml-1" />
 | 
			
		||||
        </template>
 | 
			
		||||
      </card-labels>
 | 
			
		||||
    </div>
 | 
			
		||||
    <woot-context-menu
 | 
			
		||||
      v-if="showContextMenu"
 | 
			
		||||
@@ -125,6 +129,7 @@ import alertMixin from 'shared/mixins/alertMixin';
 | 
			
		||||
import TimeAgo from 'dashboard/components/ui/TimeAgo.vue';
 | 
			
		||||
import CardLabels from './conversationCardComponents/CardLabels.vue';
 | 
			
		||||
import PriorityMark from './PriorityMark.vue';
 | 
			
		||||
import SLACardLabel from './components/SLACardLabel.vue';
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  components: {
 | 
			
		||||
@@ -135,6 +140,7 @@ export default {
 | 
			
		||||
    TimeAgo,
 | 
			
		||||
    MessagePreview,
 | 
			
		||||
    PriorityMark,
 | 
			
		||||
    SLACardLabel,
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  mixins: [inboxMixin, timeMixin, conversationMixin, alertMixin],
 | 
			
		||||
@@ -252,6 +258,9 @@ export default {
 | 
			
		||||
      const stateInbox = this.inbox;
 | 
			
		||||
      return stateInbox.name || '';
 | 
			
		||||
    },
 | 
			
		||||
    hasSlaPolicyId() {
 | 
			
		||||
      return this.chat?.sla_policy_id;
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    onCardClick(e) {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,12 +1,12 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div
 | 
			
		||||
    class="bg-white dark:bg-slate-900 flex justify-between items-center py-2 px-4 border-b border-slate-50 dark:border-slate-800/50 flex-col md:flex-row"
 | 
			
		||||
    class="flex flex-col items-center justify-between px-4 py-2 bg-white border-b dark:bg-slate-900 border-slate-50 dark:border-slate-800/50 md:flex-row"
 | 
			
		||||
  >
 | 
			
		||||
    <div
 | 
			
		||||
      class="flex-1 w-full min-w-0 flex flex-col items-center justify-center"
 | 
			
		||||
      class="flex flex-col items-center justify-center flex-1 w-full min-w-0"
 | 
			
		||||
      :class="isInboxView ? 'sm:flex-row' : 'md:flex-row'"
 | 
			
		||||
    >
 | 
			
		||||
      <div class="flex justify-start items-center min-w-0 w-fit max-w-full">
 | 
			
		||||
      <div class="flex items-center justify-start max-w-full min-w-0 w-fit">
 | 
			
		||||
        <back-button
 | 
			
		||||
          v-if="showBackButton"
 | 
			
		||||
          :back-url="backButtonUrl"
 | 
			
		||||
@@ -19,10 +19,10 @@
 | 
			
		||||
          :status="currentContact.availability_status"
 | 
			
		||||
        />
 | 
			
		||||
        <div
 | 
			
		||||
          class="items-start flex flex-col ml-2 rtl:ml-0 rtl:mr-2 min-w-0 w-fit overflow-hidden"
 | 
			
		||||
          class="flex flex-col items-start min-w-0 ml-2 overflow-hidden rtl:ml-0 rtl:mr-2 w-fit"
 | 
			
		||||
        >
 | 
			
		||||
          <div
 | 
			
		||||
            class="flex items-center flex-row gap-1 m-0 p-0 w-fit max-w-full"
 | 
			
		||||
            class="flex flex-row items-center max-w-full gap-1 p-0 m-0 w-fit"
 | 
			
		||||
          >
 | 
			
		||||
            <woot-button
 | 
			
		||||
              variant="link"
 | 
			
		||||
@@ -31,7 +31,7 @@
 | 
			
		||||
              @click.prevent="$emit('contact-panel-toggle')"
 | 
			
		||||
            >
 | 
			
		||||
              <span
 | 
			
		||||
                class="text-base leading-tight font-medium text-slate-900 dark:text-slate-100"
 | 
			
		||||
                class="text-base font-medium leading-tight text-slate-900 dark:text-slate-100"
 | 
			
		||||
              >
 | 
			
		||||
                {{ currentContact.name }}
 | 
			
		||||
              </span>
 | 
			
		||||
@@ -46,7 +46,7 @@
 | 
			
		||||
          </div>
 | 
			
		||||
 | 
			
		||||
          <div
 | 
			
		||||
            class="conversation--header--actions items-center flex text-xs gap-2 text-ellipsis overflow-hidden whitespace-nowrap"
 | 
			
		||||
            class="flex items-center gap-2 overflow-hidden text-xs conversation--header--actions text-ellipsis whitespace-nowrap"
 | 
			
		||||
          >
 | 
			
		||||
            <inbox-name v-if="hasMultipleInboxes" :inbox="inbox" />
 | 
			
		||||
            <span
 | 
			
		||||
@@ -67,9 +67,10 @@
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div
 | 
			
		||||
        class="header-actions-wrap items-center flex flex-row flex-grow justify-end mt-3 lg:mt-0"
 | 
			
		||||
        class="flex flex-row items-center justify-end flex-grow gap-2 mt-3 header-actions-wrap lg:mt-0"
 | 
			
		||||
        :class="{ 'justify-end': isContactPanelOpen }"
 | 
			
		||||
      >
 | 
			
		||||
        <SLA-card-label v-if="hasSlaPolicyId" :chat="chat" show-extended-info />
 | 
			
		||||
        <more-actions :conversation-id="currentChat.id" />
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
@@ -85,6 +86,7 @@ import inboxMixin from 'shared/mixins/inboxMixin';
 | 
			
		||||
import InboxName from '../InboxName.vue';
 | 
			
		||||
import MoreActions from './MoreActions.vue';
 | 
			
		||||
import Thumbnail from '../Thumbnail.vue';
 | 
			
		||||
import SLACardLabel from './components/SLACardLabel.vue';
 | 
			
		||||
import wootConstants from 'dashboard/constants/globals';
 | 
			
		||||
import { conversationListPageURL } from 'dashboard/helper/URLHelper';
 | 
			
		||||
import { snoozedReopenTime } from 'dashboard/helper/snoozeHelpers';
 | 
			
		||||
@@ -95,6 +97,7 @@ export default {
 | 
			
		||||
    InboxName,
 | 
			
		||||
    MoreActions,
 | 
			
		||||
    Thumbnail,
 | 
			
		||||
    SLACardLabel,
 | 
			
		||||
  },
 | 
			
		||||
  mixins: [inboxMixin, agentMixin, eventListenerMixins],
 | 
			
		||||
  props: {
 | 
			
		||||
@@ -173,6 +176,9 @@ export default {
 | 
			
		||||
    hasMultipleInboxes() {
 | 
			
		||||
      return this.$store.getters['inboxes/getInboxes'].length > 1;
 | 
			
		||||
    },
 | 
			
		||||
    hasSlaPolicyId() {
 | 
			
		||||
      return this.chat?.sla_policy_id;
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  methods: {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,47 +1,63 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div
 | 
			
		||||
    class="flex items-center px-2 truncate border min-w-fit border-slate-75 dark:border-slate-700"
 | 
			
		||||
    :class="showExtendedInfo ? 'py-[5px] rounded-lg' : 'py-0.5 gap-1 rounded'"
 | 
			
		||||
    v-if="hasSlaThreshold"
 | 
			
		||||
    class="relative flex items-center border cursor-pointer min-w-fit border-slate-75 dark:border-slate-700"
 | 
			
		||||
    :class="showExtendedInfo ? 'rounded-lg' : 'rounded'"
 | 
			
		||||
  >
 | 
			
		||||
    <div
 | 
			
		||||
      class="flex items-center gap-1"
 | 
			
		||||
      :class="
 | 
			
		||||
        showExtendedInfo &&
 | 
			
		||||
        'ltr:pr-1.5 rtl:pl-1.5 ltr:border-r rtl:border-l border-solid border-slate-75 dark:border-slate-700'
 | 
			
		||||
      "
 | 
			
		||||
      class="flex items-center w-full truncate"
 | 
			
		||||
      :class="showExtendedInfo ? 'h-[26px] px-1.5' : 'h-5 px-2 gap-1'"
 | 
			
		||||
      @mouseover="openSlaPopover()"
 | 
			
		||||
      @mouseleave="closeSlaPopover()"
 | 
			
		||||
    >
 | 
			
		||||
      <fluent-icon
 | 
			
		||||
        size="14"
 | 
			
		||||
        :icon="slaStatus.icon"
 | 
			
		||||
        type="outline"
 | 
			
		||||
        :icon-lib="isSlaMissed ? 'lucide' : 'fluent'"
 | 
			
		||||
        class="flex-shrink-0"
 | 
			
		||||
        :class="slaTextStyles"
 | 
			
		||||
      />
 | 
			
		||||
      <span
 | 
			
		||||
        v-if="showExtendedInfo"
 | 
			
		||||
        class="text-xs font-medium"
 | 
			
		||||
        :class="slaTextStyles"
 | 
			
		||||
      <div
 | 
			
		||||
        class="flex items-center gap-1"
 | 
			
		||||
        :class="
 | 
			
		||||
          showExtendedInfo &&
 | 
			
		||||
          'ltr:pr-1.5 rtl:pl-1.5 ltr:border-r rtl:border-l border-solid border-slate-75 dark:border-slate-700'
 | 
			
		||||
        "
 | 
			
		||||
      >
 | 
			
		||||
        {{ slaStatusText }}
 | 
			
		||||
        <fluent-icon
 | 
			
		||||
          size="14"
 | 
			
		||||
          :icon="slaStatus.icon"
 | 
			
		||||
          type="outline"
 | 
			
		||||
          :icon-lib="isSlaMissed ? 'lucide' : 'fluent'"
 | 
			
		||||
          class="flex-shrink-0"
 | 
			
		||||
          :class="slaTextStyles"
 | 
			
		||||
        />
 | 
			
		||||
        <span
 | 
			
		||||
          v-if="showExtendedInfo"
 | 
			
		||||
          class="text-xs font-medium"
 | 
			
		||||
          :class="slaTextStyles"
 | 
			
		||||
        >
 | 
			
		||||
          {{ slaStatusText }}
 | 
			
		||||
        </span>
 | 
			
		||||
      </div>
 | 
			
		||||
      <span
 | 
			
		||||
        class="text-xs font-medium"
 | 
			
		||||
        :class="[slaTextStyles, showExtendedInfo && 'ltr:pl-1.5 rtl:pr-1.5']"
 | 
			
		||||
      >
 | 
			
		||||
        {{ slaStatus.threshold }}
 | 
			
		||||
      </span>
 | 
			
		||||
    </div>
 | 
			
		||||
    <span
 | 
			
		||||
      class="text-xs font-medium"
 | 
			
		||||
      :class="[slaTextStyles, showExtendedInfo && 'ltr:pl-1.5 rtl:pr-1.5']"
 | 
			
		||||
    >
 | 
			
		||||
      {{ slaStatus.threshold }}
 | 
			
		||||
    </span>
 | 
			
		||||
    <SLA-popover-card
 | 
			
		||||
      v-if="showSlaPopoverCard"
 | 
			
		||||
      :all-missed-slas="slaEvents"
 | 
			
		||||
      class="right-0 top-7"
 | 
			
		||||
    />
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
import { mapGetters } from 'vuex';
 | 
			
		||||
import { evaluateSLAStatus } from '../helpers/SLAHelper';
 | 
			
		||||
import SLAPopoverCard from './SLAPopoverCard.vue';
 | 
			
		||||
 | 
			
		||||
// const REFRESH_INTERVAL = 60000;
 | 
			
		||||
const REFRESH_INTERVAL = 60000;
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  components: {
 | 
			
		||||
    SLAPopoverCard,
 | 
			
		||||
  },
 | 
			
		||||
  props: {
 | 
			
		||||
    chat: {
 | 
			
		||||
      type: Object,
 | 
			
		||||
@@ -55,19 +71,27 @@ export default {
 | 
			
		||||
  data() {
 | 
			
		||||
    return {
 | 
			
		||||
      timer: null,
 | 
			
		||||
      slaStatus: {},
 | 
			
		||||
      showSlaPopover: false,
 | 
			
		||||
      slaStatus: {
 | 
			
		||||
        threshold: null,
 | 
			
		||||
        isSlaMissed: false,
 | 
			
		||||
        type: null,
 | 
			
		||||
        icon: null,
 | 
			
		||||
      },
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
  computed: {
 | 
			
		||||
    ...mapGetters({
 | 
			
		||||
      activeSLA: 'sla/getSLAById',
 | 
			
		||||
    }),
 | 
			
		||||
    slaPolicyId() {
 | 
			
		||||
      return this.chat?.sla_policy_id;
 | 
			
		||||
    },
 | 
			
		||||
    sla() {
 | 
			
		||||
      if (!this.slaPolicyId) return null;
 | 
			
		||||
      return this.activeSLA(this.slaPolicyId);
 | 
			
		||||
    appliedSLA() {
 | 
			
		||||
      return this.chat?.applied_sla;
 | 
			
		||||
    },
 | 
			
		||||
    slaEvents() {
 | 
			
		||||
      return this.chat?.sla_events;
 | 
			
		||||
    },
 | 
			
		||||
    hasSlaThreshold() {
 | 
			
		||||
      return this.slaStatus?.threshold;
 | 
			
		||||
    },
 | 
			
		||||
    isSlaMissed() {
 | 
			
		||||
      return this.slaStatus?.isSlaMissed;
 | 
			
		||||
@@ -79,12 +103,17 @@ export default {
 | 
			
		||||
    },
 | 
			
		||||
    slaStatusText() {
 | 
			
		||||
      const upperCaseType = this.slaStatus?.type?.toUpperCase(); // FRT, NRT, or RT
 | 
			
		||||
      const statusKey = this.isSlaMissed ? 'BREACH' : 'DUE';
 | 
			
		||||
      const statusKey = this.isSlaMissed ? 'MISSED' : 'DUE';
 | 
			
		||||
 | 
			
		||||
      return this.$t(`CONVERSATION.HEADER.SLA_STATUS.${upperCaseType}`, {
 | 
			
		||||
        status: this.$t(`CONVERSATION.HEADER.SLA_STATUS.${statusKey}`),
 | 
			
		||||
      });
 | 
			
		||||
    },
 | 
			
		||||
    showSlaPopoverCard() {
 | 
			
		||||
      return (
 | 
			
		||||
        this.showExtendedInfo && this.showSlaPopover && this.slaEvents.length
 | 
			
		||||
      );
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  watch: {
 | 
			
		||||
    chat() {
 | 
			
		||||
@@ -93,10 +122,29 @@ export default {
 | 
			
		||||
  },
 | 
			
		||||
  mounted() {
 | 
			
		||||
    this.updateSlaStatus();
 | 
			
		||||
    this.createTimer();
 | 
			
		||||
  },
 | 
			
		||||
  beforeDestroy() {
 | 
			
		||||
    if (this.timer) {
 | 
			
		||||
      clearTimeout(this.timer);
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    createTimer() {
 | 
			
		||||
      this.timer = setTimeout(() => {
 | 
			
		||||
        this.updateSlaStatus();
 | 
			
		||||
        this.createTimer();
 | 
			
		||||
      }, REFRESH_INTERVAL);
 | 
			
		||||
    },
 | 
			
		||||
    updateSlaStatus() {
 | 
			
		||||
      this.slaStatus = evaluateSLAStatus(this.sla, this.chat);
 | 
			
		||||
      this.slaStatus = evaluateSLAStatus(this.appliedSLA, this.chat);
 | 
			
		||||
    },
 | 
			
		||||
    openSlaPopover() {
 | 
			
		||||
      if (!this.showExtendedInfo) return;
 | 
			
		||||
      this.showSlaPopover = true;
 | 
			
		||||
    },
 | 
			
		||||
    closeSlaPopover() {
 | 
			
		||||
      this.showSlaPopover = false;
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,45 @@
 | 
			
		||||
<script setup>
 | 
			
		||||
import { format, fromUnixTime } from 'date-fns';
 | 
			
		||||
 | 
			
		||||
defineProps({
 | 
			
		||||
  allMissedSlas: {
 | 
			
		||||
    type: Array,
 | 
			
		||||
    default: () => [],
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const formatDate = timestamp => format(fromUnixTime(timestamp), 'PP');
 | 
			
		||||
 | 
			
		||||
const upperCase = str => str.toUpperCase();
 | 
			
		||||
</script>
 | 
			
		||||
<template>
 | 
			
		||||
  <div
 | 
			
		||||
    class="absolute flex flex-col items-start bg-[#fdfdfd] dark:bg-slate-800 z-50 p-4 border border-solid border-slate-75 dark:border-slate-700 w-[384px] rounded-xl gap-4"
 | 
			
		||||
  >
 | 
			
		||||
    <div
 | 
			
		||||
      v-for="missedSLA in allMissedSlas"
 | 
			
		||||
      :key="missedSLA.id"
 | 
			
		||||
      class="flex items-center justify-between w-full"
 | 
			
		||||
    >
 | 
			
		||||
      <span
 | 
			
		||||
        class="text-sm font-normal tracking-[-0.6%] w-[140px] truncate text-slate-900 dark:text-slate-50"
 | 
			
		||||
      >
 | 
			
		||||
        {{
 | 
			
		||||
          $t(
 | 
			
		||||
            `CONVERSATION.HEADER.SLA_POPOVER.${upperCase(missedSLA.event_type)}`
 | 
			
		||||
          )
 | 
			
		||||
        }}
 | 
			
		||||
      </span>
 | 
			
		||||
      <span
 | 
			
		||||
        class="text-sm font-normal tracking-[-0.6%] text-slate-600 dark:text-slate-200"
 | 
			
		||||
      >
 | 
			
		||||
        {{ $t('CONVERSATION.HEADER.SLA_POPOVER.MISSED') }}
 | 
			
		||||
      </span>
 | 
			
		||||
      <span
 | 
			
		||||
        class="text-sm font-normal tracking-[-0.6%] text-slate-900 dark:text-slate-50"
 | 
			
		||||
      >
 | 
			
		||||
        {{ formatDate(missedSLA.created_at) }}
 | 
			
		||||
      </span>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
@@ -1,13 +1,14 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div
 | 
			
		||||
    v-show="activeLabels.length"
 | 
			
		||||
    v-if="activeLabels.length || $slots.before"
 | 
			
		||||
    ref="labelContainer"
 | 
			
		||||
    class="label-container mt-0.5 mx-2 mb-0"
 | 
			
		||||
    v-resize="computeVisibleLabelPosition"
 | 
			
		||||
  >
 | 
			
		||||
    <div
 | 
			
		||||
      class="labels-wrap flex items-end min-w-0 flex-shrink gap-y-1 flex-wrap"
 | 
			
		||||
      :class="{ expand: showAllLabels }"
 | 
			
		||||
      class="flex items-end flex-shrink min-w-0 gap-y-1"
 | 
			
		||||
      :class="{ 'h-auto overflow-visible flex-row flex-wrap': showAllLabels }"
 | 
			
		||||
    >
 | 
			
		||||
      <slot name="before" />
 | 
			
		||||
      <woot-label
 | 
			
		||||
        v-for="(label, index) in activeLabels"
 | 
			
		||||
        :key="label.id"
 | 
			
		||||
@@ -26,7 +27,7 @@
 | 
			
		||||
            ? $t('CONVERSATION.CARD.HIDE_LABELS')
 | 
			
		||||
            : $t('CONVERSATION.CARD.SHOW_LABELS')
 | 
			
		||||
        "
 | 
			
		||||
        class="show-more--button sticky flex-shrink-0 right-0 mr-6 rtl:rotate-180"
 | 
			
		||||
        class="sticky right-0 flex-shrink-0 mr-6 show-more--button rtl:rotate-180"
 | 
			
		||||
        color-scheme="secondary"
 | 
			
		||||
        variant="hollow"
 | 
			
		||||
        :icon="showAllLabels ? 'chevron-left' : 'chevron-right'"
 | 
			
		||||
@@ -59,26 +60,34 @@ export default {
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  mounted() {
 | 
			
		||||
    // the problem here is that there is a certain amount of delay between the conversation
 | 
			
		||||
    // card being mounted and the resize event eventually being triggered
 | 
			
		||||
    // This means we need to run the function immediately after the component is mounted
 | 
			
		||||
    // Happens especially when used in a virtual list.
 | 
			
		||||
    // We can make the first trigger, a standard part of the directive, in case
 | 
			
		||||
    // we face this issue again
 | 
			
		||||
    this.computeVisibleLabelPosition();
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    onShowLabels(e) {
 | 
			
		||||
      e.stopPropagation();
 | 
			
		||||
      this.showAllLabels = !this.showAllLabels;
 | 
			
		||||
      this.$nextTick(() => this.computeVisibleLabelPosition());
 | 
			
		||||
    },
 | 
			
		||||
    computeVisibleLabelPosition() {
 | 
			
		||||
      const beforeSlot = this.$slots.before ? 100 : 0;
 | 
			
		||||
      const labelContainer = this.$refs.labelContainer;
 | 
			
		||||
      const labels = this.$refs.labelContainer.querySelectorAll('.label');
 | 
			
		||||
      if (!labelContainer) return;
 | 
			
		||||
 | 
			
		||||
      const labels = Array.from(labelContainer.querySelectorAll('.label'));
 | 
			
		||||
      let labelOffset = 0;
 | 
			
		||||
      this.showExpandLabelButton = false;
 | 
			
		||||
 | 
			
		||||
      Array.from(labels).forEach((label, index) => {
 | 
			
		||||
      labels.forEach((label, index) => {
 | 
			
		||||
        labelOffset += label.offsetWidth + 8;
 | 
			
		||||
 | 
			
		||||
        if (labelOffset < labelContainer.clientWidth - 16) {
 | 
			
		||||
        if (labelOffset < labelContainer.clientWidth - 16 - beforeSlot) {
 | 
			
		||||
          this.labelPosition = index;
 | 
			
		||||
        } else {
 | 
			
		||||
          this.showExpandLabelButton = true;
 | 
			
		||||
          this.showExpandLabelButton = labels.length > 1;
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
    },
 | 
			
		||||
@@ -95,10 +104,6 @@ export default {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.labels-wrap {
 | 
			
		||||
  &.expand {
 | 
			
		||||
    @apply h-auto overflow-visible flex-row flex-wrap;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .secondary {
 | 
			
		||||
    @apply border border-solid border-slate-100 dark:border-slate-700;
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,117 @@
 | 
			
		||||
const calculateThreshold = (timeOffset, threshold) => {
 | 
			
		||||
  // Calculate the time left for the SLA to breach or the time since the SLA has missed
 | 
			
		||||
  if (threshold === null) return null;
 | 
			
		||||
  const currentTime = Math.floor(Date.now() / 1000);
 | 
			
		||||
  return timeOffset + threshold - currentTime;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const findMostUrgentSLAStatus = SLAStatuses => {
 | 
			
		||||
  // Sort the SLAs based on the threshold and return the most urgent SLA
 | 
			
		||||
  SLAStatuses.sort(
 | 
			
		||||
    (sla1, sla2) => Math.abs(sla1.threshold) - Math.abs(sla2.threshold)
 | 
			
		||||
  );
 | 
			
		||||
  return SLAStatuses[0];
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const formatSLATime = seconds => {
 | 
			
		||||
  const units = {
 | 
			
		||||
    y: 31536000, // 60 * 60 * 24 * 365
 | 
			
		||||
    mo: 2592000, // 60 * 60 * 24 * 30
 | 
			
		||||
    d: 86400, // 60 * 60 * 24
 | 
			
		||||
    h: 3600, // 60 * 60
 | 
			
		||||
    m: 60,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  if (seconds < 60) {
 | 
			
		||||
    return '1m';
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // we will only show two parts, two max granularity's, h-m, y-d, d-h, m, but no seconds
 | 
			
		||||
  const parts = [];
 | 
			
		||||
 | 
			
		||||
  Object.keys(units).forEach(unit => {
 | 
			
		||||
    const value = Math.floor(seconds / units[unit]);
 | 
			
		||||
    if (seconds < 60 && parts.length > 0) return;
 | 
			
		||||
    if (parts.length === 2) return;
 | 
			
		||||
    if (value > 0) {
 | 
			
		||||
      parts.push(value + unit);
 | 
			
		||||
      seconds -= value * units[unit];
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
  return parts.join(' ');
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const createSLAObject = (
 | 
			
		||||
  type,
 | 
			
		||||
  {
 | 
			
		||||
    sla_first_response_time_threshold: frtThreshold,
 | 
			
		||||
    sla_next_response_time_threshold: nrtThreshold,
 | 
			
		||||
    sla_resolution_time_threshold: rtThreshold,
 | 
			
		||||
    created_at: createdAt,
 | 
			
		||||
  } = {},
 | 
			
		||||
  {
 | 
			
		||||
    first_reply_created_at: firstReplyCreatedAt,
 | 
			
		||||
    waiting_since: waitingSince,
 | 
			
		||||
    status,
 | 
			
		||||
  } = {}
 | 
			
		||||
) => {
 | 
			
		||||
  // Mapping of breach types to their logic
 | 
			
		||||
  const SLATypes = {
 | 
			
		||||
    FRT: {
 | 
			
		||||
      threshold: calculateThreshold(createdAt, frtThreshold),
 | 
			
		||||
      //   Check FRT only if threshold is not null and first reply hasn't been made
 | 
			
		||||
      condition:
 | 
			
		||||
        frtThreshold !== null &&
 | 
			
		||||
        (!firstReplyCreatedAt || firstReplyCreatedAt === 0),
 | 
			
		||||
    },
 | 
			
		||||
    NRT: {
 | 
			
		||||
      threshold: calculateThreshold(waitingSince, nrtThreshold),
 | 
			
		||||
      // Check NRT only if threshold is not null, first reply has been made and we are waiting since
 | 
			
		||||
      condition:
 | 
			
		||||
        nrtThreshold !== null && !!firstReplyCreatedAt && !!waitingSince,
 | 
			
		||||
    },
 | 
			
		||||
    RT: {
 | 
			
		||||
      threshold: calculateThreshold(createdAt, rtThreshold),
 | 
			
		||||
      // Check RT only if the conversation is open and threshold is not null
 | 
			
		||||
      condition: status === 'open' && rtThreshold !== null,
 | 
			
		||||
    },
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const SLAStatus = SLATypes[type];
 | 
			
		||||
  return SLAStatus ? { ...SLAStatus, type } : null;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const evaluateSLAConditions = (appliedSla, chat) => {
 | 
			
		||||
  // Filter out the SLA based on conditions and update the object with the breach status(icon, isSlaMissed)
 | 
			
		||||
  const SLATypes = ['FRT', 'NRT', 'RT'];
 | 
			
		||||
  return SLATypes.map(type => createSLAObject(type, appliedSla, chat))
 | 
			
		||||
    .filter(SLAStatus => SLAStatus && SLAStatus.condition)
 | 
			
		||||
    .map(SLAStatus => ({
 | 
			
		||||
      ...SLAStatus,
 | 
			
		||||
      icon: SLAStatus.threshold <= 0 ? 'flame' : 'alarm',
 | 
			
		||||
      isSlaMissed: SLAStatus.threshold <= 0,
 | 
			
		||||
    }));
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const evaluateSLAStatus = (appliedSla, chat) => {
 | 
			
		||||
  if (!appliedSla || !chat)
 | 
			
		||||
    return { type: '', threshold: '', icon: '', isSlaMissed: false };
 | 
			
		||||
 | 
			
		||||
  // Filter out the SLA and create the object for each breach
 | 
			
		||||
  const SLAStatuses = evaluateSLAConditions(appliedSla, chat);
 | 
			
		||||
 | 
			
		||||
  // Return the most urgent SLA which is latest to breach or has missed
 | 
			
		||||
  const mostUrgent = findMostUrgentSLAStatus(SLAStatuses);
 | 
			
		||||
  return mostUrgent
 | 
			
		||||
    ? {
 | 
			
		||||
        type: mostUrgent.type,
 | 
			
		||||
        threshold: formatSLATime(
 | 
			
		||||
          mostUrgent.threshold <= 0
 | 
			
		||||
            ? -mostUrgent.threshold
 | 
			
		||||
            : mostUrgent.threshold
 | 
			
		||||
        ),
 | 
			
		||||
        icon: mostUrgent.icon,
 | 
			
		||||
        isSlaMissed: mostUrgent.isSlaMissed,
 | 
			
		||||
      }
 | 
			
		||||
    : { type: '', threshold: '', icon: '', isSlaMissed: false };
 | 
			
		||||
};
 | 
			
		||||
@@ -0,0 +1,150 @@
 | 
			
		||||
import { evaluateSLAStatus } from '../SLAHelper';
 | 
			
		||||
 | 
			
		||||
beforeEach(() => {
 | 
			
		||||
  jest
 | 
			
		||||
    .spyOn(Date, 'now')
 | 
			
		||||
    .mockImplementation(() => new Date('2024-01-01T00:00:00Z').getTime());
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
afterEach(() => {
 | 
			
		||||
  jest.restoreAllMocks();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
describe('SLAHelper', () => {
 | 
			
		||||
  describe('evaluateSLAStatus', () => {
 | 
			
		||||
    it('returns an empty object when sla or chat is not present', () => {
 | 
			
		||||
      expect(evaluateSLAStatus(null, null)).toEqual({
 | 
			
		||||
        type: '',
 | 
			
		||||
        threshold: '',
 | 
			
		||||
        icon: '',
 | 
			
		||||
        isSlaMissed: false,
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Case when FRT SLA is missed
 | 
			
		||||
    it('correctly identifies a missed FRT SLA', () => {
 | 
			
		||||
      const appliedSla = {
 | 
			
		||||
        sla_first_response_time_threshold: 600,
 | 
			
		||||
        sla_next_response_time_threshold: 1200,
 | 
			
		||||
        sla_resolution_time_threshold: 1800,
 | 
			
		||||
        created_at: 1704066540,
 | 
			
		||||
      };
 | 
			
		||||
      const chatMissed = {
 | 
			
		||||
        first_reply_created_at: 0,
 | 
			
		||||
        waiting_since: 0,
 | 
			
		||||
        status: 'open',
 | 
			
		||||
      };
 | 
			
		||||
      expect(evaluateSLAStatus(appliedSla, chatMissed)).toEqual({
 | 
			
		||||
        type: 'FRT',
 | 
			
		||||
        threshold: '1m',
 | 
			
		||||
        icon: 'flame',
 | 
			
		||||
        isSlaMissed: true,
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Case when FRT SLA is not missed
 | 
			
		||||
    it('correctly identifies an FRT SLA not yet breached', () => {
 | 
			
		||||
      const appliedSla = {
 | 
			
		||||
        sla_first_response_time_threshold: 600,
 | 
			
		||||
        sla_next_response_time_threshold: 1200,
 | 
			
		||||
        sla_resolution_time_threshold: 1800,
 | 
			
		||||
        created_at: 1704066660,
 | 
			
		||||
      };
 | 
			
		||||
      const chatNotMissed = {
 | 
			
		||||
        first_reply_created_at: 0,
 | 
			
		||||
        waiting_since: 0,
 | 
			
		||||
        status: 'open',
 | 
			
		||||
      };
 | 
			
		||||
      expect(evaluateSLAStatus(appliedSla, chatNotMissed)).toEqual({
 | 
			
		||||
        type: 'FRT',
 | 
			
		||||
        threshold: '1m',
 | 
			
		||||
        icon: 'alarm',
 | 
			
		||||
        isSlaMissed: false,
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Case when NRT SLA is missed
 | 
			
		||||
    it('correctly identifies a missed NRT SLA', () => {
 | 
			
		||||
      const appliedSla = {
 | 
			
		||||
        sla_first_response_time_threshold: 600,
 | 
			
		||||
        sla_next_response_time_threshold: 1200,
 | 
			
		||||
        sla_resolution_time_threshold: 1800,
 | 
			
		||||
        created_at: 1704065200,
 | 
			
		||||
      };
 | 
			
		||||
      const chatMissed = {
 | 
			
		||||
        first_reply_created_at: 1704066200,
 | 
			
		||||
        waiting_since: 1704065940,
 | 
			
		||||
        status: 'open',
 | 
			
		||||
      };
 | 
			
		||||
      expect(evaluateSLAStatus(appliedSla, chatMissed)).toEqual({
 | 
			
		||||
        type: 'NRT',
 | 
			
		||||
        threshold: '1m',
 | 
			
		||||
        icon: 'flame',
 | 
			
		||||
        isSlaMissed: true,
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Case when NRT SLA is not missed
 | 
			
		||||
    it('correctly identifies an NRT SLA not yet breached', () => {
 | 
			
		||||
      const appliedSla = {
 | 
			
		||||
        sla_first_response_time_threshold: 600,
 | 
			
		||||
        sla_next_response_time_threshold: 1200,
 | 
			
		||||
        sla_resolution_time_threshold: 1800,
 | 
			
		||||
        created_at: 1704065200 - 2000,
 | 
			
		||||
      };
 | 
			
		||||
      const chatNotMissed = {
 | 
			
		||||
        first_reply_created_at: 1704066200,
 | 
			
		||||
        waiting_since: 1704066060,
 | 
			
		||||
        status: 'open',
 | 
			
		||||
      };
 | 
			
		||||
      expect(evaluateSLAStatus(appliedSla, chatNotMissed)).toEqual({
 | 
			
		||||
        type: 'NRT',
 | 
			
		||||
        threshold: '1m',
 | 
			
		||||
        icon: 'alarm',
 | 
			
		||||
        isSlaMissed: false,
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Case when RT SLA is missed
 | 
			
		||||
    it('correctly identifies a missed RT SLA', () => {
 | 
			
		||||
      const appliedSla = {
 | 
			
		||||
        sla_first_response_time_threshold: 600,
 | 
			
		||||
        sla_next_response_time_threshold: 1200,
 | 
			
		||||
        sla_resolution_time_threshold: 1800,
 | 
			
		||||
        created_at: 1704065340,
 | 
			
		||||
      };
 | 
			
		||||
      const chatMissed = {
 | 
			
		||||
        first_reply_created_at: 1704066200,
 | 
			
		||||
        waiting_since: 0,
 | 
			
		||||
        status: 'open',
 | 
			
		||||
      };
 | 
			
		||||
      expect(evaluateSLAStatus(appliedSla, chatMissed)).toEqual({
 | 
			
		||||
        type: 'RT',
 | 
			
		||||
        threshold: '1m',
 | 
			
		||||
        icon: 'flame',
 | 
			
		||||
        isSlaMissed: true,
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Case when RT SLA is not missed
 | 
			
		||||
    it('correctly identifies an RT SLA not yet breached', () => {
 | 
			
		||||
      const appliedSla = {
 | 
			
		||||
        sla_first_response_time_threshold: 600,
 | 
			
		||||
        sla_next_response_time_threshold: 1200,
 | 
			
		||||
        sla_resolution_time_threshold: 1800,
 | 
			
		||||
        created_at: 1704065460,
 | 
			
		||||
      };
 | 
			
		||||
      const chatNotMissed = {
 | 
			
		||||
        first_reply_created_at: 1704066200,
 | 
			
		||||
        waiting_since: 0,
 | 
			
		||||
        status: 'open',
 | 
			
		||||
      };
 | 
			
		||||
      expect(evaluateSLAStatus(appliedSla, chatNotMissed)).toEqual({
 | 
			
		||||
        type: 'RT',
 | 
			
		||||
        threshold: '1m',
 | 
			
		||||
        icon: 'alarm',
 | 
			
		||||
        isSlaMissed: false,
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
@@ -69,8 +69,14 @@
 | 
			
		||||
        "FRT": "FRT {status}",
 | 
			
		||||
        "NRT": "NRT {status}",
 | 
			
		||||
        "RT": "RT {status}",
 | 
			
		||||
        "BREACH": "breach",
 | 
			
		||||
        "MISSED": "missed",
 | 
			
		||||
        "DUE": "due"
 | 
			
		||||
      },
 | 
			
		||||
      "SLA_POPOVER": {
 | 
			
		||||
        "FRT": "First Response Time",
 | 
			
		||||
        "NRT": "Next Response Time",
 | 
			
		||||
        "RT": "Resolution Time",
 | 
			
		||||
        "MISSED": "missed on"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "RESOLVE_DROPDOWN": {
 | 
			
		||||
 
 | 
			
		||||
@@ -8,6 +8,7 @@ export default {
 | 
			
		||||
        this.conversationId
 | 
			
		||||
      );
 | 
			
		||||
    },
 | 
			
		||||
    // TODO - Get rid of this from the mixin
 | 
			
		||||
    activeLabels() {
 | 
			
		||||
      return this.accountLabels.filter(({ title }) =>
 | 
			
		||||
        this.savedLabels.includes(title)
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,78 @@
 | 
			
		||||
import axios from 'axios';
 | 
			
		||||
import { actions } from '../../sla';
 | 
			
		||||
import * as types from '../../../mutation-types';
 | 
			
		||||
import SLAList from './fixtures';
 | 
			
		||||
 | 
			
		||||
const commit = jest.fn();
 | 
			
		||||
global.axios = axios;
 | 
			
		||||
jest.mock('axios');
 | 
			
		||||
 | 
			
		||||
describe('#actions', () => {
 | 
			
		||||
  beforeEach(() => {
 | 
			
		||||
    jest.clearAllMocks();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe('#get', () => {
 | 
			
		||||
    it('sends correct actions if API is success', async () => {
 | 
			
		||||
      axios.get.mockResolvedValue({
 | 
			
		||||
        data: { payload: SLAList },
 | 
			
		||||
      });
 | 
			
		||||
      await actions.get({ commit });
 | 
			
		||||
      expect(commit.mock.calls).toEqual([
 | 
			
		||||
        [types.default.SET_SLA_UI_FLAG, { isFetching: true }],
 | 
			
		||||
        [types.default.SET_SLA, SLAList],
 | 
			
		||||
        [types.default.SET_SLA_UI_FLAG, { isFetching: false }],
 | 
			
		||||
      ]);
 | 
			
		||||
    });
 | 
			
		||||
    it('sends correct actions if API is error', async () => {
 | 
			
		||||
      axios.get.mockRejectedValue({ message: 'Incorrect header' });
 | 
			
		||||
      await actions.get({ commit });
 | 
			
		||||
      expect(commit.mock.calls).toEqual([
 | 
			
		||||
        [types.default.SET_SLA_UI_FLAG, { isFetching: true }],
 | 
			
		||||
        [types.default.SET_SLA_UI_FLAG, { isFetching: false }],
 | 
			
		||||
      ]);
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe('#create', () => {
 | 
			
		||||
    it('sends correct actions if API is success', async () => {
 | 
			
		||||
      axios.post.mockResolvedValue({
 | 
			
		||||
        data: { payload: SLAList[0] },
 | 
			
		||||
      });
 | 
			
		||||
      await actions.create({ commit }, SLAList[0]);
 | 
			
		||||
      expect(commit.mock.calls).toEqual([
 | 
			
		||||
        [types.default.SET_SLA_UI_FLAG, { isCreating: true }],
 | 
			
		||||
        [types.default.ADD_SLA, SLAList[0]],
 | 
			
		||||
        [types.default.SET_SLA_UI_FLAG, { isCreating: false }],
 | 
			
		||||
      ]);
 | 
			
		||||
    });
 | 
			
		||||
    it('sends correct actions if API is error', async () => {
 | 
			
		||||
      axios.post.mockRejectedValue({ message: 'Incorrect header' });
 | 
			
		||||
      await expect(actions.create({ commit })).rejects.toThrow(Error);
 | 
			
		||||
      expect(commit.mock.calls).toEqual([
 | 
			
		||||
        [types.default.SET_SLA_UI_FLAG, { isCreating: true }],
 | 
			
		||||
        [types.default.SET_SLA_UI_FLAG, { isCreating: false }],
 | 
			
		||||
      ]);
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe('#delete', () => {
 | 
			
		||||
    it('sends correct actions if API is success', async () => {
 | 
			
		||||
      axios.delete.mockResolvedValue({});
 | 
			
		||||
      await actions.delete({ commit }, 1);
 | 
			
		||||
      expect(commit.mock.calls).toEqual([
 | 
			
		||||
        [types.default.SET_SLA_UI_FLAG, { isDeleting: true }],
 | 
			
		||||
        [types.default.DELETE_SLA, 1],
 | 
			
		||||
        [types.default.SET_SLA_UI_FLAG, { isDeleting: false }],
 | 
			
		||||
      ]);
 | 
			
		||||
    });
 | 
			
		||||
    it('sends correct actions if API is error', async () => {
 | 
			
		||||
      axios.delete.mockRejectedValue({ message: 'Incorrect header' });
 | 
			
		||||
      await expect(actions.delete({ commit })).rejects.toThrow(Error);
 | 
			
		||||
      expect(commit.mock.calls).toEqual([
 | 
			
		||||
        [types.default.SET_SLA_UI_FLAG, { isDeleting: true }],
 | 
			
		||||
        [types.default.SET_SLA_UI_FLAG, { isDeleting: false }],
 | 
			
		||||
      ]);
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										95
									
								
								app/javascript/dashboard/store/modules/specs/sla/fixtures.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										95
									
								
								app/javascript/dashboard/store/modules/specs/sla/fixtures.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,95 @@
 | 
			
		||||
export default [
 | 
			
		||||
  {
 | 
			
		||||
    id: 1,
 | 
			
		||||
    name: 'Premium SLA',
 | 
			
		||||
    description:
 | 
			
		||||
      'SLA for chatwoot cloud premium and self-hosted premium customers.  SLA for chatwoot cloud premium and self-hosted premium customers',
 | 
			
		||||
    first_response_time_threshold: 14400,
 | 
			
		||||
    next_response_time_threshold: 18000,
 | 
			
		||||
    resolution_time_threshold: 86400,
 | 
			
		||||
    only_during_business_hours: true,
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    id: 2,
 | 
			
		||||
    name: 'Enterprise SLA',
 | 
			
		||||
    description:
 | 
			
		||||
      'SLA for chatwoot enterprise and self-hosted enterprise customers.',
 | 
			
		||||
    first_response_time_threshold: 600,
 | 
			
		||||
    next_response_time_threshold: 2400,
 | 
			
		||||
    resolution_time_threshold: 3600,
 | 
			
		||||
    only_during_business_hours: false,
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    id: 3,
 | 
			
		||||
    name: 'Business SLA',
 | 
			
		||||
    description:
 | 
			
		||||
      'Chatwoot cloud Business and self-hosted Business customers SLA',
 | 
			
		||||
    first_response_time_threshold: null,
 | 
			
		||||
    next_response_time_threshold: null,
 | 
			
		||||
    resolution_time_threshold: null,
 | 
			
		||||
    only_during_business_hours: false,
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    id: 4,
 | 
			
		||||
    name: 'Hacker SLA',
 | 
			
		||||
    description: '',
 | 
			
		||||
    first_response_time_threshold: 60,
 | 
			
		||||
    next_response_time_threshold: 120,
 | 
			
		||||
    resolution_time_threshold: 180,
 | 
			
		||||
    only_during_business_hours: false,
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    id: 5,
 | 
			
		||||
    name: 'SLA',
 | 
			
		||||
    description: '',
 | 
			
		||||
    first_response_time_threshold: 120,
 | 
			
		||||
    next_response_time_threshold: 300,
 | 
			
		||||
    resolution_time_threshold: 21600,
 | 
			
		||||
    only_during_business_hours: false,
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    id: 6,
 | 
			
		||||
    name: 'ALla',
 | 
			
		||||
    description: '',
 | 
			
		||||
    first_response_time_threshold: 5400,
 | 
			
		||||
    next_response_time_threshold: 9000,
 | 
			
		||||
    resolution_time_threshold: 23040,
 | 
			
		||||
    only_during_business_hours: false,
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    id: 7,
 | 
			
		||||
    name: '10',
 | 
			
		||||
    description: '',
 | 
			
		||||
    first_response_time_threshold: 120,
 | 
			
		||||
    next_response_time_threshold: null,
 | 
			
		||||
    resolution_time_threshold: null,
 | 
			
		||||
    only_during_business_hours: false,
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    id: 8,
 | 
			
		||||
    name: '11',
 | 
			
		||||
    description: '',
 | 
			
		||||
    first_response_time_threshold: null,
 | 
			
		||||
    next_response_time_threshold: 240,
 | 
			
		||||
    resolution_time_threshold: null,
 | 
			
		||||
    only_during_business_hours: false,
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    id: 9,
 | 
			
		||||
    name: '12',
 | 
			
		||||
    description: '',
 | 
			
		||||
    first_response_time_threshold: null,
 | 
			
		||||
    next_response_time_threshold: null,
 | 
			
		||||
    resolution_time_threshold: 300,
 | 
			
		||||
    only_during_business_hours: false,
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    id: 10,
 | 
			
		||||
    name: '14',
 | 
			
		||||
    description: '',
 | 
			
		||||
    first_response_time_threshold: null,
 | 
			
		||||
    next_response_time_threshold: null,
 | 
			
		||||
    resolution_time_threshold: null,
 | 
			
		||||
    only_during_business_hours: false,
 | 
			
		||||
  },
 | 
			
		||||
];
 | 
			
		||||
@@ -0,0 +1,26 @@
 | 
			
		||||
import { getters } from '../../sla';
 | 
			
		||||
import SLAs from './fixtures';
 | 
			
		||||
 | 
			
		||||
describe('#getters', () => {
 | 
			
		||||
  it('getSLA', () => {
 | 
			
		||||
    const state = { records: SLAs };
 | 
			
		||||
    expect(getters.getSLA(state)).toEqual(SLAs);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('getUIFlags', () => {
 | 
			
		||||
    const state = {
 | 
			
		||||
      uiFlags: {
 | 
			
		||||
        isFetching: true,
 | 
			
		||||
        isCreating: false,
 | 
			
		||||
        isUpdating: false,
 | 
			
		||||
        isDeleting: false,
 | 
			
		||||
      },
 | 
			
		||||
    };
 | 
			
		||||
    expect(getters.getUIFlags(state)).toEqual({
 | 
			
		||||
      isFetching: true,
 | 
			
		||||
      isCreating: false,
 | 
			
		||||
      isUpdating: false,
 | 
			
		||||
      isDeleting: false,
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
@@ -0,0 +1,34 @@
 | 
			
		||||
import types from '../../../mutation-types';
 | 
			
		||||
import { mutations } from '../../sla';
 | 
			
		||||
import SLAs from './fixtures';
 | 
			
		||||
 | 
			
		||||
describe('#mutations', () => {
 | 
			
		||||
  describe('#SET_SLA_UI_FLAG', () => {
 | 
			
		||||
    it('set sla ui flags', () => {
 | 
			
		||||
      const state = { uiFlags: {} };
 | 
			
		||||
      mutations[types.SET_SLA_UI_FLAG](state, { isFetching: true });
 | 
			
		||||
      expect(state.uiFlags).toEqual({ isFetching: true });
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
  describe('#SET_SLA', () => {
 | 
			
		||||
    it('set sla records', () => {
 | 
			
		||||
      const state = { records: [] };
 | 
			
		||||
      mutations[types.SET_SLA](state, SLAs);
 | 
			
		||||
      expect(state.records).toEqual(SLAs);
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
  describe('#ADD_SLA', () => {
 | 
			
		||||
    it('push newly created sla to the store', () => {
 | 
			
		||||
      const state = { records: [SLAs[0]] };
 | 
			
		||||
      mutations[types.ADD_SLA](state, SLAs[1]);
 | 
			
		||||
      expect(state.records).toEqual([SLAs[0], SLAs[1]]);
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
  describe('#DELETE_SLA', () => {
 | 
			
		||||
    it('delete sla record', () => {
 | 
			
		||||
      const state = { records: [SLAs[0]] };
 | 
			
		||||
      mutations[types.DELETE_SLA](state, 1);
 | 
			
		||||
      expect(state.records).toEqual([]);
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
@@ -2,6 +2,7 @@
 | 
			
		||||
  "add-circle-outline": "M12 2c5.523 0 10 4.477 10 10s-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2Zm0 1.5a8.5 8.5 0 1 0 0 17 8.5 8.5 0 0 0 0-17ZM12 7a.75.75 0 0 1 .75.75v3.5h3.5a.75.75 0 0 1 0 1.5h-3.5v3.5a.75.75 0 0 1-1.5 0v-3.5h-3.5a.75.75 0 0 1 0-1.5h3.5v-3.5A.75.75 0 0 1 12 7Z",
 | 
			
		||||
  "add-outline": "M11.75 3a.75.75 0 0 1 .743.648l.007.102.001 7.25h7.253a.75.75 0 0 1 .102 1.493l-.102.007h-7.253l.002 7.25a.75.75 0 0 1-1.493.101l-.007-.102-.002-7.249H3.752a.75.75 0 0 1-.102-1.493L3.752 11h7.25L11 3.75a.75.75 0 0 1 .75-.75Z",
 | 
			
		||||
  "add-solid": "M11.883 3.007 12 3a1 1 0 0 1 .993.883L13 4v7h7a1 1 0 0 1 .993.883L21 12a1 1 0 0 1-.883.993L20 13h-7v7a1 1 0 0 1-.883.993L12 21a1 1 0 0 1-.993-.883L11 20v-7H4a1 1 0 0 1-.993-.883L3 12a1 1 0 0 1 .883-.993L4 11h7V4a1 1 0 0 1 .883-.993L12 3l-.117.007Z",
 | 
			
		||||
  "alarm-outline": "M13 12.6V9q0-.425-.288-.713T12 8q-.425 0-.713.288T11 9v3.975q0 .2.075.388t.225.337l2.8 2.8q.275.275.7.275t.7-.275q.275-.275.275-.7t-.275-.7L13 12.6ZM12 22q-1.875 0-3.513-.713t-2.85-1.924q-1.212-1.213-1.924-2.85T3 13q0-1.875.713-3.513t1.924-2.85q1.213-1.212 2.85-1.924T12 4q1.875 0 3.513.713t2.85 1.925q1.212 1.212 1.925 2.85T21 13q0 1.875-.713 3.513t-1.924 2.85q-1.213 1.212-2.85 1.925T12 22Zm0-9ZM2.05 7.3q-.275-.275-.275-.7t.275-.7L4.9 3.05q.275-.275.7-.275t.7.275q.275.275.275.7t-.275.7L3.45 7.3q-.275.275-.7.275t-.7-.275Zm19.9 0q-.275.275-.7.275t-.7-.275L17.7 4.45q-.275-.275-.275-.7t.275-.7q.275-.275.7-.275t.7.275l2.85 2.85q.275.275.275.7t-.275.7ZM12 20q2.925 0 4.963-2.038T19 13q0-2.925-2.038-4.963T12 6Q9.075 6 7.037 8.038T5 13q0 2.925 2.038 4.963T12 20Z",
 | 
			
		||||
  "alarm-on-outline": "m10.95 13.7l-1.425-1.425q-.3-.3-.7-.3t-.7.3q-.3.3-.3.713t.3.712l2.125 2.15q.3.3.7.3t.7-.3l4.25-4.25q.3-.3.3-.712t-.3-.713q-.3-.3-.713-.3t-.712.3L10.95 13.7ZM12 22q-1.875 0-3.512-.713t-2.85-1.924q-1.213-1.213-1.925-2.85T3 13q0-1.875.713-3.513t1.924-2.85q1.213-1.212 2.85-1.924T12 4q1.875 0 3.513.713t2.85 1.925q1.212 1.212 1.925 2.85T21 13q0 1.875-.713 3.513t-1.924 2.85q-1.213 1.212-2.85 1.925T12 22Zm0-9ZM2.05 7.3q-.275-.275-.275-.7t.275-.7L4.9 3.05q.275-.275.7-.275t.7.275q.275.275.275.7t-.275.7L3.45 7.3q-.275.275-.7.275t-.7-.275Zm19.9 0q-.275.275-.7.275t-.7-.275L17.7 4.45q-.275-.275-.275-.7t.275-.7q.275-.275.7-.275t.7.275l2.85 2.85q.275.275.275.7t-.275.7ZM12 20q2.925 0 4.963-2.038T19 13q0-2.925-2.038-4.963T12 6Q9.075 6 7.037 8.038T5 13q0 2.925 2.038 4.963T12 20Z",
 | 
			
		||||
  "alarm-off-outline": "m19.95 17.25l-1.5-1.5q.275-.675.413-1.313T19 13.1q0-2.9-2.05-5T12 6q-.7 0-1.35.113t-1.3.387L7.85 5q.95-.5 1.988-.75T12 4q1.85 0 3.488.7t2.862 1.938q1.225 1.237 1.938 2.887T21 13.1q0 1.125-.275 2.163t-.775 1.987ZM17.7 4.45q-.275-.275-.275-.7t.275-.7q.275-.275.7-.275t.7.275l2.85 2.85q.275.275.275.7t-.275.7q-.275.275-.7.275t-.7-.275L17.7 4.45ZM12 22q-1.85 0-3.488-.7T5.65 19.4q-1.225-1.2-1.938-2.825T3 13.1q0-1.55.463-2.912T4.8 7.7l-.85-.85l-.5.5q-.275.275-.7.275t-.7-.275q-.275-.275-.275-.7t.275-.7l.5-.5L1.4 4.3q-.275-.275-.275-.7t.275-.7q.275-.275.7-.275t.7.275l18.4 18.4q.275.275.275.7t-.275.7q-.275.275-.7.275t-.7-.275l-2.45-2.45q-1.125.825-2.487 1.288T12 22Zm0-1.975q1.05 0 2.05-.325t1.85-.9L6.2 9.15q-.575.875-.887 1.888T5 13.1q0 2.9 2.05 4.913T12 20.025Zm-.95-6.05Zm2.85-2.85Z",
 | 
			
		||||
  "alert-outline": "M12 1.996a7.49 7.49 0 0 1 7.496 7.25l.004.25v4.097l1.38 3.156a1.25 1.25 0 0 1-1.145 1.75L15 18.502a3 3 0 0 1-5.995.177L9 18.499H4.275a1.251 1.251 0 0 1-1.147-1.747L4.5 13.594V9.496c0-4.155 3.352-7.5 7.5-7.5ZM13.5 18.5l-3 .002a1.5 1.5 0 0 0 2.993.145l.006-.147ZM12 3.496c-3.32 0-6 2.674-6 6v4.41L4.656 17h14.697L18 13.907V9.509l-.004-.225A5.988 5.988 0 0 0 12 3.496Z",
 | 
			
		||||
@@ -112,6 +113,7 @@
 | 
			
		||||
  "error-circle-outline": "M12 2c5.523 0 10 4.478 10 10s-4.477 10-10 10S2 17.522 2 12 6.477 2 12 2Zm0 1.667c-4.595 0-8.333 3.738-8.333 8.333 0 4.595 3.738 8.333 8.333 8.333 4.595 0 8.333-3.738 8.333-8.333 0-4.595-3.738-8.333-8.333-8.333Zm-.001 10.835a.999.999 0 1 1 0 1.998.999.999 0 0 1 0-1.998ZM11.994 7a.75.75 0 0 1 .744.648l.007.101.004 4.502a.75.75 0 0 1-1.493.103l-.007-.102-.004-4.501a.75.75 0 0 1 .75-.751Z",
 | 
			
		||||
  "file-upload-outline": "M6 2a2 2 0 0 0-2 2v5.207a5.48 5.48 0 0 1 1-.185V4a1 1 0 0 1 1-1h4v3.5A1.5 1.5 0 0 0 11.5 8H15v8a1 1 0 0 1-1 1h-3.6a5.507 5.507 0 0 1-.657 1H14a2 2 0 0 0 2-2V7.414a1.5 1.5 0 0 0-.44-1.06l-3.914-3.915A1.5 1.5 0 0 0 10.586 2H6Zm8.793 5H11.5a.5.5 0 0 1-.5-.5V3.207L14.793 7ZM5.5 19a4.5 4.5 0 1 0 0-9a4.5 4.5 0 0 0 0 9Zm2.354-4.854a.5.5 0 1 1-.708.708L6 13.707V16.5a.5.5 0 0 1-1 0v-2.793l-1.146 1.147a.5.5 0 1 1-.708-.707l2-2A.5.5 0 0 1 5.497 12h.006a.498.498 0 0 1 .348.144l.003.003l2 2Z",
 | 
			
		||||
  "filter-outline": "M13.5 16a.75.75 0 0 1 0 1.5h-3a.75.75 0 0 1 0-1.5h3Zm3-5a.75.75 0 0 1 0 1.5h-9a.75.75 0 0 1 0-1.5h9Zm3-5a.75.75 0 0 1 0 1.5h-15a.75.75 0 0 1 0-1.5h15Z",
 | 
			
		||||
  "flame-outline": "M8.5 14.5A2.5 2.5 0 0 0 11 12c0-1.38-.5-2-1-3c-1.072-2.143-.224-4.054 2-6c.5 2.5 2 4.9 4 6.5c2 1.6 3 3.5 3 5.5a7 7 0 1 1-14 0c0-1.153.433-2.294 1-3a2.5 2.5 0 0 0 2.5 2.5",
 | 
			
		||||
  "flash-on-outline": "m8.294 14-1.767 7.068c-.187.746.736 1.256 1.269.701L19.79 9.27A.75.75 0 0 0 19.25 8h-4.46l1.672-5.013A.75.75 0 0 0 15.75 2h-7a.75.75 0 0 0-.721.544l-3 10.5A.75.75 0 0 0 5.75 14h2.544Zm4.745-5.487a.75.75 0 0 0 .711.987h3.74l-8.824 9.196 1.316-5.264a.75.75 0 0 0-.727-.932h-2.51l2.57-9h5.394l-1.67 5.013Z",
 | 
			
		||||
  "flash-settings-outline": "M6.19 2.77c.13-.455.547-.77 1.02-.77h5.25c.724 0 1.236.71 1.007 1.398l-.002.008L12.204 7h2.564c.946 0 1.407 1.144.766 1.811l-.003.004l-.237.242a5.545 5.545 0 0 0-1.374-.027l.894-.912a.056.056 0 0 0 .017-.032a.084.084 0 0 0-.007-.044a.079.079 0 0 0-.025-.034c-.005-.004-.013-.008-.031-.008h-3.27a.5.5 0 0 1-.471-.666L12.52 3.08a.062.062 0 0 0-.06-.08H7.211a.062.062 0 0 0-.06.045l-2.25 7.874c-.01.04.019.08.06.08H6.87a.5.5 0 0 1 .485.62l-1.325 5.3a.086.086 0 0 0-.003.03a.02.02 0 0 0 .003.011a.08.08 0 0 0 .072.04a.03.03 0 0 0 .01-.004a.087.087 0 0 0 .024-.018l.003-.004l2.882-2.94a5.573 5.573 0 0 0 .054 1.372l-2.22 2.267c-.754.782-2.059.06-1.795-.996l1.17-4.679H4.96a1.062 1.062 0 0 1-1.021-1.354l2.25-7.873Zm5.877 8.673a2 2 0 0 1-1.431 2.478l-.461.118a4.702 4.702 0 0 0 .01 1.016l.35.083a2 2 0 0 1 1.456 2.519l-.127.422c.257.204.537.378.835.518l.325-.344a2 2 0 0 1 2.91.002l.337.358c.292-.135.568-.302.822-.498l-.157-.556a2 2 0 0 1 1.431-2.479l.46-.117a4.702 4.702 0 0 0-.01-1.017l-.348-.082a2 2 0 0 1-1.456-2.52l.126-.421a4.32 4.32 0 0 0-.835-.519l-.325.344a2 2 0 0 1-2.91-.001l-.337-.358a4.316 4.316 0 0 0-.821.497l.156.557ZM14.5 15.5a1 1 0 1 1 0-2a1 1 0 0 1 0 2Z",
 | 
			
		||||
  "folder-outline": "M8.207 4c.46 0 .908.141 1.284.402l.156.12L12.022 6.5h7.728a2.25 2.25 0 0 1 2.229 1.938l.016.158.005.154v9a2.25 2.25 0 0 1-2.096 2.245L19.75 20H4.25a2.25 2.25 0 0 1-2.245-2.096L2 17.75V6.25a2.25 2.25 0 0 1 2.096-2.245L4.25 4h3.957Zm1.44 5.979a2.25 2.25 0 0 1-1.244.512l-.196.009-4.707-.001v7.251c0 .38.282.694.648.743l.102.007h15.5a.75.75 0 0 0 .743-.648l.007-.102v-9a.75.75 0 0 0-.648-.743L19.75 8h-7.729L9.647 9.979ZM8.207 5.5H4.25a.75.75 0 0 0-.743.648L3.5 6.25v2.749L8.207 9a.75.75 0 0 0 .395-.113l.085-.06 1.891-1.578-1.89-1.575a.75.75 0 0 0-.377-.167L8.207 5.5Z",
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										15
									
								
								app/models/concerns/push_data_helper.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								app/models/concerns/push_data_helper.rb
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,15 @@
 | 
			
		||||
module PushDataHelper
 | 
			
		||||
  extend ActiveSupport::Concern
 | 
			
		||||
 | 
			
		||||
  def push_event_data
 | 
			
		||||
    Conversations::EventDataPresenter.new(self).push_data
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def lock_event_data
 | 
			
		||||
    Conversations::EventDataPresenter.new(self).lock_data
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def webhook_data
 | 
			
		||||
    Conversations::EventDataPresenter.new(self).push_data
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
@@ -57,6 +57,7 @@ class Conversation < ApplicationRecord
 | 
			
		||||
  include ActivityMessageHandler
 | 
			
		||||
  include UrlHelper
 | 
			
		||||
  include SortHandler
 | 
			
		||||
  include PushDataHelper
 | 
			
		||||
  include ConversationMuteHelpers
 | 
			
		||||
 | 
			
		||||
  validates :account_id, presence: true
 | 
			
		||||
@@ -171,18 +172,6 @@ class Conversation < ApplicationRecord
 | 
			
		||||
    unread_messages.where(account_id: account_id).incoming.last(10)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def push_event_data
 | 
			
		||||
    Conversations::EventDataPresenter.new(self).push_data
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def lock_event_data
 | 
			
		||||
    Conversations::EventDataPresenter.new(self).lock_data
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def webhook_data
 | 
			
		||||
    Conversations::EventDataPresenter.new(self).push_data
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def cached_label_list_array
 | 
			
		||||
    (cached_label_list || '').split(',').map(&:strip)
 | 
			
		||||
  end
 | 
			
		||||
@@ -207,6 +196,10 @@ class Conversation < ApplicationRecord
 | 
			
		||||
    "#{ENV.fetch('FRONTEND_URL', nil)}/survey/responses/#{uuid}"
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def dispatch_conversation_updated_event(previous_changes = nil)
 | 
			
		||||
    dispatcher_dispatch(CONVERSATION_UPDATED, previous_changes)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  private
 | 
			
		||||
 | 
			
		||||
  def execute_after_update_commit_callbacks
 | 
			
		||||
@@ -245,13 +238,17 @@ class Conversation < ApplicationRecord
 | 
			
		||||
  def notify_conversation_updation
 | 
			
		||||
    return unless previous_changes.keys.present? && allowed_keys?
 | 
			
		||||
 | 
			
		||||
    dispatcher_dispatch(CONVERSATION_UPDATED, previous_changes)
 | 
			
		||||
    dispatch_conversation_updated_event(previous_changes)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def list_of_keys
 | 
			
		||||
    %w[team_id assignee_id status snoozed_until custom_attributes label_list waiting_since first_reply_created_at
 | 
			
		||||
       priority]
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def allowed_keys?
 | 
			
		||||
    (
 | 
			
		||||
      previous_changes.keys.intersect?(%w[team_id assignee_id status snoozed_until custom_attributes label_list waiting_since first_reply_created_at
 | 
			
		||||
                                          priority]) ||
 | 
			
		||||
      previous_changes.keys.intersect?(list_of_keys) ||
 | 
			
		||||
      (previous_changes['additional_attributes'].present? && previous_changes['additional_attributes'][1].keys.intersect?(%w[conversation_language]))
 | 
			
		||||
    )
 | 
			
		||||
  end
 | 
			
		||||
@@ -314,3 +311,4 @@ end
 | 
			
		||||
 | 
			
		||||
Conversation.include_mod_with('Concerns::Conversation')
 | 
			
		||||
Conversation.include_mod_with('SentimentAnalysisHelper')
 | 
			
		||||
Conversation.prepend_mod_with('Conversation')
 | 
			
		||||
 
 | 
			
		||||
@@ -41,6 +41,8 @@ class AppliedSla < ApplicationRecord
 | 
			
		||||
                                      }
 | 
			
		||||
  scope :missed, -> { where(sla_status: :missed) }
 | 
			
		||||
 | 
			
		||||
  after_update_commit :push_conversation_event
 | 
			
		||||
 | 
			
		||||
  def push_event_data
 | 
			
		||||
    {
 | 
			
		||||
      id: id,
 | 
			
		||||
@@ -59,6 +61,16 @@ class AppliedSla < ApplicationRecord
 | 
			
		||||
 | 
			
		||||
  private
 | 
			
		||||
 | 
			
		||||
  def push_conversation_event
 | 
			
		||||
    # right now we simply use `CONVERSATION_UPDATED` event to notify the frontend
 | 
			
		||||
    # we can eventually start using `CONVERSATION_SLA_UPDATED` event as required later
 | 
			
		||||
    # for now the updated event should suffice
 | 
			
		||||
 | 
			
		||||
    return unless saved_change_to_sla_status?
 | 
			
		||||
 | 
			
		||||
    conversation.dispatch_conversation_updated_event
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def ensure_account_id
 | 
			
		||||
    self.account_id ||= sla_policy&.account_id
 | 
			
		||||
  end
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										5
									
								
								enterprise/app/models/enterprise/conversation.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								enterprise/app/models/enterprise/conversation.rb
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,5 @@
 | 
			
		||||
module Enterprise::Conversation
 | 
			
		||||
  def list_of_keys
 | 
			
		||||
    super + %w[sla_policy_id]
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
@@ -3,7 +3,8 @@ module Enterprise::Conversations::EventDataPresenter
 | 
			
		||||
    if account.feature_enabled?('sla')
 | 
			
		||||
      super.merge(
 | 
			
		||||
        applied_sla: applied_sla&.push_event_data,
 | 
			
		||||
        sla_events: sla_events.map(&:push_event_data)
 | 
			
		||||
        sla_events: sla_events.map(&:push_event_data),
 | 
			
		||||
        sla_policy_id: sla_policy_id
 | 
			
		||||
      )
 | 
			
		||||
    else
 | 
			
		||||
      super
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user