Enhancement: New dropdown component for agents and team.

This commit is contained in:
sivin-git 2021-04-19 11:21:32 +05:30
parent c95aeb894f
commit f6a56b6c7e
4 changed files with 524 additions and 60 deletions

View file

@ -9,7 +9,6 @@
</template>
<script>
/* global bus */
import WootSnackbar from './Snackbar';
export default {

View file

@ -1,5 +1,5 @@
<template>
<div class="conv-header">
<div v-on-clickaway="onCloseDropdown" class="conv-header">
<div class="user">
<Thumbnail
:src="currentContact.thumbnail"
@ -30,31 +30,64 @@
class="header-actions-wrap"
:class="{ 'has-open-sidebar': isContactPanelOpen }"
>
<div class="multiselect-box multiselect-wrap--small">
<i class="icon ion-headphone" />
<multiselect
v-model="currentChat.meta.assignee"
:loading="uiFlags.isFetching"
:allow-empty="true"
:deselect-label="$t('CONVERSATION.ASSIGNMENT.REMOVE')"
:options="agentList"
:placeholder="$t('CONVERSATION.ASSIGNMENT.SELECT_AGENT')"
:select-label="$t('CONVERSATION.ASSIGNMENT.ASSIGN')"
label="name"
selected-label
track-by="id"
@select="assignAgent"
@remove="removeAgent"
<div class="dropdown-wrap">
<button
:v-model="currentChat.meta.assignee"
class="button-input"
@click="toggleDropdown"
>
<span slot="noResult">{{ $t('AGENT_MGMT.SEARCH.NO_RESULTS') }}</span>
</multiselect>
<Thumbnail
v-if="
currentChat.meta.assignee &&
currentChat.meta.assignee.name &&
currentChat.meta.assignee &&
currentChat.meta.assignee.id
"
:src="
currentChat.meta.assignee && currentChat.meta.assignee.thumbnail
"
size="24px"
:badge="chatMetadata.channel"
:username="
currentChat.meta.assignee && currentChat.meta.assignee.name
"
/>
<div v-if="!currentChat.meta.assignee" class="ion-headphone"></div>
<div class="name-icon-wrap">
<div class="name-wrap">
<div v-if="!currentChat.meta.assignee" class="name select-agent">
{{ 'Select Agent' }}
</div>
<div v-else class="name">
{{
currentChat.meta.assignee && currentChat.meta.assignee.name
}}
</div>
</div>
<i v-if="showSearchDropdown" class="icon ion-chevron-up" />
<i v-else class="icon ion-chevron-down" />
</div>
</button>
<div
:class="{ 'dropdown-pane--open': showSearchDropdown }"
class="dropdown-pane"
>
<select-menu
v-if="showSearchDropdown"
:options="agentList"
:value="currentChat.meta.assignee"
@click="onClick"
/>
</div>
</div>
<more-actions :conversation-id="currentChat.id" />
</div>
</div>
</template>
<script>
import { mixin as clickaway } from 'vue-clickaway';
import { mapGetters } from 'vuex';
import SelectMenu from 'shared/components/ui/DropdownWithSearch';
import MoreActions from './MoreActions';
import Thumbnail from '../Thumbnail';
@ -62,8 +95,11 @@ export default {
components: {
MoreActions,
Thumbnail,
SelectMenu,
},
mixins: [clickaway],
props: {
chat: {
type: Object,
@ -78,6 +114,7 @@ export default {
data() {
return {
currentChatAssignee: null,
showSearchDropdown: false,
};
},
@ -101,17 +138,7 @@ export default {
agentList() {
const { inbox_id: inboxId } = this.chat;
const agents = this.getAgents(inboxId) || [];
return [
{
confirmed: true,
name: 'None',
id: 0,
role: 'agent',
account_id: 0,
email: 'None',
},
...agents,
];
return [...agents];
},
},
@ -128,18 +155,114 @@ export default {
},
removeAgent() {},
toggleDropdown() {
this.showSearchDropdown = !this.showSearchDropdown;
},
onCloseDropdown() {
this.showSearchDropdown = false;
},
onClick(selectedItem) {
if (
this.currentChat.meta.assignee &&
this.currentChat.meta.assignee.id === selectedItem.id
) {
this.currentChat.meta.assignee = '';
} else {
this.currentChat.meta.assignee = selectedItem;
}
this.assignAgent(this.currentChat.meta.assignee);
},
},
};
</script>
<style lang="scss" scoped>
.text-truncate {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.conv-header {
flex: 0 0 var(--space-jumbo);
.text-truncate {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.dropdown-wrap {
display: flex;
position: relative;
margin-right: var(--space-one);
.button-input {
display: flex;
cursor: pointer;
border: 1px solid lightgray;
border-radius: var(--border-radius-normal);
width: 21rem;
justify-content: flex-end;
background: white;
font-size: var(--font-size-small);
padding: 0.6rem;
}
.ion-headphone {
padding: 0.5rem;
}
.name-icon-wrap {
display: flex;
justify-content: flex-start;
padding: var(--space-smaller) var(--space-small);
width: 17rem;
.name-wrap {
width: 14rem;
padding: 0 var(--space-smaller);
text-align: start;
line-height: var(--space-normal);
.select-agent {
color: var(--b-600);
}
.name {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
}
.icon {
display: flex;
justify-content: space-between;
padding: 0.1rem;
}
.dropdown-pane {
top: 4rem;
right: 0;
max-width: 19rem;
&::v-deep {
.dropdown-menu__item .button {
width: 18.6rem;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
padding: var(--space-smaller) var(--space-small);
}
.name-icon-wrap {
width: 16rem;
}
.name {
width: 12rem;
}
}
}
}
}
</style>

View file

@ -12,33 +12,95 @@
<label class="multiselect__label">
{{ $t('CONVERSATION_SIDEBAR.ASSIGNEE_LABEL') }}
</label>
<multiselect
v-model="assignedAgent"
:options="agentsList"
label="name"
track-by="id"
deselect-label=""
select-label=""
selected-label=""
:placeholder="$t('CONVERSATION_SIDEBAR.SELECT.PLACEHOLDER')"
:allow-empty="true"
/>
<div v-on-clickaway="onCloseDropdown" class="dropdown-wrap">
<button
:v-model="assignedAgent"
class="button-input"
@click="toggleDropdown"
>
<Thumbnail
v-if="
assignedAgent &&
assignedAgent.name &&
assignedAgent &&
assignedAgent.id
"
:src="assignedAgent && assignedAgent.thumbnail"
size="24px"
:badge="assignedAgent && assignedAgent.channel"
:username="assignedAgent && assignedAgent.name"
/>
<div class="name-icon-wrap">
<div v-if="!assignedAgent" class="name select-agent">
{{ 'Select Agent' }}
</div>
<div v-else class="name">
{{ assignedAgent && assignedAgent.name }}
</div>
<i v-if="showSearchDropdown" class="icon ion-close-round" />
<i v-else class="icon ion-chevron-down" />
</div>
</button>
<div
:class="{ 'dropdown-pane--open': showSearchDropdown }"
class="dropdown-pane"
>
<select-menu
v-if="showSearchDropdown"
:options="agentsList"
:value="assignedAgent"
@click="onClick"
/>
</div>
</div>
</div>
<div class="multiselect-wrap--small">
<label class="multiselect__label">
{{ $t('CONVERSATION_SIDEBAR.TEAM_LABEL') }}
</label>
<multiselect
v-model="assignedTeam"
:options="teamsList"
label="name"
track-by="id"
deselect-label=""
select-label=""
selected-label=""
:placeholder="$t('CONVERSATION_SIDEBAR.SELECT.PLACEHOLDER')"
:allow-empty="true"
/>
<div v-on-clickaway="onCloseDropdownTeam" class="dropdown-wrap">
<button
:v-model="assignedTeam"
class="button-input"
@click="toggleDropdownTeam"
>
<Thumbnail
v-if="
assignedTeam &&
assignedTeam.name &&
assignedTeam &&
assignedTeam.id
"
:src="assignedTeam && assignedTeam.thumbnail"
size="24px"
:badge="assignedTeam.channel"
:username="assignedTeam && assignedTeam.name"
/>
<div class="name-icon-wrap">
<div v-if="!assignedTeam" class="name select-agent">
{{ 'Select Agent' }}
</div>
<div v-else class="name">
{{ assignedTeam && assignedTeam.name }}
</div>
<i v-if="showSearchDropdownTeam" class="icon ion-close-round" />
<i v-else class="icon ion-chevron-down" />
</div>
</button>
<div
:class="{ 'dropdown-pane--open': showSearchDropdownTeam }"
class="dropdown-pane"
>
<select-menu
v-if="showSearchDropdownTeam"
:options="teamsList"
:value="assignedTeam"
@click="onClickTeam"
/>
</div>
</div>
</div>
</div>
<div v-if="browser.browser_name" class="conversation--details">
@ -105,6 +167,7 @@
<script>
import { mapGetters } from 'vuex';
import alertMixin from 'shared/mixins/alertMixin';
import { mixin as clickaway } from 'vue-clickaway';
import ContactConversations from './ContactConversations.vue';
import ContactDetailsItem from './ContactDetailsItem.vue';
@ -112,6 +175,8 @@ import ContactInfo from './contact/ContactInfo';
import ConversationLabels from './labels/LabelBox.vue';
import ContactCustomAttributes from './ContactCustomAttributes';
import flag from 'country-code-emoji';
import SelectMenu from 'shared/components/ui/DropdownWithSearch';
import Thumbnail from 'components/widgets/Thumbnail.vue';
export default {
components: {
@ -120,8 +185,10 @@ export default {
ContactDetailsItem,
ContactInfo,
ConversationLabels,
Thumbnail,
SelectMenu,
},
mixins: [alertMixin],
mixins: [alertMixin, clickaway],
props: {
conversationId: {
type: [Number, String],
@ -136,6 +203,13 @@ export default {
default: () => {},
},
},
data() {
return {
currentChatAssignee: null,
showSearchDropdown: false,
showSearchDropdownTeam: false,
};
},
computed: {
...mapGetters({
currentChat: 'getSelectedChat',
@ -271,6 +345,34 @@ export default {
openTranscriptModal() {
this.showTranscriptModal = true;
},
toggleDropdown() {
this.showSearchDropdown = !this.showSearchDropdown;
},
toggleDropdownTeam() {
this.showSearchDropdownTeam = !this.showSearchDropdownTeam;
},
onClick(selectedItem) {
if (this.assignedAgent && this.assignedAgent.id === selectedItem.id) {
this.assignedAgent = '';
} else {
this.assignedAgent = selectedItem;
}
return this.assignedAgent;
},
onClickTeam(selectedItemTeam) {
if (this.assignedTeam && this.assignedTeam.id === selectedItemTeam.id) {
this.assignedTeam = '';
} else {
this.assignedTeam = selectedItemTeam;
}
return this.assignedTeam;
},
onCloseDropdown() {
this.showSearchDropdown = false;
},
onCloseDropdownTeam() {
this.showSearchDropdownTeam = false;
},
},
};
</script>
@ -315,7 +417,7 @@ export default {
.label {
color: #fff;
padding: 0.2rem;
padding: var(--space-micro);
}
}
@ -342,4 +444,75 @@ export default {
.multiselect__label {
margin-bottom: var(--space-smaller);
}
.conversation--actions {
.dropdown-wrap {
display: flex;
position: relative;
margin-right: var(--space-one);
.button-input {
display: flex;
width: 34rem;
cursor: pointer;
justify-content: flex-start;
background: white;
font-size: var(--font-size-small);
padding: var(--space-small) 0 var(--space-one) 0;
}
&::v-deep .user-thumbnail-box {
margin-right: var(--space-one);
}
.name-icon-wrap {
display: flex;
justify-content: space-between;
width: 100%;
padding: var(--space-smaller) 0;
line-height: var(--space-normal);
.select-agent {
color: var(--b-600);
}
.name {
display: flex;
justify-content: space-between;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.icon {
display: flex;
justify-content: space-between;
padding: 0.1rem;
}
}
.dropdown-pane {
top: 4rem;
right: 0;
width: 93.5%;
&::v-deep {
.dropdown-menu__item .button {
width: 100%;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
padding: var(--space-smaller) var(--space-small);
.name-icon-wrap {
width: 100%;
}
.name {
width: 100%;
}
}
}
}
}
}
</style>

View file

@ -0,0 +1,169 @@
<template>
<div class="dropdown-search-wrap">
<div class="search-wrap">
<input
ref="searchbar"
v-model="search"
type="text"
class="search-input"
autofocus="true"
placeholder="Filter"
/>
</div>
<div class="list-wrap">
<div class="list">
<woot-dropdown-menu>
<woot-dropdown-item
v-for="option in filteredOptions"
:key="option.id"
>
<button
class="button clear"
:class="{ active: option.id === (value && value.id) }"
@click="() => onclick(option)"
>
<Thumbnail
:src="option.thumbnail"
size="25px"
:username="option.name"
/>
<div class="name-icon-wrap">
<div class="name">
{{ option.name }}
</div>
<i
v-if="option.id === (value && value.id)"
class="icon ion-checkmark-round"
/>
</div>
</button>
</woot-dropdown-item>
</woot-dropdown-menu>
<div v-if="noResult" class="button clear no-result">
{{ $t('AGENT_MGMT.SEARCH.NO_RESULTS') }}
</div>
</div>
</div>
</div>
</template>
<script>
import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem.vue';
import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu.vue';
import Thumbnail from 'components/widgets/Thumbnail.vue';
export default {
components: {
WootDropdownItem,
WootDropdownMenu,
Thumbnail,
},
props: {
options: {
type: Array,
default: () => [],
},
value: {
type: Object,
default: () => ({}),
},
},
data() {
return {
search: '',
};
},
computed: {
filteredOptions() {
return this.options.filter(option => {
return option.name.toLowerCase().includes(this.search.toLowerCase());
});
},
noResult() {
return this.filteredOptions.length === 0 && this.search !== '';
},
},
mounted() {
this.focusInput();
},
methods: {
onclick(option) {
this.$emit('click', option);
},
focusInput() {
this.$refs.searchbar.focus();
},
},
};
</script>
<style lang="scss" scoped>
.dropdown-search-wrap {
width: 100%;
.search-wrap {
margin-bottom: var(--space-small);
.search-input {
margin: 0;
width: 100%;
border: none;
height: var(--space-large);
font-size: var(--font-size-small);
padding: var(--space-small);
background-color: var(--color-background);
}
input:focus {
border: 1px solid var(--w-500);
}
}
.list-wrap {
display: flex;
justify-content: flex-start;
align-items: flex-start;
.list {
width: 100%;
.button {
display: flex;
justify-content: flex-start;
&.active {
display: flex;
font-weight: 600;
color: #1a4d8f;
}
.name-icon-wrap {
display: flex;
justify-content: space-between;
padding: 0.5rem;
}
.name {
padding: 0 var(--space-smaller);
line-height: var(--space-normal);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.icon {
margin-left: var(--space-smaller);
}
}
.no-result {
display: flex;
justify-content: center;
}
}
}
}
</style>