Feature: Improve label experience (#975)
Co-authored-by: Sojan <sojan@pepalo.com>
This commit is contained in:
parent
8b61452d56
commit
97ad39713b
56 changed files with 1712 additions and 284 deletions
|
@ -1,8 +1,13 @@
|
|||
<template>
|
||||
<div class="conv-details--item">
|
||||
<h4 class="conv-details--item__label">
|
||||
<i v-if="icon" :class="icon" class="conv-details--item__icon"></i>
|
||||
{{ title }}
|
||||
<div>
|
||||
<i v-if="icon" :class="icon" class="conv-details--item__icon"></i>
|
||||
{{ title }}
|
||||
</div>
|
||||
<button v-if="showEdit" @click="onEdit">
|
||||
{{ $t('CONTACT_PANEL.EDIT_LABEL') }}
|
||||
</button>
|
||||
</h4>
|
||||
<div v-if="value" class="conv-details--item__value">
|
||||
{{ value }}
|
||||
|
@ -16,6 +21,12 @@ export default {
|
|||
title: { type: String, required: true },
|
||||
icon: { type: String, default: '' },
|
||||
value: { type: [String, Number], default: '' },
|
||||
showEdit: { type: Boolean, default: false },
|
||||
},
|
||||
methods: {
|
||||
onEdit() {
|
||||
this.$emit('edit');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -31,14 +42,18 @@ export default {
|
|||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.conv-details--item__icon {
|
||||
padding-right: $space-smaller;
|
||||
}
|
||||
|
||||
.conv-details--item__label {
|
||||
font-weight: $font-weight-medium;
|
||||
margin-bottom: $space-micro;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
font-size: $font-size-small;
|
||||
font-weight: $font-weight-medium;
|
||||
justify-content: space-between;
|
||||
margin-bottom: $space-micro;
|
||||
|
||||
button {
|
||||
cursor: pointer;
|
||||
color: $color-body;
|
||||
}
|
||||
}
|
||||
|
||||
.conv-details--item__value {
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<div class="medium-3 bg-white contact--panel">
|
||||
<div class="contact--profile">
|
||||
<span class="close-button" @click="onPanelToggle">
|
||||
<i class="ion-close-round"></i>
|
||||
<i class="ion-chevron-right" />
|
||||
</span>
|
||||
<div class="contact--info">
|
||||
<thumbnail
|
||||
|
@ -107,7 +107,7 @@ import { mapGetters } from 'vuex';
|
|||
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
||||
import ContactConversations from './ContactConversations.vue';
|
||||
import ContactDetailsItem from './ContactDetailsItem.vue';
|
||||
import ConversationLabels from './ConversationLabels.vue';
|
||||
import ConversationLabels from './labels/LabelBox.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
@ -168,12 +168,15 @@ export default {
|
|||
watch: {
|
||||
conversationId(newConversationId, prevConversationId) {
|
||||
if (newConversationId && newConversationId !== prevConversationId) {
|
||||
this.$store.dispatch('contacts/show', { id: this.contactId });
|
||||
this.getContactDetails();
|
||||
}
|
||||
},
|
||||
contactId() {
|
||||
this.getContactDetails();
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.$store.dispatch('contacts/show', { id: this.contactId });
|
||||
this.getContactDetails();
|
||||
},
|
||||
methods: {
|
||||
onPanelToggle() {
|
||||
|
@ -182,6 +185,11 @@ export default {
|
|||
mute() {
|
||||
this.$store.dispatch('muteConversation', this.conversationId);
|
||||
},
|
||||
getContactDetails() {
|
||||
if (this.contactId) {
|
||||
this.$store.dispatch('contacts/show', { id: this.contactId });
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -1,173 +0,0 @@
|
|||
<template>
|
||||
<div
|
||||
class="contact-conversation--panel sidebar-labels-wrap"
|
||||
:class="hasEditedClass"
|
||||
>
|
||||
<div
|
||||
v-if="!conversationUiFlags.isFetching"
|
||||
class="contact-conversation--list"
|
||||
>
|
||||
<label class="select-tags">
|
||||
<contact-details-item
|
||||
:title="$t('CONTACT_PANEL.LABELS.TITLE')"
|
||||
icon="ion-pricetags"
|
||||
/>
|
||||
<multiselect
|
||||
v-model="selectedLabels"
|
||||
:options="savedLabels"
|
||||
:tag-placeholder="$t('CONTACT_PANEL.LABELS.TAG_PLACEHOLDER')"
|
||||
:placeholder="$t('CONTACT_PANEL.LABELS.PLACEHOLDER')"
|
||||
:multiple="true"
|
||||
:taggable="true"
|
||||
hide-selected
|
||||
:show-labels="false"
|
||||
@tag="addLabel"
|
||||
/>
|
||||
</label>
|
||||
<div class="row align-middle align-justify">
|
||||
<span v-if="labelUiFlags.isError" class="error">{{
|
||||
$t('CONTACT_PANEL.LABELS.UPDATE_ERROR')
|
||||
}}</span>
|
||||
<button
|
||||
v-if="hasEdited"
|
||||
type="button"
|
||||
class="button nice tiny"
|
||||
@click="onUpdateLabels"
|
||||
>
|
||||
<spinner v-if="labelUiFlags.isUpdating" size="tiny" />
|
||||
{{
|
||||
labelUiFlags.isUpdating
|
||||
? 'saving...'
|
||||
: $t('CONTACT_PANEL.LABELS.UPDATE_BUTTON')
|
||||
}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<spinner v-else></spinner>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import ContactDetailsItem from './ContactDetailsItem';
|
||||
import Spinner from 'shared/components/Spinner';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ContactDetailsItem,
|
||||
Spinner,
|
||||
},
|
||||
props: {
|
||||
conversationId: {
|
||||
type: [String, Number],
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isSearching: false,
|
||||
selectedLabels: [],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
hasEdited() {
|
||||
if (this.selectedLabels.length !== this.savedLabels.length) {
|
||||
return true;
|
||||
}
|
||||
const isSame = this.selectedLabels.every(label =>
|
||||
this.savedLabels.includes(label)
|
||||
);
|
||||
return !isSame;
|
||||
},
|
||||
savedLabels() {
|
||||
const saved = this.$store.getters[
|
||||
'conversationLabels/getConversationLabels'
|
||||
](this.conversationId);
|
||||
return saved;
|
||||
},
|
||||
hasEditedClass() {
|
||||
return this.hasEdited ? 'has-edited' : '';
|
||||
},
|
||||
...mapGetters({
|
||||
conversationUiFlags: 'contactConversations/getUIFlags',
|
||||
labelUiFlags: 'conversationLabels/getUIFlags',
|
||||
}),
|
||||
},
|
||||
watch: {
|
||||
conversationId(newConversationId, prevConversationId) {
|
||||
if (newConversationId && newConversationId !== prevConversationId) {
|
||||
this.fetchLabels(newConversationId);
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
const { conversationId } = this;
|
||||
this.fetchLabels(conversationId);
|
||||
},
|
||||
methods: {
|
||||
addLabel(label) {
|
||||
this.selectedLabels = [...this.selectedLabels, label];
|
||||
},
|
||||
onUpdateLabels() {
|
||||
this.$store.dispatch('conversationLabels/update', {
|
||||
conversationId: this.conversationId,
|
||||
labels: this.selectedLabels,
|
||||
});
|
||||
},
|
||||
async fetchLabels(conversationId) {
|
||||
try {
|
||||
await this.$store.dispatch('conversationLabels/get', conversationId);
|
||||
this.selectedLabels = [...this.savedLabels];
|
||||
// eslint-disable-next-line no-empty
|
||||
} catch (error) {}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~dashboard/assets/scss/variables';
|
||||
@import '~dashboard/assets/scss/mixins';
|
||||
|
||||
.contact-conversation--panel {
|
||||
padding: $space-normal;
|
||||
}
|
||||
|
||||
.conversation--label {
|
||||
color: $color-white;
|
||||
margin-right: $space-small;
|
||||
font-size: $font-size-small;
|
||||
padding: $space-smaller;
|
||||
}
|
||||
|
||||
.select-tags {
|
||||
.multiselect {
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
transition: $transition-ease-in;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.button {
|
||||
margin-top: $space-small;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.no-results-wrap {
|
||||
padding: 0 $space-small;
|
||||
}
|
||||
|
||||
.no-results {
|
||||
margin: $space-normal 0 0 0;
|
||||
color: $color-gray;
|
||||
font-weight: $font-weight-normal;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: $alert-color;
|
||||
font-size: $font-size-mini;
|
||||
font-weight: $font-weight-medium;
|
||||
}
|
||||
</style>
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<section class="app-content columns">
|
||||
<chat-list :conversation-inbox="inboxId"></chat-list>
|
||||
<chat-list :conversation-inbox="inboxId" :label="label"></chat-list>
|
||||
<conversation-box
|
||||
:inbox-id="inboxId"
|
||||
:is-contact-panel-open="isContactPanelOpen"
|
||||
|
@ -30,19 +30,34 @@ export default {
|
|||
ContactPanel,
|
||||
ConversationBox,
|
||||
},
|
||||
props: {
|
||||
inboxId: {
|
||||
type: [String, Number],
|
||||
default: 0,
|
||||
},
|
||||
conversationId: {
|
||||
type: [String, Number],
|
||||
default: 0,
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
panelToggleState: false,
|
||||
panelToggleState: true,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
chatList: 'getAllConversations',
|
||||
currentChat: 'getSelectedChat',
|
||||
}),
|
||||
isContactPanelOpen: {
|
||||
get() {
|
||||
if (this.conversationId) {
|
||||
if (this.currentChat.id) {
|
||||
return this.panelToggleState;
|
||||
}
|
||||
return false;
|
||||
|
@ -52,9 +67,11 @@ export default {
|
|||
},
|
||||
},
|
||||
},
|
||||
props: ['inboxId', 'conversationId'],
|
||||
|
||||
mounted() {
|
||||
this.$store.dispatch('labels/get');
|
||||
this.$store.dispatch('agents/get');
|
||||
|
||||
this.initialize();
|
||||
this.$watch('$store.state.route', () => this.initialize());
|
||||
this.$watch('chatList.length', () => {
|
||||
|
@ -65,26 +82,8 @@ export default {
|
|||
|
||||
methods: {
|
||||
initialize() {
|
||||
switch (this.$store.state.route.name) {
|
||||
case 'inbox_conversation':
|
||||
this.setActiveChat();
|
||||
break;
|
||||
case 'inbox_dashboard':
|
||||
if (this.inboxId) {
|
||||
this.$store.dispatch('setActiveInbox', this.inboxId);
|
||||
}
|
||||
break;
|
||||
case 'conversation_through_inbox':
|
||||
if (this.inboxId) {
|
||||
this.$store.dispatch('setActiveInbox', this.inboxId);
|
||||
}
|
||||
this.setActiveChat();
|
||||
break;
|
||||
default:
|
||||
this.$store.dispatch('setActiveInbox', null);
|
||||
this.$store.dispatch('clearSelectedState');
|
||||
break;
|
||||
}
|
||||
this.$store.dispatch('setActiveInbox', this.inboxId);
|
||||
this.setActiveChat();
|
||||
},
|
||||
|
||||
fetchConversation() {
|
||||
|
@ -103,11 +102,17 @@ export default {
|
|||
},
|
||||
|
||||
setActiveChat() {
|
||||
const chat = this.findConversation();
|
||||
if (!chat) return;
|
||||
this.$store.dispatch('setActiveChat', chat).then(() => {
|
||||
bus.$emit('scrollToMessage');
|
||||
});
|
||||
if (this.conversationId) {
|
||||
const chat = this.findConversation();
|
||||
if (!chat) {
|
||||
return;
|
||||
}
|
||||
this.$store.dispatch('setActiveChat', chat).then(() => {
|
||||
bus.$emit('scrollToMessage');
|
||||
});
|
||||
} else {
|
||||
this.$store.dispatch('clearSelectedState');
|
||||
}
|
||||
},
|
||||
onToggleContactPanel() {
|
||||
this.isContactPanelOpen = !this.isContactPanelOpen;
|
||||
|
|
|
@ -45,5 +45,24 @@ export default {
|
|||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/label/:label'),
|
||||
name: 'label_conversations',
|
||||
roles: ['administrator', 'agent'],
|
||||
component: ConversationView,
|
||||
props: route => ({ label: route.params.label }),
|
||||
},
|
||||
{
|
||||
path: frontendURL(
|
||||
'accounts/:accountId/label/:label/conversations/:conversation_id'
|
||||
),
|
||||
name: 'conversations_through_label',
|
||||
roles: ['administrator', 'agent'],
|
||||
component: ConversationView,
|
||||
props: route => ({
|
||||
conversationId: route.params.conversation_id,
|
||||
label: route.params.label,
|
||||
}),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
|
@ -0,0 +1,133 @@
|
|||
<template>
|
||||
<woot-modal :show.sync="show" :on-close="onClose">
|
||||
<div class="column content-box">
|
||||
<woot-modal-header
|
||||
:header-title="
|
||||
$t('CONTACT_PANEL.LABELS.MODAL.TITLE') + ' #' + conversationId
|
||||
"
|
||||
/>
|
||||
<div class="content">
|
||||
<div class="label-content--block">
|
||||
<div class="label-content--title">
|
||||
{{ $t('CONTACT_PANEL.LABELS.MODAL.ACTIVE_LABELS') }}
|
||||
<span v-tooltip.bottom="$t('CONTACT_PANEL.LABELS.MODAL.REMOVE')">
|
||||
<i class="ion-ios-help-outline" />
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="activeList.length">
|
||||
<woot-label
|
||||
v-for="label in activeList"
|
||||
:key="label.id"
|
||||
:title="label.title"
|
||||
:description="label.description"
|
||||
:bg-color="label.color"
|
||||
:show-icon="true"
|
||||
@click="onRemove"
|
||||
/>
|
||||
</div>
|
||||
<p v-else>
|
||||
{{ $t('CONTACT_PANEL.LABELS.NO_AVAILABLE_LABELS') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="label-content--block">
|
||||
<div class="label-content--title">
|
||||
{{ $t('CONTACT_PANEL.LABELS.MODAL.INACTIVE_LABELS') }}
|
||||
<span v-tooltip.bottom="$t('CONTACT_PANEL.LABELS.MODAL.ADD')">
|
||||
<i class="ion-ios-help-outline" />
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="inactiveList.length">
|
||||
<woot-label
|
||||
v-for="label in inactiveList"
|
||||
:key="label.id"
|
||||
:title="label.title"
|
||||
:description="label.description"
|
||||
:bg-color="label.color"
|
||||
:show-icon="true"
|
||||
icon="ion-plus"
|
||||
@click="onAdd"
|
||||
/>
|
||||
</div>
|
||||
<p v-else>
|
||||
{{ $t('CONTACT_PANEL.LABELS.NO_LABELS_TO_ADD') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</woot-modal>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
show: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
conversationId: {
|
||||
type: [String, Number],
|
||||
default: '',
|
||||
},
|
||||
accountLabels: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
savedLabels: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
onClose: {
|
||||
type: Function,
|
||||
default: () => [],
|
||||
},
|
||||
updateLabels: {
|
||||
type: Function,
|
||||
default: () => {},
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
activeList() {
|
||||
return this.accountLabels.filter(accountLabel =>
|
||||
this.savedLabels.includes(accountLabel.title)
|
||||
);
|
||||
},
|
||||
inactiveList() {
|
||||
return this.accountLabels.filter(
|
||||
accountLabel => !this.savedLabels.includes(accountLabel.title)
|
||||
);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onAdd(label) {
|
||||
const activeLabels = this.activeList.map(
|
||||
activeLabel => activeLabel.title
|
||||
);
|
||||
this.updateLabels([...activeLabels, label]);
|
||||
},
|
||||
|
||||
onRemove(label) {
|
||||
const activeLabels = this.activeList
|
||||
.filter(activeLabel => activeLabel.title !== label)
|
||||
.map(activeLabel => activeLabel.title);
|
||||
this.updateLabels(activeLabels);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '~dashboard/assets/scss/variables';
|
||||
|
||||
.label-content--block {
|
||||
margin-bottom: $space-normal;
|
||||
}
|
||||
|
||||
.label-content--title {
|
||||
font-weight: $font-weight-bold;
|
||||
margin-bottom: $space-small;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding-top: $space-normal;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,160 @@
|
|||
<template>
|
||||
<div class="contact-conversation--panel sidebar-labels-wrap">
|
||||
<div
|
||||
v-if="!conversationUiFlags.isFetching"
|
||||
class="contact-conversation--list"
|
||||
>
|
||||
<contact-details-item
|
||||
:title="$t('CONTACT_PANEL.LABELS.TITLE')"
|
||||
icon="ion-pricetags"
|
||||
:show-edit="true"
|
||||
@edit="onEdit"
|
||||
/>
|
||||
<woot-label
|
||||
v-for="label in activeLabels"
|
||||
:key="label.id"
|
||||
:title="label.title"
|
||||
:description="label.description"
|
||||
:bg-color="label.color"
|
||||
/>
|
||||
<div v-if="!activeLabels.length">
|
||||
{{ $t('CONTACT_PANEL.LABELS.NO_AVAILABLE_LABELS') }}
|
||||
</div>
|
||||
<add-label-to-conversation
|
||||
v-if="isEditing"
|
||||
:conversation-id="conversationId"
|
||||
:account-labels="accountLabels"
|
||||
:saved-labels="savedLabels"
|
||||
:show.sync="isEditing"
|
||||
:on-close="closeEditModal"
|
||||
:update-labels="onUpdateLabels"
|
||||
/>
|
||||
</div>
|
||||
<spinner v-else></spinner>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/* global bus */
|
||||
import { mapGetters } from 'vuex';
|
||||
import AddLabelToConversation from './AddLabelToConversation';
|
||||
import ContactDetailsItem from '../ContactDetailsItem';
|
||||
import Spinner from 'shared/components/Spinner';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
AddLabelToConversation,
|
||||
ContactDetailsItem,
|
||||
Spinner,
|
||||
},
|
||||
props: {
|
||||
conversationId: {
|
||||
type: [String, Number],
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isEditing: false,
|
||||
selectedLabels: [],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
savedLabels() {
|
||||
return this.$store.getters['conversationLabels/getConversationLabels'](
|
||||
this.conversationId
|
||||
);
|
||||
},
|
||||
...mapGetters({
|
||||
conversationUiFlags: 'contactConversations/getUIFlags',
|
||||
labelUiFlags: 'conversationLabels/getUIFlags',
|
||||
accountLabels: 'labels/getLabels',
|
||||
}),
|
||||
activeLabels() {
|
||||
return this.accountLabels.filter(({ title }) =>
|
||||
this.savedLabels.includes(title)
|
||||
);
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
conversationId(newConversationId, prevConversationId) {
|
||||
if (newConversationId && newConversationId !== prevConversationId) {
|
||||
this.fetchLabels(newConversationId);
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
const { conversationId } = this;
|
||||
this.fetchLabels(conversationId);
|
||||
},
|
||||
methods: {
|
||||
async onUpdateLabels(selectedLabels) {
|
||||
try {
|
||||
await this.$store.dispatch('conversationLabels/update', {
|
||||
conversationId: this.conversationId,
|
||||
labels: selectedLabels,
|
||||
});
|
||||
} catch (error) {
|
||||
// Ignore error
|
||||
}
|
||||
},
|
||||
onEdit() {
|
||||
this.isEditing = true;
|
||||
},
|
||||
closeEditModal() {
|
||||
bus.$emit('fetch_conversation_stats');
|
||||
this.isEditing = false;
|
||||
},
|
||||
async fetchLabels(conversationId) {
|
||||
this.$store.dispatch('conversationLabels/get', conversationId);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~dashboard/assets/scss/variables';
|
||||
@import '~dashboard/assets/scss/mixins';
|
||||
|
||||
.contact-conversation--panel {
|
||||
padding: $space-normal;
|
||||
}
|
||||
|
||||
.conversation--label {
|
||||
color: $color-white;
|
||||
margin-right: $space-small;
|
||||
font-size: $font-size-small;
|
||||
padding: $space-smaller;
|
||||
}
|
||||
|
||||
.select-tags {
|
||||
.multiselect {
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
transition: $transition-ease-in;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.button {
|
||||
margin-top: $space-small;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.no-results-wrap {
|
||||
padding: 0 $space-small;
|
||||
}
|
||||
|
||||
.no-results {
|
||||
margin: $space-normal 0 0 0;
|
||||
color: $color-gray;
|
||||
font-weight: $font-weight-normal;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: $alert-color;
|
||||
font-size: $font-size-mini;
|
||||
font-weight: $font-weight-medium;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,123 @@
|
|||
<template>
|
||||
<modal :show.sync="show" :on-close="onClose">
|
||||
<div class="column content-box">
|
||||
<woot-modal-header
|
||||
:header-title="$t('LABEL_MGMT.ADD.TITLE')"
|
||||
:header-content="$t('LABEL_MGMT.ADD.DESC')"
|
||||
/>
|
||||
<form class="row" @submit.prevent="addLabel">
|
||||
<woot-input
|
||||
v-model.trim="title"
|
||||
:class="{ error: $v.title.$error }"
|
||||
class="medium-12 columns"
|
||||
:label="$t('LABEL_MGMT.FORM.NAME.LABEL')"
|
||||
:placeholder="$t('LABEL_MGMT.FORM.NAME.PLACEHOLDER')"
|
||||
@input="$v.title.$touch"
|
||||
/>
|
||||
|
||||
<woot-input
|
||||
v-model.trim="description"
|
||||
:class="{ error: $v.description.$error }"
|
||||
class="medium-12 columns"
|
||||
:label="$t('LABEL_MGMT.FORM.DESCRIPTION.LABEL')"
|
||||
:placeholder="$t('LABEL_MGMT.FORM.DESCRIPTION.PLACEHOLDER')"
|
||||
@input="$v.description.$touch"
|
||||
/>
|
||||
|
||||
<div class="medium-12">
|
||||
<label>
|
||||
{{ $t('LABEL_MGMT.FORM.COLOR.LABEL') }}
|
||||
<woot-color-picker v-model="color" />
|
||||
</label>
|
||||
</div>
|
||||
<div class="medium-12">
|
||||
<input v-model="showOnSidebar" type="checkbox" :value="true" />
|
||||
<label for="conversation_creation">
|
||||
{{ $t('LABEL_MGMT.FORM.SHOW_ON_SIDEBAR.LABEL') }}
|
||||
</label>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<div class="medium-12 columns">
|
||||
<woot-submit-button
|
||||
:disabled="$v.title.$invalid || uiFlags.isCreating"
|
||||
:button-text="$t('LABEL_MGMT.FORM.CREATE')"
|
||||
:loading="uiFlags.isCreating"
|
||||
/>
|
||||
<button class="button clear" @click.prevent="onClose">
|
||||
{{ $t('LABEL_MGMT.FORM.CANCEL') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import WootSubmitButton from '../../../../components/buttons/FormSubmitButton';
|
||||
import Modal from '../../../../components/Modal';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import { mapGetters } from 'vuex';
|
||||
import validations from './validations';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
WootSubmitButton,
|
||||
Modal,
|
||||
},
|
||||
mixins: [alertMixin],
|
||||
props: {
|
||||
onClose: {
|
||||
type: Function,
|
||||
default: () => {},
|
||||
},
|
||||
show: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
color: '#000',
|
||||
description: '',
|
||||
title: '',
|
||||
showOnSidebar: true,
|
||||
};
|
||||
},
|
||||
validations,
|
||||
computed: {
|
||||
...mapGetters({
|
||||
uiFlags: 'labels/getUIFlags',
|
||||
}),
|
||||
},
|
||||
mounted() {
|
||||
this.color = this.getRandomColor();
|
||||
},
|
||||
methods: {
|
||||
getRandomColor() {
|
||||
const letters = '0123456789ABCDEF';
|
||||
let color = '#';
|
||||
for (let i = 0; i < 6; i += 1) {
|
||||
color += letters[Math.floor(Math.random() * 16)];
|
||||
}
|
||||
return color;
|
||||
},
|
||||
addLabel() {
|
||||
this.$store
|
||||
.dispatch('labels/create', {
|
||||
color: this.color,
|
||||
description: this.description,
|
||||
title: this.title,
|
||||
show_on_sidebar: this.showOnSidebar,
|
||||
})
|
||||
.then(() => {
|
||||
this.showAlert(this.$t('LABEL_MGMT.ADD.API.SUCCESS_MESSAGE'));
|
||||
this.onClose();
|
||||
})
|
||||
.catch(() => {
|
||||
this.showAlert(this.$t('LABEL_MGMT.ADD.API.ERROR_MESSAGE'));
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
|
@ -0,0 +1,129 @@
|
|||
<template>
|
||||
<modal :show.sync="show" :on-close="onClose">
|
||||
<div class="column content-box">
|
||||
<woot-modal-header :header-title="pageTitle" />
|
||||
<form class="row" @submit.prevent="editLabel">
|
||||
<woot-input
|
||||
v-model.trim="title"
|
||||
:class="{ error: $v.title.$error }"
|
||||
class="medium-12 columns"
|
||||
:label="$t('LABEL_MGMT.FORM.NAME.LABEL')"
|
||||
:placeholder="$t('LABEL_MGMT.FORM.NAME.PLACEHOLDER')"
|
||||
@input="$v.title.$touch"
|
||||
/>
|
||||
|
||||
<woot-input
|
||||
v-model.trim="description"
|
||||
:class="{ error: $v.description.$error }"
|
||||
class="medium-12 columns"
|
||||
:label="$t('LABEL_MGMT.FORM.DESCRIPTION.LABEL')"
|
||||
:placeholder="$t('LABEL_MGMT.FORM.DESCRIPTION.PLACEHOLDER')"
|
||||
@input="$v.description.$touch"
|
||||
/>
|
||||
|
||||
<div class="medium-12">
|
||||
<label>
|
||||
{{ $t('LABEL_MGMT.FORM.COLOR.LABEL') }}
|
||||
<woot-color-picker v-model="color" />
|
||||
</label>
|
||||
</div>
|
||||
<div class="medium-12">
|
||||
<input v-model="showOnSidebar" type="checkbox" :value="true" />
|
||||
<label for="conversation_creation">
|
||||
{{ $t('LABEL_MGMT.FORM.SHOW_ON_SIDEBAR.LABEL') }}
|
||||
</label>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<div class="medium-12 columns">
|
||||
<woot-submit-button
|
||||
:disabled="$v.title.$invalid || uiFlags.isUpdating"
|
||||
:button-text="$t('LABEL_MGMT.FORM.EDIT')"
|
||||
:loading="uiFlags.isUpdating"
|
||||
/>
|
||||
<button class="button clear" @click.prevent="onClose">
|
||||
{{ $t('LABEL_MGMT.FORM.CANCEL') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
|
||||
import WootSubmitButton from '../../../../components/buttons/FormSubmitButton';
|
||||
import Modal from '../../../../components/Modal';
|
||||
import validations from './validations';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
WootSubmitButton,
|
||||
Modal,
|
||||
},
|
||||
mixins: [alertMixin],
|
||||
props: {
|
||||
show: {
|
||||
type: Boolean,
|
||||
default: () => {},
|
||||
},
|
||||
selectedResponse: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
onClose: {
|
||||
type: Function,
|
||||
default: () => {},
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
title: '',
|
||||
description: '',
|
||||
showOnSidebar: true,
|
||||
color: '',
|
||||
};
|
||||
},
|
||||
validations,
|
||||
computed: {
|
||||
...mapGetters({
|
||||
uiFlags: 'labels/getUIFlags',
|
||||
}),
|
||||
pageTitle() {
|
||||
return `${this.$t('LABEL_MGMT.EDIT.TITLE')} - ${
|
||||
this.selectedResponse.title
|
||||
}`;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.setFormValues();
|
||||
},
|
||||
methods: {
|
||||
setFormValues() {
|
||||
this.title = this.selectedResponse.title;
|
||||
this.description = this.selectedResponse.description;
|
||||
this.showOnSidebar = this.selectedResponse.show_on_sidebar;
|
||||
this.color = this.selectedResponse.color;
|
||||
},
|
||||
editLabel() {
|
||||
this.$store
|
||||
.dispatch('labels/update', {
|
||||
id: this.selectedResponse.id,
|
||||
color: this.color,
|
||||
description: this.description,
|
||||
title: this.title,
|
||||
show_on_sidebar: this.showOnSidebar,
|
||||
})
|
||||
.then(() => {
|
||||
this.showAlert(this.$t('LABEL_MGMT.EDIT.API.SUCCESS_MESSAGE'));
|
||||
setTimeout(() => this.onClose(), 10);
|
||||
})
|
||||
.catch(() => {
|
||||
this.showAlert(this.$t('LABEL_MGMT.EDIT.API.ERROR_MESSAGE'));
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
|
@ -0,0 +1,201 @@
|
|||
<template>
|
||||
<div class="column content-box">
|
||||
<button
|
||||
class="button nice icon success button--fixed-right-top"
|
||||
@click="openAddPopup"
|
||||
>
|
||||
<i class="icon ion-android-add-circle"></i>
|
||||
{{ $t('LABEL_MGMT.HEADER_BTN_TXT') }}
|
||||
</button>
|
||||
<div class="row">
|
||||
<div class="small-8 columns">
|
||||
<p
|
||||
v-if="!uiFlags.isFetching && !records.length"
|
||||
class="no-items-error-message"
|
||||
>
|
||||
{{ $t('LABEL_MGMT.LIST.404') }}
|
||||
</p>
|
||||
<woot-loading-state
|
||||
v-if="uiFlags.isFetching"
|
||||
:message="$t('LABEL_MGMT.LOADING')"
|
||||
/>
|
||||
<table v-if="!uiFlags.isFetching && records.length" class="woot-table">
|
||||
<thead>
|
||||
<th
|
||||
v-for="thHeader in $t('LABEL_MGMT.LIST.TABLE_HEADER')"
|
||||
:key="thHeader"
|
||||
>
|
||||
{{ thHeader }}
|
||||
</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(label, index) in records" :key="label.title">
|
||||
<td>{{ label.title }}</td>
|
||||
<td>{{ label.description }}</td>
|
||||
<td>
|
||||
<div class="label-color--container">
|
||||
<span
|
||||
class="label-color--display"
|
||||
:style="{ backgroundColor: label.color }"
|
||||
/>
|
||||
{{ label.color }}
|
||||
</div>
|
||||
</td>
|
||||
<td class="button-wrapper">
|
||||
<woot-submit-button
|
||||
:button-text="$t('LABEL_MGMT.FORM.EDIT')"
|
||||
icon-class="ion-edit"
|
||||
button-class="link hollow grey-btn"
|
||||
@click="openEditPopup(label)"
|
||||
/>
|
||||
|
||||
<woot-submit-button
|
||||
:button-text="$t('LABEL_MGMT.FORM.DELETE')"
|
||||
:loading="loading[label.id]"
|
||||
icon-class="ion-close-circled"
|
||||
button-class="link hollow grey-btn"
|
||||
@click="openDeletePopup(label, index)"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="small-4 columns">
|
||||
<span v-html="$t('LABEL_MGMT.SIDEBAR_TXT')"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<add-label
|
||||
v-if="showAddPopup"
|
||||
:show.sync="showAddPopup"
|
||||
:on-close="hideAddPopup"
|
||||
/>
|
||||
|
||||
<edit-label
|
||||
v-if="showEditPopup"
|
||||
:show.sync="showEditPopup"
|
||||
:selected-response="selectedResponse"
|
||||
:on-close="hideEditPopup"
|
||||
/>
|
||||
|
||||
<woot-delete-modal
|
||||
:show.sync="showDeleteConfirmationPopup"
|
||||
:on-close="closeDeletePopup"
|
||||
:on-confirm="confirmDeletion"
|
||||
:title="$t('LABEL_MGMT.DELETE.CONFIRM.TITLE')"
|
||||
:message="deleteMessage"
|
||||
:confirm-text="deleteConfirmText"
|
||||
:reject-text="deleteRejectText"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
import AddLabel from './AddLabel';
|
||||
import EditLabel from './EditLabel';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
AddLabel,
|
||||
EditLabel,
|
||||
},
|
||||
mixins: [alertMixin],
|
||||
data() {
|
||||
return {
|
||||
loading: {},
|
||||
showAddPopup: false,
|
||||
showEditPopup: false,
|
||||
showDeleteConfirmationPopup: false,
|
||||
selectedResponse: {},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
records: 'labels/getLabels',
|
||||
uiFlags: 'labels/getUIFlags',
|
||||
}),
|
||||
// Delete Modal
|
||||
deleteConfirmText() {
|
||||
return `${this.$t('LABEL_MGMT.DELETE.CONFIRM.YES')} ${
|
||||
this.selectedResponse.title
|
||||
}`;
|
||||
},
|
||||
deleteRejectText() {
|
||||
return `${this.$t('LABEL_MGMT.DELETE.CONFIRM.NO')} ${
|
||||
this.selectedResponse.title
|
||||
}`;
|
||||
},
|
||||
deleteMessage() {
|
||||
return `${this.$t('LABEL_MGMT.DELETE.CONFIRM.MESSAGE')} ${
|
||||
this.selectedResponse.title
|
||||
} ?`;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.$store.dispatch('labels/get');
|
||||
},
|
||||
methods: {
|
||||
openAddPopup() {
|
||||
this.showAddPopup = true;
|
||||
},
|
||||
hideAddPopup() {
|
||||
this.showAddPopup = false;
|
||||
},
|
||||
|
||||
openEditPopup(response) {
|
||||
this.showEditPopup = true;
|
||||
this.selectedResponse = response;
|
||||
},
|
||||
hideEditPopup() {
|
||||
this.showEditPopup = false;
|
||||
},
|
||||
|
||||
openDeletePopup(response) {
|
||||
this.showDeleteConfirmationPopup = true;
|
||||
this.selectedResponse = response;
|
||||
},
|
||||
closeDeletePopup() {
|
||||
this.showDeleteConfirmationPopup = false;
|
||||
},
|
||||
|
||||
confirmDeletion() {
|
||||
this.loading[this.selectedResponse.id] = true;
|
||||
this.closeDeletePopup();
|
||||
this.deleteLabel(this.selectedResponse.id);
|
||||
},
|
||||
deleteLabel(id) {
|
||||
this.$store
|
||||
.dispatch('labels/delete', id)
|
||||
.then(() => {
|
||||
this.showAlert(this.$t('LABEL_MGMT.DELETE.API.SUCCESS_MESSAGE'));
|
||||
})
|
||||
.catch(() => {
|
||||
this.showAlert(this.$t('LABEL_MGMT.DELETE.API.ERROR_MESSAGE'));
|
||||
})
|
||||
.finally(() => {
|
||||
this.loading[this.selectedResponse.id] = false;
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '~dashboard/assets/scss/variables';
|
||||
|
||||
.label-color--container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.label-color--display {
|
||||
border-radius: $space-smaller;
|
||||
height: $space-normal;
|
||||
margin-right: $space-smaller;
|
||||
width: $space-normal;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,31 @@
|
|||
import SettingsContent from '../Wrapper';
|
||||
import Index from './Index';
|
||||
import { frontendURL } from '../../../../helper/URLHelper';
|
||||
|
||||
export default {
|
||||
routes: [
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/settings/labels'),
|
||||
component: SettingsContent,
|
||||
props: {
|
||||
headerTitle: 'LABEL_MGMT.HEADER',
|
||||
icon: 'ion-pricetags',
|
||||
showNewButton: false,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'labels_wrapper',
|
||||
roles: ['administrator'],
|
||||
redirect: 'list',
|
||||
},
|
||||
{
|
||||
path: 'list',
|
||||
name: 'labels_list',
|
||||
roles: ['administrator'],
|
||||
component: Index,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
|
@ -0,0 +1,10 @@
|
|||
import { validLabelCharacters } from '../validations';
|
||||
|
||||
describe('#validLabelCharacters', () => {
|
||||
it('validates the label', () => {
|
||||
expect(validLabelCharacters('')).toEqual(false);
|
||||
expect(validLabelCharacters('str str')).toEqual(false);
|
||||
expect(validLabelCharacters('str_str')).toEqual(true);
|
||||
expect(validLabelCharacters('str-str')).toEqual(true);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,16 @@
|
|||
import { required, minLength } from 'vuelidate/lib/validators';
|
||||
|
||||
export const validLabelCharacters = (str = '') => /^[\w-_]+$/g.test(str);
|
||||
|
||||
export default {
|
||||
title: {
|
||||
required,
|
||||
minLength: minLength(2),
|
||||
validLabelCharacters,
|
||||
},
|
||||
description: {},
|
||||
color: {
|
||||
required,
|
||||
},
|
||||
showOnSidebar: {},
|
||||
};
|
|
@ -1,11 +1,12 @@
|
|||
import { frontendURL } from '../../../helper/URLHelper';
|
||||
import account from './account/account.routes';
|
||||
import agent from './agents/agent.routes';
|
||||
import canned from './canned/canned.routes';
|
||||
import inbox from './inbox/inbox.routes';
|
||||
import integrations from './integrations/integrations.routes';
|
||||
import labels from './labels/labels.routes';
|
||||
import profile from './profile/profile.routes';
|
||||
import reports from './reports/reports.routes';
|
||||
import integrations from './integrations/integrations.routes';
|
||||
import account from './account/account.routes';
|
||||
import store from '../../../store';
|
||||
|
||||
export default {
|
||||
|
@ -21,12 +22,13 @@ export default {
|
|||
return frontendURL('accounts/:accountId/settings/canned-response');
|
||||
},
|
||||
},
|
||||
...account.routes,
|
||||
...agent.routes,
|
||||
...canned.routes,
|
||||
...inbox.routes,
|
||||
...integrations.routes,
|
||||
...labels.routes,
|
||||
...profile.routes,
|
||||
...reports.routes,
|
||||
...integrations.routes,
|
||||
...account.routes,
|
||||
],
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue