diff --git a/app/javascript/dashboard/api/contacts.js b/app/javascript/dashboard/api/contacts.js
index 1a66db0e1..cc8448873 100644
--- a/app/javascript/dashboard/api/contacts.js
+++ b/app/javascript/dashboard/api/contacts.js
@@ -18,6 +18,14 @@ class ContactAPI extends ApiClient {
return axios.get(`${this.url}/${contactId}/contactable_inboxes`);
}
+ getContactLabels(contactId) {
+ return axios.get(`${this.url}/${contactId}/labels`);
+ }
+
+ updateContactLabels(contactId, labels) {
+ return axios.post(`${this.url}/${contactId}/labels`, { labels });
+ }
+
search(search = '', page = 1, sortAttr = 'name') {
return axios.get(
`${this.url}/search?q=${search}&page=${page}&sort=${sortAttr}`
diff --git a/app/javascript/dashboard/api/specs/contacts.spec.js b/app/javascript/dashboard/api/specs/contacts.spec.js
index ae1e1e0e1..a7080a634 100644
--- a/app/javascript/dashboard/api/specs/contacts.spec.js
+++ b/app/javascript/dashboard/api/specs/contacts.spec.js
@@ -34,6 +34,25 @@ describe('#ContactsAPI', () => {
'/api/v1/contacts/1/contactable_inboxes'
);
});
+
+ it('#getContactLabels', () => {
+ contactAPI.getContactLabels(1);
+ expect(context.axiosMock.get).toHaveBeenCalledWith(
+ '/api/v1/contacts/1/labels'
+ );
+ });
+
+ it('#updateContactLabels', () => {
+ const labels = ['support-query'];
+ contactAPI.updateContactLabels(1, labels);
+ expect(context.axiosMock.post).toHaveBeenCalledWith(
+ '/api/v1/contacts/1/labels',
+ {
+ labels,
+ }
+ );
+ });
+
it('#search', () => {
contactAPI.search('leads', 1, 'date');
expect(context.axiosMock.get).toHaveBeenCalledWith(
diff --git a/app/javascript/dashboard/components/widgets/LabelSelector.stories.js b/app/javascript/dashboard/components/widgets/LabelSelector.stories.js
new file mode 100644
index 000000000..77ca54b25
--- /dev/null
+++ b/app/javascript/dashboard/components/widgets/LabelSelector.stories.js
@@ -0,0 +1,67 @@
+import { action } from '@storybook/addon-actions';
+import LabelSelector from './LabelSelector';
+
+export default {
+ title: 'Components/Label/Contact Label',
+ component: LabelSelector,
+ argTypes: {
+ contactId: {
+ control: {
+ type: 'text ,number',
+ },
+ },
+ },
+};
+
+const Template = (args, { argTypes }) => ({
+ props: Object.keys(argTypes),
+ components: { LabelSelector },
+ template:
+ '',
+});
+
+export const ContactLabel = Template.bind({});
+ContactLabel.args = {
+ onAdd: action('Added'),
+ onRemove: action('Removed'),
+ allLabels: [
+ {
+ id: '1',
+ title: 'sales',
+ description: '',
+ color: '#0a5dd1',
+ },
+ {
+ id: '2',
+ title: 'refund',
+ description: '',
+ color: '#8442f5',
+ },
+ {
+ id: '3',
+ title: 'testing',
+ description: '',
+ color: '#f542f5',
+ },
+ {
+ id: '4',
+ title: 'scheduled',
+ description: '',
+ color: '#42d1f5',
+ },
+ ],
+ savedLabels: [
+ {
+ id: '2',
+ title: 'refund',
+ description: '',
+ color: '#8442f5',
+ },
+ {
+ id: '4',
+ title: 'scheduled',
+ description: '',
+ color: '#42d1f5',
+ },
+ ],
+};
diff --git a/app/javascript/dashboard/components/widgets/LabelSelector.vue b/app/javascript/dashboard/components/widgets/LabelSelector.vue
new file mode 100644
index 000000000..8a6270bb8
--- /dev/null
+++ b/app/javascript/dashboard/components/widgets/LabelSelector.vue
@@ -0,0 +1,116 @@
+
+
+
+
+ {{ $t('CONTACT_PANEL.LABELS.CONTACT.TITLE') }}
+
+
+
+
+
+
+
+
diff --git a/app/javascript/dashboard/i18n/locale/en/contact.json b/app/javascript/dashboard/i18n/locale/en/contact.json
index 949637088..84e6303fc 100644
--- a/app/javascript/dashboard/i18n/locale/en/contact.json
+++ b/app/javascript/dashboard/i18n/locale/en/contact.json
@@ -18,19 +18,14 @@
"TITLE": "Previous Conversations"
},
"LABELS": {
- "TITLE": "Conversation Labels",
- "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",
- "ADD_BUTTON": "Add Labels",
- "UPDATE_BUTTON": "Update labels",
- "UPDATE_ERROR": "Couldn't update labels, try again."
+ "CONTACT": {
+ "TITLE": "Contact Labels",
+ "ERROR": "Couldn't update labels"
+ },
+ "CONVERSATION": {
+ "TITLE": "Conversation Labels",
+ "ADD_BUTTON": "Add Labels"
},
- "NO_LABELS_TO_ADD": "There are no more labels defined in the account.",
- "NO_AVAILABLE_LABELS": "There are no labels added to this conversation.",
"LABEL_SELECT": {
"TITLE": "Add Labels",
"PLACEHOLDER": "Search labels",
diff --git a/app/javascript/dashboard/routes/dashboard/contacts/components/ContactInfoPanel.vue b/app/javascript/dashboard/routes/dashboard/contacts/components/ContactInfoPanel.vue
index 62970ec64..3ddfc0a59 100644
--- a/app/javascript/dashboard/routes/dashboard/contacts/components/ContactInfoPanel.vue
+++ b/app/javascript/dashboard/routes/dashboard/contacts/components/ContactInfoPanel.vue
@@ -8,6 +8,7 @@
v-if="hasContactAttributes"
:custom-attributes="contact.custom_attributes"
/>
+
+
+
+
+
+
+
diff --git a/app/javascript/dashboard/routes/dashboard/conversation/labels/LabelBox.vue b/app/javascript/dashboard/routes/dashboard/conversation/labels/LabelBox.vue
index 56aef8547..162f853ed 100644
--- a/app/javascript/dashboard/routes/dashboard/conversation/labels/LabelBox.vue
+++ b/app/javascript/dashboard/routes/dashboard/conversation/labels/LabelBox.vue
@@ -5,7 +5,7 @@
class="contact-conversation--list"
>
@@ -30,7 +30,6 @@
v-if="showSearchDropdownLabel"
:account-labels="accountLabels"
:selected-labels="savedLabels"
- :conversation-id="conversationId"
@add="addItem"
@remove="removeItem"
/>
@@ -61,7 +60,7 @@ export default {
mixins: [clickaway],
props: {
conversationId: {
- type: [String, Number],
+ type: Number,
required: true,
},
},
diff --git a/app/javascript/dashboard/store/index.js b/app/javascript/dashboard/store/index.js
index 867dcecf8..2dcf1f9e3 100755
--- a/app/javascript/dashboard/store/index.js
+++ b/app/javascript/dashboard/store/index.js
@@ -7,6 +7,7 @@ import auth from './modules/auth';
import cannedResponse from './modules/cannedResponse';
import contactConversations from './modules/contactConversations';
import contacts from './modules/contacts';
+import contactLabels from './modules/contactLabels';
import notifications from './modules/notifications';
import conversationLabels from './modules/conversationLabels';
import conversationMetadata from './modules/conversationMetadata';
@@ -38,6 +39,7 @@ export default new Vuex.Store({
cannedResponse,
contactConversations,
contacts,
+ contactLabels,
notifications,
conversationLabels,
conversationMetadata,
diff --git a/app/javascript/dashboard/store/modules/contactLabels.js b/app/javascript/dashboard/store/modules/contactLabels.js
new file mode 100644
index 000000000..8ee6c09a6
--- /dev/null
+++ b/app/javascript/dashboard/store/modules/contactLabels.js
@@ -0,0 +1,89 @@
+import Vue from 'vue';
+import types from '../mutation-types';
+import ContactAPI from '../../api/contacts';
+
+const state = {
+ records: {},
+ uiFlags: {
+ isFetching: false,
+ isUpdating: false,
+ isError: false,
+ },
+};
+
+export const getters = {
+ getUIFlags($state) {
+ return $state.uiFlags;
+ },
+ getContactLabels: $state => id => {
+ return $state.records[Number(id)] || [];
+ },
+};
+
+export const actions = {
+ get: async ({ commit }, contactId) => {
+ commit(types.SET_CONTACT_LABELS_UI_FLAG, {
+ isFetching: true,
+ });
+ try {
+ const response = await ContactAPI.getContactLabels(contactId);
+ commit(types.SET_CONTACT_LABELS, {
+ id: contactId,
+ data: response.data.payload,
+ });
+ commit(types.SET_CONTACT_LABELS_UI_FLAG, {
+ isFetching: false,
+ });
+ } catch (error) {
+ commit(types.SET_CONTACT_LABELS_UI_FLAG, {
+ isFetching: false,
+ });
+ }
+ },
+ update: async ({ commit }, { contactId, labels }) => {
+ commit(types.SET_CONTACT_LABELS_UI_FLAG, {
+ isUpdating: true,
+ });
+ try {
+ const response = await ContactAPI.updateContactLabels(contactId, labels);
+ commit(types.SET_CONTACT_LABELS, {
+ id: contactId,
+ data: response.data.payload,
+ });
+ commit(types.SET_CONTACT_LABELS_UI_FLAG, {
+ isUpdating: false,
+ isError: false,
+ });
+ } catch (error) {
+ commit(types.SET_CONTACT_LABELS_UI_FLAG, {
+ isUpdating: false,
+ isError: true,
+ });
+ throw new Error(error);
+ }
+ },
+
+ setContactLabel({ commit }, { id, data }) {
+ commit(types.SET_CONTACT_LABELS, { id, data });
+ },
+};
+
+export const mutations = {
+ [types.SET_CONTACT_LABELS_UI_FLAG]($state, data) {
+ $state.uiFlags = {
+ ...$state.uiFlags,
+ ...data,
+ };
+ },
+ [types.SET_CONTACT_LABELS]: ($state, { id, data }) => {
+ Vue.set($state.records, id, data);
+ },
+};
+
+export default {
+ namespaced: true,
+ state,
+ getters,
+ actions,
+ mutations,
+};
diff --git a/app/javascript/dashboard/store/modules/specs/contactLabels/actions.spec.js b/app/javascript/dashboard/store/modules/specs/contactLabels/actions.spec.js
new file mode 100644
index 000000000..0f5222603
--- /dev/null
+++ b/app/javascript/dashboard/store/modules/specs/contactLabels/actions.spec.js
@@ -0,0 +1,73 @@
+import axios from 'axios';
+import { actions } from '../../contactLabels';
+import * as types from '../../../mutation-types';
+
+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: ['customer-success', 'on-hold'] },
+ });
+ await actions.get({ commit }, 1);
+ expect(commit.mock.calls).toEqual([
+ [types.default.SET_CONTACT_LABELS_UI_FLAG, { isFetching: true }],
+
+ [
+ types.default.SET_CONTACT_LABELS,
+ { id: 1, data: ['customer-success', 'on-hold'] },
+ ],
+ [types.default.SET_CONTACT_LABELS_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_CONTACT_LABELS_UI_FLAG, { isFetching: true }],
+ [types.default.SET_CONTACT_LABELS_UI_FLAG, { isFetching: false }],
+ ]);
+ });
+ });
+
+ describe('#update', () => {
+ it('updates correct actions if API is success', async () => {
+ axios.post.mockResolvedValue({
+ data: { payload: { contactId: '1', labels: ['on-hold'] } },
+ });
+ await actions.update({ commit }, { contactId: '1', labels: ['on-hold'] });
+
+ expect(commit.mock.calls).toEqual([
+ [types.default.SET_CONTACT_LABELS_UI_FLAG, { isUpdating: true }],
+ [
+ types.default.SET_CONTACT_LABELS,
+ {
+ id: '1',
+ data: { contactId: '1', labels: ['on-hold'] },
+ },
+ ],
+ [
+ types.default.SET_CONTACT_LABELS_UI_FLAG,
+ { isUpdating: false, isError: false },
+ ],
+ ]);
+ });
+
+ it('sends correct actions if API is error', async () => {
+ axios.post.mockRejectedValue({ message: 'Incorrect header' });
+ await expect(
+ actions.update({ commit }, { contactId: '1', labels: ['on-hold'] })
+ ).rejects.toThrow(Error);
+ expect(commit.mock.calls).toEqual([
+ [types.default.SET_CONTACT_LABELS_UI_FLAG, { isUpdating: true }],
+ [
+ types.default.SET_CONTACT_LABELS_UI_FLAG,
+ { isUpdating: false, isError: true },
+ ],
+ ]);
+ });
+ });
+});
diff --git a/app/javascript/dashboard/store/modules/specs/contactLabels/getters.spec.js b/app/javascript/dashboard/store/modules/specs/contactLabels/getters.spec.js
new file mode 100644
index 000000000..9acb0decf
--- /dev/null
+++ b/app/javascript/dashboard/store/modules/specs/contactLabels/getters.spec.js
@@ -0,0 +1,24 @@
+import { getters } from '../../contactLabels';
+
+describe('#getters', () => {
+ it('getContactLabels', () => {
+ const state = {
+ records: { 1: ['customer-success', 'on-hold'] },
+ };
+ expect(getters.getContactLabels(state)(1)).toEqual([
+ 'customer-success',
+ 'on-hold',
+ ]);
+ });
+
+ it('getUIFlags', () => {
+ const state = {
+ uiFlags: {
+ isFetching: true,
+ },
+ };
+ expect(getters.getUIFlags(state)).toEqual({
+ isFetching: true,
+ });
+ });
+});
diff --git a/app/javascript/dashboard/store/modules/specs/contactLabels/mutations.spec.js b/app/javascript/dashboard/store/modules/specs/contactLabels/mutations.spec.js
new file mode 100644
index 000000000..099281363
--- /dev/null
+++ b/app/javascript/dashboard/store/modules/specs/contactLabels/mutations.spec.js
@@ -0,0 +1,29 @@
+import * as types from '../../../mutation-types';
+import { mutations } from '../../contactLabels';
+
+describe('#mutations', () => {
+ describe('#SET_CONTACT_LABELS_UI_FLAG', () => {
+ it('set ui flags', () => {
+ const state = { uiFlags: { isFetching: true } };
+ mutations[types.default.SET_CONTACT_LABELS_UI_FLAG](state, {
+ isFetching: false,
+ });
+ expect(state.uiFlags).toEqual({
+ isFetching: false,
+ });
+ });
+ });
+
+ describe('#SET_CONTACT_LABELS', () => {
+ it('set contact labels', () => {
+ const state = { records: {} };
+ mutations[types.default.SET_CONTACT_LABELS](state, {
+ id: 1,
+ data: ['customer-success', 'on-hold'],
+ });
+ expect(state.records).toEqual({
+ 1: ['customer-success', 'on-hold'],
+ });
+ });
+ });
+});
diff --git a/app/javascript/dashboard/store/mutation-types.js b/app/javascript/dashboard/store/mutation-types.js
index f2d101302..7ccb6bccb 100755
--- a/app/javascript/dashboard/store/mutation-types.js
+++ b/app/javascript/dashboard/store/mutation-types.js
@@ -119,6 +119,10 @@ export default {
SET_CONTACT_CONVERSATIONS: 'SET_CONTACT_CONVERSATIONS',
ADD_CONTACT_CONVERSATION: 'ADD_CONTACT_CONVERSATION',
+ // Contact Label
+ SET_CONTACT_LABELS_UI_FLAG: 'SET_CONTACT_LABELS_UI_FLAG',
+ SET_CONTACT_LABELS: 'SET_CONTACT_LABELS',
+
// Conversation Label
SET_CONVERSATION_LABELS_UI_FLAG: 'SET_CONVERSATION_LABELS_UI_FLAG',
SET_CONVERSATION_LABELS: 'SET_CONVERSATION_LABELS',
diff --git a/app/javascript/shared/components/ui/dropdown/AddLabel.vue b/app/javascript/shared/components/ui/dropdown/AddLabel.vue
index 4254749c1..9b54450d9 100644
--- a/app/javascript/shared/components/ui/dropdown/AddLabel.vue
+++ b/app/javascript/shared/components/ui/dropdown/AddLabel.vue
@@ -2,7 +2,7 @@
diff --git a/app/javascript/shared/components/ui/label/LabelDropdown.vue b/app/javascript/shared/components/ui/label/LabelDropdown.vue
index a6e2dd131..58bcda4c6 100644
--- a/app/javascript/shared/components/ui/label/LabelDropdown.vue
+++ b/app/javascript/shared/components/ui/label/LabelDropdown.vue
@@ -41,10 +41,6 @@ export default {
},
props: {
- conversationId: {
- type: [String, Number],
- required: true,
- },
accountLabels: {
type: Array,
default: () => [],
diff --git a/app/javascript/shared/components/ui/label/LabelDropdownItem.vue b/app/javascript/shared/components/ui/label/LabelDropdownItem.vue
index e692bfd0b..68f2528e5 100644
--- a/app/javascript/shared/components/ui/label/LabelDropdownItem.vue
+++ b/app/javascript/shared/components/ui/label/LabelDropdownItem.vue
@@ -9,9 +9,11 @@
class="label-color--display"
:style="{ backgroundColor: color }"
/>
- {{ title }}
+ {{ title }}
+
+
+
-
@@ -47,9 +49,14 @@ export default {
.item-wrap {
display: flex;
+ ::v-deep .button__content {
+ width: 100%;
+ }
+
.button-wrap {
display: flex;
justify-content: space-between;
+ width: 100%;
&.active {
display: flex;
@@ -59,14 +66,24 @@ export default {
.name-label-wrap {
display: flex;
- }
+ min-width: 0;
+ width: 100%;
- .label-color--display {
- margin-right: var(--space-small);
- }
+ .label-color--display {
+ margin-right: var(--space-small);
+ }
- .icon {
- font-size: var(--font-size-small);
+ .label-text {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ line-height: 1.1;
+ padding-right: var(--space-small);
+ }
+
+ .icon {
+ font-size: var(--font-size-small);
+ }
}
}