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
|
@ -6,13 +6,14 @@ class ConversationApi extends ApiClient {
|
||||||
super('conversations', { accountScoped: true });
|
super('conversations', { accountScoped: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
get({ inboxId, status, assigneeType, page }) {
|
get({ inboxId, status, assigneeType, page, labels }) {
|
||||||
return axios.get(this.url, {
|
return axios.get(this.url, {
|
||||||
params: {
|
params: {
|
||||||
inbox_id: inboxId,
|
inbox_id: inboxId,
|
||||||
status,
|
status,
|
||||||
assignee_type: assigneeType,
|
assignee_type: assigneeType,
|
||||||
page,
|
page,
|
||||||
|
labels,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -44,12 +45,13 @@ class ConversationApi extends ApiClient {
|
||||||
return axios.post(`${this.url}/${conversationId}/mute`);
|
return axios.post(`${this.url}/${conversationId}/mute`);
|
||||||
}
|
}
|
||||||
|
|
||||||
meta({ inboxId, status, assigneeType }) {
|
meta({ inboxId, status, assigneeType, labels }) {
|
||||||
return axios.get(`${this.url}/meta`, {
|
return axios.get(`${this.url}/meta`, {
|
||||||
params: {
|
params: {
|
||||||
inbox_id: inboxId,
|
inbox_id: inboxId,
|
||||||
status,
|
status,
|
||||||
assignee_type: assigneeType,
|
assignee_type: assigneeType,
|
||||||
|
labels,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
9
app/javascript/dashboard/api/labels.js
Normal file
9
app/javascript/dashboard/api/labels.js
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import ApiClient from './ApiClient';
|
||||||
|
|
||||||
|
class LabelsAPI extends ApiClient {
|
||||||
|
constructor() {
|
||||||
|
super('labels', { accountScoped: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new LabelsAPI();
|
14
app/javascript/dashboard/api/specs/labels.spec.js
Normal file
14
app/javascript/dashboard/api/specs/labels.spec.js
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
import labels from '../labels';
|
||||||
|
import ApiClient from '../ApiClient';
|
||||||
|
|
||||||
|
describe('#LabelsAPI', () => {
|
||||||
|
it('creates correct instance', () => {
|
||||||
|
expect(labels).toBeInstanceOf(ApiClient);
|
||||||
|
expect(labels).toHaveProperty('get');
|
||||||
|
expect(labels).toHaveProperty('show');
|
||||||
|
expect(labels).toHaveProperty('create');
|
||||||
|
expect(labels).toHaveProperty('update');
|
||||||
|
expect(labels).toHaveProperty('delete');
|
||||||
|
expect(labels.url).toBe('/api/v1/labels');
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,6 +1,6 @@
|
||||||
.button {
|
.button {
|
||||||
font-weight: $font-weight-medium;
|
|
||||||
font-family: $body-font-family;
|
font-family: $body-font-family;
|
||||||
|
font-weight: $font-weight-medium;
|
||||||
|
|
||||||
&.round {
|
&.round {
|
||||||
border-radius: 1000px;
|
border-radius: 1000px;
|
||||||
|
@ -20,10 +20,11 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.tooltip {
|
.tooltip {
|
||||||
max-width: 15rem;
|
|
||||||
padding: $space-smaller $space-small;
|
|
||||||
border-radius: $space-smaller;
|
border-radius: $space-smaller;
|
||||||
font-size: $font-size-mini;
|
font-size: $font-size-mini;
|
||||||
|
max-width: 15rem;
|
||||||
|
padding: $space-smaller $space-small;
|
||||||
|
z-index: 9999;
|
||||||
}
|
}
|
||||||
|
|
||||||
code {
|
code {
|
||||||
|
|
|
@ -382,7 +382,7 @@ $label-color: $primary-color;
|
||||||
$label-color-alt: $black;
|
$label-color-alt: $black;
|
||||||
$label-palette: $foundation-palette;
|
$label-palette: $foundation-palette;
|
||||||
$label-font-size: $font-size-micro;
|
$label-font-size: $font-size-micro;
|
||||||
$label-padding: $space-micro $space-smaller;
|
$label-padding: $space-smaller $space-small;
|
||||||
$label-radius: $space-micro;
|
$label-radius: $space-micro;
|
||||||
|
|
||||||
// 21. Media Object
|
// 21. Media Object
|
||||||
|
|
|
@ -67,6 +67,10 @@
|
||||||
font-size: $font-size-small;
|
font-size: $font-size-small;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
@include padding($space-large);
|
||||||
|
}
|
||||||
|
|
||||||
form {
|
form {
|
||||||
@include padding($space-large);
|
@include padding($space-large);
|
||||||
align-self: center;
|
align-self: center;
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
<div class="chat-list__top">
|
<div class="chat-list__top">
|
||||||
<h1 class="page-title">
|
<h1 class="page-title">
|
||||||
<woot-sidemenu-icon />
|
<woot-sidemenu-icon />
|
||||||
{{ inbox.name || $t('CHAT_LIST.TAB_HEADING') }}
|
{{ pageTitle }}
|
||||||
</h1>
|
</h1>
|
||||||
<chat-filter @statusFilterChange="updateStatusType" />
|
<chat-filter @statusFilterChange="updateStatusType" />
|
||||||
</div>
|
</div>
|
||||||
|
@ -15,14 +15,15 @@
|
||||||
@chatTabChange="updateAssigneeTab"
|
@chatTabChange="updateAssigneeTab"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<p v-if="!chatListLoading && !getChatsForTab().length" class="content-box">
|
<p v-if="!chatListLoading && !conversationList.length" class="content-box">
|
||||||
{{ $t('CHAT_LIST.LIST.404') }}
|
{{ $t('CHAT_LIST.LIST.404') }}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="conversations-list">
|
<div class="conversations-list">
|
||||||
<conversation-card
|
<conversation-card
|
||||||
v-for="chat in getChatsForTab()"
|
v-for="chat in conversationList"
|
||||||
:key="chat.id"
|
:key="chat.id"
|
||||||
|
:active-label="label"
|
||||||
:chat="chat"
|
:chat="chat"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
@ -40,7 +41,7 @@
|
||||||
|
|
||||||
<p
|
<p
|
||||||
v-if="
|
v-if="
|
||||||
getChatsForTab().length &&
|
conversationList.length &&
|
||||||
hasCurrentPageEndReached &&
|
hasCurrentPageEndReached &&
|
||||||
!chatListLoading
|
!chatListLoading
|
||||||
"
|
"
|
||||||
|
@ -72,7 +73,16 @@ export default {
|
||||||
ChatFilter,
|
ChatFilter,
|
||||||
},
|
},
|
||||||
mixins: [timeMixin, conversationMixin],
|
mixins: [timeMixin, conversationMixin],
|
||||||
props: ['conversationInbox'],
|
props: {
|
||||||
|
conversationInbox: {
|
||||||
|
type: [String, Number],
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
activeAssigneeTab: wootConstants.ASSIGNEE_TYPE.ME,
|
activeAssigneeTab: wootConstants.ASSIGNEE_TYPE.ME,
|
||||||
|
@ -119,18 +129,51 @@ export default {
|
||||||
assigneeType: this.activeAssigneeTab,
|
assigneeType: this.activeAssigneeTab,
|
||||||
status: this.activeStatus,
|
status: this.activeStatus,
|
||||||
page: this.currentPage + 1,
|
page: this.currentPage + 1,
|
||||||
|
labels: this.label ? [this.label] : undefined,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
pageTitle() {
|
||||||
|
if (this.inbox.name) {
|
||||||
|
return this.inbox.name;
|
||||||
|
}
|
||||||
|
if (this.label) {
|
||||||
|
return `#${this.label}`;
|
||||||
|
}
|
||||||
|
return this.$t('CHAT_LIST.TAB_HEADING');
|
||||||
|
},
|
||||||
|
conversationList() {
|
||||||
|
let conversationList = [];
|
||||||
|
if (this.activeAssigneeTab === 'me') {
|
||||||
|
conversationList = this.mineChatsList.slice();
|
||||||
|
} else if (this.activeAssigneeTab === 'unassigned') {
|
||||||
|
conversationList = this.unAssignedChatsList.slice();
|
||||||
|
} else {
|
||||||
|
conversationList = this.allChatList.slice();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.label) {
|
||||||
|
return conversationList;
|
||||||
|
}
|
||||||
|
|
||||||
|
return conversationList.filter(conversation => {
|
||||||
|
const labels = this.$store.getters[
|
||||||
|
'conversationLabels/getConversationLabels'
|
||||||
|
](conversation.id);
|
||||||
|
return labels.includes(this.label);
|
||||||
|
});
|
||||||
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
conversationInbox() {
|
conversationInbox() {
|
||||||
this.resetAndFetchData();
|
this.resetAndFetchData();
|
||||||
},
|
},
|
||||||
|
label() {
|
||||||
|
this.resetAndFetchData();
|
||||||
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.$store.dispatch('setChatFilter', this.activeStatus);
|
this.$store.dispatch('setChatFilter', this.activeStatus);
|
||||||
this.resetAndFetchData();
|
this.resetAndFetchData();
|
||||||
this.$store.dispatch('agents/get');
|
|
||||||
|
|
||||||
bus.$on('fetch_conversation_stats', () => {
|
bus.$on('fetch_conversation_stats', () => {
|
||||||
this.$store.dispatch('conversationStats/get', this.conversationFilters);
|
this.$store.dispatch('conversationStats/get', this.conversationFilters);
|
||||||
|
@ -159,17 +202,6 @@ export default {
|
||||||
this.resetAndFetchData();
|
this.resetAndFetchData();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
getChatsForTab() {
|
|
||||||
let copyList = [];
|
|
||||||
if (this.activeAssigneeTab === 'me') {
|
|
||||||
copyList = this.mineChatsList.slice();
|
|
||||||
} else if (this.activeAssigneeTab === 'unassigned') {
|
|
||||||
copyList = this.unAssignedChatsList.slice();
|
|
||||||
} else {
|
|
||||||
copyList = this.allChatList.slice();
|
|
||||||
}
|
|
||||||
return copyList;
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -6,6 +6,7 @@ import Code from './Code';
|
||||||
import ColorPicker from './widgets/ColorPicker';
|
import ColorPicker from './widgets/ColorPicker';
|
||||||
import DeleteModal from './widgets/modal/DeleteModal.vue';
|
import DeleteModal from './widgets/modal/DeleteModal.vue';
|
||||||
import Input from './widgets/forms/Input.vue';
|
import Input from './widgets/forms/Input.vue';
|
||||||
|
import Label from './widgets/Label.vue';
|
||||||
import LoadingState from './widgets/LoadingState';
|
import LoadingState from './widgets/LoadingState';
|
||||||
import Modal from './Modal';
|
import Modal from './Modal';
|
||||||
import ModalHeader from './ModalHeader';
|
import ModalHeader from './ModalHeader';
|
||||||
|
@ -25,6 +26,7 @@ const WootUIKit = {
|
||||||
DeleteModal,
|
DeleteModal,
|
||||||
Input,
|
Input,
|
||||||
LoadingState,
|
LoadingState,
|
||||||
|
Label,
|
||||||
Modal,
|
Modal,
|
||||||
ModalHeader,
|
ModalHeader,
|
||||||
ReportStatsCard,
|
ReportStatsCard,
|
||||||
|
|
|
@ -18,6 +18,11 @@
|
||||||
:key="inboxSection.toState"
|
:key="inboxSection.toState"
|
||||||
:menu-item="inboxSection"
|
:menu-item="inboxSection"
|
||||||
/>
|
/>
|
||||||
|
<sidebar-item
|
||||||
|
v-if="shouldShowInboxes"
|
||||||
|
:key="labelSection.toState"
|
||||||
|
:menu-item="labelSection"
|
||||||
|
/>
|
||||||
</transition-group>
|
</transition-group>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -125,6 +130,7 @@ export default {
|
||||||
inboxes: 'inboxes/getInboxes',
|
inboxes: 'inboxes/getInboxes',
|
||||||
accountId: 'getCurrentAccountId',
|
accountId: 'getCurrentAccountId',
|
||||||
currentRole: 'getCurrentRole',
|
currentRole: 'getCurrentRole',
|
||||||
|
accountLabels: 'labels/getLabelsOnSidebar',
|
||||||
}),
|
}),
|
||||||
sidemenuItems() {
|
sidemenuItems() {
|
||||||
return getSidebarItems(this.accountId);
|
return getSidebarItems(this.accountId);
|
||||||
|
@ -170,6 +176,25 @@ export default {
|
||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
labelSection() {
|
||||||
|
return {
|
||||||
|
icon: 'ion-pound',
|
||||||
|
label: 'LABELS',
|
||||||
|
hasSubMenu: true,
|
||||||
|
key: 'label',
|
||||||
|
cssClass: 'menu-title align-justify',
|
||||||
|
toState: frontendURL(`accounts/${this.accountId}/settings/labels`),
|
||||||
|
toStateName: 'labels_list',
|
||||||
|
children: this.accountLabels.map(label => ({
|
||||||
|
id: label.id,
|
||||||
|
label: label.title,
|
||||||
|
color: label.color,
|
||||||
|
toState: frontendURL(
|
||||||
|
`accounts/${this.accountId}/label/${label.title}`
|
||||||
|
),
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
},
|
||||||
dashboardPath() {
|
dashboardPath() {
|
||||||
return frontendURL(`accounts/${this.accountId}/dashboard`);
|
return frontendURL(`accounts/${this.accountId}/dashboard`);
|
||||||
},
|
},
|
||||||
|
|
|
@ -36,7 +36,13 @@
|
||||||
v-if="computedInboxClass(child)"
|
v-if="computedInboxClass(child)"
|
||||||
class="inbox-icon"
|
class="inbox-icon"
|
||||||
:class="computedInboxClass(child)"
|
:class="computedInboxClass(child)"
|
||||||
></i>
|
/>
|
||||||
|
<span
|
||||||
|
v-if="child.color"
|
||||||
|
class="label-color--display"
|
||||||
|
:style="{ backgroundColor: child.color }"
|
||||||
|
/>
|
||||||
|
|
||||||
{{ child.label }}
|
{{ child.label }}
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
|
@ -126,8 +132,22 @@ export default {
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
@import '~dashboard/assets/scss/variables';
|
||||||
|
|
||||||
.sub-menu-title {
|
.sub-menu-title {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.wrap {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label-color--display {
|
||||||
|
border-radius: $space-smaller;
|
||||||
|
height: $space-normal;
|
||||||
|
margin-right: $space-small;
|
||||||
|
width: $space-normal;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
91
app/javascript/dashboard/components/widgets/Label.vue
Normal file
91
app/javascript/dashboard/components/widgets/Label.vue
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
:class="labelClass"
|
||||||
|
:style="{ background: bgColor, color: textColor }"
|
||||||
|
:title="description"
|
||||||
|
>
|
||||||
|
<span v-if="!href">{{ title }}</span>
|
||||||
|
<a v-else :href="href" :style="{ color: textColor }">{{ title }}</a>
|
||||||
|
<i v-if="showIcon" class="label--icon" :class="icon" @click="onClick" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
href: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
bgColor: {
|
||||||
|
type: String,
|
||||||
|
default: '#1f93ff',
|
||||||
|
},
|
||||||
|
small: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
showIcon: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
type: String,
|
||||||
|
default: 'ion-close',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
textColor() {
|
||||||
|
const color = this.bgColor.replace('#', '');
|
||||||
|
const r = parseInt(color.slice(0, 2), 16);
|
||||||
|
const g = parseInt(color.slice(2, 4), 16);
|
||||||
|
const b = parseInt(color.slice(4, 6), 16);
|
||||||
|
// http://stackoverflow.com/a/3943023/112731
|
||||||
|
return r * 0.299 + g * 0.587 + b * 0.114 > 186 ? '#000000' : '#FFFFFF';
|
||||||
|
},
|
||||||
|
labelClass() {
|
||||||
|
return `label ${this.small ? 'small' : ''}`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onClick() {
|
||||||
|
this.$emit('click', this.title);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
@import '~dashboard/assets/scss/variables';
|
||||||
|
|
||||||
|
.label {
|
||||||
|
display: inline-block;
|
||||||
|
font-size: $font-size-small;
|
||||||
|
line-height: 1;
|
||||||
|
margin: $space-micro;
|
||||||
|
|
||||||
|
&.small {
|
||||||
|
font-size: $font-size-mini;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.label--icon {
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: $font-size-micro;
|
||||||
|
line-height: 1.5;
|
||||||
|
margin-left: $space-smaller;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -57,6 +57,10 @@ export default {
|
||||||
|
|
||||||
mixins: [timeMixin, conversationMixin],
|
mixins: [timeMixin, conversationMixin],
|
||||||
props: {
|
props: {
|
||||||
|
activeLabel: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
chat: {
|
chat: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => {},
|
default: () => {},
|
||||||
|
@ -116,7 +120,12 @@ export default {
|
||||||
methods: {
|
methods: {
|
||||||
cardClick(chat) {
|
cardClick(chat) {
|
||||||
const { activeInbox } = this;
|
const { activeInbox } = this;
|
||||||
const path = conversationUrl(this.accountId, activeInbox, chat.id);
|
const path = conversationUrl({
|
||||||
|
accountId: this.accountId,
|
||||||
|
activeInbox,
|
||||||
|
id: chat.id,
|
||||||
|
label: this.activeLabel,
|
||||||
|
});
|
||||||
router.push({ path: frontendURL(path) });
|
router.push({ path: frontendURL(path) });
|
||||||
},
|
},
|
||||||
inboxName(inboxId) {
|
inboxName(inboxId) {
|
||||||
|
|
|
@ -5,11 +5,14 @@ export const frontendURL = (path, params) => {
|
||||||
return `/app/${path}${stringifiedParams}`;
|
return `/app/${path}${stringifiedParams}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const conversationUrl = (accountId, activeInbox, id) => {
|
export const conversationUrl = ({ accountId, activeInbox, id, label }) => {
|
||||||
const path = activeInbox
|
if (activeInbox) {
|
||||||
? `accounts/${accountId}/inbox/${activeInbox}/conversations/${id}`
|
return `accounts/${accountId}/inbox/${activeInbox}/conversations/${id}`;
|
||||||
: `accounts/${accountId}/conversations/${id}`;
|
}
|
||||||
return path;
|
if (label) {
|
||||||
|
return `accounts/${accountId}/label/${label}/conversations/${id}`;
|
||||||
|
}
|
||||||
|
return `accounts/${accountId}/conversations/${id}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const accountIdFromPathname = pathname => {
|
export const accountIdFromPathname = pathname => {
|
||||||
|
|
|
@ -7,15 +7,20 @@ import {
|
||||||
describe('#URL Helpers', () => {
|
describe('#URL Helpers', () => {
|
||||||
describe('conversationUrl', () => {
|
describe('conversationUrl', () => {
|
||||||
it('should return direct conversation URL if activeInbox is nil', () => {
|
it('should return direct conversation URL if activeInbox is nil', () => {
|
||||||
expect(conversationUrl(1, undefined, 1)).toBe(
|
expect(conversationUrl({ accountId: 1, id: 1 })).toBe(
|
||||||
'accounts/1/conversations/1'
|
'accounts/1/conversations/1'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
it('should return ibox conversation URL if activeInbox is not nil', () => {
|
it('should return inbox conversation URL if activeInbox is not nil', () => {
|
||||||
expect(conversationUrl(1, 2, 1)).toBe(
|
expect(conversationUrl({ accountId: 1, id: 1, activeInbox: 2 })).toBe(
|
||||||
'accounts/1/inbox/2/conversations/1'
|
'accounts/1/inbox/2/conversations/1'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
it('should return correct conversation URL if label is active', () => {
|
||||||
|
expect(
|
||||||
|
conversationUrl({ accountId: 1, label: 'customer-support', id: 1 })
|
||||||
|
).toBe('accounts/1/label/customer-support/conversations/1');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('frontendURL', () => {
|
describe('frontendURL', () => {
|
||||||
|
@ -27,16 +32,6 @@ describe('#URL Helpers', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
/*
|
|
||||||
|
|
||||||
export const accountIdFromPathname = pathname => {
|
|
||||||
const isInsideAccountScopedURLs = pathname.includes('/app/accounts');
|
|
||||||
const accountId = isInsideAccountScopedURLs ? pathname.split('/')[3] : '';
|
|
||||||
return Number(accountId);
|
|
||||||
};
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
describe('accountIdFromPathname', () => {
|
describe('accountIdFromPathname', () => {
|
||||||
it('should return account id if accont scoped url is passed', () => {
|
it('should return account id if accont scoped url is passed', () => {
|
||||||
expect(accountIdFromPathname('/app/accounts/1/settings/general')).toBe(1);
|
expect(accountIdFromPathname('/app/accounts/1/settings/general')).toBe(1);
|
||||||
|
|
|
@ -10,6 +10,8 @@ export const getSidebarItems = accountId => ({
|
||||||
'settings_account_reports',
|
'settings_account_reports',
|
||||||
'profile_settings',
|
'profile_settings',
|
||||||
'profile_settings_index',
|
'profile_settings_index',
|
||||||
|
'label_conversations',
|
||||||
|
'conversations_through_label',
|
||||||
],
|
],
|
||||||
menuItems: {
|
menuItems: {
|
||||||
assignedToMe: {
|
assignedToMe: {
|
||||||
|
@ -40,9 +42,8 @@ export const getSidebarItems = accountId => ({
|
||||||
settings: {
|
settings: {
|
||||||
routes: [
|
routes: [
|
||||||
'agent_list',
|
'agent_list',
|
||||||
'agent_new',
|
|
||||||
'canned_list',
|
'canned_list',
|
||||||
'canned_new',
|
'labels_list',
|
||||||
'settings_inbox',
|
'settings_inbox',
|
||||||
'settings_inbox_new',
|
'settings_inbox_new',
|
||||||
'settings_inbox_list',
|
'settings_inbox_list',
|
||||||
|
@ -78,6 +79,13 @@ export const getSidebarItems = accountId => ({
|
||||||
toState: frontendURL(`accounts/${accountId}/settings/inboxes/list`),
|
toState: frontendURL(`accounts/${accountId}/settings/inboxes/list`),
|
||||||
toStateName: 'settings_inbox_list',
|
toStateName: 'settings_inbox_list',
|
||||||
},
|
},
|
||||||
|
labels: {
|
||||||
|
icon: 'ion-pricetags',
|
||||||
|
label: 'LABELS',
|
||||||
|
hasSubMenu: false,
|
||||||
|
toState: frontendURL(`accounts/${accountId}/settings/labels/list`),
|
||||||
|
toStateName: 'labels_list',
|
||||||
|
},
|
||||||
cannedResponses: {
|
cannedResponses: {
|
||||||
icon: 'ion-chatbox-working',
|
icon: 'ion-chatbox-working',
|
||||||
label: 'CANNED_RESPONSES',
|
label: 'CANNED_RESPONSES',
|
||||||
|
|
|
@ -11,11 +11,19 @@
|
||||||
},
|
},
|
||||||
"LABELS": {
|
"LABELS": {
|
||||||
"TITLE": "Conversation Labels",
|
"TITLE": "Conversation Labels",
|
||||||
"UPDATE_BUTTON": "Update Labels",
|
"MODAL": {
|
||||||
"UPDATE_ERROR": "Couldn't update labels, try again.",
|
"TITLE": "Labels for",
|
||||||
"TAG_PLACEHOLDER": "Add new label",
|
"ACTIVE_LABELS": "Labels added to the conversation",
|
||||||
"PLACEHOLDER": "Search or add a label"
|
"INACTIVE_LABELS": "Labels available in the account",
|
||||||
|
"REMOVE": "Click on X icon to remove the label",
|
||||||
|
"ADD": "Click on + icon to add the label",
|
||||||
|
"UPDATE_BUTTON": "Update labels",
|
||||||
|
"UPDATE_ERROR": "Couldn't update labels, try again."
|
||||||
},
|
},
|
||||||
"MUTE_CONTACT": "Mute Contact"
|
"NO_LABELS_TO_ADD": "There are no more labels defined in the account.",
|
||||||
|
"NO_AVAILABLE_LABELS": "There are no labels added to this conversation."
|
||||||
|
},
|
||||||
|
"MUTE_CONTACT": "Mute Contact",
|
||||||
|
"EDIT_LABEL": "Edit"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/* eslint-disable */
|
|
||||||
import { default as _agentMgmt } from './agentMgmt.json';
|
import { default as _agentMgmt } from './agentMgmt.json';
|
||||||
|
import { default as _labelsMgmt } from './labelsMgmt.json';
|
||||||
import { default as _cannedMgmt } from './cannedMgmt.json';
|
import { default as _cannedMgmt } from './cannedMgmt.json';
|
||||||
import { default as _chatlist } from './chatlist.json';
|
import { default as _chatlist } from './chatlist.json';
|
||||||
import { default as _contact } from './contact.json';
|
import { default as _contact } from './contact.json';
|
||||||
|
@ -23,6 +23,7 @@ export default {
|
||||||
..._inboxMgmt,
|
..._inboxMgmt,
|
||||||
..._login,
|
..._login,
|
||||||
..._report,
|
..._report,
|
||||||
|
..._labelsMgmt,
|
||||||
..._resetPassword,
|
..._resetPassword,
|
||||||
..._setNewPassword,
|
..._setNewPassword,
|
||||||
..._settings,
|
..._settings,
|
||||||
|
|
68
app/javascript/dashboard/i18n/locale/en/labelsMgmt.json
Normal file
68
app/javascript/dashboard/i18n/locale/en/labelsMgmt.json
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
{
|
||||||
|
"LABEL_MGMT": {
|
||||||
|
"HEADER": "Labels",
|
||||||
|
"HEADER_BTN_TXT": "Add label",
|
||||||
|
"LOADING": "Fetching labels",
|
||||||
|
"SEARCH_404": "There are no items matching this query",
|
||||||
|
"SIDEBAR_TXT": "<p><b>Labels</b> <p>Labels help you to categorize conversations and prioritize them. You can assign label to a conversation from the sidepanel. <br /><br />Labels are tied to the account and can be used to create custom workflows in your organization. You can assign custom color to a label, it makes it easier to identify the label. You will be able to display the label on the sidebar to filter the conversations easily.</p>",
|
||||||
|
"LIST": {
|
||||||
|
"404": "There are no labels available in this account.",
|
||||||
|
"TITLE": "Manage labels",
|
||||||
|
"DESC": "Labels let you group the conversations together.",
|
||||||
|
"TABLE_HEADER": [
|
||||||
|
"Name",
|
||||||
|
"Description",
|
||||||
|
"Color"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"FORM": {
|
||||||
|
"NAME": {
|
||||||
|
"LABEL": "Label Name",
|
||||||
|
"PLACEHOLDER": "Label name",
|
||||||
|
"ERROR": "Label Name is required"
|
||||||
|
},
|
||||||
|
"DESCRIPTION": {
|
||||||
|
"LABEL": "Description",
|
||||||
|
"PLACEHOLDER": "Label Description"
|
||||||
|
},
|
||||||
|
"COLOR": {
|
||||||
|
"LABEL": "Color"
|
||||||
|
},
|
||||||
|
"SHOW_ON_SIDEBAR": {
|
||||||
|
"LABEL": "Show label on sidebar"
|
||||||
|
},
|
||||||
|
"EDIT": "Edit",
|
||||||
|
"CREATE": "Create",
|
||||||
|
"DELETE": "Delete",
|
||||||
|
"CANCEL": "Cancel"
|
||||||
|
},
|
||||||
|
"ADD": {
|
||||||
|
"TITLE": "Add label",
|
||||||
|
"DESC": "Labels let you group the conversations together.",
|
||||||
|
"API": {
|
||||||
|
"SUCCESS_MESSAGE": "Label added successfully",
|
||||||
|
"ERROR_MESSAGE": "There was an error, please try again"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"EDIT": {
|
||||||
|
"TITLE": "Edit label",
|
||||||
|
"API": {
|
||||||
|
"SUCCESS_MESSAGE": "Label updated successfully",
|
||||||
|
"ERROR_MESSAGE": "There was an error, please try again"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"DELETE": {
|
||||||
|
"BUTTON_TEXT": "Delete",
|
||||||
|
"API": {
|
||||||
|
"SUCCESS_MESSAGE": "Label deleted successfully",
|
||||||
|
"ERROR_MESSAGE": "There was an error, please try again"
|
||||||
|
},
|
||||||
|
"CONFIRM": {
|
||||||
|
"TITLE": "Confirm Deletion",
|
||||||
|
"MESSAGE": "Are you sure to delete ",
|
||||||
|
"YES": "Yes, Delete ",
|
||||||
|
"NO": "No, Keep "
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -103,6 +103,7 @@
|
||||||
"INBOXES": "Inboxes",
|
"INBOXES": "Inboxes",
|
||||||
"CANNED_RESPONSES": "Canned Responses",
|
"CANNED_RESPONSES": "Canned Responses",
|
||||||
"INTEGRATIONS": "Integrations",
|
"INTEGRATIONS": "Integrations",
|
||||||
"ACCOUNT_SETTINGS": "Account Settings"
|
"ACCOUNT_SETTINGS": "Account Settings",
|
||||||
|
"LABELS": "Labels"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,13 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="conv-details--item">
|
<div class="conv-details--item">
|
||||||
<h4 class="conv-details--item__label">
|
<h4 class="conv-details--item__label">
|
||||||
|
<div>
|
||||||
<i v-if="icon" :class="icon" class="conv-details--item__icon"></i>
|
<i v-if="icon" :class="icon" class="conv-details--item__icon"></i>
|
||||||
{{ title }}
|
{{ title }}
|
||||||
|
</div>
|
||||||
|
<button v-if="showEdit" @click="onEdit">
|
||||||
|
{{ $t('CONTACT_PANEL.EDIT_LABEL') }}
|
||||||
|
</button>
|
||||||
</h4>
|
</h4>
|
||||||
<div v-if="value" class="conv-details--item__value">
|
<div v-if="value" class="conv-details--item__value">
|
||||||
{{ value }}
|
{{ value }}
|
||||||
|
@ -16,6 +21,12 @@ export default {
|
||||||
title: { type: String, required: true },
|
title: { type: String, required: true },
|
||||||
icon: { type: String, default: '' },
|
icon: { type: String, default: '' },
|
||||||
value: { type: [String, Number], default: '' },
|
value: { type: [String, Number], default: '' },
|
||||||
|
showEdit: { type: Boolean, default: false },
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onEdit() {
|
||||||
|
this.$emit('edit');
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
@ -31,14 +42,18 @@ export default {
|
||||||
padding-bottom: 0;
|
padding-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.conv-details--item__icon {
|
|
||||||
padding-right: $space-smaller;
|
|
||||||
}
|
|
||||||
|
|
||||||
.conv-details--item__label {
|
.conv-details--item__label {
|
||||||
font-weight: $font-weight-medium;
|
align-items: center;
|
||||||
margin-bottom: $space-micro;
|
display: flex;
|
||||||
font-size: $font-size-small;
|
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 {
|
.conv-details--item__value {
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
<div class="medium-3 bg-white contact--panel">
|
<div class="medium-3 bg-white contact--panel">
|
||||||
<div class="contact--profile">
|
<div class="contact--profile">
|
||||||
<span class="close-button" @click="onPanelToggle">
|
<span class="close-button" @click="onPanelToggle">
|
||||||
<i class="ion-close-round"></i>
|
<i class="ion-chevron-right" />
|
||||||
</span>
|
</span>
|
||||||
<div class="contact--info">
|
<div class="contact--info">
|
||||||
<thumbnail
|
<thumbnail
|
||||||
|
@ -107,7 +107,7 @@ import { mapGetters } from 'vuex';
|
||||||
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
||||||
import ContactConversations from './ContactConversations.vue';
|
import ContactConversations from './ContactConversations.vue';
|
||||||
import ContactDetailsItem from './ContactDetailsItem.vue';
|
import ContactDetailsItem from './ContactDetailsItem.vue';
|
||||||
import ConversationLabels from './ConversationLabels.vue';
|
import ConversationLabels from './labels/LabelBox.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
|
@ -168,12 +168,15 @@ export default {
|
||||||
watch: {
|
watch: {
|
||||||
conversationId(newConversationId, prevConversationId) {
|
conversationId(newConversationId, prevConversationId) {
|
||||||
if (newConversationId && newConversationId !== prevConversationId) {
|
if (newConversationId && newConversationId !== prevConversationId) {
|
||||||
this.$store.dispatch('contacts/show', { id: this.contactId });
|
this.getContactDetails();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
contactId() {
|
||||||
|
this.getContactDetails();
|
||||||
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.$store.dispatch('contacts/show', { id: this.contactId });
|
this.getContactDetails();
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
onPanelToggle() {
|
onPanelToggle() {
|
||||||
|
@ -182,6 +185,11 @@ export default {
|
||||||
mute() {
|
mute() {
|
||||||
this.$store.dispatch('muteConversation', this.conversationId);
|
this.$store.dispatch('muteConversation', this.conversationId);
|
||||||
},
|
},
|
||||||
|
getContactDetails() {
|
||||||
|
if (this.contactId) {
|
||||||
|
this.$store.dispatch('contacts/show', { id: this.contactId });
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</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>
|
<template>
|
||||||
<section class="app-content columns">
|
<section class="app-content columns">
|
||||||
<chat-list :conversation-inbox="inboxId"></chat-list>
|
<chat-list :conversation-inbox="inboxId" :label="label"></chat-list>
|
||||||
<conversation-box
|
<conversation-box
|
||||||
:inbox-id="inboxId"
|
:inbox-id="inboxId"
|
||||||
:is-contact-panel-open="isContactPanelOpen"
|
:is-contact-panel-open="isContactPanelOpen"
|
||||||
|
@ -30,19 +30,34 @@ export default {
|
||||||
ContactPanel,
|
ContactPanel,
|
||||||
ConversationBox,
|
ConversationBox,
|
||||||
},
|
},
|
||||||
|
props: {
|
||||||
|
inboxId: {
|
||||||
|
type: [String, Number],
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
conversationId: {
|
||||||
|
type: [String, Number],
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
panelToggleState: false,
|
panelToggleState: true,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters({
|
...mapGetters({
|
||||||
chatList: 'getAllConversations',
|
chatList: 'getAllConversations',
|
||||||
|
currentChat: 'getSelectedChat',
|
||||||
}),
|
}),
|
||||||
isContactPanelOpen: {
|
isContactPanelOpen: {
|
||||||
get() {
|
get() {
|
||||||
if (this.conversationId) {
|
if (this.currentChat.id) {
|
||||||
return this.panelToggleState;
|
return this.panelToggleState;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
@ -52,9 +67,11 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
props: ['inboxId', 'conversationId'],
|
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
|
this.$store.dispatch('labels/get');
|
||||||
|
this.$store.dispatch('agents/get');
|
||||||
|
|
||||||
this.initialize();
|
this.initialize();
|
||||||
this.$watch('$store.state.route', () => this.initialize());
|
this.$watch('$store.state.route', () => this.initialize());
|
||||||
this.$watch('chatList.length', () => {
|
this.$watch('chatList.length', () => {
|
||||||
|
@ -65,26 +82,8 @@ export default {
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
initialize() {
|
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);
|
this.$store.dispatch('setActiveInbox', this.inboxId);
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'conversation_through_inbox':
|
|
||||||
if (this.inboxId) {
|
|
||||||
this.$store.dispatch('setActiveInbox', this.inboxId);
|
|
||||||
}
|
|
||||||
this.setActiveChat();
|
this.setActiveChat();
|
||||||
break;
|
|
||||||
default:
|
|
||||||
this.$store.dispatch('setActiveInbox', null);
|
|
||||||
this.$store.dispatch('clearSelectedState');
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
fetchConversation() {
|
fetchConversation() {
|
||||||
|
@ -103,11 +102,17 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
setActiveChat() {
|
setActiveChat() {
|
||||||
|
if (this.conversationId) {
|
||||||
const chat = this.findConversation();
|
const chat = this.findConversation();
|
||||||
if (!chat) return;
|
if (!chat) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.$store.dispatch('setActiveChat', chat).then(() => {
|
this.$store.dispatch('setActiveChat', chat).then(() => {
|
||||||
bus.$emit('scrollToMessage');
|
bus.$emit('scrollToMessage');
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
this.$store.dispatch('clearSelectedState');
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onToggleContactPanel() {
|
onToggleContactPanel() {
|
||||||
this.isContactPanelOpen = !this.isContactPanelOpen;
|
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 { frontendURL } from '../../../helper/URLHelper';
|
||||||
|
import account from './account/account.routes';
|
||||||
import agent from './agents/agent.routes';
|
import agent from './agents/agent.routes';
|
||||||
import canned from './canned/canned.routes';
|
import canned from './canned/canned.routes';
|
||||||
import inbox from './inbox/inbox.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 profile from './profile/profile.routes';
|
||||||
import reports from './reports/reports.routes';
|
import reports from './reports/reports.routes';
|
||||||
import integrations from './integrations/integrations.routes';
|
|
||||||
import account from './account/account.routes';
|
|
||||||
import store from '../../../store';
|
import store from '../../../store';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
@ -21,12 +22,13 @@ export default {
|
||||||
return frontendURL('accounts/:accountId/settings/canned-response');
|
return frontendURL('accounts/:accountId/settings/canned-response');
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
...account.routes,
|
||||||
...agent.routes,
|
...agent.routes,
|
||||||
...canned.routes,
|
...canned.routes,
|
||||||
...inbox.routes,
|
...inbox.routes,
|
||||||
|
...integrations.routes,
|
||||||
|
...labels.routes,
|
||||||
...profile.routes,
|
...profile.routes,
|
||||||
...reports.routes,
|
...reports.routes,
|
||||||
...integrations.routes,
|
|
||||||
...account.routes,
|
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
|
@ -18,6 +18,7 @@ import globalConfig from 'shared/store/globalConfig';
|
||||||
import inboxes from './modules/inboxes';
|
import inboxes from './modules/inboxes';
|
||||||
import inboxMembers from './modules/inboxMembers';
|
import inboxMembers from './modules/inboxMembers';
|
||||||
import integrations from './modules/integrations';
|
import integrations from './modules/integrations';
|
||||||
|
import labels from './modules/labels';
|
||||||
import reports from './modules/reports';
|
import reports from './modules/reports';
|
||||||
import userNotificationSettings from './modules/userNotificationSettings';
|
import userNotificationSettings from './modules/userNotificationSettings';
|
||||||
import webhooks from './modules/webhooks';
|
import webhooks from './modules/webhooks';
|
||||||
|
@ -35,13 +36,14 @@ export default new Vuex.Store({
|
||||||
conversationLabels,
|
conversationLabels,
|
||||||
conversationMetadata,
|
conversationMetadata,
|
||||||
conversationPage,
|
conversationPage,
|
||||||
conversationStats,
|
|
||||||
conversations,
|
conversations,
|
||||||
|
conversationStats,
|
||||||
conversationTypingStatus,
|
conversationTypingStatus,
|
||||||
globalConfig,
|
globalConfig,
|
||||||
inboxes,
|
inboxes,
|
||||||
inboxMembers,
|
inboxMembers,
|
||||||
integrations,
|
integrations,
|
||||||
|
labels,
|
||||||
reports,
|
reports,
|
||||||
userNotificationSettings,
|
userNotificationSettings,
|
||||||
webhooks,
|
webhooks,
|
||||||
|
|
|
@ -71,12 +71,18 @@ export const mutations = {
|
||||||
|
|
||||||
[types.default.SET_CONTACTS]: ($state, data) => {
|
[types.default.SET_CONTACTS]: ($state, data) => {
|
||||||
data.forEach(contact => {
|
data.forEach(contact => {
|
||||||
Vue.set($state.records, contact.id, contact);
|
Vue.set($state.records, contact.id, {
|
||||||
|
...($state.records[contact.id] || {}),
|
||||||
|
...contact,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
[types.default.SET_CONTACT_ITEM]: ($state, data) => {
|
[types.default.SET_CONTACT_ITEM]: ($state, data) => {
|
||||||
Vue.set($state.records, data.id, data);
|
Vue.set($state.records, data.id, {
|
||||||
|
...($state.records[data.id] || {}),
|
||||||
|
...data,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
[types.default.EDIT_CONTACT]: ($state, data) => {
|
[types.default.EDIT_CONTACT]: ($state, data) => {
|
||||||
|
|
|
@ -64,6 +64,12 @@ export const actions = {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
setBulkConversationLabels({ commit }, conversations) {
|
||||||
|
commit(types.default.SET_BULK_CONVERSATION_LABELS, conversations);
|
||||||
|
},
|
||||||
|
setConversationLabel({ commit }, { id, data }) {
|
||||||
|
commit(types.default.SET_CONVERSATION_LABELS, { id, data });
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const mutations = {
|
export const mutations = {
|
||||||
|
@ -76,6 +82,11 @@ export const mutations = {
|
||||||
[types.default.SET_CONVERSATION_LABELS]: ($state, { id, data }) => {
|
[types.default.SET_CONVERSATION_LABELS]: ($state, { id, data }) => {
|
||||||
Vue.set($state.records, id, data);
|
Vue.set($state.records, id, data);
|
||||||
},
|
},
|
||||||
|
[types.default.SET_BULK_CONVERSATION_LABELS]: ($state, conversations) => {
|
||||||
|
conversations.forEach(conversation => {
|
||||||
|
Vue.set($state.records, conversation.id, conversation.labels);
|
||||||
|
});
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|
|
@ -8,7 +8,7 @@ const actions = {
|
||||||
getConversation: async ({ commit }, conversationId) => {
|
getConversation: async ({ commit }, conversationId) => {
|
||||||
try {
|
try {
|
||||||
const response = await ConversationApi.show(conversationId);
|
const response = await ConversationApi.show(conversationId);
|
||||||
commit(types.default.ADD_CONVERSATION, response.data);
|
commit(types.default.UPDATE_CONVERSATION, response.data);
|
||||||
commit(
|
commit(
|
||||||
`contacts/${types.default.SET_CONTACT_ITEM}`,
|
`contacts/${types.default.SET_CONTACT_ITEM}`,
|
||||||
response.data.meta.sender
|
response.data.meta.sender
|
||||||
|
@ -26,6 +26,7 @@ const actions = {
|
||||||
const { payload: chatList, meta: metaData } = data;
|
const { payload: chatList, meta: metaData } = data;
|
||||||
commit(types.default.SET_ALL_CONVERSATION, chatList);
|
commit(types.default.SET_ALL_CONVERSATION, chatList);
|
||||||
dispatch('conversationStats/set', metaData);
|
dispatch('conversationStats/set', metaData);
|
||||||
|
dispatch('conversationLabels/setBulkConversationLabels', chatList);
|
||||||
commit(types.default.CLEAR_LIST_LOADING_STATUS);
|
commit(types.default.CLEAR_LIST_LOADING_STATUS);
|
||||||
commit(
|
commit(
|
||||||
`contacts/${types.default.SET_CONTACTS}`,
|
`contacts/${types.default.SET_CONTACTS}`,
|
||||||
|
|
|
@ -142,13 +142,15 @@ const mutations = {
|
||||||
if (currentConversationIndex > -1) {
|
if (currentConversationIndex > -1) {
|
||||||
const currentConversation = {
|
const currentConversation = {
|
||||||
...allConversations[currentConversationIndex],
|
...allConversations[currentConversationIndex],
|
||||||
status: conversation.status,
|
...conversation,
|
||||||
};
|
};
|
||||||
Vue.set(allConversations, currentConversationIndex, currentConversation);
|
Vue.set(allConversations, currentConversationIndex, currentConversation);
|
||||||
if (_state.selectedChat.id === conversation.id) {
|
if (_state.selectedChat.id === conversation.id) {
|
||||||
_state.selectedChat.status = conversation.status;
|
_state.selectedChat.status = conversation.status;
|
||||||
window.bus.$emit('scrollToMessage');
|
window.bus.$emit('scrollToMessage');
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
_state.allConversations.push(conversation);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -190,7 +192,7 @@ const mutations = {
|
||||||
},
|
},
|
||||||
|
|
||||||
[types.default.SET_ACTIVE_INBOX](_state, inboxId) {
|
[types.default.SET_ACTIVE_INBOX](_state, inboxId) {
|
||||||
_state.currentInbox = inboxId;
|
_state.currentInbox = inboxId ? parseInt(inboxId, 10) : null;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
97
app/javascript/dashboard/store/modules/labels.js
Normal file
97
app/javascript/dashboard/store/modules/labels.js
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
import * as MutationHelpers from 'shared/helpers/vuex/mutationHelpers';
|
||||||
|
import types from '../mutation-types';
|
||||||
|
import LabelsAPI from '../../api/labels';
|
||||||
|
|
||||||
|
export const state = {
|
||||||
|
records: [],
|
||||||
|
uiFlags: {
|
||||||
|
isFetching: false,
|
||||||
|
isFetchingItem: false,
|
||||||
|
isCreating: false,
|
||||||
|
isDeleting: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getters = {
|
||||||
|
getLabels(_state) {
|
||||||
|
return _state.records;
|
||||||
|
},
|
||||||
|
getUIFlags(_state) {
|
||||||
|
return _state.uiFlags;
|
||||||
|
},
|
||||||
|
getLabelsOnSidebar(_state) {
|
||||||
|
return _state.records.filter(record => record.show_on_sidebar);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const actions = {
|
||||||
|
get: async function getLabels({ commit }) {
|
||||||
|
commit(types.SET_LABEL_UI_FLAG, { isFetching: true });
|
||||||
|
try {
|
||||||
|
const response = await LabelsAPI.get();
|
||||||
|
commit(types.SET_LABELS, response.data.payload);
|
||||||
|
} catch (error) {
|
||||||
|
// Ignore error
|
||||||
|
} finally {
|
||||||
|
commit(types.SET_LABEL_UI_FLAG, { isFetching: false });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
create: async function createLabels({ commit }, cannedObj) {
|
||||||
|
commit(types.SET_LABEL_UI_FLAG, { isCreating: true });
|
||||||
|
try {
|
||||||
|
const response = await LabelsAPI.create(cannedObj);
|
||||||
|
commit(types.ADD_LABEL, response.data);
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(error);
|
||||||
|
} finally {
|
||||||
|
commit(types.SET_LABEL_UI_FLAG, { isCreating: false });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
update: async function updateLabels({ commit }, { id, ...updateObj }) {
|
||||||
|
commit(types.SET_LABEL_UI_FLAG, { isUpdating: true });
|
||||||
|
try {
|
||||||
|
const response = await LabelsAPI.update(id, updateObj);
|
||||||
|
commit(types.EDIT_LABEL, response.data);
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(error);
|
||||||
|
} finally {
|
||||||
|
commit(types.SET_LABEL_UI_FLAG, { isUpdating: false });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
delete: async function deleteLabels({ commit }, id) {
|
||||||
|
commit(types.SET_LABEL_UI_FLAG, { isDeleting: true });
|
||||||
|
try {
|
||||||
|
await LabelsAPI.delete(id);
|
||||||
|
commit(types.DELETE_LABEL, id);
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(error);
|
||||||
|
} finally {
|
||||||
|
commit(types.SET_LABEL_UI_FLAG, { isDeleting: false });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const mutations = {
|
||||||
|
[types.SET_LABEL_UI_FLAG](_state, data) {
|
||||||
|
_state.uiFlags = {
|
||||||
|
..._state.uiFlags,
|
||||||
|
...data,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
[types.SET_LABELS]: MutationHelpers.set,
|
||||||
|
[types.ADD_LABEL]: MutationHelpers.create,
|
||||||
|
[types.EDIT_LABEL]: MutationHelpers.update,
|
||||||
|
[types.DELETE_LABEL]: MutationHelpers.destroy,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
namespaced: true,
|
||||||
|
state,
|
||||||
|
getters,
|
||||||
|
actions,
|
||||||
|
mutations,
|
||||||
|
};
|
|
@ -74,4 +74,33 @@ describe('#actions', () => {
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('#setBulkConversationLabels', () => {
|
||||||
|
it('it send correct mutations', () => {
|
||||||
|
actions.setBulkConversationLabels({ commit }, [
|
||||||
|
{ id: 1, labels: ['customer-support'] },
|
||||||
|
]);
|
||||||
|
expect(commit.mock.calls).toEqual([
|
||||||
|
[
|
||||||
|
types.default.SET_BULK_CONVERSATION_LABELS,
|
||||||
|
[{ id: 1, labels: ['customer-support'] }],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('#setBulkConversationLabels', () => {
|
||||||
|
it('it send correct mutations', () => {
|
||||||
|
actions.setConversationLabel(
|
||||||
|
{ commit },
|
||||||
|
{ id: 1, data: ['customer-support'] }
|
||||||
|
);
|
||||||
|
expect(commit.mock.calls).toEqual([
|
||||||
|
[
|
||||||
|
types.default.SET_CONVERSATION_LABELS,
|
||||||
|
{ id: 1, data: ['customer-support'] },
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -15,7 +15,7 @@ describe('#mutations', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('#SET_CONVERSATION_LABELS', () => {
|
describe('#SET_CONVERSATION_LABELS', () => {
|
||||||
it('set contact conversation records', () => {
|
it('set contact labels', () => {
|
||||||
const state = { records: {} };
|
const state = { records: {} };
|
||||||
mutations[types.default.SET_CONVERSATION_LABELS](state, {
|
mutations[types.default.SET_CONVERSATION_LABELS](state, {
|
||||||
id: 1,
|
id: 1,
|
||||||
|
@ -26,4 +26,24 @@ describe('#mutations', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('#SET_BULK_CONVERSATION_LABELS', () => {
|
||||||
|
it('set contact labels in bulk', () => {
|
||||||
|
const state = { records: {} };
|
||||||
|
mutations[types.default.SET_BULK_CONVERSATION_LABELS](state, [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
labels: ['customer-success', 'on-hold'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
labels: ['customer-success'],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
expect(state.records).toEqual({
|
||||||
|
1: ['customer-success', 'on-hold'],
|
||||||
|
2: ['customer-success'],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -15,7 +15,7 @@ describe('#actions', () => {
|
||||||
await actions.getConversation({ commit }, 1);
|
await actions.getConversation({ commit }, 1);
|
||||||
expect(commit.mock.calls).toEqual([
|
expect(commit.mock.calls).toEqual([
|
||||||
[
|
[
|
||||||
types.default.ADD_CONVERSATION,
|
types.default.UPDATE_CONVERSATION,
|
||||||
{ id: 1, meta: { sender: { id: 1, name: 'Contact 1' } } },
|
{ id: 1, meta: { sender: { id: 1, name: 'Contact 1' } } },
|
||||||
],
|
],
|
||||||
['contacts/SET_CONTACT_ITEM', { id: 1, name: 'Contact 1' }],
|
['contacts/SET_CONTACT_ITEM', { id: 1, name: 'Contact 1' }],
|
||||||
|
|
|
@ -0,0 +1,94 @@
|
||||||
|
import axios from 'axios';
|
||||||
|
import { actions } from '../../labels';
|
||||||
|
import * as types from '../../../mutation-types';
|
||||||
|
import labelsList from './fixtures';
|
||||||
|
|
||||||
|
const commit = jest.fn();
|
||||||
|
global.axios = axios;
|
||||||
|
jest.mock('axios');
|
||||||
|
|
||||||
|
describe('#actions', () => {
|
||||||
|
describe('#get', () => {
|
||||||
|
it('sends correct actions if API is success', async () => {
|
||||||
|
axios.get.mockResolvedValue({ data: { payload: labelsList } });
|
||||||
|
await actions.get({ commit });
|
||||||
|
expect(commit.mock.calls).toEqual([
|
||||||
|
[types.default.SET_LABEL_UI_FLAG, { isFetching: true }],
|
||||||
|
[types.default.SET_LABELS, labelsList],
|
||||||
|
[types.default.SET_LABEL_UI_FLAG, { isFetching: false }],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
it('sends correct actions if API is error', async () => {
|
||||||
|
axios.get.mockRejectedValue({ message: 'Incorrect header' });
|
||||||
|
await actions.get({ commit });
|
||||||
|
expect(commit.mock.calls).toEqual([
|
||||||
|
[types.default.SET_LABEL_UI_FLAG, { isFetching: true }],
|
||||||
|
[types.default.SET_LABEL_UI_FLAG, { isFetching: false }],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('#create', () => {
|
||||||
|
it('sends correct actions if API is success', async () => {
|
||||||
|
axios.post.mockResolvedValue({ data: labelsList[0] });
|
||||||
|
await actions.create({ commit }, labelsList[0]);
|
||||||
|
expect(commit.mock.calls).toEqual([
|
||||||
|
[types.default.SET_LABEL_UI_FLAG, { isCreating: true }],
|
||||||
|
[types.default.ADD_LABEL, labelsList[0]],
|
||||||
|
[types.default.SET_LABEL_UI_FLAG, { isCreating: false }],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
it('sends correct actions if API is error', async () => {
|
||||||
|
axios.post.mockRejectedValue({ message: 'Incorrect header' });
|
||||||
|
await expect(actions.create({ commit })).rejects.toThrow(Error);
|
||||||
|
expect(commit.mock.calls).toEqual([
|
||||||
|
[types.default.SET_LABEL_UI_FLAG, { isCreating: true }],
|
||||||
|
[types.default.SET_LABEL_UI_FLAG, { isCreating: false }],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('#update', () => {
|
||||||
|
it('sends correct actions if API is success', async () => {
|
||||||
|
axios.patch.mockResolvedValue({ data: labelsList[0] });
|
||||||
|
await actions.update({ commit }, labelsList[0]);
|
||||||
|
expect(commit.mock.calls).toEqual([
|
||||||
|
[types.default.SET_LABEL_UI_FLAG, { isUpdating: true }],
|
||||||
|
[types.default.EDIT_LABEL, labelsList[0]],
|
||||||
|
[types.default.SET_LABEL_UI_FLAG, { isUpdating: false }],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
it('sends correct actions if API is error', async () => {
|
||||||
|
axios.patch.mockRejectedValue({ message: 'Incorrect header' });
|
||||||
|
await expect(actions.update({ commit }, labelsList[0])).rejects.toThrow(
|
||||||
|
Error
|
||||||
|
);
|
||||||
|
expect(commit.mock.calls).toEqual([
|
||||||
|
[types.default.SET_LABEL_UI_FLAG, { isUpdating: true }],
|
||||||
|
[types.default.SET_LABEL_UI_FLAG, { isUpdating: false }],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('#delete', () => {
|
||||||
|
it('sends correct actions if API is success', async () => {
|
||||||
|
axios.delete.mockResolvedValue({ data: labelsList[0] });
|
||||||
|
await actions.delete({ commit }, labelsList[0].id);
|
||||||
|
expect(commit.mock.calls).toEqual([
|
||||||
|
[types.default.SET_LABEL_UI_FLAG, { isDeleting: true }],
|
||||||
|
[types.default.DELETE_LABEL, labelsList[0].id],
|
||||||
|
[types.default.SET_LABEL_UI_FLAG, { isDeleting: false }],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
it('sends correct actions if API is error', async () => {
|
||||||
|
axios.delete.mockRejectedValue({ message: 'Incorrect header' });
|
||||||
|
await expect(
|
||||||
|
actions.delete({ commit }, labelsList[0].id)
|
||||||
|
).rejects.toThrow(Error);
|
||||||
|
expect(commit.mock.calls).toEqual([
|
||||||
|
[types.default.SET_LABEL_UI_FLAG, { isDeleting: true }],
|
||||||
|
[types.default.SET_LABEL_UI_FLAG, { isDeleting: false }],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,37 @@
|
||||||
|
export default [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: 'customer-support',
|
||||||
|
description: 'Customer support queries',
|
||||||
|
color: '#0076FF',
|
||||||
|
show_on_sidebar: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
title: 'saas-customer',
|
||||||
|
description: 'Customers who have account on app.chatwoot.com',
|
||||||
|
color: '#A8DBCB',
|
||||||
|
show_on_sidebar: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
title: 'hosted-customer',
|
||||||
|
description: 'Customers who have self-hosted instance',
|
||||||
|
color: '#F50471',
|
||||||
|
show_on_sidebar: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 6,
|
||||||
|
title: 'enterprise',
|
||||||
|
description: 'Customers who are using enterprise',
|
||||||
|
color: '#A90CFD',
|
||||||
|
show_on_sidebar: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 7,
|
||||||
|
title: 'billing-enquiry',
|
||||||
|
description: 'Queries on billing issues',
|
||||||
|
color: '#74B57A',
|
||||||
|
show_on_sidebar: false,
|
||||||
|
},
|
||||||
|
];
|
|
@ -0,0 +1,30 @@
|
||||||
|
import { getters } from '../../labels';
|
||||||
|
import labels from './fixtures';
|
||||||
|
describe('#getters', () => {
|
||||||
|
it('getLabels', () => {
|
||||||
|
const state = { records: labels };
|
||||||
|
expect(getters.getLabels(state)).toEqual(labels);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('getLabelsOnSidebar', () => {
|
||||||
|
const state = { records: labels };
|
||||||
|
expect(getters.getLabelsOnSidebar(state)).toEqual([labels[0]]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('getUIFlags', () => {
|
||||||
|
const state = {
|
||||||
|
uiFlags: {
|
||||||
|
isFetching: true,
|
||||||
|
isCreating: false,
|
||||||
|
isUpdating: false,
|
||||||
|
isDeleting: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
expect(getters.getUIFlags(state)).toEqual({
|
||||||
|
isFetching: true,
|
||||||
|
isCreating: false,
|
||||||
|
isUpdating: false,
|
||||||
|
isDeleting: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,39 @@
|
||||||
|
import types from '../../../mutation-types';
|
||||||
|
import { mutations } from '../../labels';
|
||||||
|
import labels from './fixtures';
|
||||||
|
describe('#mutations', () => {
|
||||||
|
describe('#SET_LABELS', () => {
|
||||||
|
it('set label records', () => {
|
||||||
|
const state = { records: [] };
|
||||||
|
mutations[types.SET_LABELS](state, labels);
|
||||||
|
expect(state.records).toEqual(labels);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('#ADD_LABEL', () => {
|
||||||
|
it('push newly created label to the store', () => {
|
||||||
|
const state = { records: [labels[0]] };
|
||||||
|
mutations[types.ADD_LABEL](state, labels[1]);
|
||||||
|
expect(state.records).toEqual([labels[0], labels[1]]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('#EDIT_LABEL', () => {
|
||||||
|
it('update label record', () => {
|
||||||
|
const state = { records: [labels[0]] };
|
||||||
|
mutations[types.EDIT_LABEL](state, {
|
||||||
|
id: 1,
|
||||||
|
title: 'customer-support-queries',
|
||||||
|
});
|
||||||
|
expect(state.records[0].title).toEqual('customer-support-queries');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('#DELETE_LABEL', () => {
|
||||||
|
it('delete label record', () => {
|
||||||
|
const state = { records: [labels[0]] };
|
||||||
|
mutations[types.DELETE_LABEL](state, 1);
|
||||||
|
expect(state.records).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -67,6 +67,13 @@ export default {
|
||||||
EDIT_CANNED: 'EDIT_CANNED',
|
EDIT_CANNED: 'EDIT_CANNED',
|
||||||
DELETE_CANNED: 'DELETE_CANNED',
|
DELETE_CANNED: 'DELETE_CANNED',
|
||||||
|
|
||||||
|
// Labels
|
||||||
|
SET_LABEL_UI_FLAG: 'SET_LABEL_UI_FLAG',
|
||||||
|
SET_LABELS: 'SET_LABELS',
|
||||||
|
ADD_LABEL: 'ADD_LABEL',
|
||||||
|
EDIT_LABEL: 'EDIT_LABEL',
|
||||||
|
DELETE_LABEL: 'DELETE_LABEL',
|
||||||
|
|
||||||
// Integrations
|
// Integrations
|
||||||
SET_INTEGRATIONS_UI_FLAG: 'SET_INTEGRATIONS_UI_FLAG',
|
SET_INTEGRATIONS_UI_FLAG: 'SET_INTEGRATIONS_UI_FLAG',
|
||||||
SET_INTEGRATIONS: 'SET_INTEGRATIONS',
|
SET_INTEGRATIONS: 'SET_INTEGRATIONS',
|
||||||
|
@ -92,6 +99,7 @@ export default {
|
||||||
// Conversation Label
|
// Conversation Label
|
||||||
SET_CONVERSATION_LABELS_UI_FLAG: 'SET_CONVERSATION_LABELS_UI_FLAG',
|
SET_CONVERSATION_LABELS_UI_FLAG: 'SET_CONVERSATION_LABELS_UI_FLAG',
|
||||||
SET_CONVERSATION_LABELS: 'SET_CONVERSATION_LABELS',
|
SET_CONVERSATION_LABELS: 'SET_CONVERSATION_LABELS',
|
||||||
|
SET_BULK_CONVERSATION_LABELS: 'SET_BULK_CONVERSATION_LABELS',
|
||||||
|
|
||||||
// Reports
|
// Reports
|
||||||
SET_ACCOUNT_REPORTS: 'SET_ACCOUNT_REPORTS',
|
SET_ACCOUNT_REPORTS: 'SET_ACCOUNT_REPORTS',
|
||||||
|
|
|
@ -14,7 +14,18 @@
|
||||||
# Indexes
|
# Indexes
|
||||||
#
|
#
|
||||||
# index_labels_on_account_id (account_id)
|
# index_labels_on_account_id (account_id)
|
||||||
|
# index_labels_on_title_and_account_id (title,account_id) UNIQUE
|
||||||
#
|
#
|
||||||
class Label < ApplicationRecord
|
class Label < ApplicationRecord
|
||||||
|
include RegexHelper
|
||||||
belongs_to :account
|
belongs_to :account
|
||||||
|
|
||||||
|
validates :title,
|
||||||
|
presence: { message: 'must not be blank' },
|
||||||
|
format: { with: UNICODE_CHARACTER_NUMBER_HYPHEN_UNDERSCORE },
|
||||||
|
uniqueness: { scope: :account_id }
|
||||||
|
|
||||||
|
before_validation do
|
||||||
|
self.title = title.downcase if attribute_present?('title')
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
class LabelPolicy < ApplicationPolicy
|
class LabelPolicy < ApplicationPolicy
|
||||||
def index?
|
def index?
|
||||||
@account_user.administrator?
|
@account_user.administrator? || @account_user.agent?
|
||||||
end
|
end
|
||||||
|
|
||||||
def update?
|
def update?
|
||||||
|
@ -14,4 +14,8 @@ class LabelPolicy < ApplicationPolicy
|
||||||
def create?
|
def create?
|
||||||
@account_user.administrator?
|
@account_user.administrator?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def destroy?
|
||||||
|
@account_user.administrator?
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -24,3 +24,4 @@ json.agent_last_seen_at conversation.agent_last_seen_at.to_i
|
||||||
json.unread_count conversation.unread_incoming_messages.count
|
json.unread_count conversation.unread_incoming_messages.count
|
||||||
json.additional_attributes conversation.additional_attributes
|
json.additional_attributes conversation.additional_attributes
|
||||||
json.account_id conversation.account_id
|
json.account_id conversation.account_id
|
||||||
|
json.labels conversation.label_list
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
class MigrateAndAddUniqueIndexToLabels < ActiveRecord::Migration[6.0]
|
||||||
|
def change
|
||||||
|
add_index :labels, [:title, :account_id], unique: true
|
||||||
|
migrate_existing_tags
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def migrate_existing_tags
|
||||||
|
::ActsAsTaggableOn::Tag.all.each do |tag|
|
||||||
|
tag.taggings.each do |tagging|
|
||||||
|
ensure_label_for_account(tag.name, tagging.taggable.account)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def ensure_label_for_account(name, account)
|
||||||
|
account.labels.where(title: name.downcase).first_or_create
|
||||||
|
end
|
||||||
|
end
|
|
@ -10,7 +10,7 @@
|
||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema.define(version: 2020_06_10_143132) do
|
ActiveRecord::Schema.define(version: 2020_06_25_124400) do
|
||||||
|
|
||||||
# These are extensions that must be enabled in order to support this database
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "pg_stat_statements"
|
enable_extension "pg_stat_statements"
|
||||||
|
@ -280,6 +280,7 @@ ActiveRecord::Schema.define(version: 2020_06_10_143132) do
|
||||||
t.datetime "created_at", precision: 6, null: false
|
t.datetime "created_at", precision: 6, null: false
|
||||||
t.datetime "updated_at", precision: 6, null: false
|
t.datetime "updated_at", precision: 6, null: false
|
||||||
t.index ["account_id"], name: "index_labels_on_account_id"
|
t.index ["account_id"], name: "index_labels_on_account_id"
|
||||||
|
t.index ["title", "account_id"], name: "index_labels_on_title_and_account_id", unique: true
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "messages", id: :serial, force: :cascade do |t|
|
create_table "messages", id: :serial, force: :cascade do |t|
|
||||||
|
|
8
lib/regex_helper.rb
Normal file
8
lib/regex_helper.rb
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
module RegexHelper
|
||||||
|
# user https://rubular.com/ to quickly validate your regex
|
||||||
|
|
||||||
|
# the following regext needs atleast one character which should be
|
||||||
|
# valid unicode letter, unicode number, underscore, hyphen
|
||||||
|
# shouldn't start with a underscore or hyphen
|
||||||
|
UNICODE_CHARACTER_NUMBER_HYPHEN_UNDERSCORE = Regexp.new('\A[\p{L}\p{N}]+[\p{L}\p{N}_-]+\Z')
|
||||||
|
end
|
|
@ -76,7 +76,7 @@ RSpec.describe 'Label API', type: :request do
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'PATCH /api/v1/accounts/{account.id}/labels/:id' do
|
describe 'PATCH /api/v1/accounts/{account.id}/labels/:id' do
|
||||||
let(:valid_params) { { title: 'Test 2' } }
|
let(:valid_params) { { title: 'Test_2' } }
|
||||||
|
|
||||||
context 'when it is an unauthenticated user' do
|
context 'when it is an unauthenticated user' do
|
||||||
it 'returns unauthorized' do
|
it 'returns unauthorized' do
|
||||||
|
@ -97,7 +97,7 @@ RSpec.describe 'Label API', type: :request do
|
||||||
as: :json
|
as: :json
|
||||||
|
|
||||||
expect(response).to have_http_status(:success)
|
expect(response).to have_http_status(:success)
|
||||||
expect(label.reload.title).to eq('Test 2')
|
expect(label.reload.title).to eq('test_2')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,6 +3,6 @@
|
||||||
FactoryBot.define do
|
FactoryBot.define do
|
||||||
factory :label do
|
factory :label do
|
||||||
account
|
account
|
||||||
sequence(:title) { |n| "Label #{n}" }
|
sequence(:title) { |n| "Label_#{n}" }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -4,4 +4,39 @@ RSpec.describe Label, type: :model do
|
||||||
describe 'associations' do
|
describe 'associations' do
|
||||||
it { is_expected.to belong_to(:account) }
|
it { is_expected.to belong_to(:account) }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe 'title validations' do
|
||||||
|
it 'would not let you start title without numbers or letters' do
|
||||||
|
label = FactoryBot.build(:label, title: '_12')
|
||||||
|
expect(label.valid?).to eq false
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'would not let you use special characters' do
|
||||||
|
label = FactoryBot.build(:label, title: 'jell;;2_12')
|
||||||
|
expect(label.valid?).to eq false
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'would not allow space' do
|
||||||
|
label = FactoryBot.build(:label, title: 'heeloo _12')
|
||||||
|
expect(label.valid?).to eq false
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'allows foreign charactes' do
|
||||||
|
label = FactoryBot.build(:label, title: '学中文_12')
|
||||||
|
expect(label.valid?).to eq true
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'converts uppercase letters to lowercase' do
|
||||||
|
label = FactoryBot.build(:label, title: 'Hello_World')
|
||||||
|
expect(label.valid?).to eq true
|
||||||
|
expect(label.title).to eq 'hello_world'
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'validates uniqueness of label name for account' do
|
||||||
|
account = create(:account)
|
||||||
|
label = FactoryBot.create(:label, account: account)
|
||||||
|
duplicate_label = FactoryBot.build(:label, title: label.title, account: account)
|
||||||
|
expect(duplicate_label.valid?).to eq false
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue