mirror of
https://github.com/lingble/chatwoot.git
synced 2025-11-01 11:37:58 +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>
|
<template>
|
||||||
<div
|
<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="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="{
|
:class="[
|
||||||
hide: !showConversationList,
|
{ hidden: !showConversationList },
|
||||||
'list--full-width': isOnExpandedLayout,
|
isOnExpandedLayout ? 'basis-full' : 'flex-basis-clamp',
|
||||||
}"
|
]"
|
||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
<div
|
<div
|
||||||
class="flex items-center justify-between py-0 px-4"
|
class="flex items-center justify-between px-4 py-0"
|
||||||
:class="{
|
:class="{
|
||||||
'pb-3 border-b border-slate-75 dark:border-slate-700':
|
'pb-3 border-b border-slate-75 dark:border-slate-700':
|
||||||
hasAppliedFiltersOrActiveFolders,
|
hasAppliedFiltersOrActiveFolders,
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
>
|
>
|
||||||
<div class="flex max-w-[85%] justify-center items-center">
|
<div class="flex max-w-[85%] justify-center items-center">
|
||||||
<h1
|
<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"
|
:title="pageTitle"
|
||||||
>
|
>
|
||||||
{{ pageTitle }}
|
{{ pageTitle }}
|
||||||
@@ -107,7 +107,7 @@
|
|||||||
|
|
||||||
<p
|
<p
|
||||||
v-if="!chatListLoading && !conversationList.length"
|
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') }}
|
{{ $t('CHAT_LIST.LIST.404') }}
|
||||||
</p>
|
</p>
|
||||||
@@ -127,7 +127,7 @@
|
|||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
ref="conversationList"
|
ref="conversationList"
|
||||||
class="conversations-list flex-1"
|
class="flex-1 conversations-list"
|
||||||
:class="{ 'overflow-hidden': isContextMenuOpen }"
|
:class="{ 'overflow-hidden': isContextMenuOpen }"
|
||||||
>
|
>
|
||||||
<virtual-list
|
<virtual-list
|
||||||
@@ -136,16 +136,16 @@
|
|||||||
:data-sources="conversationList"
|
:data-sources="conversationList"
|
||||||
:data-component="itemComponent"
|
:data-component="itemComponent"
|
||||||
:extra-props="virtualListExtraProps"
|
:extra-props="virtualListExtraProps"
|
||||||
class="w-full overflow-auto h-full"
|
class="w-full h-full overflow-auto"
|
||||||
footer-tag="div"
|
footer-tag="div"
|
||||||
>
|
>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<div v-if="chatListLoading" class="text-center">
|
<div v-if="chatListLoading" class="text-center">
|
||||||
<span class="spinner mt-4 mb-4" />
|
<span class="mt-4 mb-4 spinner" />
|
||||||
</div>
|
</div>
|
||||||
<p
|
<p
|
||||||
v-if="showEndOfListMessage"
|
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') }}
|
{{ $t('CHAT_LIST.EOF') }}
|
||||||
</p>
|
</p>
|
||||||
@@ -1034,24 +1034,10 @@ export default {
|
|||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.conversations-list-wrap {
|
|
||||||
&.hide {
|
|
||||||
@apply hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.list--full-width {
|
|
||||||
@apply basis-full;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.conversations-list {
|
.conversations-list {
|
||||||
@apply overflow-hidden hover:overflow-y-auto;
|
@apply overflow-hidden hover:overflow-y-auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.load-more--button {
|
|
||||||
@apply text-center rounded-none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab--chat-type {
|
.tab--chat-type {
|
||||||
@apply py-0 px-4;
|
@apply py-0 px-4;
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="ltr:mr-1 rtl:ml-1 mb-1"
|
class="inline-flex ltr:mr-1 rtl:ml-1 mb-1"
|
||||||
:class="labelClass"
|
:class="labelClass"
|
||||||
:style="labelStyle"
|
:style="labelStyle"
|
||||||
:title="description"
|
:title="description"
|
||||||
@@ -111,7 +111,7 @@ export default {
|
|||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.label {
|
.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 {
|
&.small {
|
||||||
@apply text-xs py-0.5 px-1 leading-tight h-5;
|
@apply text-xs py-0.5 px-1 leading-tight h-5;
|
||||||
|
|||||||
@@ -86,7 +86,11 @@
|
|||||||
{{ unreadCount > 9 ? '9+' : unreadCount }}
|
{{ unreadCount > 9 ? '9+' : unreadCount }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
<woot-context-menu
|
<woot-context-menu
|
||||||
v-if="showContextMenu"
|
v-if="showContextMenu"
|
||||||
@@ -125,6 +129,7 @@ import alertMixin from 'shared/mixins/alertMixin';
|
|||||||
import TimeAgo from 'dashboard/components/ui/TimeAgo.vue';
|
import TimeAgo from 'dashboard/components/ui/TimeAgo.vue';
|
||||||
import CardLabels from './conversationCardComponents/CardLabels.vue';
|
import CardLabels from './conversationCardComponents/CardLabels.vue';
|
||||||
import PriorityMark from './PriorityMark.vue';
|
import PriorityMark from './PriorityMark.vue';
|
||||||
|
import SLACardLabel from './components/SLACardLabel.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
@@ -135,6 +140,7 @@ export default {
|
|||||||
TimeAgo,
|
TimeAgo,
|
||||||
MessagePreview,
|
MessagePreview,
|
||||||
PriorityMark,
|
PriorityMark,
|
||||||
|
SLACardLabel,
|
||||||
},
|
},
|
||||||
|
|
||||||
mixins: [inboxMixin, timeMixin, conversationMixin, alertMixin],
|
mixins: [inboxMixin, timeMixin, conversationMixin, alertMixin],
|
||||||
@@ -252,6 +258,9 @@ export default {
|
|||||||
const stateInbox = this.inbox;
|
const stateInbox = this.inbox;
|
||||||
return stateInbox.name || '';
|
return stateInbox.name || '';
|
||||||
},
|
},
|
||||||
|
hasSlaPolicyId() {
|
||||||
|
return this.chat?.sla_policy_id;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
onCardClick(e) {
|
onCardClick(e) {
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<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
|
<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'"
|
: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
|
<back-button
|
||||||
v-if="showBackButton"
|
v-if="showBackButton"
|
||||||
:back-url="backButtonUrl"
|
:back-url="backButtonUrl"
|
||||||
@@ -19,10 +19,10 @@
|
|||||||
:status="currentContact.availability_status"
|
:status="currentContact.availability_status"
|
||||||
/>
|
/>
|
||||||
<div
|
<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
|
<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
|
<woot-button
|
||||||
variant="link"
|
variant="link"
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
@click.prevent="$emit('contact-panel-toggle')"
|
@click.prevent="$emit('contact-panel-toggle')"
|
||||||
>
|
>
|
||||||
<span
|
<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 }}
|
{{ currentContact.name }}
|
||||||
</span>
|
</span>
|
||||||
@@ -46,7 +46,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<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" />
|
<inbox-name v-if="hasMultipleInboxes" :inbox="inbox" />
|
||||||
<span
|
<span
|
||||||
@@ -67,9 +67,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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 }"
|
:class="{ 'justify-end': isContactPanelOpen }"
|
||||||
>
|
>
|
||||||
|
<SLA-card-label v-if="hasSlaPolicyId" :chat="chat" show-extended-info />
|
||||||
<more-actions :conversation-id="currentChat.id" />
|
<more-actions :conversation-id="currentChat.id" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -85,6 +86,7 @@ import inboxMixin from 'shared/mixins/inboxMixin';
|
|||||||
import InboxName from '../InboxName.vue';
|
import InboxName from '../InboxName.vue';
|
||||||
import MoreActions from './MoreActions.vue';
|
import MoreActions from './MoreActions.vue';
|
||||||
import Thumbnail from '../Thumbnail.vue';
|
import Thumbnail from '../Thumbnail.vue';
|
||||||
|
import SLACardLabel from './components/SLACardLabel.vue';
|
||||||
import wootConstants from 'dashboard/constants/globals';
|
import wootConstants from 'dashboard/constants/globals';
|
||||||
import { conversationListPageURL } from 'dashboard/helper/URLHelper';
|
import { conversationListPageURL } from 'dashboard/helper/URLHelper';
|
||||||
import { snoozedReopenTime } from 'dashboard/helper/snoozeHelpers';
|
import { snoozedReopenTime } from 'dashboard/helper/snoozeHelpers';
|
||||||
@@ -95,6 +97,7 @@ export default {
|
|||||||
InboxName,
|
InboxName,
|
||||||
MoreActions,
|
MoreActions,
|
||||||
Thumbnail,
|
Thumbnail,
|
||||||
|
SLACardLabel,
|
||||||
},
|
},
|
||||||
mixins: [inboxMixin, agentMixin, eventListenerMixins],
|
mixins: [inboxMixin, agentMixin, eventListenerMixins],
|
||||||
props: {
|
props: {
|
||||||
@@ -173,6 +176,9 @@ export default {
|
|||||||
hasMultipleInboxes() {
|
hasMultipleInboxes() {
|
||||||
return this.$store.getters['inboxes/getInboxes'].length > 1;
|
return this.$store.getters['inboxes/getInboxes'].length > 1;
|
||||||
},
|
},
|
||||||
|
hasSlaPolicyId() {
|
||||||
|
return this.chat?.sla_policy_id;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
|||||||
@@ -1,47 +1,63 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="flex items-center px-2 truncate border min-w-fit border-slate-75 dark:border-slate-700"
|
v-if="hasSlaThreshold"
|
||||||
:class="showExtendedInfo ? 'py-[5px] rounded-lg' : 'py-0.5 gap-1 rounded'"
|
class="relative flex items-center border cursor-pointer min-w-fit border-slate-75 dark:border-slate-700"
|
||||||
|
:class="showExtendedInfo ? 'rounded-lg' : 'rounded'"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="flex items-center gap-1"
|
class="flex items-center w-full truncate"
|
||||||
:class="
|
:class="showExtendedInfo ? 'h-[26px] px-1.5' : 'h-5 px-2 gap-1'"
|
||||||
showExtendedInfo &&
|
@mouseover="openSlaPopover()"
|
||||||
'ltr:pr-1.5 rtl:pl-1.5 ltr:border-r rtl:border-l border-solid border-slate-75 dark:border-slate-700'
|
@mouseleave="closeSlaPopover()"
|
||||||
"
|
|
||||||
>
|
>
|
||||||
<fluent-icon
|
<div
|
||||||
size="14"
|
class="flex items-center gap-1"
|
||||||
:icon="slaStatus.icon"
|
:class="
|
||||||
type="outline"
|
showExtendedInfo &&
|
||||||
:icon-lib="isSlaMissed ? 'lucide' : 'fluent'"
|
'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-shrink-0"
|
"
|
||||||
:class="slaTextStyles"
|
|
||||||
/>
|
|
||||||
<span
|
|
||||||
v-if="showExtendedInfo"
|
|
||||||
class="text-xs font-medium"
|
|
||||||
:class="slaTextStyles"
|
|
||||||
>
|
>
|
||||||
{{ 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>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<span
|
<SLA-popover-card
|
||||||
class="text-xs font-medium"
|
v-if="showSlaPopoverCard"
|
||||||
:class="[slaTextStyles, showExtendedInfo && 'ltr:pl-1.5 rtl:pr-1.5']"
|
:all-missed-slas="slaEvents"
|
||||||
>
|
class="right-0 top-7"
|
||||||
{{ slaStatus.threshold }}
|
/>
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapGetters } from 'vuex';
|
|
||||||
import { evaluateSLAStatus } from '../helpers/SLAHelper';
|
import { evaluateSLAStatus } from '../helpers/SLAHelper';
|
||||||
|
import SLAPopoverCard from './SLAPopoverCard.vue';
|
||||||
|
|
||||||
// const REFRESH_INTERVAL = 60000;
|
const REFRESH_INTERVAL = 60000;
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
components: {
|
||||||
|
SLAPopoverCard,
|
||||||
|
},
|
||||||
props: {
|
props: {
|
||||||
chat: {
|
chat: {
|
||||||
type: Object,
|
type: Object,
|
||||||
@@ -55,19 +71,27 @@ export default {
|
|||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
timer: null,
|
timer: null,
|
||||||
slaStatus: {},
|
showSlaPopover: false,
|
||||||
|
slaStatus: {
|
||||||
|
threshold: null,
|
||||||
|
isSlaMissed: false,
|
||||||
|
type: null,
|
||||||
|
icon: null,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters({
|
|
||||||
activeSLA: 'sla/getSLAById',
|
|
||||||
}),
|
|
||||||
slaPolicyId() {
|
slaPolicyId() {
|
||||||
return this.chat?.sla_policy_id;
|
return this.chat?.sla_policy_id;
|
||||||
},
|
},
|
||||||
sla() {
|
appliedSLA() {
|
||||||
if (!this.slaPolicyId) return null;
|
return this.chat?.applied_sla;
|
||||||
return this.activeSLA(this.slaPolicyId);
|
},
|
||||||
|
slaEvents() {
|
||||||
|
return this.chat?.sla_events;
|
||||||
|
},
|
||||||
|
hasSlaThreshold() {
|
||||||
|
return this.slaStatus?.threshold;
|
||||||
},
|
},
|
||||||
isSlaMissed() {
|
isSlaMissed() {
|
||||||
return this.slaStatus?.isSlaMissed;
|
return this.slaStatus?.isSlaMissed;
|
||||||
@@ -79,12 +103,17 @@ export default {
|
|||||||
},
|
},
|
||||||
slaStatusText() {
|
slaStatusText() {
|
||||||
const upperCaseType = this.slaStatus?.type?.toUpperCase(); // FRT, NRT, or RT
|
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}`, {
|
return this.$t(`CONVERSATION.HEADER.SLA_STATUS.${upperCaseType}`, {
|
||||||
status: this.$t(`CONVERSATION.HEADER.SLA_STATUS.${statusKey}`),
|
status: this.$t(`CONVERSATION.HEADER.SLA_STATUS.${statusKey}`),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
showSlaPopoverCard() {
|
||||||
|
return (
|
||||||
|
this.showExtendedInfo && this.showSlaPopover && this.slaEvents.length
|
||||||
|
);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
chat() {
|
chat() {
|
||||||
@@ -93,10 +122,29 @@ export default {
|
|||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.updateSlaStatus();
|
this.updateSlaStatus();
|
||||||
|
this.createTimer();
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
if (this.timer) {
|
||||||
|
clearTimeout(this.timer);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
createTimer() {
|
||||||
|
this.timer = setTimeout(() => {
|
||||||
|
this.updateSlaStatus();
|
||||||
|
this.createTimer();
|
||||||
|
}, REFRESH_INTERVAL);
|
||||||
|
},
|
||||||
updateSlaStatus() {
|
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>
|
<template>
|
||||||
<div
|
<div
|
||||||
v-show="activeLabels.length"
|
v-if="activeLabels.length || $slots.before"
|
||||||
ref="labelContainer"
|
ref="labelContainer"
|
||||||
class="label-container mt-0.5 mx-2 mb-0"
|
v-resize="computeVisibleLabelPosition"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="labels-wrap flex items-end min-w-0 flex-shrink gap-y-1 flex-wrap"
|
class="flex items-end flex-shrink min-w-0 gap-y-1"
|
||||||
:class="{ expand: showAllLabels }"
|
:class="{ 'h-auto overflow-visible flex-row flex-wrap': showAllLabels }"
|
||||||
>
|
>
|
||||||
|
<slot name="before" />
|
||||||
<woot-label
|
<woot-label
|
||||||
v-for="(label, index) in activeLabels"
|
v-for="(label, index) in activeLabels"
|
||||||
:key="label.id"
|
:key="label.id"
|
||||||
@@ -26,7 +27,7 @@
|
|||||||
? $t('CONVERSATION.CARD.HIDE_LABELS')
|
? $t('CONVERSATION.CARD.HIDE_LABELS')
|
||||||
: $t('CONVERSATION.CARD.SHOW_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"
|
color-scheme="secondary"
|
||||||
variant="hollow"
|
variant="hollow"
|
||||||
:icon="showAllLabels ? 'chevron-left' : 'chevron-right'"
|
:icon="showAllLabels ? 'chevron-left' : 'chevron-right'"
|
||||||
@@ -59,26 +60,34 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
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();
|
this.computeVisibleLabelPosition();
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
onShowLabels(e) {
|
onShowLabels(e) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
this.showAllLabels = !this.showAllLabels;
|
this.showAllLabels = !this.showAllLabels;
|
||||||
|
this.$nextTick(() => this.computeVisibleLabelPosition());
|
||||||
},
|
},
|
||||||
computeVisibleLabelPosition() {
|
computeVisibleLabelPosition() {
|
||||||
|
const beforeSlot = this.$slots.before ? 100 : 0;
|
||||||
const labelContainer = this.$refs.labelContainer;
|
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;
|
let labelOffset = 0;
|
||||||
this.showExpandLabelButton = false;
|
this.showExpandLabelButton = false;
|
||||||
|
labels.forEach((label, index) => {
|
||||||
Array.from(labels).forEach((label, index) => {
|
|
||||||
labelOffset += label.offsetWidth + 8;
|
labelOffset += label.offsetWidth + 8;
|
||||||
|
if (labelOffset < labelContainer.clientWidth - 16 - beforeSlot) {
|
||||||
if (labelOffset < labelContainer.clientWidth - 16) {
|
|
||||||
this.labelPosition = index;
|
this.labelPosition = index;
|
||||||
} else {
|
} else {
|
||||||
this.showExpandLabelButton = true;
|
this.showExpandLabelButton = labels.length > 1;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -95,10 +104,6 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.labels-wrap {
|
.labels-wrap {
|
||||||
&.expand {
|
|
||||||
@apply h-auto overflow-visible flex-row flex-wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.secondary {
|
.secondary {
|
||||||
@apply border border-solid border-slate-100 dark:border-slate-700;
|
@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}",
|
"FRT": "FRT {status}",
|
||||||
"NRT": "NRT {status}",
|
"NRT": "NRT {status}",
|
||||||
"RT": "RT {status}",
|
"RT": "RT {status}",
|
||||||
"BREACH": "breach",
|
"MISSED": "missed",
|
||||||
"DUE": "due"
|
"DUE": "due"
|
||||||
|
},
|
||||||
|
"SLA_POPOVER": {
|
||||||
|
"FRT": "First Response Time",
|
||||||
|
"NRT": "Next Response Time",
|
||||||
|
"RT": "Resolution Time",
|
||||||
|
"MISSED": "missed on"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"RESOLVE_DROPDOWN": {
|
"RESOLVE_DROPDOWN": {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export default {
|
|||||||
this.conversationId
|
this.conversationId
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
// TODO - Get rid of this from the mixin
|
||||||
activeLabels() {
|
activeLabels() {
|
||||||
return this.accountLabels.filter(({ title }) =>
|
return this.accountLabels.filter(({ title }) =>
|
||||||
this.savedLabels.includes(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-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-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",
|
"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-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",
|
"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",
|
"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",
|
"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",
|
"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",
|
"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-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",
|
"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",
|
"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 ActivityMessageHandler
|
||||||
include UrlHelper
|
include UrlHelper
|
||||||
include SortHandler
|
include SortHandler
|
||||||
|
include PushDataHelper
|
||||||
include ConversationMuteHelpers
|
include ConversationMuteHelpers
|
||||||
|
|
||||||
validates :account_id, presence: true
|
validates :account_id, presence: true
|
||||||
@@ -171,18 +172,6 @@ class Conversation < ApplicationRecord
|
|||||||
unread_messages.where(account_id: account_id).incoming.last(10)
|
unread_messages.where(account_id: account_id).incoming.last(10)
|
||||||
end
|
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
|
def cached_label_list_array
|
||||||
(cached_label_list || '').split(',').map(&:strip)
|
(cached_label_list || '').split(',').map(&:strip)
|
||||||
end
|
end
|
||||||
@@ -207,6 +196,10 @@ class Conversation < ApplicationRecord
|
|||||||
"#{ENV.fetch('FRONTEND_URL', nil)}/survey/responses/#{uuid}"
|
"#{ENV.fetch('FRONTEND_URL', nil)}/survey/responses/#{uuid}"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def dispatch_conversation_updated_event(previous_changes = nil)
|
||||||
|
dispatcher_dispatch(CONVERSATION_UPDATED, previous_changes)
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def execute_after_update_commit_callbacks
|
def execute_after_update_commit_callbacks
|
||||||
@@ -245,13 +238,17 @@ class Conversation < ApplicationRecord
|
|||||||
def notify_conversation_updation
|
def notify_conversation_updation
|
||||||
return unless previous_changes.keys.present? && allowed_keys?
|
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
|
end
|
||||||
|
|
||||||
def allowed_keys?
|
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
|
previous_changes.keys.intersect?(list_of_keys) ||
|
||||||
priority]) ||
|
|
||||||
(previous_changes['additional_attributes'].present? && previous_changes['additional_attributes'][1].keys.intersect?(%w[conversation_language]))
|
(previous_changes['additional_attributes'].present? && previous_changes['additional_attributes'][1].keys.intersect?(%w[conversation_language]))
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
@@ -314,3 +311,4 @@ end
|
|||||||
|
|
||||||
Conversation.include_mod_with('Concerns::Conversation')
|
Conversation.include_mod_with('Concerns::Conversation')
|
||||||
Conversation.include_mod_with('SentimentAnalysisHelper')
|
Conversation.include_mod_with('SentimentAnalysisHelper')
|
||||||
|
Conversation.prepend_mod_with('Conversation')
|
||||||
|
|||||||
@@ -41,6 +41,8 @@ class AppliedSla < ApplicationRecord
|
|||||||
}
|
}
|
||||||
scope :missed, -> { where(sla_status: :missed) }
|
scope :missed, -> { where(sla_status: :missed) }
|
||||||
|
|
||||||
|
after_update_commit :push_conversation_event
|
||||||
|
|
||||||
def push_event_data
|
def push_event_data
|
||||||
{
|
{
|
||||||
id: id,
|
id: id,
|
||||||
@@ -59,6 +61,16 @@ class AppliedSla < ApplicationRecord
|
|||||||
|
|
||||||
private
|
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
|
def ensure_account_id
|
||||||
self.account_id ||= sla_policy&.account_id
|
self.account_id ||= sla_policy&.account_id
|
||||||
end
|
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')
|
if account.feature_enabled?('sla')
|
||||||
super.merge(
|
super.merge(
|
||||||
applied_sla: applied_sla&.push_event_data,
|
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
|
else
|
||||||
super
|
super
|
||||||
|
|||||||
Reference in New Issue
Block a user