feat: Display "Snoozed Until" time on conversation header (#3028)
This commit is contained in:
35
app/javascript/dashboard/components/widgets/InboxName.vue
Normal file
35
app/javascript/dashboard/components/widgets/InboxName.vue
Normal file
@@ -0,0 +1,35 @@
|
||||
<template>
|
||||
<span class="inbox--name">
|
||||
<i :class="computedInboxClass" />
|
||||
{{ inbox.name }}
|
||||
</span>
|
||||
</template>
|
||||
<script>
|
||||
import { getInboxClassByType } from 'dashboard/helper/inbox';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
inbox: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
computedInboxClass() {
|
||||
const { phone_number: phoneNumber, channel_type: type } = this.inbox;
|
||||
const classByType = getInboxClassByType(type, phoneNumber);
|
||||
return classByType;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style scoped>
|
||||
.inbox--name {
|
||||
padding: var(--space-micro) 0;
|
||||
line-height: var(--space-slab);
|
||||
font-weight: var(--font-weight-medium);
|
||||
background: none;
|
||||
color: var(--s-500);
|
||||
font-size: var(--font-size-mini);
|
||||
}
|
||||
</style>
|
||||
@@ -19,11 +19,7 @@
|
||||
/>
|
||||
<div class="conversation--details columns">
|
||||
<div class="conversation--metadata">
|
||||
<span v-if="showInboxName" class="label">
|
||||
<i :class="computedInboxClass" />
|
||||
{{ inboxName }}
|
||||
</span>
|
||||
|
||||
<inbox-name v-if="showInboxName" :inbox="inbox" />
|
||||
<span
|
||||
v-if="showAssignee && assignee"
|
||||
class="label assignee-label text-truncate"
|
||||
@@ -72,16 +68,17 @@
|
||||
import { mapGetters } from 'vuex';
|
||||
import { MESSAGE_TYPE } from 'widget/helpers/constants';
|
||||
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
||||
import { getInboxClassByType } from 'dashboard/helper/inbox';
|
||||
import Thumbnail from '../Thumbnail';
|
||||
import conversationMixin from '../../../mixins/conversations';
|
||||
import timeMixin from '../../../mixins/time';
|
||||
import router from '../../../routes';
|
||||
import { frontendURL, conversationUrl } from '../../../helper/URLHelper';
|
||||
import InboxName from '../InboxName';
|
||||
import inboxMixin from 'shared/mixins/inboxMixin';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
InboxName,
|
||||
Thumbnail,
|
||||
},
|
||||
|
||||
@@ -192,12 +189,6 @@ export default {
|
||||
return stateInbox;
|
||||
},
|
||||
|
||||
computedInboxClass() {
|
||||
const { phone_number: phoneNumber, channel_type: type } = this.inbox;
|
||||
const classByType = getInboxClassByType(type, phoneNumber);
|
||||
return classByType;
|
||||
},
|
||||
|
||||
showInboxName() {
|
||||
return (
|
||||
!this.hideInboxName &&
|
||||
@@ -244,15 +235,6 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
.conversation--details .label {
|
||||
padding: var(--space-micro) 0 var(--space-micro) 0;
|
||||
line-height: var(--space-slab);
|
||||
font-weight: var(--font-weight-medium);
|
||||
background: none;
|
||||
color: var(--s-500);
|
||||
font-size: var(--font-size-mini);
|
||||
}
|
||||
|
||||
.conversation--details {
|
||||
.conversation--user {
|
||||
padding-top: var(--space-micro);
|
||||
@@ -276,6 +258,15 @@ export default {
|
||||
justify-content: space-between;
|
||||
padding-right: var(--space-normal);
|
||||
|
||||
.label {
|
||||
padding: var(--space-micro) 0 var(--space-micro) 0;
|
||||
line-height: var(--space-slab);
|
||||
font-weight: var(--font-weight-medium);
|
||||
background: none;
|
||||
color: var(--s-500);
|
||||
font-size: var(--font-size-mini);
|
||||
}
|
||||
|
||||
.assignee-label {
|
||||
max-width: 50%;
|
||||
}
|
||||
|
||||
@@ -12,20 +12,23 @@
|
||||
<h3 class="user--name text-truncate">
|
||||
{{ currentContact.name }}
|
||||
</h3>
|
||||
<woot-button
|
||||
class="user--profile__button"
|
||||
size="small"
|
||||
variant="link"
|
||||
@click="$emit('contact-panel-toggle')"
|
||||
>
|
||||
{{
|
||||
`${
|
||||
isContactPanelOpen
|
||||
? $t('CONVERSATION.HEADER.CLOSE')
|
||||
: $t('CONVERSATION.HEADER.OPEN')
|
||||
} ${$t('CONVERSATION.HEADER.DETAILS')}`
|
||||
}}
|
||||
</woot-button>
|
||||
<div class="conversation--header--actions">
|
||||
<inbox-name :inbox="inbox" class="margin-right-small" />
|
||||
<span
|
||||
v-if="isSnoozed"
|
||||
class="snoozed--display-text margin-right-small"
|
||||
>
|
||||
{{ snoozedDisplayText }}
|
||||
</span>
|
||||
<woot-button
|
||||
class="user--profile__button margin-right-small"
|
||||
size="small"
|
||||
variant="link"
|
||||
@click="$emit('contact-panel-toggle')"
|
||||
>
|
||||
{{ contactPanelToggleText }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
@@ -44,9 +47,13 @@ import agentMixin from '../../../mixins/agentMixin.js';
|
||||
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
||||
import inboxMixin from 'shared/mixins/inboxMixin';
|
||||
import { hasPressedAltAndOKey } from 'shared/helpers/KeyboardHelpers';
|
||||
import wootConstants from '../../../constants';
|
||||
import differenceInHours from 'date-fns/differenceInHours';
|
||||
import InboxName from '../InboxName';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
InboxName,
|
||||
MoreActions,
|
||||
Thumbnail,
|
||||
},
|
||||
@@ -61,39 +68,50 @@ export default {
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
currentChatAssignee: null,
|
||||
inboxId: null,
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters({
|
||||
uiFlags: 'inboxAssignableAgents/getUIFlags',
|
||||
currentChat: 'getSelectedChat',
|
||||
}),
|
||||
|
||||
chatMetadata() {
|
||||
return this.chat.meta;
|
||||
},
|
||||
|
||||
inbox() {
|
||||
const { inbox_id: inboxId } = this.chat;
|
||||
const stateInbox = this.$store.getters['inboxes/getInbox'](inboxId);
|
||||
return stateInbox;
|
||||
},
|
||||
|
||||
currentContact() {
|
||||
return this.$store.getters['contacts/getContact'](
|
||||
this.chat.meta.sender.id
|
||||
);
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
const { inbox_id: inboxId } = this.chat;
|
||||
this.inboxId = inboxId;
|
||||
isSnoozed() {
|
||||
return this.currentChat.status === wootConstants.STATUS_TYPE.SNOOZED;
|
||||
},
|
||||
snoozedDisplayText() {
|
||||
const { snoozed_until: snoozedUntil } = this.currentChat;
|
||||
if (snoozedUntil) {
|
||||
// When the snooze is applied, it schedules the unsnooze event to next day/week 9AM.
|
||||
// By that logic if the time difference is less than or equal to 24 + 9 hours we can consider it tomorrow.
|
||||
const MAX_TIME_DIFFERENCE = 33;
|
||||
const isSnoozedUntilTomorrow =
|
||||
differenceInHours(new Date(snoozedUntil), new Date()) <=
|
||||
MAX_TIME_DIFFERENCE;
|
||||
return this.$t(
|
||||
isSnoozedUntilTomorrow
|
||||
? 'CONVERSATION.HEADER.SNOOZED_UNTIL_TOMORROW'
|
||||
: 'CONVERSATION.HEADER.SNOOZED_UNTIL_NEXT_WEEK'
|
||||
);
|
||||
}
|
||||
return this.$t('CONVERSATION.HEADER.SNOOZED_UNTIL_NEXT_REPLY');
|
||||
},
|
||||
contactPanelToggleText() {
|
||||
return `${
|
||||
this.isContactPanelOpen
|
||||
? this.$t('CONVERSATION.HEADER.CLOSE')
|
||||
: this.$t('CONVERSATION.HEADER.OPEN')
|
||||
} ${this.$t('CONVERSATION.HEADER.DETAILS')}`;
|
||||
},
|
||||
inbox() {
|
||||
const { inbox_id: inboxId } = this.chat;
|
||||
return this.$store.getters['inboxes/getInbox'](inboxId);
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
@@ -129,4 +147,28 @@ export default {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.user--name {
|
||||
display: inline-block;
|
||||
font-size: var(--font-size-medium);
|
||||
line-height: 1.3;
|
||||
margin: 0;
|
||||
text-transform: capitalize;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.conversation--header--actions {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
font-size: var(--font-size-mini);
|
||||
|
||||
.user--profile__button {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.snoozed--display-text {
|
||||
font-weight: var(--font-weight-medium);
|
||||
color: var(--y-900);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,41 +1,29 @@
|
||||
<template>
|
||||
<div class="flex-container actions--container">
|
||||
<woot-button
|
||||
v-if="!currentChat.muted"
|
||||
v-tooltip="$t('CONTACT_PANEL.MUTE_CONTACT')"
|
||||
class="hollow secondary actions--button"
|
||||
icon="ion-volume-mute"
|
||||
@click="mute"
|
||||
/>
|
||||
<woot-button
|
||||
v-else
|
||||
v-tooltip.left="$t('CONTACT_PANEL.UNMUTE_CONTACT')"
|
||||
class="hollow secondary actions--button"
|
||||
icon="ion-volume-medium"
|
||||
@click="unmute"
|
||||
/>
|
||||
<woot-button
|
||||
v-tooltip="$t('CONTACT_PANEL.SEND_TRANSCRIPT')"
|
||||
class="hollow secondary actions--button"
|
||||
icon="ion-share"
|
||||
@click="toggleEmailActionsModal"
|
||||
/>
|
||||
<resolve-action
|
||||
:conversation-id="currentChat.id"
|
||||
:status="currentChat.status"
|
||||
/>
|
||||
<woot-button
|
||||
class="more--button"
|
||||
variant="clear"
|
||||
size="large"
|
||||
color-scheme="secondary"
|
||||
icon="ion-android-more-vertical"
|
||||
@click="toggleConversationActions"
|
||||
/>
|
||||
<div
|
||||
v-if="showConversationActions"
|
||||
v-on-clickaway="hideConversationActions"
|
||||
class="dropdown-pane dropdowm--bottom"
|
||||
:class="{ 'dropdown-pane--open': showConversationActions }"
|
||||
>
|
||||
<woot-dropdown-menu>
|
||||
<woot-dropdown-item v-if="!currentChat.muted">
|
||||
<button class="button clear alert " @click="mute">
|
||||
<span>{{ $t('CONTACT_PANEL.MUTE_CONTACT') }}</span>
|
||||
</button>
|
||||
</woot-dropdown-item>
|
||||
<woot-dropdown-item v-else>
|
||||
<button class="button clear alert" @click="unmute">
|
||||
<span>{{ $t('CONTACT_PANEL.UNMUTE_CONTACT') }}</span>
|
||||
</button>
|
||||
</woot-dropdown-item>
|
||||
<woot-dropdown-item>
|
||||
<button class="button clear" @click="toggleEmailActionsModal">
|
||||
{{ $t('CONTACT_PANEL.SEND_TRANSCRIPT') }}
|
||||
</button>
|
||||
</woot-dropdown-item>
|
||||
</woot-dropdown-menu>
|
||||
</div>
|
||||
<email-transcript-modal
|
||||
v-if="showEmailActionsModal"
|
||||
:show="showEmailActionsModal"
|
||||
@@ -50,13 +38,9 @@ import { mixin as clickaway } from 'vue-clickaway';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import EmailTranscriptModal from './EmailTranscriptModal';
|
||||
import ResolveAction from '../../buttons/ResolveAction';
|
||||
import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem.vue';
|
||||
import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
WootDropdownMenu,
|
||||
WootDropdownItem,
|
||||
EmailTranscriptModal,
|
||||
ResolveAction,
|
||||
},
|
||||
@@ -97,7 +81,16 @@ export default {
|
||||
};
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
@import '~dashboard/assets/scss/mixins';
|
||||
.actions--container {
|
||||
align-items: center;
|
||||
|
||||
.button {
|
||||
font-size: var(--font-size-large);
|
||||
margin-right: var(--space-small);
|
||||
border-color: var(--color-border);
|
||||
color: var(--s-400);
|
||||
}
|
||||
}
|
||||
|
||||
.more--button {
|
||||
align-items: center;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { createLocalVue, mount } from '@vue/test-utils';
|
||||
import Vuex from 'vuex';
|
||||
import VueI18n from 'vue-i18n';
|
||||
import VTooltip from 'v-tooltip';
|
||||
|
||||
import Button from 'dashboard/components/buttons/Button';
|
||||
import i18n from 'dashboard/i18n';
|
||||
@@ -10,6 +11,7 @@ import MoreActions from '../MoreActions';
|
||||
const localVue = createLocalVue();
|
||||
localVue.use(Vuex);
|
||||
localVue.use(VueI18n);
|
||||
localVue.use(VTooltip);
|
||||
|
||||
localVue.component('woot-button', Button);
|
||||
|
||||
@@ -63,21 +65,9 @@ describe('MoveActions', () => {
|
||||
moreActions = mount(MoreActions, { store, localVue, i18n: i18nConfig });
|
||||
});
|
||||
|
||||
it('opens the menu when user clicks "more"', async () => {
|
||||
expect(moreActions.find('.dropdown-pane').exists()).toBe(false);
|
||||
|
||||
await moreActions.find('.more--button').trigger('click');
|
||||
|
||||
expect(moreActions.find('.dropdown-pane').exists()).toBe(true);
|
||||
});
|
||||
|
||||
describe('muting discussion', () => {
|
||||
it('triggers "muteConversation"', async () => {
|
||||
await moreActions.find('.more--button').trigger('click');
|
||||
|
||||
await moreActions
|
||||
.find('.dropdown-pane button:first-child')
|
||||
.trigger('click');
|
||||
await moreActions.find('button:first-child').trigger('click');
|
||||
|
||||
expect(muteConversation).toBeCalledWith(
|
||||
expect.any(Object),
|
||||
@@ -87,11 +77,7 @@ describe('MoveActions', () => {
|
||||
});
|
||||
|
||||
it('shows alert', async () => {
|
||||
await moreActions.find('.more--button').trigger('click');
|
||||
|
||||
await moreActions
|
||||
.find('.dropdown-pane button:first-child')
|
||||
.trigger('click');
|
||||
await moreActions.find('button:first-child').trigger('click');
|
||||
|
||||
expect(window.bus.$emit).toBeCalledWith(
|
||||
'newToastMessage',
|
||||
@@ -106,11 +92,7 @@ describe('MoveActions', () => {
|
||||
});
|
||||
|
||||
it('triggers "unmuteConversation"', async () => {
|
||||
await moreActions.find('.more--button').trigger('click');
|
||||
|
||||
await moreActions
|
||||
.find('.dropdown-pane button:first-child')
|
||||
.trigger('click');
|
||||
await moreActions.find('button:first-child').trigger('click');
|
||||
|
||||
expect(unmuteConversation).toBeCalledWith(
|
||||
expect.any(Object),
|
||||
@@ -120,11 +102,7 @@ describe('MoveActions', () => {
|
||||
});
|
||||
|
||||
it('shows alert', async () => {
|
||||
await moreActions.find('.more--button').trigger('click');
|
||||
|
||||
await moreActions
|
||||
.find('.dropdown-pane button:first-child')
|
||||
.trigger('click');
|
||||
await moreActions.find('button:first-child').trigger('click');
|
||||
|
||||
expect(window.bus.$emit).toBeCalledWith(
|
||||
'newToastMessage',
|
||||
|
||||
Reference in New Issue
Block a user