From bfca4852c82680ccf589b18910025caf4bcf33f6 Mon Sep 17 00:00:00 2001 From: Nithin David Thomas <1277421+nithindavid@users.noreply.github.com> Date: Wed, 27 Jul 2022 12:24:43 +0530 Subject: [PATCH] feat: Create store to manage Portals (#5020) Co-authored-by: Muhsin Keloth Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Co-authored-by: Pranav Raj S --- .../dashboard/api/helpCenter/portals.js | 9 ++ .../modules/helpCenterPortals/actions.js | 71 ++++++++++ .../modules/helpCenterPortals/getters.js | 26 ++++ .../store/modules/helpCenterPortals/index.js | 36 +++++ .../modules/helpCenterPortals/mutations.js | 76 +++++++++++ .../helpCenterPortals/specs/actions.spec.js | 126 ++++++++++++++++++ .../helpCenterPortals/specs/fixtures.js | 75 +++++++++++ .../helpCenterPortals/specs/getters.spec.js | 45 +++++++ .../helpCenterPortals/specs/mutations.spec.js | 95 +++++++++++++ 9 files changed, 559 insertions(+) create mode 100644 app/javascript/dashboard/api/helpCenter/portals.js create mode 100644 app/javascript/dashboard/store/modules/helpCenterPortals/actions.js create mode 100644 app/javascript/dashboard/store/modules/helpCenterPortals/getters.js create mode 100755 app/javascript/dashboard/store/modules/helpCenterPortals/index.js create mode 100644 app/javascript/dashboard/store/modules/helpCenterPortals/mutations.js create mode 100644 app/javascript/dashboard/store/modules/helpCenterPortals/specs/actions.spec.js create mode 100644 app/javascript/dashboard/store/modules/helpCenterPortals/specs/fixtures.js create mode 100644 app/javascript/dashboard/store/modules/helpCenterPortals/specs/getters.spec.js create mode 100644 app/javascript/dashboard/store/modules/helpCenterPortals/specs/mutations.spec.js diff --git a/app/javascript/dashboard/api/helpCenter/portals.js b/app/javascript/dashboard/api/helpCenter/portals.js new file mode 100644 index 000000000..8b8e8f797 --- /dev/null +++ b/app/javascript/dashboard/api/helpCenter/portals.js @@ -0,0 +1,9 @@ +import ApiClient from '../ApiClient'; + +class PortalsAPI extends ApiClient { + constructor() { + super('portals', { accountScoped: true }); + } +} + +export default new PortalsAPI(); diff --git a/app/javascript/dashboard/store/modules/helpCenterPortals/actions.js b/app/javascript/dashboard/store/modules/helpCenterPortals/actions.js new file mode 100644 index 000000000..04fffea95 --- /dev/null +++ b/app/javascript/dashboard/store/modules/helpCenterPortals/actions.js @@ -0,0 +1,71 @@ +import PortalsAPI from 'dashboard/api/helpCenter/portals.js'; +import { throwErrorMessage } from 'dashboard/store/utils/api'; +import { types } from './mutations'; + +export const actions = { + index: async ({ commit }) => { + try { + commit(types.SET_UI_FLAG, { isFetching: true }); + const { data } = await PortalsAPI.get(); + const portalIds = data.map(portal => portal.id); + commit(types.ADD_MANY_PORTALS_ENTRY, data); + commit(types.ADD_MANY_PORTALS_IDS, portalIds); + } catch (error) { + 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 PortalsAPI.create(params); + const { id: portalId } = data; + commit(types.ADD_PORTAL_ENTRY, data); + commit(types.ADD_PORTAL_ID, portalId); + } catch (error) { + throwErrorMessage(error); + } finally { + commit(types.SET_UI_FLAG, { isCreating: false }); + } + }, + + update: async ({ commit }, params) => { + const portalId = params.id; + commit(types.SET_HELP_PORTAL_UI_FLAG, { + uiFlags: { isUpdating: true }, + portalId, + }); + try { + const { data } = await PortalsAPI.update(params); + commit(types.UPDATE_PORTAL_ENTRY, data); + } catch (error) { + throwErrorMessage(error); + } finally { + commit(types.SET_HELP_PORTAL_UI_FLAG, { + uiFlags: { isUpdating: false }, + portalId, + }); + } + }, + + delete: async ({ commit }, portalId) => { + commit(types.SET_HELP_PORTAL_UI_FLAG, { + uiFlags: { isDeleting: true }, + portalId, + }); + try { + await PortalsAPI.delete(portalId); + commit(types.REMOVE_PORTAL_ENTRY, portalId); + commit(types.REMOVE_PORTAL_ID, portalId); + } catch (error) { + throwErrorMessage(error); + } finally { + commit(types.SET_HELP_PORTAL_UI_FLAG, { + uiFlags: { isDeleting: false }, + portalId, + }); + } + }, +}; diff --git a/app/javascript/dashboard/store/modules/helpCenterPortals/getters.js b/app/javascript/dashboard/store/modules/helpCenterPortals/getters.js new file mode 100644 index 000000000..a874d3740 --- /dev/null +++ b/app/javascript/dashboard/store/modules/helpCenterPortals/getters.js @@ -0,0 +1,26 @@ +export const getters = { + uiFlagsIn: state => portalId => { + const uiFlags = state.portals.uiFlags.byId[portalId]; + if (uiFlags) return uiFlags; + return { isFetching: false, isUpdating: false, isDeleting: false }; + }, + + isFetchingPortals: state => state.uiFlags.isFetching, + portalById: (...getterArguments) => portalId => { + const [state] = getterArguments; + const portal = state.portals.byId[portalId]; + + return { + ...portal, + }; + }, + allPortals: (...getterArguments) => { + const [state, _getters] = getterArguments; + + const portals = state.portals.allIds.map(id => { + return _getters.portalById(id); + }); + return portals; + }, + count: state => state.portals.allIds.length || 0, +}; diff --git a/app/javascript/dashboard/store/modules/helpCenterPortals/index.js b/app/javascript/dashboard/store/modules/helpCenterPortals/index.js new file mode 100755 index 000000000..8da656717 --- /dev/null +++ b/app/javascript/dashboard/store/modules/helpCenterPortals/index.js @@ -0,0 +1,36 @@ +import { getters } from './getters'; +import { actions } from './actions'; +import { mutations } from './mutations'; + +export const defaultPortalFlags = { + isFetching: false, + isUpdating: false, + isDeleting: false, +}; + +const state = { + portals: { + byId: {}, + allIds: [], + uiFlags: { + byId: { + // 1: { isFetching: false, isUpdating: false, isDeleting: false }, + }, + }, + meta: { + byId: {}, + }, + }, + uiFlags: { + allFetched: false, + isFetching: false, + }, +}; + +export default { + namespaced: true, + state, + getters, + actions, + mutations, +}; diff --git a/app/javascript/dashboard/store/modules/helpCenterPortals/mutations.js b/app/javascript/dashboard/store/modules/helpCenterPortals/mutations.js new file mode 100644 index 000000000..76099f5c5 --- /dev/null +++ b/app/javascript/dashboard/store/modules/helpCenterPortals/mutations.js @@ -0,0 +1,76 @@ +import Vue from 'vue'; +import { defaultPortalFlags } from './index'; + +export const types = { + SET_UI_FLAG: 'setUIFlag', + ADD_PORTAL_ENTRY: 'addPortalEntry', + ADD_MANY_PORTALS_ENTRY: 'addManyPortalsEntry', + ADD_PORTAL_ID: 'addPortalId', + ADD_MANY_PORTALS_IDS: 'addManyPortalsIds', + UPDATE_PORTAL_ENTRY: 'updatePortalEntry', + REMOVE_PORTAL_ENTRY: 'removePortalEntry', + REMOVE_PORTAL_ID: 'removePortalId', + SET_HELP_PORTAL_UI_FLAG: 'setHelpCenterUIFlag', +}; + +export const mutations = { + [types.SET_UI_FLAG]($state, uiFlags) { + $state.uiFlags = { + ...$state.uiFlags, + ...uiFlags, + }; + }, + + [types.ADD_PORTAL_ENTRY]($state, portal) { + Vue.set($state.portals.byId, portal.id, { + ...portal, + }); + }, + + [types.ADD_MANY_PORTALS_ENTRY]($state, portals) { + const allPortals = { ...$state.portals.byId }; + portals.forEach(portal => { + allPortals[portal.id] = portal; + }); + Vue.set($state.portals, 'byId', { + allPortals, + }); + }, + + [types.ADD_PORTAL_ID]($state, portalId) { + $state.portals.allIds.push(portalId); + }, + + [types.ADD_MANY_PORTALS_IDS]($state, portalIds) { + $state.portals.allIds.push(...portalIds); + }, + + [types.UPDATE_PORTAL_ENTRY]($state, portal) { + const portalId = portal.id; + if (!$state.portals.allIds.includes(portalId)) return; + + Vue.set($state.portals.byId, portalId, { + ...portal, + }); + }, + + [types.REMOVE_PORTAL_ENTRY]($state, portalId) { + if (!portalId) return; + + const { [portalId]: toBeRemoved, ...newById } = $state.portals.byId; + Vue.set($state.portals, 'byId', newById); + }, + + [types.REMOVE_PORTAL_ID]($state, portalId) { + $state.portals.allIds = $state.portals.allIds.filter(id => id !== portalId); + }, + + [types.SET_HELP_PORTAL_UI_FLAG]($state, { portalId, uiFlags }) { + const flags = $state.portals.uiFlags.byId[portalId]; + Vue.set($state.portals.uiFlags.byId, portalId, { + ...defaultPortalFlags, + ...flags, + ...uiFlags, + }); + }, +}; diff --git a/app/javascript/dashboard/store/modules/helpCenterPortals/specs/actions.spec.js b/app/javascript/dashboard/store/modules/helpCenterPortals/specs/actions.spec.js new file mode 100644 index 000000000..40735c0da --- /dev/null +++ b/app/javascript/dashboard/store/modules/helpCenterPortals/specs/actions.spec.js @@ -0,0 +1,126 @@ +import axios from 'axios'; +import { actions } from '../actions'; +import { types } from '../mutations'; +import { apiResponse } from './fixtures'; + +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: apiResponse }); + await actions.index({ commit }); + expect(commit.mock.calls).toEqual([ + [types.SET_UI_FLAG, { isFetching: true }], + [types.ADD_MANY_PORTALS_ENTRY, apiResponse], + [types.ADD_MANY_PORTALS_IDS, [1, 2]], + [types.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.SET_UI_FLAG, { isFetching: true }], + [types.SET_UI_FLAG, { isFetching: false }], + ]); + }); + }); + + describe('#create', () => { + it('sends correct actions if API is success', async () => { + axios.post.mockResolvedValue({ data: apiResponse[1] }); + await actions.create( + { commit }, + { + color: 'red', + custom_domain: 'domain_for_help', + header_text: 'Domain Header', + } + ); + expect(commit.mock.calls).toEqual([ + [types.SET_UI_FLAG, { isCreating: true }], + [types.ADD_PORTAL_ENTRY, apiResponse[1]], + [types.ADD_PORTAL_ID, 2], + [types.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 }, {})).rejects.toThrow(Error); + expect(commit.mock.calls).toEqual([ + [types.SET_UI_FLAG, { isCreating: true }], + [types.SET_UI_FLAG, { isCreating: false }], + ]); + }); + }); + + describe('#update', () => { + it('sends correct actions if API is success', async () => { + axios.patch.mockResolvedValue({ data: apiResponse[1] }); + await actions.update({ commit }, apiResponse[1]); + expect(commit.mock.calls).toEqual([ + [ + types.SET_HELP_PORTAL_UI_FLAG, + { uiFlags: { isUpdating: true }, portalId: 2 }, + ], + [types.UPDATE_PORTAL_ENTRY, apiResponse[1]], + [ + types.SET_HELP_PORTAL_UI_FLAG, + { uiFlags: { isUpdating: false }, portalId: 2 }, + ], + ]); + }); + it('sends correct actions if API is error', async () => { + axios.patch.mockRejectedValue({ message: 'Incorrect header' }); + await expect(actions.update({ commit }, apiResponse[1])).rejects.toThrow( + Error + ); + expect(commit.mock.calls).toEqual([ + [ + types.SET_HELP_PORTAL_UI_FLAG, + { uiFlags: { isUpdating: true }, portalId: 2 }, + ], + [ + types.SET_HELP_PORTAL_UI_FLAG, + { uiFlags: { isUpdating: false }, portalId: 2 }, + ], + ]); + }); + }); + + describe('#delete', () => { + it('sends correct actions if API is success', async () => { + axios.delete.mockResolvedValue({}); + await actions.delete({ commit }, 2); + expect(commit.mock.calls).toEqual([ + [ + types.SET_HELP_PORTAL_UI_FLAG, + { uiFlags: { isDeleting: true }, portalId: 2 }, + ], + [types.REMOVE_PORTAL_ENTRY, 2], + [types.REMOVE_PORTAL_ID, 2], + [ + types.SET_HELP_PORTAL_UI_FLAG, + { uiFlags: { isDeleting: false }, portalId: 2 }, + ], + ]); + }); + it('sends correct actions if API is error', async () => { + axios.delete.mockRejectedValue({ message: 'Incorrect header' }); + await expect(actions.delete({ commit }, 2)).rejects.toThrow(Error); + expect(commit.mock.calls).toEqual([ + [ + types.SET_HELP_PORTAL_UI_FLAG, + { uiFlags: { isDeleting: true }, portalId: 2 }, + ], + [ + types.SET_HELP_PORTAL_UI_FLAG, + { uiFlags: { isDeleting: false }, portalId: 2 }, + ], + ]); + }); + }); +}); diff --git a/app/javascript/dashboard/store/modules/helpCenterPortals/specs/fixtures.js b/app/javascript/dashboard/store/modules/helpCenterPortals/specs/fixtures.js new file mode 100644 index 000000000..5b7085ef7 --- /dev/null +++ b/app/javascript/dashboard/store/modules/helpCenterPortals/specs/fixtures.js @@ -0,0 +1,75 @@ +export default { + portals: { + byId: { + 1: { + id: 1, + color: 'red', + custom_domain: 'domain_for_help', + header_text: 'Domain Header', + homepage_link: 'help-center', + name: 'help name', + page_title: 'page title', + slug: 'domain', + archived: false, + config: { + allowed_locales: ['en'], + }, + }, + 2: { + id: 2, + color: 'green', + custom_domain: 'campaign_for_help', + header_text: 'Campaign Header', + homepage_link: 'help-center', + name: 'help name', + page_title: 'campaign title', + slug: 'campaign', + archived: false, + config: { + allowed_locales: ['en'], + }, + }, + }, + allIds: [1, 2], + uiFlags: { + byId: { + 1: { isFetching: false, isUpdating: true, isDeleting: false }, + }, + }, + }, + uiFlags: { + allFetched: false, + isFetching: true, + }, +}; + +export const apiResponse = [ + { + id: 1, + color: 'red', + custom_domain: 'domain_for_help', + header_text: 'Domain Header', + homepage_link: 'help-center', + name: 'help name', + page_title: 'page title', + slug: 'domain', + archived: false, + config: { + allowed_locales: ['en'], + }, + }, + { + id: 2, + color: 'green', + custom_domain: 'campaign_for_help', + header_text: 'Campaign Header', + homepage_link: 'help-center', + name: 'help name', + page_title: 'campaign title', + slug: 'campaign', + archived: false, + config: { + allowed_locales: ['en'], + }, + }, +]; diff --git a/app/javascript/dashboard/store/modules/helpCenterPortals/specs/getters.spec.js b/app/javascript/dashboard/store/modules/helpCenterPortals/specs/getters.spec.js new file mode 100644 index 000000000..eeb82d649 --- /dev/null +++ b/app/javascript/dashboard/store/modules/helpCenterPortals/specs/getters.spec.js @@ -0,0 +1,45 @@ +import { getters } from '../getters'; +import portal from './fixtures'; + +describe('#getters', () => { + it('getUIFlagsIn', () => { + const state = portal; + expect(getters.uiFlagsIn(state)(1)).toEqual({ + isFetching: false, + isUpdating: true, + isDeleting: false, + }); + }); + + it('isFetchingPortals', () => { + const state = portal; + expect(getters.isFetchingPortals(state)).toEqual(true); + }); + + it('portalById', () => { + const state = portal; + expect(getters.portalById(state)(1)).toEqual({ + id: 1, + color: 'red', + custom_domain: 'domain_for_help', + header_text: 'Domain Header', + homepage_link: 'help-center', + name: 'help name', + page_title: 'page title', + slug: 'domain', + archived: false, + config: { + allowed_locales: ['en'], + }, + }); + }); + + it('allPortals', () => { + const state = portal; + expect(getters.allPortals(state, getters).length).toEqual(2); + }); + it('count', () => { + const state = portal; + expect(getters.count(state)).toEqual(2); + }); +}); diff --git a/app/javascript/dashboard/store/modules/helpCenterPortals/specs/mutations.spec.js b/app/javascript/dashboard/store/modules/helpCenterPortals/specs/mutations.spec.js new file mode 100644 index 000000000..dd08b6362 --- /dev/null +++ b/app/javascript/dashboard/store/modules/helpCenterPortals/specs/mutations.spec.js @@ -0,0 +1,95 @@ +import { mutations, types } from '../mutations'; +import portal from './fixtures'; + +describe('#mutations', () => { + let state = {}; + beforeEach(() => { + state = { ...portal }; + }); + + 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('It updates keys when passed as parameters', () => { + mutations[types.SET_UI_FLAG](state, { isFetching: false }); + expect(state.uiFlags).toEqual({ + allFetched: false, + isFetching: false, + }); + }); + }); + + describe('[types.ADD_PORTAL_ENTRY]', () => { + it('does not add empty objects to state', () => { + mutations[types.ADD_PORTAL_ENTRY](state, {}); + expect(state).toEqual(portal); + }); + it('does adds helpcenter object to state', () => { + mutations[types.ADD_PORTAL_ENTRY](state, { id: 3 }); + expect(state.portals.byId[3]).toEqual({ id: 3 }); + }); + }); + + describe('[types.ADD_PORTAL_ID]', () => { + it('adds helpcenter id to state', () => { + mutations[types.ADD_PORTAL_ID](state, 12); + expect(state.portals.allIds).toEqual([1, 2, 12]); + }); + }); + + describe('[types.UPDATE_PORTAL_ENTRY]', () => { + it('does not updates if empty object is passed', () => { + mutations[types.UPDATE_PORTAL_ENTRY](state, {}); + expect(state).toEqual(portal); + }); + it('does not updates if object id is not present ', () => { + mutations[types.UPDATE_PORTAL_ENTRY](state, { id: 5 }); + expect(state).toEqual(portal); + }); + it(' updates if object with id already present in the state', () => { + mutations[types.UPDATE_PORTAL_ENTRY](state, { + id: 2, + name: 'Updated name', + }); + expect(state.portals.byId[2].name).toEqual('Updated name'); + }); + }); + + describe('[types.REMOVE_PORTAL_ENTRY]', () => { + it('does not remove object entry if no id is passed', () => { + mutations[types.REMOVE_PORTAL_ENTRY](state, undefined); + expect(state).toEqual({ ...portal }); + }); + it('removes object entry with to conversation if outgoing', () => { + mutations[types.REMOVE_PORTAL_ENTRY](state, 2); + expect(state.portals.byId[2]).toEqual(undefined); + }); + }); + + describe('[types.REMOVE_PORTAL_ID]', () => { + it('removes id from state', () => { + mutations[types.REMOVE_PORTAL_ID](state, 2); + expect(state.portals.allIds).toEqual([1, 12]); + }); + }); + + describe('[types.SET_HELP_PORTAL_UI_FLAG]', () => { + it('sets correct flag in state', () => { + mutations[types.SET_HELP_PORTAL_UI_FLAG](state, { + portalId: 1, + uiFlags: { isFetching: true }, + }); + expect(state.portals.uiFlags.byId[1]).toEqual({ + isFetching: true, + isUpdating: true, + isDeleting: false, + }); + }); + }); +});