Compare commits
17 commits
develop
...
ui/agent-d
Author | SHA1 | Date | |
---|---|---|---|
|
f5df7b45f2 | ||
|
309eba5d6a | ||
|
cd9b81f075 | ||
|
422177f72c | ||
|
3e5e6e8eaa | ||
|
4f79c13977 | ||
|
3d27d5a1d5 | ||
|
6884771911 | ||
|
9316995e62 | ||
|
37b722bb4d | ||
|
2ea7b440e5 | ||
|
0e88f1fc38 | ||
|
80d00bcc76 | ||
|
ffcec61a1f | ||
|
59f5121676 | ||
|
cbf7b7211d | ||
|
f6a56b6c7e |
10 changed files with 923 additions and 125 deletions
|
@ -32,33 +32,11 @@
|
|||
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=""
|
||||
:options="agentList"
|
||||
:placeholder="$t('CONVERSATION.ASSIGNMENT.SELECT_AGENT')"
|
||||
select-label=""
|
||||
label="name"
|
||||
selected-label
|
||||
track-by="id"
|
||||
@select="assignAgent"
|
||||
@remove="removeAgent"
|
||||
>
|
||||
<template slot="option" slot-scope="props">
|
||||
<div class="option__desc">
|
||||
<availability-status-badge
|
||||
:status="props.option.availability_status"
|
||||
/>
|
||||
<span class="option__title">{{ props.option.name }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<span slot="noResult">{{ $t('AGENT_MGMT.SEARCH.NO_RESULTS') }}</span>
|
||||
</multiselect>
|
||||
</div>
|
||||
<agent-dropdown
|
||||
:assigned-agent="currentChat.meta.assignee"
|
||||
:agents-list="agentList"
|
||||
@click="onClickAssignAgent"
|
||||
/>
|
||||
<more-actions :conversation-id="currentChat.id" />
|
||||
</div>
|
||||
</div>
|
||||
|
@ -67,13 +45,13 @@
|
|||
import { mapGetters } from 'vuex';
|
||||
import MoreActions from './MoreActions';
|
||||
import Thumbnail from '../Thumbnail';
|
||||
import AvailabilityStatusBadge from '../conversation/AvailabilityStatusBadge';
|
||||
import AgentDropdown from 'shared/components/ui/AgentDropdown.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
MoreActions,
|
||||
Thumbnail,
|
||||
AvailabilityStatusBadge,
|
||||
AgentDropdown,
|
||||
},
|
||||
|
||||
props: {
|
||||
|
@ -90,6 +68,7 @@ export default {
|
|||
data() {
|
||||
return {
|
||||
currentChatAssignee: null,
|
||||
showSearchDropdown: false,
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -113,17 +92,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];
|
||||
},
|
||||
},
|
||||
|
||||
|
@ -139,31 +108,35 @@ export default {
|
|||
});
|
||||
},
|
||||
removeAgent() {},
|
||||
|
||||
onClickAssignAgent(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);
|
||||
}
|
||||
|
||||
.option__desc {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.text-truncate {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.option__desc {
|
||||
&::v-deep .status-badge {
|
||||
margin-right: var(--space-small);
|
||||
min-width: 0;
|
||||
flex-shrink: 0;
|
||||
::v-deep.dropdown-wrap {
|
||||
width: 21rem;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -90,7 +90,14 @@
|
|||
}
|
||||
},
|
||||
"SEARCH": {
|
||||
"NO_RESULTS": "No agents found."
|
||||
"NO_RESULTS": "No results found."
|
||||
},
|
||||
"SELECTOR": {
|
||||
"TITLE": {
|
||||
"AGENT": "Select Agent",
|
||||
"TEAM": "Select Team"
|
||||
},
|
||||
"PLACEHOLDER": "None"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -65,10 +65,6 @@ export default {
|
|||
position: relative;
|
||||
border-left: 1px solid var(--color-border);
|
||||
padding: var(--space-medium) var(--space-two);
|
||||
|
||||
.contact-labels {
|
||||
padding-bottom: var(--space-normal);
|
||||
}
|
||||
}
|
||||
|
||||
.close-button {
|
||||
|
|
|
@ -6,45 +6,29 @@
|
|||
<contact-info :contact="contact" :channel-type="channelType" />
|
||||
<div class="conversation--actions">
|
||||
<div class="multiselect-wrap--small">
|
||||
<contact-details-item
|
||||
:title="$t('CONVERSATION_SIDEBAR.ASSIGNEE_LABEL')"
|
||||
icon="ion-headphone"
|
||||
emoji="🧑🚀"
|
||||
>
|
||||
<template v-slot:button>
|
||||
<woot-button
|
||||
v-if="showSelfAssign"
|
||||
icon="ion-arrow-right-c"
|
||||
variant="link"
|
||||
size="small"
|
||||
class-names="button-content"
|
||||
@click="onSelfAssign"
|
||||
>
|
||||
{{ $t('CONVERSATION_SIDEBAR.SELF_ASSIGN') }}
|
||||
</woot-button>
|
||||
</template>
|
||||
</contact-details-item>
|
||||
<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"
|
||||
>
|
||||
<template slot="option" slot-scope="props">
|
||||
<div class="option__desc">
|
||||
<availability-status-badge
|
||||
:status="props.option.availability_status"
|
||||
/>
|
||||
<span class="option__title">{{ props.option.name }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<span slot="noResult">{{ $t('AGENT_MGMT.SEARCH.NO_RESULTS') }}</span>
|
||||
</multiselect>
|
||||
<div class="self-assign">
|
||||
<contact-details-item
|
||||
:title="$t('CONVERSATION_SIDEBAR.ASSIGNEE_LABEL')"
|
||||
icon="ion-headphone"
|
||||
emoji="🧑🚀"
|
||||
>
|
||||
</contact-details-item>
|
||||
<woot-button
|
||||
v-if="showSelfAssign"
|
||||
icon="ion-arrow-right-c"
|
||||
variant="link"
|
||||
size="small"
|
||||
class-names="button-content"
|
||||
@click="onSelfAssign"
|
||||
>
|
||||
{{ $t('CONVERSATION_SIDEBAR.SELF_ASSIGN') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
<agent-dropdown
|
||||
:assigned-agent="assignedAgent"
|
||||
:agents-list="agentsList"
|
||||
@click="onClickShowAgent"
|
||||
/>
|
||||
</div>
|
||||
<div class="multiselect-wrap--small">
|
||||
<contact-details-item
|
||||
|
@ -52,19 +36,11 @@
|
|||
icon="ion-ios-people"
|
||||
emoji="🎢"
|
||||
/>
|
||||
<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"
|
||||
>
|
||||
<span slot="noResult">{{ $t('AGENT_MGMT.SEARCH.NO_RESULTS') }}</span>
|
||||
</multiselect>
|
||||
<teams-dropdown
|
||||
:assigned-team="assignedTeam"
|
||||
:teams-list="teamsList"
|
||||
@click="onClickShowTeam"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<conversation-labels :conversation-id="conversationId" />
|
||||
|
@ -131,15 +107,16 @@
|
|||
<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';
|
||||
import ContactInfo from './contact/ContactInfo';
|
||||
import ConversationLabels from './labels/LabelBox.vue';
|
||||
import ContactCustomAttributes from './ContactCustomAttributes';
|
||||
import AvailabilityStatusBadge from 'dashboard/components/widgets/conversation/AvailabilityStatusBadge.vue';
|
||||
|
||||
import flag from 'country-code-emoji';
|
||||
import AgentDropdown from 'shared/components/ui/AgentDropdown.vue';
|
||||
import TeamsDropdown from 'shared/components/ui/TeamsDropdown.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
@ -148,9 +125,10 @@ export default {
|
|||
ContactDetailsItem,
|
||||
ContactInfo,
|
||||
ConversationLabels,
|
||||
AvailabilityStatusBadge,
|
||||
AgentDropdown,
|
||||
TeamsDropdown,
|
||||
},
|
||||
mixins: [alertMixin],
|
||||
mixins: [alertMixin, clickaway],
|
||||
props: {
|
||||
conversationId: {
|
||||
type: [Number, String],
|
||||
|
@ -165,6 +143,13 @@ export default {
|
|||
default: () => {},
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
currentChatAssignee: null,
|
||||
showSearchDropdown: false,
|
||||
showSearchDropdownTeam: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
currentChat: 'getSelectedChat',
|
||||
|
@ -310,6 +295,34 @@ export default {
|
|||
openTranscriptModal() {
|
||||
this.showTranscriptModal = true;
|
||||
},
|
||||
toggleDropdown() {
|
||||
this.showSearchDropdown = !this.showSearchDropdown;
|
||||
},
|
||||
toggleDropdownTeam() {
|
||||
this.showSearchDropdownTeam = !this.showSearchDropdownTeam;
|
||||
},
|
||||
onClickShowAgent(selectedItem) {
|
||||
if (this.assignedAgent && this.assignedAgent.id === selectedItem.id) {
|
||||
this.assignedAgent = null;
|
||||
} else {
|
||||
this.assignedAgent = selectedItem;
|
||||
}
|
||||
return this.assignedAgent;
|
||||
},
|
||||
onClickShowTeam(selectedItemTeam) {
|
||||
if (this.assignedTeam && this.assignedTeam.id === selectedItemTeam.id) {
|
||||
this.assignedTeam = null;
|
||||
} else {
|
||||
this.assignedTeam = selectedItemTeam;
|
||||
}
|
||||
return this.assignedTeam;
|
||||
},
|
||||
onCloseDropdown() {
|
||||
this.showSearchDropdown = false;
|
||||
},
|
||||
onCloseDropdownTeam() {
|
||||
this.showSearchDropdownTeam = false;
|
||||
},
|
||||
onSelfAssign() {
|
||||
const {
|
||||
account_id,
|
||||
|
@ -353,6 +366,11 @@ export default {
|
|||
}
|
||||
}
|
||||
|
||||
::v-deep .dropdown-wrap {
|
||||
margin-top: var(--space-micro);
|
||||
margin-bottom: var(--space-small);
|
||||
}
|
||||
|
||||
::v-deep {
|
||||
.contact--profile {
|
||||
padding-bottom: var(--space-slab);
|
||||
|
@ -391,7 +409,7 @@ export default {
|
|||
|
||||
.label {
|
||||
color: #fff;
|
||||
padding: 0.2rem;
|
||||
padding: var(--space-micro);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -411,14 +429,15 @@ export default {
|
|||
margin-bottom: var(--space-normal);
|
||||
}
|
||||
|
||||
.option__desc {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.multiselect__label {
|
||||
margin-bottom: var(--space-smaller);
|
||||
}
|
||||
|
||||
&::v-deep .status-badge {
|
||||
margin-right: var(--space-small);
|
||||
min-width: 0;
|
||||
flex-shrink: 0;
|
||||
.self-assign {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
.button-content {
|
||||
margin-bottom: var(--space-small);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
53
app/javascript/shared/components/ui/AgentDropdown.stories.js
Normal file
53
app/javascript/shared/components/ui/AgentDropdown.stories.js
Normal file
|
@ -0,0 +1,53 @@
|
|||
import { action } from '@storybook/addon-actions';
|
||||
import AgentDropdownSelector from './AgentDropdown';
|
||||
|
||||
export default {
|
||||
title: 'Components/Dropdown/Agent Dropdown',
|
||||
component: AgentDropdownSelector,
|
||||
argTypes: {
|
||||
agentsList: {
|
||||
control: {
|
||||
type: 'object',
|
||||
},
|
||||
},
|
||||
assignedAgent: {
|
||||
control: {
|
||||
type: 'object',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const Template = (args, { argTypes }) => ({
|
||||
props: Object.keys(argTypes),
|
||||
components: { AgentDropdownSelector },
|
||||
template:
|
||||
'<agent-dropdown-selector v-bind="$props" @click="onClick"></agent-dropdown-selector>',
|
||||
});
|
||||
|
||||
export const AgentDropdown = Template.bind({});
|
||||
AgentDropdown.args = {
|
||||
onClick: action('Opened'),
|
||||
agentsList: [
|
||||
{
|
||||
account_id: 1,
|
||||
availability_status: 'online',
|
||||
available_name: 'John',
|
||||
email: 'john@gmail.com',
|
||||
id: 1,
|
||||
name: 'John',
|
||||
role: 'administrator',
|
||||
thumbnail: 'https://randomuser.me/api/portraits/men/4.jpg',
|
||||
},
|
||||
],
|
||||
assignedAgent: {
|
||||
account_id: 1,
|
||||
availability_status: 'online',
|
||||
available_name: 'John',
|
||||
email: 'john@gmail.com',
|
||||
id: 1,
|
||||
name: 'John',
|
||||
role: 'administrator',
|
||||
thumbnail: 'https://randomuser.me/api/portraits/men/4.jpg',
|
||||
},
|
||||
};
|
167
app/javascript/shared/components/ui/AgentDropdown.vue
Normal file
167
app/javascript/shared/components/ui/AgentDropdown.vue
Normal file
|
@ -0,0 +1,167 @@
|
|||
<template>
|
||||
<div v-on-clickaway="onCloseDropdownAgent" class="dropdown-wrap">
|
||||
<button
|
||||
:v-model="assignedAgent"
|
||||
class="button-input"
|
||||
@click="toggleDropdownAgent"
|
||||
>
|
||||
<thumbnail
|
||||
v-if="
|
||||
assignedAgent &&
|
||||
assignedAgent.name &&
|
||||
assignedAgent &&
|
||||
assignedAgent.id
|
||||
"
|
||||
:src="assignedAgent && assignedAgent.thumbnail"
|
||||
size="24px"
|
||||
:status="assignedAgent && assignedAgent.availability_status"
|
||||
:badge="assignedAgent && assignedAgent.channel"
|
||||
:username="assignedAgent && assignedAgent.name"
|
||||
/>
|
||||
<div class="name-icon-wrap">
|
||||
<div v-if="!noAssignedAgent" class="name select-agent">
|
||||
{{ $t('AGENT_MGMT.SELECTOR.PLACEHOLDER') }}
|
||||
</div>
|
||||
<div v-else class="name" :title="assignedAgent.name">
|
||||
{{ assignedAgent.name }}
|
||||
</div>
|
||||
<i v-if="showSearchDropdownAgent" class="icon ion-chevron-up" />
|
||||
<i v-else class="icon ion-chevron-down" />
|
||||
</div>
|
||||
</button>
|
||||
<div
|
||||
:class="{ 'dropdown-pane--open': showSearchDropdownAgent }"
|
||||
class="dropdown-pane"
|
||||
>
|
||||
<h4 class="text-block-title">
|
||||
{{ $t('AGENT_MGMT.SELECTOR.TITLE.AGENT') }}
|
||||
</h4>
|
||||
<agents-list
|
||||
v-if="showSearchDropdownAgent"
|
||||
:options="agentsList"
|
||||
:value="assignedAgent"
|
||||
@click="showAgent"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
||||
import agentsList from 'shared/components/ui/AgentDropdownList.vue';
|
||||
import { mixin as clickaway } from 'vue-clickaway';
|
||||
export default {
|
||||
components: {
|
||||
Thumbnail,
|
||||
agentsList,
|
||||
},
|
||||
mixins: [clickaway],
|
||||
props: {
|
||||
agentsList: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
assignedAgent: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showSearchDropdownAgent: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
noAssignedAgent() {
|
||||
if (this.assignedAgent && this.assignedAgent.id) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
toggleDropdownAgent() {
|
||||
this.showSearchDropdownAgent = !this.showSearchDropdownAgent;
|
||||
},
|
||||
onCloseDropdownAgent() {
|
||||
this.showSearchDropdownAgent = false;
|
||||
},
|
||||
showAgent(value) {
|
||||
this.$emit('click', value);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.dropdown-wrap {
|
||||
display: flex;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
margin-right: var(--space-one);
|
||||
margin-bottom: var(--space-small);
|
||||
|
||||
.button-input {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
cursor: pointer;
|
||||
justify-content: flex-start;
|
||||
background: white;
|
||||
font-size: var(--font-size-small);
|
||||
border: 1px solid lightgray;
|
||||
border-radius: var(--border-radius-normal);
|
||||
padding: 0.6rem;
|
||||
}
|
||||
|
||||
&::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);
|
||||
min-width: 0;
|
||||
|
||||
.select-agent {
|
||||
color: var(--b-600);
|
||||
}
|
||||
|
||||
.name {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
margin-right: var(--space-small);
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-pane {
|
||||
box-sizing: border-box;
|
||||
top: 4rem;
|
||||
right: 0;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
|
||||
&::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>
|
|
@ -0,0 +1,55 @@
|
|||
import { action } from '@storybook/addon-actions';
|
||||
import AgentDropdownList from './AgentDropdownList';
|
||||
|
||||
export default {
|
||||
title: 'Components/Dropdown/Agent List',
|
||||
component: AgentDropdownList,
|
||||
argTypes: {
|
||||
options: {
|
||||
control: {
|
||||
type: 'object',
|
||||
},
|
||||
},
|
||||
value: {
|
||||
control: {
|
||||
type: 'object',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const Template = (args, { argTypes }) => ({
|
||||
props: Object.keys(argTypes),
|
||||
components: { AgentDropdownList },
|
||||
template:
|
||||
'<agent-dropdown-list v-bind="$props" @click="onClick"></agent-dropdown-list>',
|
||||
});
|
||||
|
||||
export const AgentList = Template.bind({});
|
||||
AgentList.args = {
|
||||
onClick: action('Added'),
|
||||
options: [
|
||||
{
|
||||
id: '1',
|
||||
name: 'John',
|
||||
availability_status: 'online',
|
||||
role: 'administrator',
|
||||
thumbnail: 'https://randomuser.me/api/portraits/men/4.jpg',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'Tim Donarld',
|
||||
availability_status: 'offline',
|
||||
role: 'administrator',
|
||||
thumbnail: '',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: 'James Philip',
|
||||
availability_status: 'busy',
|
||||
role: 'agent',
|
||||
thumbnail: 'https://randomuser.me/api/portraits/men/17.jpg',
|
||||
},
|
||||
],
|
||||
value: { id: '1' },
|
||||
};
|
183
app/javascript/shared/components/ui/AgentDropdownList.vue
Normal file
183
app/javascript/shared/components/ui/AgentDropdownList.vue
Normal file
|
@ -0,0 +1,183 @@
|
|||
<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"
|
||||
:status="option.availability_status"
|
||||
/>
|
||||
<div class="name-icon-wrap">
|
||||
<div class="name" :title="option.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="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 'dashboard/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%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: 16rem;
|
||||
|
||||
.search-wrap {
|
||||
margin-bottom: var(--space-small);
|
||||
flex: 0 0 auto;
|
||||
max-height: var(--space-large);
|
||||
|
||||
.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;
|
||||
flex: 1 1 auto;
|
||||
overflow: auto;
|
||||
|
||||
.list {
|
||||
width: 100%;
|
||||
max-height: 12rem;
|
||||
|
||||
.button {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
|
||||
&.active {
|
||||
display: flex;
|
||||
font-weight: var(--font-weight-bold);
|
||||
color: var(--w-700);
|
||||
}
|
||||
|
||||
.name-icon-wrap {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.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;
|
||||
color: var(--s-700);
|
||||
width: 100%;
|
||||
padding: var(--space-small) var(--space-one);
|
||||
font-weight: var(--font-weight-medium);
|
||||
font-size: var(--font-size-small);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
163
app/javascript/shared/components/ui/TeamsDropdown.vue
Normal file
163
app/javascript/shared/components/ui/TeamsDropdown.vue
Normal file
|
@ -0,0 +1,163 @@
|
|||
<template>
|
||||
<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="!noAssignedTeam" class="name select-agent">
|
||||
{{ $t('AGENT_MGMT.SELECTOR.PLACEHOLDER') }}
|
||||
</div>
|
||||
<div v-else class="name" :title="assignedTeam.name">
|
||||
{{ assignedTeam.name }}
|
||||
</div>
|
||||
|
||||
<i v-if="showSearchDropdownTeam" class="icon ion-chevron-up" />
|
||||
<i v-else class="icon ion-chevron-down" />
|
||||
</div>
|
||||
</button>
|
||||
<div
|
||||
:class="{ 'dropdown-pane--open': showSearchDropdownTeam }"
|
||||
class="dropdown-pane"
|
||||
>
|
||||
<h4 class="text-block-title">
|
||||
{{ $t('AGENT_MGMT.SELECTOR.TITLE.TEAM') }}
|
||||
</h4>
|
||||
<teams-list
|
||||
v-if="showSearchDropdownTeam"
|
||||
:options="teamsList"
|
||||
:value="assignedTeam"
|
||||
@click="showTeam"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import teamsList from 'shared/components/ui/TeamsDropdownList.vue';
|
||||
import { mixin as clickaway } from 'vue-clickaway';
|
||||
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
||||
export default {
|
||||
components: {
|
||||
teamsList,
|
||||
Thumbnail,
|
||||
},
|
||||
mixins: [clickaway],
|
||||
props: {
|
||||
teamsList: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
assignedTeam: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showSearchDropdownTeam: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
noAssignedTeam() {
|
||||
if (this.assignedTeam && this.assignedTeam.id) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
toggleDropdownTeam() {
|
||||
this.showSearchDropdownTeam = !this.showSearchDropdownTeam;
|
||||
},
|
||||
onCloseDropdownTeam() {
|
||||
this.showSearchDropdownTeam = false;
|
||||
},
|
||||
showTeam(value) {
|
||||
this.$emit('click', value);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.dropdown-wrap {
|
||||
display: flex;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
margin-right: var(--space-one);
|
||||
|
||||
.button-input {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
cursor: pointer;
|
||||
justify-content: flex-start;
|
||||
background: white;
|
||||
font-size: var(--font-size-small);
|
||||
cursor: pointer;
|
||||
border: 1px solid lightgray;
|
||||
border-radius: var(--border-radius-normal);
|
||||
padding: 0.6rem;
|
||||
}
|
||||
|
||||
&::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);
|
||||
min-width: 0;
|
||||
|
||||
.select-agent {
|
||||
color: var(--b-600);
|
||||
}
|
||||
|
||||
.name {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
margin-right: var(--space-small);
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-pane {
|
||||
box-sizing: border-box;
|
||||
top: 4rem;
|
||||
right: 0;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
|
||||
&::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>
|
182
app/javascript/shared/components/ui/TeamsDropdownList.vue
Normal file
182
app/javascript/shared/components/ui/TeamsDropdownList.vue
Normal file
|
@ -0,0 +1,182 @@
|
|||
<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" :title="option.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="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 'dashboard/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%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: 16rem;
|
||||
|
||||
.search-wrap {
|
||||
margin-bottom: var(--space-small);
|
||||
flex: 0 0 auto;
|
||||
max-height: var(--space-large);
|
||||
|
||||
.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;
|
||||
flex: 1 1 auto;
|
||||
overflow: auto;
|
||||
|
||||
.list {
|
||||
width: 100%;
|
||||
max-height: 12rem;
|
||||
|
||||
.button {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
|
||||
&.active {
|
||||
display: flex;
|
||||
font-weight: var(--font-weight-bold);
|
||||
color: var(--w-700);
|
||||
}
|
||||
|
||||
.name-icon-wrap {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.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;
|
||||
color: var(--s-700);
|
||||
width: 100%;
|
||||
padding: var(--space-small) var(--space-one);
|
||||
font-weight: var(--font-weight-medium);
|
||||
font-size: var(--font-size-small);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
Loading…
Reference in a new issue