diff --git a/app/javascript/dashboard/api/helpCenter/articles.js b/app/javascript/dashboard/api/helpCenter/articles.js new file mode 100644 index 000000000..c252ce4c2 --- /dev/null +++ b/app/javascript/dashboard/api/helpCenter/articles.js @@ -0,0 +1,9 @@ +import ApiClient from '../ApiClient'; + +class ArticlesAPI extends ApiClient { + constructor() { + super('articles', { accountScoped: true }); + } +} + +export default new ArticlesAPI(); diff --git a/app/javascript/dashboard/store/modules/helpCenterArticles/actions.js b/app/javascript/dashboard/store/modules/helpCenterArticles/actions.js new file mode 100644 index 000000000..d484dd40c --- /dev/null +++ b/app/javascript/dashboard/store/modules/helpCenterArticles/actions.js @@ -0,0 +1,83 @@ +import articlesAPI from 'dashboard/api/helpCenter/articles.js'; +import { throwErrorMessage } from 'dashboard/store/utils/api'; +import types from '../../mutation-types'; +export const actions = { + index: async ({ commit }) => { + try { + commit(types.SET_UI_FLAG, { isFetching: true }); + const { data } = await articlesAPI.get(); + const articleIds = data.map(article => article.id); + commit(types.ADD_MANY_ARTICLES, data); + commit(types.ADD_MANY_ARTICLES_ID, articleIds); + return articleIds; + } catch (error) { + return throwErrorMessage(error); + } finally { + commit(types.SET_UI_FLAG, { isFetching: false }); + } + }, + + create: async ({ commit }, params) => { + commit(types.SET_UI_FLAG, { isCreating: true }); + try { + const { data } = await articlesAPI.create(params); + const { id: articleId } = data; + commit(types.ADD_ARTICLE, data); + commit(types.ADD_ARTICLE_ID, articleId); + return articleId; + } catch (error) { + return throwErrorMessage(error); + } finally { + commit(types.SET_UI_FLAG, { isCreating: false }); + } + }, + update: async ({ commit }, params) => { + const articleId = params.id; + commit(types.ADD_ARTICLE_FLAG, { + uiFlags: { + isUpdating: true, + }, + articleId, + }); + try { + const { data } = await articlesAPI.update(params); + + commit(types.UPDATE_ARTICLE, data); + + return articleId; + } catch (error) { + return throwErrorMessage(error); + } finally { + commit(types.ADD_ARTICLE_FLAG, { + uiFlags: { + isUpdating: false, + }, + articleId, + }); + } + }, + delete: async ({ commit }, articleId) => { + commit(types.ADD_ARTICLE_FLAG, { + uiFlags: { + isDeleting: true, + }, + articleId, + }); + try { + await articlesAPI.delete(articleId); + + commit(types.REMOVE_ARTICLE, articleId); + commit(types.REMOVE_ARTICLE_ID, articleId); + return articleId; + } catch (error) { + return throwErrorMessage(error); + } finally { + commit(types.ADD_ARTICLE_FLAG, { + uiFlags: { + isDeleting: false, + }, + articleId, + }); + } + }, +}; diff --git a/app/javascript/dashboard/store/modules/helpCenterArticles/getters.js b/app/javascript/dashboard/store/modules/helpCenterArticles/getters.js new file mode 100644 index 000000000..1cd5d94b0 --- /dev/null +++ b/app/javascript/dashboard/store/modules/helpCenterArticles/getters.js @@ -0,0 +1,23 @@ +export const getters = { + uiFlagsIn: state => helpCenterId => { + const uiFlags = state.articles.uiFlags.byId[helpCenterId]; + if (uiFlags) return uiFlags; + return { isFetching: false, isUpdating: false, isDeleting: false }; + }, + isFetchingHelpCenterArticles: state => state.uiFlags.isFetching, + articleById: (...getterArguments) => articleId => { + const [state] = getterArguments; + const article = state.articles.byId[articleId]; + + if (!article) return undefined; + + return article; + }, + allArticles: (...getterArguments) => { + const [state, _getters] = getterArguments; + const articles = state.articles.allIds.map(id => { + return _getters.articleById(id); + }); + return articles; + }, +}; diff --git a/app/javascript/dashboard/store/modules/helpCenterArticles/index.js b/app/javascript/dashboard/store/modules/helpCenterArticles/index.js new file mode 100755 index 000000000..bbb10f8a2 --- /dev/null +++ b/app/javascript/dashboard/store/modules/helpCenterArticles/index.js @@ -0,0 +1,30 @@ +import { getters } from './getters'; +import { actions } from './actions'; +import { mutations } from './mutations'; + +export const defaultHelpCenterFlags = { + isFetching: false, + isUpdating: false, + isDeleting: false, +}; +const state = { + articles: { + byId: {}, + allIds: [], + uiFlags: { + byId: {}, + }, + }, + uiFlags: { + allFetched: false, + isFetching: false, + }, +}; + +export default { + namespaced: true, + state, + getters, + actions, + mutations, +}; diff --git a/app/javascript/dashboard/store/modules/helpCenterArticles/mutations.js b/app/javascript/dashboard/store/modules/helpCenterArticles/mutations.js new file mode 100644 index 000000000..3657a1e71 --- /dev/null +++ b/app/javascript/dashboard/store/modules/helpCenterArticles/mutations.js @@ -0,0 +1,65 @@ +import types from '../../mutation-types'; +import Vue from 'vue'; + +export const mutations = { + [types.SET_UI_FLAG](_state, uiFlags) { + _state.uiFlags = { + ..._state.uiFlags, + ...uiFlags, + }; + }, + + [types.ADD_ARTICLE]: ($state, article) => { + if (!article.id) return; + + Vue.set($state.articles.byId, article.id, { + ...article, + }); + }, + [types.ADD_MANY_ARTICLES]($state, articles) { + const allArticles = { ...$state.articles.byId }; + articles.forEach(article => { + allArticles[article.id] = article; + }); + Vue.set($state.articles, 'byId', { + allArticles, + }); + }, + [types.ADD_MANY_ARTICLES_ID]($state, articleIds) { + $state.articles.allIds.push(...articleIds); + }, + + [types.ADD_ARTICLE_ID]: ($state, articleId) => { + $state.articles.allIds.push(articleId); + }, + [types.ADD_ARTICLE_FLAG]: ($state, { articleId, uiFlags }) => { + const flags = $state.articles.uiFlags.byId[articleId]; + Vue.set($state.articles.uiFlags.byId, articleId, { + ...{ + isFetching: false, + isUpdating: false, + isDeleting: false, + }, + ...flags, + ...uiFlags, + }); + }, + [types.UPDATE_ARTICLE]($state, article) { + const articleId = article.id; + + if (!$state.articles.allIds.includes(articleId)) return; + + Vue.set($state.articles.byId, articleId, { + ...article, + }); + }, + [types.REMOVE_ARTICLE]($state, articleId) { + const { [articleId]: toBeRemoved, ...newById } = $state.articles.byId; + Vue.set($state.articles, 'byId', newById); + }, + [types.REMOVE_ARTICLE_ID]($state, articleId) { + $state.articles.allIds = $state.articles.allIds.filter( + id => id !== articleId + ); + }, +}; diff --git a/app/javascript/dashboard/store/modules/helpCenterArticles/specs/action.spec.js b/app/javascript/dashboard/store/modules/helpCenterArticles/specs/action.spec.js new file mode 100644 index 000000000..c2ff2c74b --- /dev/null +++ b/app/javascript/dashboard/store/modules/helpCenterArticles/specs/action.spec.js @@ -0,0 +1,137 @@ +import axios from 'axios'; +import { actions } from '../actions'; +import * as types from '../../../mutation-types'; +const articleList = [ + { + id: 1, + category_id: 1, + title: 'Documents are required to complete KYC', + }, +]; +const commit = jest.fn(); +global.axios = axios; +jest.mock('axios'); + +describe('#actions', () => { + describe('#index', () => { + it('sends correct actions if API is success', async () => { + axios.get.mockResolvedValue({ data: articleList }); + await actions.index({ commit }); + expect(commit.mock.calls).toEqual([ + [types.default.SET_UI_FLAG, { isFetching: true }], + [ + types.default.ADD_MANY_ARTICLES, + [ + { + id: 1, + category_id: 1, + title: 'Documents are required to complete KYC', + }, + ], + ], + [types.default.ADD_MANY_ARTICLES_ID, [1]], + [types.default.SET_UI_FLAG, { isFetching: false }], + ]); + }); + it('sends correct actions if API is error', async () => { + axios.get.mockRejectedValue({ message: 'Incorrect header' }); + await expect(actions.index({ commit })).rejects.toThrow(Error); + expect(commit.mock.calls).toEqual([ + [types.default.SET_UI_FLAG, { isFetching: true }], + [types.default.SET_UI_FLAG, { isFetching: false }], + ]); + }); + }); + + describe('#create', () => { + it('sends correct actions if API is success', async () => { + axios.post.mockResolvedValue({ data: articleList[0] }); + await actions.create({ commit }, articleList[0]); + expect(commit.mock.calls).toEqual([ + [types.default.SET_UI_FLAG, { isCreating: true }], + [types.default.ADD_ARTICLE, articleList[0]], + [types.default.ADD_ARTICLE_ID, 1], + [types.default.SET_UI_FLAG, { isCreating: false }], + ]); + }); + it('sends correct actions if API is error', async () => { + axios.post.mockRejectedValue({ message: 'Incorrect header' }); + await expect(actions.create({ commit }, articleList[0])).rejects.toThrow( + Error + ); + expect(commit.mock.calls).toEqual([ + [types.default.SET_UI_FLAG, { isCreating: true }], + [types.default.SET_UI_FLAG, { isCreating: false }], + ]); + }); + }); + + describe('#update', () => { + it('sends correct actions if API is success', async () => { + axios.patch.mockResolvedValue({ data: articleList[0] }); + await actions.update({ commit }, articleList[0]); + expect(commit.mock.calls).toEqual([ + [ + types.default.ADD_ARTICLE_FLAG, + { uiFlags: { isUpdating: true }, articleId: 1 }, + ], + [types.default.UPDATE_ARTICLE, articleList[0]], + [ + types.default.ADD_ARTICLE_FLAG, + { uiFlags: { isUpdating: false }, articleId: 1 }, + ], + ]); + }); + it('sends correct actions if API is error', async () => { + axios.patch.mockRejectedValue({ message: 'Incorrect header' }); + await expect(actions.update({ commit }, articleList[0])).rejects.toThrow( + Error + ); + expect(commit.mock.calls).toEqual([ + [ + types.default.ADD_ARTICLE_FLAG, + { uiFlags: { isUpdating: true }, articleId: 1 }, + ], + [ + types.default.ADD_ARTICLE_FLAG, + { uiFlags: { isUpdating: false }, articleId: 1 }, + ], + ]); + }); + }); + + describe('#delete', () => { + it('sends correct actions if API is success', async () => { + axios.delete.mockResolvedValue({ data: articleList[0] }); + await actions.delete({ commit }, articleList[0].id); + expect(commit.mock.calls).toEqual([ + [ + types.default.ADD_ARTICLE_FLAG, + { uiFlags: { isDeleting: true }, articleId: 1 }, + ], + [types.default.REMOVE_ARTICLE, articleList[0].id], + [types.default.REMOVE_ARTICLE_ID, articleList[0].id], + [ + types.default.ADD_ARTICLE_FLAG, + { uiFlags: { isDeleting: false }, articleId: 1 }, + ], + ]); + }); + it('sends correct actions if API is error', async () => { + axios.delete.mockRejectedValue({ message: 'Incorrect header' }); + await expect( + actions.delete({ commit }, articleList[0].id) + ).rejects.toThrow(Error); + expect(commit.mock.calls).toEqual([ + [ + types.default.ADD_ARTICLE_FLAG, + { uiFlags: { isDeleting: true }, articleId: 1 }, + ], + [ + types.default.ADD_ARTICLE_FLAG, + { uiFlags: { isDeleting: false }, articleId: 1 }, + ], + ]); + }); + }); +}); diff --git a/app/javascript/dashboard/store/modules/helpCenterArticles/specs/fixtures.js b/app/javascript/dashboard/store/modules/helpCenterArticles/specs/fixtures.js new file mode 100644 index 000000000..326f3c2fb --- /dev/null +++ b/app/javascript/dashboard/store/modules/helpCenterArticles/specs/fixtures.js @@ -0,0 +1,53 @@ +export default { + articles: { + byId: { + 1: { + id: 1, + category_id: 1, + title: 'Documents are required to complete KYC', + content: + 'The submission of the following documents is mandatory to complete registration, ID proof - PAN Card, Address proof', + description: 'Documents are required to complete KYC', + status: 'draft', + account_id: 1, + views: 122, + author: { + id: 5, + account_id: 1, + email: 'tom@furrent.com', + available_name: 'Tom', + name: 'Tom Jose', + }, + }, + 2: { + id: 2, + category_id: 1, + title: + 'How do I change my registered email address and/or phone number?', + content: + 'Kindly login to your Furrent account to chat with us or submit a request and we would be glad to help you update the contact details on your account.', + description: 'Change my registered email address and/or phone number', + status: 'draft', + account_id: 1, + views: 121, + author: { + id: 5, + account_id: 1, + email: 'tom@furrent.com', + available_name: 'Tom', + name: 'Tom Jose', + }, + }, + }, + allIds: [1, 2], + uiFlags: { + byId: { + 1: { isFetching: false, isUpdating: true, isDeleting: false }, + }, + }, + }, + uiFlags: { + allFetched: false, + isFetching: true, + }, +}; diff --git a/app/javascript/dashboard/store/modules/helpCenterArticles/specs/getters.spec.js b/app/javascript/dashboard/store/modules/helpCenterArticles/specs/getters.spec.js new file mode 100644 index 000000000..ab271d3a0 --- /dev/null +++ b/app/javascript/dashboard/store/modules/helpCenterArticles/specs/getters.spec.js @@ -0,0 +1,40 @@ +import { getters } from '../getters'; +import articles from './fixtures'; +describe('#getters', () => { + let state = {}; + beforeEach(() => { + state = articles; + }); + it('uiFlagsIn', () => { + expect(getters.uiFlagsIn(state)(1)).toEqual({ + isFetching: false, + isUpdating: true, + isDeleting: false, + }); + }); + + it('articleById', () => { + expect(getters.articleById(state)(1)).toEqual({ + id: 1, + category_id: 1, + title: 'Documents are required to complete KYC', + content: + 'The submission of the following documents is mandatory to complete registration, ID proof - PAN Card, Address proof', + description: 'Documents are required to complete KYC', + status: 'draft', + account_id: 1, + views: 122, + author: { + id: 5, + account_id: 1, + email: 'tom@furrent.com', + available_name: 'Tom', + name: 'Tom Jose', + }, + }); + }); + + it('isFetchingHelpCenters', () => { + expect(getters.isFetchingHelpCenterArticles(state)).toEqual(true); + }); +}); diff --git a/app/javascript/dashboard/store/modules/helpCenterArticles/specs/mutation.spec.js b/app/javascript/dashboard/store/modules/helpCenterArticles/specs/mutation.spec.js new file mode 100644 index 000000000..ce3b6b33a --- /dev/null +++ b/app/javascript/dashboard/store/modules/helpCenterArticles/specs/mutation.spec.js @@ -0,0 +1,90 @@ +import { mutations } from '../mutations'; +import article from './fixtures'; +import types from '../../../mutation-types'; + +describe('#mutations', () => { + let state = {}; + beforeEach(() => { + state = article; + }); + + describe('#SET_UI_FLAG', () => { + it('It returns default flags if empty object passed', () => { + mutations[types.SET_UI_FLAG](state, {}); + expect(state.uiFlags).toEqual({ + allFetched: false, + isFetching: true, + }); + }); + it('Update flags when flag passed as parameters', () => { + mutations[types.SET_UI_FLAG](state, { isFetching: true }); + expect(state.uiFlags).toEqual({ + allFetched: false, + isFetching: true, + }); + }); + }); + + describe('#ADD_ARTICLE', () => { + it('add valid article to state', () => { + mutations[types.ADD_ARTICLE](state, { + id: 3, + category_id: 1, + title: + 'How do I change my registered email address and/or phone number?', + }); + expect(state.articles.byId[3]).toEqual({ + id: 3, + category_id: 1, + title: + 'How do I change my registered email address and/or phone number?', + }); + }); + it('does not add article with empty data passed', () => { + mutations[types.ADD_ARTICLE](state, {}); + expect(state).toEqual(article); + }); + }); + + describe('#ADD_ARTICLE_ID', () => { + it('add valid article id to state', () => { + mutations[types.ADD_ARTICLE_ID](state, 3); + expect(state.articles.allIds).toEqual([1, 2, 3]); + }); + it('Does not invalid article with empty data passed', () => { + mutations[types.ADD_ARTICLE_ID](state, {}); + expect(state).toEqual(article); + }); + }); + + describe('#UPDATE_ARTICLE', () => { + it('does not updates if empty object is passed', () => { + mutations[types.UPDATE_ARTICLE](state, {}); + expect(state).toEqual(article); + }); + it('does not updates if object id is not present ', () => { + mutations[types.UPDATE_ARTICLE](state, { id: 5 }); + expect(state).toEqual(article); + }); + it(' updates if object with id already present in the state', () => { + mutations[types.UPDATE_ARTICLE](state, { + id: 2, + title: 'How do I change my registered email address', + }); + expect(state.articles.byId[2].title).toEqual( + 'How do I change my registered email address' + ); + }); + }); + + describe('#REMOVE_ARTICLE', () => { + it('does not remove object entry if no id is passed', () => { + mutations[types.REMOVE_ARTICLE](state, undefined); + expect(state).toEqual({ ...article }); + }); + it('removes article if valid article id passed', () => { + mutations[types.REMOVE_ARTICLE](state, 2); + expect(state.articles.byId[2]).toEqual(undefined); + }); + }); +}); diff --git a/app/javascript/dashboard/store/mutation-types.js b/app/javascript/dashboard/store/mutation-types.js index 40a44c066..034190507 100755 --- a/app/javascript/dashboard/store/mutation-types.js +++ b/app/javascript/dashboard/store/mutation-types.js @@ -220,4 +220,15 @@ export default { CREATE_DASHBOARD_APP: 'CREATE_DASHBOARD_APP', EDIT_DASHBOARD_APP: 'EDIT_DASHBOARD_APP', DELETE_DASHBOARD_APP: 'DELETE_DASHBOARD_APP', + + // Help center + ADD_ARTICLE: 'ADD_ARTICLE', + ADD_ARTICLE_ID: 'ADD_ARTICLE_ID', + ADD_MANY_ARTICLES: 'ADD_MANY_ARTICLES', + ADD_MANY_ARTICLES_ID: 'ADD_MANY_ARTICLES_ID', + ADD_ARTICLE_FLAG: 'ADD_ARTICLE_FLAG', + UPDATE_ARTICLE: 'UPDATE_ARTICLE', + REMOVE_ARTICLE: 'REMOVE_ARTICLE', + REMOVE_ARTICLE_ID: 'REMOVE_ARTICLE_ID', + SET_UI_FLAG: 'SET_UI_FLAG', };