Enhancement: New dropdown component for agents and team.
This commit is contained in:
parent
c95aeb894f
commit
f6a56b6c7e
4 changed files with 524 additions and 60 deletions
|
@ -9,7 +9,6 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
/* global bus */
|
||||
import WootSnackbar from './Snackbar';
|
||||
|
||||
export default {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
169
app/javascript/shared/components/ui/DropdownWithSearch.vue
Normal file
169
app/javascript/shared/components/ui/DropdownWithSearch.vue
Normal 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>
|
Loading…
Reference in a new issue