diff --git a/.codeclimate.yml b/.codeclimate.yml
index 611533a7e..d0cde2858 100644
--- a/.codeclimate.yml
+++ b/.codeclimate.yml
@@ -11,3 +11,16 @@ plugins:
enabled: true
brakeman:
enabled: true
+
+exclude_patterns:
+ - "spec/"
+ - "**/specs/"
+ - "db/*"
+ - "bin/**/*"
+ - "db/**/*"
+ - "config/**/*"
+ - "public/**/*"
+ - "vendor/**/*"
+ - "node_modules/**/*"
+ - "lib/tasks/auto_annotate_models.rake"
+ - "app/test-matchers.js"
diff --git a/app/controllers/api/v1/widget/messages_controller.rb b/app/controllers/api/v1/widget/messages_controller.rb
index e8fe9e813..69a84e31b 100644
--- a/app/controllers/api/v1/widget/messages_controller.rb
+++ b/app/controllers/api/v1/widget/messages_controller.rb
@@ -28,7 +28,7 @@ class Api::V1::Widget::MessagesController < ActionController::Base
account_id: conversation.account_id,
inbox_id: conversation.inbox_id,
message_type: :incoming,
- content: permitted_params[:content]
+ content: permitted_params[:message][:content]
}
end
@@ -65,7 +65,8 @@ class Api::V1::Widget::MessagesController < ActionController::Base
def message_finder_params
{
- filter_internal_messages: true
+ filter_internal_messages: true,
+ before: permitted_params[:before]
}
end
@@ -78,7 +79,7 @@ class Api::V1::Widget::MessagesController < ActionController::Base
end
def permitted_params
- params.fetch(:message).permit(:content)
+ params.permit(:before, message: [:content])
end
def secret_key
diff --git a/app/javascript/dashboard/assets/scss/_mixins.scss b/app/javascript/dashboard/assets/scss/_mixins.scss
index bb9b23f4a..0b7a73c2c 100644
--- a/app/javascript/dashboard/assets/scss/_mixins.scss
+++ b/app/javascript/dashboard/assets/scss/_mixins.scss
@@ -149,7 +149,9 @@
@mixin color-spinner() {
@keyframes spinner {
- to {transform: rotate(360deg);}
+ to {
+ transform: rotate(360deg);
+ }
}
&:before {
diff --git a/app/javascript/shared/components/Spinner.vue b/app/javascript/shared/components/Spinner.vue
new file mode 100644
index 000000000..6f34313e5
--- /dev/null
+++ b/app/javascript/shared/components/Spinner.vue
@@ -0,0 +1,67 @@
+
+
+
+
+
diff --git a/app/javascript/shared/components/specs/Spinner.spec.js b/app/javascript/shared/components/specs/Spinner.spec.js
new file mode 100644
index 000000000..caf24c1ed
--- /dev/null
+++ b/app/javascript/shared/components/specs/Spinner.spec.js
@@ -0,0 +1,10 @@
+import { mount } from '@vue/test-utils';
+import Spinner from '../Spinner';
+
+describe('Spinner', () => {
+ test('matches snapshot', () => {
+ const wrapper = mount(Spinner);
+ expect(wrapper.isVueInstance()).toBeTruthy();
+ expect(wrapper.element).toMatchSnapshot();
+ });
+});
diff --git a/app/javascript/shared/components/specs/__snapshots__/Spinner.spec.js.snap b/app/javascript/shared/components/specs/__snapshots__/Spinner.spec.js.snap
new file mode 100644
index 000000000..4c2f77a5d
--- /dev/null
+++ b/app/javascript/shared/components/specs/__snapshots__/Spinner.spec.js.snap
@@ -0,0 +1,7 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Spinner matches snapshot 1`] = `
+
+`;
diff --git a/app/javascript/widget/api/conversation.js b/app/javascript/widget/api/conversation.js
index d4eb2f509..b7a243f67 100755
--- a/app/javascript/widget/api/conversation.js
+++ b/app/javascript/widget/api/conversation.js
@@ -7,9 +7,9 @@ const sendMessageAPI = async content => {
return result;
};
-const getConversationAPI = async conversationId => {
- const urlData = endPoints.getConversation(conversationId);
- const result = await API.get(urlData.url);
+const getConversationAPI = async ({ before }) => {
+ const urlData = endPoints.getConversation({ before });
+ const result = await API.get(urlData.url, { params: urlData.params });
return result;
};
diff --git a/app/javascript/widget/api/endPoints.js b/app/javascript/widget/api/endPoints.js
index d933da581..7933edea7 100755
--- a/app/javascript/widget/api/endPoints.js
+++ b/app/javascript/widget/api/endPoints.js
@@ -7,8 +7,9 @@ const sendMessage = content => ({
},
});
-const getConversation = () => ({
+const getConversation = ({ before }) => ({
url: `/api/v1/widget/messages${window.location.search}`,
+ params: { before },
});
export default {
diff --git a/app/javascript/widget/api/specs/endPoints.spec.js b/app/javascript/widget/api/specs/endPoints.spec.js
new file mode 100644
index 000000000..58663d448
--- /dev/null
+++ b/app/javascript/widget/api/specs/endPoints.spec.js
@@ -0,0 +1,25 @@
+import endPoints from '../endPoints';
+
+describe('#sendMessage', () => {
+ it('returns correct payload', () => {
+ expect(endPoints.sendMessage('hello')).toEqual({
+ url: `/api/v1/widget/messages`,
+ params: {
+ message: {
+ content: 'hello',
+ },
+ },
+ });
+ });
+});
+
+describe('#getConversation', () => {
+ it('returns correct payload', () => {
+ expect(endPoints.getConversation({ before: 123 })).toEqual({
+ url: `/api/v1/widget/messages`,
+ params: {
+ before: 123,
+ },
+ });
+ });
+});
diff --git a/app/javascript/widget/components/ChatSendButton.vue b/app/javascript/widget/components/ChatSendButton.vue
index 7e43c4028..f7bc77ed3 100755
--- a/app/javascript/widget/components/ChatSendButton.vue
+++ b/app/javascript/widget/components/ChatSendButton.vue
@@ -13,7 +13,7 @@
-
-
-
diff --git a/app/javascript/widget/store/modules/conversation.js b/app/javascript/widget/store/modules/conversation.js
index 6f1f25953..d00d9a820 100755
--- a/app/javascript/widget/store/modules/conversation.js
+++ b/app/javascript/widget/store/modules/conversation.js
@@ -21,42 +21,54 @@ export const findUndeliveredMessage = (messageInbox, { content }) =>
);
export const DEFAULT_CONVERSATION = 'default';
+
const state = {
conversations: {},
+ uiFlags: {
+ allMessagesLoaded: false,
+ isFetchingList: false,
+ },
};
-const getters = {
+export const getters = {
getConversation: _state => _state.conversations,
getConversationSize: _state => Object.keys(_state.conversations).length,
+ getAllMessagesLoaded: _state => _state.uiFlags.allMessagesLoaded,
+ getIsFetchingList: _state => _state.uiFlags.isFetchingList,
+ getEarliestMessage: _state => {
+ const conversation = Object.values(_state.conversations);
+ if (conversation.length) {
+ return conversation[0];
+ }
+ return {};
+ },
};
-const actions = {
+export const actions = {
sendMessage: async ({ commit }, params) => {
const { content } = params;
- commit('pushMessageToConversations', createTemporaryMessage(content));
+ commit('pushMessageToConversation', createTemporaryMessage(content));
await sendMessageAPI(content);
},
- fetchOldConversations: async ({ commit }) => {
+ fetchOldConversations: async ({ commit }, { before } = {}) => {
try {
- const { data } = await getConversationAPI();
- commit('initMessagesInConversation', data);
+ commit('setConversationListLoading', true);
+ const { data } = await getConversationAPI({ before });
+ commit('setMessagesInConversation', data);
+ commit('setConversationListLoading', false);
} catch (error) {
- // Handle error
+ commit('setConversationListLoading', false);
}
},
addMessage({ commit }, data) {
- commit('pushMessageToConversations', data);
+ commit('pushMessageToConversation', data);
},
};
-const mutations = {
- initInboxInConversations($state, lastConversation) {
- Vue.set($state.conversations, lastConversation, {});
- },
-
- pushMessageToConversations($state, message) {
+export const mutations = {
+ pushMessageToConversation($state, message) {
const { id, status, message_type: type } = message;
const messagesInbox = $state.conversations;
const isMessageIncoming = type === MESSAGE_TYPE.INCOMING;
@@ -71,7 +83,6 @@ const mutations = {
messagesInbox,
message
);
-
if (!messageInConversation) {
Vue.set(messagesInbox, id, message);
} else {
@@ -80,12 +91,17 @@ const mutations = {
}
},
- initMessagesInConversation(_state, payload) {
+ setConversationListLoading($state, status) {
+ $state.uiFlags.isFetchingList = status;
+ },
+
+ setMessagesInConversation($state, payload) {
if (!payload.length) {
+ $state.uiFlags.allMessagesLoaded = true;
return;
}
- payload.map(message => Vue.set(_state.conversations, message.id, message));
+ payload.map(message => Vue.set($state.conversations, message.id, message));
},
};
diff --git a/app/javascript/widget/store/modules/specs/conversation/actions.spec.js b/app/javascript/widget/store/modules/specs/conversation/actions.spec.js
new file mode 100644
index 000000000..1cb8075c1
--- /dev/null
+++ b/app/javascript/widget/store/modules/specs/conversation/actions.spec.js
@@ -0,0 +1,32 @@
+import { actions } from '../../conversation';
+import getUuid from '../../../../helpers/uuid';
+
+jest.mock('../../../../helpers/uuid');
+
+const commit = jest.fn();
+
+describe('#actions', () => {
+ describe('#addMessage', () => {
+ it('sends correct mutations', () => {
+ actions.addMessage({ commit }, { id: 1 });
+ expect(commit).toBeCalledWith('pushMessageToConversation', { id: 1 });
+ });
+ });
+
+ describe('#sendMessage', () => {
+ it('sends correct mutations', () => {
+ const mockDate = new Date(1466424490000);
+ getUuid.mockImplementationOnce(() => '1111');
+ const spy = jest.spyOn(global, 'Date').mockImplementation(() => mockDate);
+ actions.sendMessage({ commit }, { content: 'hello' });
+ spy.mockRestore();
+ expect(commit).toBeCalledWith('pushMessageToConversation', {
+ id: '1111',
+ content: 'hello',
+ status: 'in_progress',
+ created_at: 1466424490000,
+ message_type: 0,
+ });
+ });
+ });
+});
diff --git a/app/javascript/widget/store/modules/specs/conversation/getters.spec.js b/app/javascript/widget/store/modules/specs/conversation/getters.spec.js
new file mode 100644
index 000000000..93d0729a4
--- /dev/null
+++ b/app/javascript/widget/store/modules/specs/conversation/getters.spec.js
@@ -0,0 +1,56 @@
+import { getters } from '../../conversation';
+
+describe('#getters', () => {
+ it('getConversation', () => {
+ const state = {
+ conversations: {
+ 1: {
+ content: 'hello',
+ },
+ },
+ };
+ expect(getters.getConversation(state)).toEqual({
+ 1: {
+ content: 'hello',
+ },
+ });
+ });
+
+ it('getConversationSize', () => {
+ const state = {
+ conversations: {
+ 1: {
+ content: 'hello',
+ },
+ },
+ };
+ expect(getters.getConversationSize(state)).toEqual(1);
+ });
+
+ it('getEarliestMessage', () => {
+ const state = {
+ conversations: {
+ 1: {
+ content: 'hello',
+ },
+ 2: {
+ content: 'hello1',
+ },
+ },
+ };
+ expect(getters.getEarliestMessage(state)).toEqual({
+ content: 'hello',
+ });
+ });
+
+ it('uiFlags', () => {
+ const state = {
+ uiFlags: {
+ allMessagesLoaded: false,
+ isFetchingList: false,
+ },
+ };
+ expect(getters.getAllMessagesLoaded(state)).toEqual(false);
+ expect(getters.getIsFetchingList(state)).toEqual(false);
+ });
+});
diff --git a/app/javascript/widget/store/modules/specs/conversation/mutations.spec.js b/app/javascript/widget/store/modules/specs/conversation/mutations.spec.js
new file mode 100644
index 000000000..b465ac92e
--- /dev/null
+++ b/app/javascript/widget/store/modules/specs/conversation/mutations.spec.js
@@ -0,0 +1,95 @@
+import { mutations } from '../../conversation';
+
+const temporaryMessagePayload = {
+ content: 'hello',
+ id: 1,
+ message_type: 0,
+ status: 'in_progress',
+};
+
+const incomingMessagePayload = {
+ content: 'hello',
+ id: 1,
+ message_type: 0,
+ status: 'sent',
+};
+
+const outgoingMessagePayload = {
+ content: 'hello',
+ id: 1,
+ message_type: 1,
+ status: 'sent',
+};
+
+describe('#mutations', () => {
+ describe('#pushMessageToConversation', () => {
+ it('add message to conversation if outgoing', () => {
+ const state = { conversations: {} };
+ mutations.pushMessageToConversation(state, outgoingMessagePayload);
+ expect(state.conversations).toEqual({
+ 1: outgoingMessagePayload,
+ });
+ });
+
+ it('add message to conversation if message in undelivered', () => {
+ const state = { conversations: {} };
+ mutations.pushMessageToConversation(state, temporaryMessagePayload);
+ expect(state.conversations).toEqual({
+ 1: temporaryMessagePayload,
+ });
+ });
+
+ it('replaces temporary message in conversation with actual message', () => {
+ const state = {
+ conversations: {
+ rand_id_123: {
+ content: 'hello',
+ id: 'rand_id_123',
+ message_type: 0,
+ status: 'in_progress',
+ },
+ },
+ };
+ mutations.pushMessageToConversation(state, incomingMessagePayload);
+ expect(state.conversations).toEqual({
+ 1: incomingMessagePayload,
+ });
+ });
+
+ it('adds message in conversation if it is a new message', () => {
+ const state = { conversations: {} };
+ mutations.pushMessageToConversation(state, incomingMessagePayload);
+ expect(state.conversations).toEqual({
+ 1: incomingMessagePayload,
+ });
+ });
+ });
+
+ describe('#setConversationListLoading', () => {
+ it('set status correctly', () => {
+ const state = { uiFlags: { isFetchingList: false } };
+ mutations.setConversationListLoading(state, true);
+ expect(state.uiFlags.isFetchingList).toEqual(true);
+ });
+ });
+
+ describe('#setMessagesInConversation', () => {
+ it('sets allMessagesLoaded flag if payload is empty', () => {
+ const state = { uiFlags: { allMessagesLoaded: false } };
+ mutations.setMessagesInConversation(state, []);
+ expect(state.uiFlags.allMessagesLoaded).toEqual(true);
+ });
+
+ it('sets messages if payload is not empty', () => {
+ const state = {
+ uiFlags: { allMessagesLoaded: false },
+ conversations: {},
+ };
+ mutations.setMessagesInConversation(state, [{ id: 1, content: 'hello' }]);
+ expect(state.conversations).toEqual({
+ 1: { id: 1, content: 'hello' },
+ });
+ expect(state.uiFlags.allMessagesLoaded).toEqual(false);
+ });
+ });
+});
diff --git a/app/javascript/widget/store/modules/specs/conversation.spec.js b/app/javascript/widget/store/modules/specs/conversation/utils.spec.js
similarity index 96%
rename from app/javascript/widget/store/modules/specs/conversation.spec.js
rename to app/javascript/widget/store/modules/specs/conversation/utils.spec.js
index 03a97618b..f7843de5c 100644
--- a/app/javascript/widget/store/modules/specs/conversation.spec.js
+++ b/app/javascript/widget/store/modules/specs/conversation/utils.spec.js
@@ -1,7 +1,7 @@
import {
findUndeliveredMessage,
createTemporaryMessage,
-} from '../conversation';
+} from '../../conversation';
describe('#findUndeliveredMessage', () => {
it('returns message objects if exist', () => {