diff --git a/app/javascript/dashboard/api/inbox/conversation.js b/app/javascript/dashboard/api/inbox/conversation.js index d3942efea..3f1b2e199 100644 --- a/app/javascript/dashboard/api/inbox/conversation.js +++ b/app/javascript/dashboard/api/inbox/conversation.js @@ -6,13 +6,14 @@ class ConversationApi extends ApiClient { super('conversations', { accountScoped: true }); } - get({ inboxId, status, assigneeType, page }) { + get({ inboxId, status, assigneeType, page, labels }) { return axios.get(this.url, { params: { inbox_id: inboxId, status, assignee_type: assigneeType, page, + labels, }, }); } @@ -44,12 +45,13 @@ class ConversationApi extends ApiClient { return axios.post(`${this.url}/${conversationId}/mute`); } - meta({ inboxId, status, assigneeType }) { + meta({ inboxId, status, assigneeType, labels }) { return axios.get(`${this.url}/meta`, { params: { inbox_id: inboxId, status, assignee_type: assigneeType, + labels, }, }); } diff --git a/app/javascript/dashboard/api/labels.js b/app/javascript/dashboard/api/labels.js new file mode 100644 index 000000000..8a088e840 --- /dev/null +++ b/app/javascript/dashboard/api/labels.js @@ -0,0 +1,9 @@ +import ApiClient from './ApiClient'; + +class LabelsAPI extends ApiClient { + constructor() { + super('labels', { accountScoped: true }); + } +} + +export default new LabelsAPI(); diff --git a/app/javascript/dashboard/api/specs/labels.spec.js b/app/javascript/dashboard/api/specs/labels.spec.js new file mode 100644 index 000000000..f43c807c0 --- /dev/null +++ b/app/javascript/dashboard/api/specs/labels.spec.js @@ -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'); + }); +}); diff --git a/app/javascript/dashboard/assets/scss/_foundation-custom.scss b/app/javascript/dashboard/assets/scss/_foundation-custom.scss index a544ae883..900dd4d9d 100644 --- a/app/javascript/dashboard/assets/scss/_foundation-custom.scss +++ b/app/javascript/dashboard/assets/scss/_foundation-custom.scss @@ -1,6 +1,6 @@ .button { - font-weight: $font-weight-medium; font-family: $body-font-family; + font-weight: $font-weight-medium; &.round { border-radius: 1000px; @@ -20,10 +20,11 @@ } .tooltip { - max-width: 15rem; - padding: $space-smaller $space-small; border-radius: $space-smaller; font-size: $font-size-mini; + max-width: 15rem; + padding: $space-smaller $space-small; + z-index: 9999; } code { diff --git a/app/javascript/dashboard/assets/scss/_foundation-settings.scss b/app/javascript/dashboard/assets/scss/_foundation-settings.scss index 169198dae..afa81cd80 100644 --- a/app/javascript/dashboard/assets/scss/_foundation-settings.scss +++ b/app/javascript/dashboard/assets/scss/_foundation-settings.scss @@ -382,7 +382,7 @@ $label-color: $primary-color; $label-color-alt: $black; $label-palette: $foundation-palette; $label-font-size: $font-size-micro; -$label-padding: $space-micro $space-smaller; +$label-padding: $space-smaller $space-small; $label-radius: $space-micro; // 21. Media Object diff --git a/app/javascript/dashboard/assets/scss/widgets/_modal.scss b/app/javascript/dashboard/assets/scss/widgets/_modal.scss index 0ae8ecf21..8c4e656c4 100644 --- a/app/javascript/dashboard/assets/scss/widgets/_modal.scss +++ b/app/javascript/dashboard/assets/scss/widgets/_modal.scss @@ -67,6 +67,10 @@ font-size: $font-size-small; } + .content { + @include padding($space-large); + } + form { @include padding($space-large); align-self: center; diff --git a/app/javascript/dashboard/components/ChatList.vue b/app/javascript/dashboard/components/ChatList.vue index 9adb05dcc..5ca82a407 100644 --- a/app/javascript/dashboard/components/ChatList.vue +++ b/app/javascript/dashboard/components/ChatList.vue @@ -3,7 +3,7 @@

- {{ inbox.name || $t('CHAT_LIST.TAB_HEADING') }} + {{ pageTitle }}

@@ -15,14 +15,15 @@ @chatTabChange="updateAssigneeTab" /> -

+

{{ $t('CHAT_LIST.LIST.404') }}

@@ -40,7 +41,7 @@

{ + const labels = this.$store.getters[ + 'conversationLabels/getConversationLabels' + ](conversation.id); + return labels.includes(this.label); + }); + }, }, watch: { conversationInbox() { this.resetAndFetchData(); }, + label() { + this.resetAndFetchData(); + }, }, mounted() { this.$store.dispatch('setChatFilter', this.activeStatus); this.resetAndFetchData(); - this.$store.dispatch('agents/get'); bus.$on('fetch_conversation_stats', () => { this.$store.dispatch('conversationStats/get', this.conversationFilters); @@ -159,17 +202,6 @@ export default { 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; - }, }, }; diff --git a/app/javascript/dashboard/components/index.js b/app/javascript/dashboard/components/index.js index 24a81ee20..4fbfa5338 100644 --- a/app/javascript/dashboard/components/index.js +++ b/app/javascript/dashboard/components/index.js @@ -6,6 +6,7 @@ import Code from './Code'; import ColorPicker from './widgets/ColorPicker'; import DeleteModal from './widgets/modal/DeleteModal.vue'; import Input from './widgets/forms/Input.vue'; +import Label from './widgets/Label.vue'; import LoadingState from './widgets/LoadingState'; import Modal from './Modal'; import ModalHeader from './ModalHeader'; @@ -25,6 +26,7 @@ const WootUIKit = { DeleteModal, Input, LoadingState, + Label, Modal, ModalHeader, ReportStatsCard, diff --git a/app/javascript/dashboard/components/layout/Sidebar.vue b/app/javascript/dashboard/components/layout/Sidebar.vue index 88578e635..4ec6c239f 100644 --- a/app/javascript/dashboard/components/layout/Sidebar.vue +++ b/app/javascript/dashboard/components/layout/Sidebar.vue @@ -18,6 +18,11 @@ :key="inboxSection.toState" :menu-item="inboxSection" /> +

@@ -125,6 +130,7 @@ export default { inboxes: 'inboxes/getInboxes', accountId: 'getCurrentAccountId', currentRole: 'getCurrentRole', + accountLabels: 'labels/getLabelsOnSidebar', }), sidemenuItems() { 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() { return frontendURL(`accounts/${this.accountId}/dashboard`); }, diff --git a/app/javascript/dashboard/components/layout/SidebarItem.vue b/app/javascript/dashboard/components/layout/SidebarItem.vue index 35356a685..7ef2dfa88 100644 --- a/app/javascript/dashboard/components/layout/SidebarItem.vue +++ b/app/javascript/dashboard/components/layout/SidebarItem.vue @@ -36,7 +36,13 @@ v-if="computedInboxClass(child)" class="inbox-icon" :class="computedInboxClass(child)" - > + /> + + {{ child.label }} @@ -126,8 +132,22 @@ export default { }; diff --git a/app/javascript/dashboard/components/widgets/Label.vue b/app/javascript/dashboard/components/widgets/Label.vue new file mode 100644 index 000000000..9b56a4c91 --- /dev/null +++ b/app/javascript/dashboard/components/widgets/Label.vue @@ -0,0 +1,91 @@ + + + + diff --git a/app/javascript/dashboard/components/widgets/conversation/ConversationCard.vue b/app/javascript/dashboard/components/widgets/conversation/ConversationCard.vue index d63940127..0f3a2cd66 100644 --- a/app/javascript/dashboard/components/widgets/conversation/ConversationCard.vue +++ b/app/javascript/dashboard/components/widgets/conversation/ConversationCard.vue @@ -57,6 +57,10 @@ export default { mixins: [timeMixin, conversationMixin], props: { + activeLabel: { + type: String, + default: '', + }, chat: { type: Object, default: () => {}, @@ -116,7 +120,12 @@ export default { methods: { cardClick(chat) { 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) }); }, inboxName(inboxId) { diff --git a/app/javascript/dashboard/helper/URLHelper.js b/app/javascript/dashboard/helper/URLHelper.js index 5e6cae568..97744e43b 100644 --- a/app/javascript/dashboard/helper/URLHelper.js +++ b/app/javascript/dashboard/helper/URLHelper.js @@ -5,11 +5,14 @@ export const frontendURL = (path, params) => { return `/app/${path}${stringifiedParams}`; }; -export const conversationUrl = (accountId, activeInbox, id) => { - const path = activeInbox - ? `accounts/${accountId}/inbox/${activeInbox}/conversations/${id}` - : `accounts/${accountId}/conversations/${id}`; - return path; +export const conversationUrl = ({ accountId, activeInbox, id, label }) => { + if (activeInbox) { + return `accounts/${accountId}/inbox/${activeInbox}/conversations/${id}`; + } + if (label) { + return `accounts/${accountId}/label/${label}/conversations/${id}`; + } + return `accounts/${accountId}/conversations/${id}`; }; export const accountIdFromPathname = pathname => { diff --git a/app/javascript/dashboard/helper/specs/URLHelper.spec.js b/app/javascript/dashboard/helper/specs/URLHelper.spec.js index 5c45cf269..354cffcd6 100644 --- a/app/javascript/dashboard/helper/specs/URLHelper.spec.js +++ b/app/javascript/dashboard/helper/specs/URLHelper.spec.js @@ -7,15 +7,20 @@ import { describe('#URL Helpers', () => { describe('conversationUrl', () => { 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' ); }); - it('should return ibox conversation URL if activeInbox is not nil', () => { - expect(conversationUrl(1, 2, 1)).toBe( + it('should return inbox conversation URL if activeInbox is not nil', () => { + expect(conversationUrl({ accountId: 1, id: 1, activeInbox: 2 })).toBe( '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', () => { @@ -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', () => { it('should return account id if accont scoped url is passed', () => { expect(accountIdFromPathname('/app/accounts/1/settings/general')).toBe(1); diff --git a/app/javascript/dashboard/i18n/default-sidebar.js b/app/javascript/dashboard/i18n/default-sidebar.js index 4b479f5f6..246bc0af2 100644 --- a/app/javascript/dashboard/i18n/default-sidebar.js +++ b/app/javascript/dashboard/i18n/default-sidebar.js @@ -10,6 +10,8 @@ export const getSidebarItems = accountId => ({ 'settings_account_reports', 'profile_settings', 'profile_settings_index', + 'label_conversations', + 'conversations_through_label', ], menuItems: { assignedToMe: { @@ -40,9 +42,8 @@ export const getSidebarItems = accountId => ({ settings: { routes: [ 'agent_list', - 'agent_new', 'canned_list', - 'canned_new', + 'labels_list', 'settings_inbox', 'settings_inbox_new', 'settings_inbox_list', @@ -78,6 +79,13 @@ export const getSidebarItems = accountId => ({ toState: frontendURL(`accounts/${accountId}/settings/inboxes/list`), toStateName: 'settings_inbox_list', }, + labels: { + icon: 'ion-pricetags', + label: 'LABELS', + hasSubMenu: false, + toState: frontendURL(`accounts/${accountId}/settings/labels/list`), + toStateName: 'labels_list', + }, cannedResponses: { icon: 'ion-chatbox-working', label: 'CANNED_RESPONSES', diff --git a/app/javascript/dashboard/i18n/locale/en/contact.json b/app/javascript/dashboard/i18n/locale/en/contact.json index eb7b2d354..e5c3ec25d 100644 --- a/app/javascript/dashboard/i18n/locale/en/contact.json +++ b/app/javascript/dashboard/i18n/locale/en/contact.json @@ -11,11 +11,19 @@ }, "LABELS": { "TITLE": "Conversation Labels", - "UPDATE_BUTTON": "Update Labels", - "UPDATE_ERROR": "Couldn't update labels, try again.", - "TAG_PLACEHOLDER": "Add new label", - "PLACEHOLDER": "Search or add a label" + "MODAL": { + "TITLE": "Labels for", + "ACTIVE_LABELS": "Labels added to the conversation", + "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." + }, + "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" + "MUTE_CONTACT": "Mute Contact", + "EDIT_LABEL": "Edit" } } diff --git a/app/javascript/dashboard/i18n/locale/en/index.js b/app/javascript/dashboard/i18n/locale/en/index.js index 558cc9b3a..5dfc44e4b 100644 --- a/app/javascript/dashboard/i18n/locale/en/index.js +++ b/app/javascript/dashboard/i18n/locale/en/index.js @@ -1,5 +1,5 @@ -/* eslint-disable */ import { default as _agentMgmt } from './agentMgmt.json'; +import { default as _labelsMgmt } from './labelsMgmt.json'; import { default as _cannedMgmt } from './cannedMgmt.json'; import { default as _chatlist } from './chatlist.json'; import { default as _contact } from './contact.json'; @@ -23,6 +23,7 @@ export default { ..._inboxMgmt, ..._login, ..._report, + ..._labelsMgmt, ..._resetPassword, ..._setNewPassword, ..._settings, diff --git a/app/javascript/dashboard/i18n/locale/en/labelsMgmt.json b/app/javascript/dashboard/i18n/locale/en/labelsMgmt.json new file mode 100644 index 000000000..b0dbe439f --- /dev/null +++ b/app/javascript/dashboard/i18n/locale/en/labelsMgmt.json @@ -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": "

Labels

Labels help you to categorize conversations and prioritize them. You can assign label to a conversation from the sidepanel.

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.

", + "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 " + } + } + } +} diff --git a/app/javascript/dashboard/i18n/locale/en/settings.json b/app/javascript/dashboard/i18n/locale/en/settings.json index 38f5a4aac..67470727f 100644 --- a/app/javascript/dashboard/i18n/locale/en/settings.json +++ b/app/javascript/dashboard/i18n/locale/en/settings.json @@ -103,6 +103,7 @@ "INBOXES": "Inboxes", "CANNED_RESPONSES": "Canned Responses", "INTEGRATIONS": "Integrations", - "ACCOUNT_SETTINGS": "Account Settings" + "ACCOUNT_SETTINGS": "Account Settings", + "LABELS": "Labels" } } diff --git a/app/javascript/dashboard/routes/dashboard/conversation/ContactDetailsItem.vue b/app/javascript/dashboard/routes/dashboard/conversation/ContactDetailsItem.vue index 41068876f..fda504c35 100644 --- a/app/javascript/dashboard/routes/dashboard/conversation/ContactDetailsItem.vue +++ b/app/javascript/dashboard/routes/dashboard/conversation/ContactDetailsItem.vue @@ -1,8 +1,13 @@