Feature: Improve label experience (#975)

Co-authored-by: Sojan <sojan@pepalo.com>
This commit is contained in:
Pranav Raj S 2020-06-25 21:04:03 +05:30 committed by GitHub
parent 8b61452d56
commit 97ad39713b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
56 changed files with 1712 additions and 284 deletions

View file

@ -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 {

View file

@ -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>

View file

@ -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>

View file

@ -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;

View file

@ -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,
}),
},
],
};

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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,
},
],
},
],
};

View file

@ -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);
});
});

View file

@ -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: {},
};

View file

@ -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,
],
};