From ebea5428bcf7995bfc5258c6be79899fcfc1407e Mon Sep 17 00:00:00 2001 From: Nithin David Thomas <1277421+nithindavid@users.noreply.github.com> Date: Wed, 31 Aug 2022 19:59:05 +0530 Subject: [PATCH 1/3] chore: Moves portal slug as ID to query resources in vue store (#5359) --- .../helpcenter/components/AddCategory.vue | 6 +-- .../components/HelpCenterLayout.vue | 29 +++++++--- .../helpcenter/components/PortalListItem.vue | 8 ++- .../helpcenter/components/PortalPopover.vue | 11 ++-- .../helpcenter/pages/articles/EditArticle.vue | 3 +- .../pages/articles/ListAllArticles.vue | 3 +- .../helpcenter/pages/articles/NewArticle.vue | 3 +- .../pages/portals/PortalCustomization.vue | 2 +- .../modules/helpCenterPortals/actions.js | 54 ++++++++----------- .../modules/helpCenterPortals/getters.js | 13 ++--- .../store/modules/helpCenterPortals/index.js | 1 - .../modules/helpCenterPortals/mutations.js | 43 +++++++-------- .../helpCenterPortals/specs/actions.spec.js | 53 ++++++++---------- .../helpCenterPortals/specs/fixtures.js | 1 - .../helpCenterPortals/specs/getters.spec.js | 4 +- .../helpCenterPortals/specs/mutations.spec.js | 35 +++++------- 16 files changed, 121 insertions(+), 148 deletions(-) diff --git a/app/javascript/dashboard/routes/dashboard/helpcenter/components/AddCategory.vue b/app/javascript/dashboard/routes/dashboard/helpcenter/components/AddCategory.vue index fc56a684f..c3f053559 100644 --- a/app/javascript/dashboard/routes/dashboard/helpcenter/components/AddCategory.vue +++ b/app/javascript/dashboard/routes/dashboard/helpcenter/components/AddCategory.vue @@ -67,7 +67,6 @@ diff --git a/app/javascript/dashboard/routes/dashboard/helpcenter/components/PortalPopover.vue b/app/javascript/dashboard/routes/dashboard/helpcenter/components/PortalPopover.vue index 0e74f403a..539c15e7d 100644 --- a/app/javascript/dashboard/routes/dashboard/helpcenter/components/PortalPopover.vue +++ b/app/javascript/dashboard/routes/dashboard/helpcenter/components/PortalPopover.vue @@ -24,7 +24,7 @@ v-for="portal in portals" :key="portal.id" :portal="portal" - :active="portal.id === activePortal.id" + :active="portal.slug === activePortalSlug" @open-portal-page="openPortalPage" /> @@ -32,7 +32,7 @@ {{ $t('HELP_CENTER.PORTAL.POPOVER.CANCEL_BUTTON_LABEL') }} - + {{ $t('HELP_CENTER.PORTAL.POPOVER.CHOOSE_LOCALE_BUTTON') }} @@ -52,11 +52,12 @@ export default { type: Array, default: () => [], }, - activePortal: { - type: Object, - default: () => ({}), + activePortalSlug: { + type: String, + default: '', }, }, + methods: { closePortalPopover() { this.$emit('close-popover'); diff --git a/app/javascript/dashboard/routes/dashboard/helpcenter/pages/articles/EditArticle.vue b/app/javascript/dashboard/routes/dashboard/helpcenter/pages/articles/EditArticle.vue index 0aefc7902..f89e8b304 100644 --- a/app/javascript/dashboard/routes/dashboard/helpcenter/pages/articles/EditArticle.vue +++ b/app/javascript/dashboard/routes/dashboard/helpcenter/pages/articles/EditArticle.vue @@ -58,7 +58,6 @@ export default { ...mapGetters({ isFetching: 'articles/isFetching', articles: 'articles/articles', - selectedPortal: 'portals/getSelectedPortal', }), article() { return this.$store.getters['articles/articleById'](this.articleId); @@ -67,7 +66,7 @@ export default { return this.$route.params.articleSlug; }, selectedPortalSlug() { - return this.portalSlug || this.selectedPortal?.slug; + return this.$route.params.portalSlug; }, }, mounted() { diff --git a/app/javascript/dashboard/routes/dashboard/helpcenter/pages/articles/ListAllArticles.vue b/app/javascript/dashboard/routes/dashboard/helpcenter/pages/articles/ListAllArticles.vue index fcb25546b..73ccda403 100644 --- a/app/javascript/dashboard/routes/dashboard/helpcenter/pages/articles/ListAllArticles.vue +++ b/app/javascript/dashboard/routes/dashboard/helpcenter/pages/articles/ListAllArticles.vue @@ -46,7 +46,6 @@ export default { ...mapGetters({ articles: 'articles/allArticles', categories: 'categories/allCategories', - selectedPortal: 'portals/getSelectedPortal', uiFlags: 'articles/uiFlags', meta: 'articles/getMeta', isFetching: 'articles/isFetching', @@ -64,7 +63,7 @@ export default { return this.isFetching && !this.articles.length; }, selectedPortalSlug() { - return this.selectedPortal?.slug; + return this.$route.params.portalSlug; }, selectedCategorySlug() { const { categorySlug } = this.$route.params; diff --git a/app/javascript/dashboard/routes/dashboard/helpcenter/pages/articles/NewArticle.vue b/app/javascript/dashboard/routes/dashboard/helpcenter/pages/articles/NewArticle.vue index 778dc38cc..bc8ad4fab 100644 --- a/app/javascript/dashboard/routes/dashboard/helpcenter/pages/articles/NewArticle.vue +++ b/app/javascript/dashboard/routes/dashboard/helpcenter/pages/articles/NewArticle.vue @@ -46,7 +46,6 @@ export default { }, computed: { ...mapGetters({ - selectedPortal: 'portals/getSelectedPortal', currentUserID: 'getCurrentUserID', articles: 'articles/articles', categories: 'categories/allCategories', @@ -58,7 +57,7 @@ export default { return { title: this.articleTitle, content: this.articleContent }; }, selectedPortalSlug() { - return this.portalSlug || this.selectedPortal?.slug; + return this.$route.params.portalSlug; }, categoryId() { return this.categories.length ? this.categories[0].id : null; diff --git a/app/javascript/dashboard/routes/dashboard/helpcenter/pages/portals/PortalCustomization.vue b/app/javascript/dashboard/routes/dashboard/helpcenter/pages/portals/PortalCustomization.vue index 37363d41c..845af7e6f 100644 --- a/app/javascript/dashboard/routes/dashboard/helpcenter/pages/portals/PortalCustomization.vue +++ b/app/javascript/dashboard/routes/dashboard/helpcenter/pages/portals/PortalCustomization.vue @@ -86,7 +86,7 @@ export default { }), createdPortalSlug() { const { - params: { portal_slug: slug }, + params: { portalSlug: slug }, } = this.$route; return slug; }, diff --git a/app/javascript/dashboard/store/modules/helpCenterPortals/actions.js b/app/javascript/dashboard/store/modules/helpCenterPortals/actions.js index d04137e63..e52272b9b 100644 --- a/app/javascript/dashboard/store/modules/helpCenterPortals/actions.js +++ b/app/javascript/dashboard/store/modules/helpCenterPortals/actions.js @@ -3,21 +3,17 @@ import { throwErrorMessage } from 'dashboard/store/utils/api'; import { types } from './mutations'; const portalAPIs = new PortalAPI(); export const actions = { - index: async ({ commit, state, dispatch }) => { + index: async ({ commit }) => { try { commit(types.SET_UI_FLAG, { isFetching: true }); const { data: { payload, meta }, } = await portalAPIs.get(); commit(types.CLEAR_PORTALS); - const portalIds = payload.map(portal => portal.id); + const portalSlugs = payload.map(portal => portal.slug); commit(types.ADD_MANY_PORTALS_ENTRY, payload); - commit(types.ADD_MANY_PORTALS_IDS, portalIds); - const { selectedPortalId } = state; - // Check if selected portal is still in the portals list - if (!portalIds.includes(selectedPortalId)) { - dispatch('setPortalId', portalIds[0]); - } + commit(types.ADD_MANY_PORTALS_IDS, portalSlugs); + commit(types.SET_PORTALS_META, meta); } catch (error) { throwErrorMessage(error); @@ -26,20 +22,13 @@ export const actions = { } }, - create: async ({ commit, state, dispatch }, params) => { + create: async ({ commit }, params) => { commit(types.SET_UI_FLAG, { isCreating: true }); try { const { data } = await portalAPIs.create(params); - const { id: portalId } = data; + const { slug: portalSlug } = data; commit(types.ADD_PORTAL_ENTRY, data); - commit(types.ADD_PORTAL_ID, portalId); - const { - portals: { selectedPortalId }, - } = state; - // Check if there are any selected portal - if (!selectedPortalId) { - dispatch('setPortalId', portalId); - } + commit(types.ADD_PORTAL_ID, portalSlug); } catch (error) { throwErrorMessage(error); } finally { @@ -47,47 +36,46 @@ export const actions = { } }, - update: async ({ commit }, params) => { - const portalId = params.id; - const portalSlug = params.slug; + update: async ({ commit }, { portalObj }) => { + const portalSlug = portalObj.slug; commit(types.SET_HELP_PORTAL_UI_FLAG, { uiFlags: { isUpdating: true }, - portalId, + portalSlug, }); try { - const { data } = await portalAPIs.updatePortal({ portalSlug, params }); + const { data } = await portalAPIs.updatePortal({ + portalSlug, + portalObj, + }); commit(types.UPDATE_PORTAL_ENTRY, data); } catch (error) { throwErrorMessage(error); } finally { commit(types.SET_HELP_PORTAL_UI_FLAG, { uiFlags: { isUpdating: false }, - portalId, + portalSlug, }); } }, - delete: async ({ commit }, portalId) => { + delete: async ({ commit }, { portalSlug }) => { commit(types.SET_HELP_PORTAL_UI_FLAG, { uiFlags: { isDeleting: true }, - portalId, + portalSlug, }); try { - await portalAPIs.delete(portalId); - commit(types.REMOVE_PORTAL_ENTRY, portalId); - commit(types.REMOVE_PORTAL_ID, portalId); + await portalAPIs.delete(portalSlug); + commit(types.REMOVE_PORTAL_ENTRY, portalSlug); + commit(types.REMOVE_PORTAL_ID, portalSlug); } catch (error) { throwErrorMessage(error); } finally { commit(types.SET_HELP_PORTAL_UI_FLAG, { uiFlags: { isDeleting: false }, - portalId, + portalSlug, }); } }, - setPortalId: async ({ commit }, portalId) => { - commit(types.SET_SELECTED_PORTAL_ID, portalId); - }, updatePortal: async ({ commit }, portal) => { commit(types.UPDATE_PORTAL_ENTRY, portal); }, diff --git a/app/javascript/dashboard/store/modules/helpCenterPortals/getters.js b/app/javascript/dashboard/store/modules/helpCenterPortals/getters.js index 201308714..c3cc790aa 100644 --- a/app/javascript/dashboard/store/modules/helpCenterPortals/getters.js +++ b/app/javascript/dashboard/store/modules/helpCenterPortals/getters.js @@ -6,18 +6,16 @@ export const getters = { }, isFetchingPortals: state => state.uiFlags.isFetching, - portalById: (...getterArguments) => portalId => { + portalBySlug: (...getterArguments) => portalId => { const [state] = getterArguments; const portal = state.portals.byId[portalId]; - return { - ...portal, - }; + return portal; }, allPortals: (...getterArguments) => { const [state, _getters] = getterArguments; const portals = state.portals.allIds.map(id => { - return _getters.portalById(id); + return _getters.portalBySlug(id); }); return portals; }, @@ -25,9 +23,4 @@ export const getters = { getMeta: state => { return state.meta; }, - getSelectedPortal: (...getterArguments) => { - const [state, _getters] = getterArguments; - const { selectedPortalId } = state.portals; - return _getters.portalById(selectedPortalId); - }, }; diff --git a/app/javascript/dashboard/store/modules/helpCenterPortals/index.js b/app/javascript/dashboard/store/modules/helpCenterPortals/index.js index 59a971443..fecb17d4d 100755 --- a/app/javascript/dashboard/store/modules/helpCenterPortals/index.js +++ b/app/javascript/dashboard/store/modules/helpCenterPortals/index.js @@ -25,7 +25,6 @@ const state = { meta: { byId: {}, }, - selectedPortalId: null, }, uiFlags: { allFetched: false, diff --git a/app/javascript/dashboard/store/modules/helpCenterPortals/mutations.js b/app/javascript/dashboard/store/modules/helpCenterPortals/mutations.js index 7843ef631..421b0dc7e 100644 --- a/app/javascript/dashboard/store/modules/helpCenterPortals/mutations.js +++ b/app/javascript/dashboard/store/modules/helpCenterPortals/mutations.js @@ -9,7 +9,6 @@ export const types = { ADD_PORTAL_ID: 'addPortalId', CLEAR_PORTALS: 'clearPortals', ADD_MANY_PORTALS_IDS: 'addManyPortalsIds', - SET_SELECTED_PORTAL_ID: 'setSelectedPortalId', UPDATE_PORTAL_ENTRY: 'updatePortalEntry', REMOVE_PORTAL_ENTRY: 'removePortalEntry', REMOVE_PORTAL_ID: 'removePortalId', @@ -25,7 +24,7 @@ export const mutations = { }, [types.ADD_PORTAL_ENTRY]($state, portal) { - Vue.set($state.portals.byId, portal.id, { + Vue.set($state.portals.byId, portal.slug, { ...portal, }); }, @@ -33,7 +32,7 @@ export const mutations = { [types.ADD_MANY_PORTALS_ENTRY]($state, portals) { const allPortals = { ...$state.portals.byId }; portals.forEach(portal => { - allPortals[portal.id] = portal; + allPortals[portal.slug] = portal; }); Vue.set($state.portals, 'byId', allPortals); }, @@ -41,7 +40,7 @@ export const mutations = { [types.CLEAR_PORTALS]: $state => { Vue.set($state.portals, 'byId', {}); Vue.set($state.portals, 'allIds', []); - Vue.set($state.portals, 'uiFlags.byId', {}); + Vue.set($state.portals.uiFlags, 'byId', {}); }, [types.SET_PORTALS_META]: ($state, data) => { @@ -50,41 +49,39 @@ export const mutations = { Vue.set($state.meta, 'currentPage', currentPage); }, - [types.SET_SELECTED_PORTAL_ID]: ($state, portalId) => { - Vue.set($state.portals, 'selectedPortalId', portalId); + [types.ADD_PORTAL_ID]($state, portalSlug) { + $state.portals.allIds.push(portalSlug); }, - [types.ADD_PORTAL_ID]($state, portalId) { - $state.portals.allIds.push(portalId); - }, - - [types.ADD_MANY_PORTALS_IDS]($state, portalIds) { - $state.portals.allIds.push(...portalIds); + [types.ADD_MANY_PORTALS_IDS]($state, portalSlugs) { + $state.portals.allIds.push(...portalSlugs); }, [types.UPDATE_PORTAL_ENTRY]($state, portal) { - const portalId = portal.id; - if (!$state.portals.allIds.includes(portalId)) return; + const portalSlug = portal.slug; + if (!$state.portals.allIds.includes(portalSlug)) return; - Vue.set($state.portals.byId, portalId, { + Vue.set($state.portals.byId, portalSlug, { ...portal, }); }, - [types.REMOVE_PORTAL_ENTRY]($state, portalId) { - if (!portalId) return; + [types.REMOVE_PORTAL_ENTRY]($state, portalSlug) { + if (!portalSlug) return; - const { [portalId]: toBeRemoved, ...newById } = $state.portals.byId; + const { [portalSlug]: 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.REMOVE_PORTAL_ID]($state, portalSlug) { + $state.portals.allIds = $state.portals.allIds.filter( + slug => slug !== portalSlug + ); }, - [types.SET_HELP_PORTAL_UI_FLAG]($state, { portalId, uiFlags }) { - const flags = $state.portals.uiFlags.byId[portalId]; - Vue.set($state.portals.uiFlags.byId, portalId, { + [types.SET_HELP_PORTAL_UI_FLAG]($state, { portalSlug, uiFlags }) { + const flags = $state.portals.uiFlags.byId[portalSlug]; + Vue.set($state.portals.uiFlags.byId, portalSlug, { ...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 index c78a2a97c..2e21a1111 100644 --- a/app/javascript/dashboard/store/modules/helpCenterPortals/specs/actions.spec.js +++ b/app/javascript/dashboard/store/modules/helpCenterPortals/specs/actions.spec.js @@ -15,16 +15,13 @@ describe('#actions', () => { await actions.index({ commit, dispatch, - state: { - selectedPortalId: 4, - }, + state: {}, }); - expect(dispatch.mock.calls).toMatchObject([['setPortalId', 1]]); expect(commit.mock.calls).toEqual([ [types.SET_UI_FLAG, { isFetching: true }], [types.CLEAR_PORTALS], [types.ADD_MANY_PORTALS_ENTRY, apiResponse.payload], - [types.ADD_MANY_PORTALS_IDS, [1, 2]], + [types.ADD_MANY_PORTALS_IDS, ['domain', 'campaign']], [types.SET_PORTALS_META, { current_page: 1, portals_count: 1 }], [types.SET_UI_FLAG, { isFetching: false }], ]); @@ -43,7 +40,7 @@ describe('#actions', () => { it('sends correct actions if API is success', async () => { axios.post.mockResolvedValue({ data: apiResponse.payload[1] }); await actions.create( - { commit, dispatch, state: { portals: { selectedPortalId: null } } }, + { commit, dispatch, state: { portals: {} } }, { color: 'red', custom_domain: 'domain_for_help', @@ -53,17 +50,14 @@ describe('#actions', () => { expect(commit.mock.calls).toEqual([ [types.SET_UI_FLAG, { isCreating: true }], [types.ADD_PORTAL_ENTRY, apiResponse.payload[1]], - [types.ADD_PORTAL_ID, 2], + [types.ADD_PORTAL_ID, 'campaign'], [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, dispatch, state: { portals: { selectedPortalId: null } } }, - {} - ) + actions.create({ commit, dispatch, state: { portals: {} } }, {}) ).rejects.toThrow(Error); expect(commit.mock.calls).toEqual([ [types.SET_UI_FLAG, { isCreating: true }], @@ -75,32 +69,32 @@ describe('#actions', () => { describe('#update', () => { it('sends correct actions if API is success', async () => { axios.patch.mockResolvedValue({ data: apiResponse.payload[1] }); - await actions.update({ commit }, apiResponse.payload[1]); + await actions.update({ commit }, { portalObj: apiResponse.payload[1] }); expect(commit.mock.calls).toEqual([ [ types.SET_HELP_PORTAL_UI_FLAG, - { uiFlags: { isUpdating: true }, portalId: 2 }, + { uiFlags: { isUpdating: true }, portalSlug: 'campaign' }, ], [types.UPDATE_PORTAL_ENTRY, apiResponse.payload[1]], [ types.SET_HELP_PORTAL_UI_FLAG, - { uiFlags: { isUpdating: false }, portalId: 2 }, + { uiFlags: { isUpdating: false }, portalSlug: 'campaign' }, ], ]); }); it('sends correct actions if API is error', async () => { axios.patch.mockRejectedValue({ message: 'Incorrect header' }); await expect( - actions.update({ commit }, apiResponse.payload[1]) + actions.update({ commit }, { portalObj: apiResponse.payload[1] }) ).rejects.toThrow(Error); expect(commit.mock.calls).toEqual([ [ types.SET_HELP_PORTAL_UI_FLAG, - { uiFlags: { isUpdating: true }, portalId: 2 }, + { uiFlags: { isUpdating: true }, portalSlug: 'campaign' }, ], [ types.SET_HELP_PORTAL_UI_FLAG, - { uiFlags: { isUpdating: false }, portalId: 2 }, + { uiFlags: { isUpdating: false }, portalSlug: 'campaign' }, ], ]); }); @@ -109,40 +103,35 @@ describe('#actions', () => { describe('#delete', () => { it('sends correct actions if API is success', async () => { axios.delete.mockResolvedValue({}); - await actions.delete({ commit }, 2); + await actions.delete({ commit }, { portalSlug: 'campaign' }); expect(commit.mock.calls).toEqual([ [ types.SET_HELP_PORTAL_UI_FLAG, - { uiFlags: { isDeleting: true }, portalId: 2 }, + { uiFlags: { isDeleting: true }, portalSlug: 'campaign' }, ], - [types.REMOVE_PORTAL_ENTRY, 2], - [types.REMOVE_PORTAL_ID, 2], + [types.REMOVE_PORTAL_ENTRY, 'campaign'], + [types.REMOVE_PORTAL_ID, 'campaign'], [ types.SET_HELP_PORTAL_UI_FLAG, - { uiFlags: { isDeleting: false }, portalId: 2 }, + { uiFlags: { isDeleting: false }, portalSlug: 'campaign' }, ], ]); }); it('sends correct actions if API is error', async () => { axios.delete.mockRejectedValue({ message: 'Incorrect header' }); - await expect(actions.delete({ commit }, 2)).rejects.toThrow(Error); + await expect( + actions.delete({ commit }, { portalSlug: 'campaign' }) + ).rejects.toThrow(Error); expect(commit.mock.calls).toEqual([ [ types.SET_HELP_PORTAL_UI_FLAG, - { uiFlags: { isDeleting: true }, portalId: 2 }, + { uiFlags: { isDeleting: true }, portalSlug: 'campaign' }, ], [ types.SET_HELP_PORTAL_UI_FLAG, - { uiFlags: { isDeleting: false }, portalId: 2 }, + { uiFlags: { isDeleting: false }, portalSlug: 'campaign' }, ], ]); }); }); - describe('#setPortalId', () => { - it('sends correct actions', async () => { - axios.delete.mockResolvedValue({}); - await actions.setPortalId({ commit }, 1); - expect(commit.mock.calls).toEqual([[types.SET_SELECTED_PORTAL_ID, 1]]); - }); - }); }); diff --git a/app/javascript/dashboard/store/modules/helpCenterPortals/specs/fixtures.js b/app/javascript/dashboard/store/modules/helpCenterPortals/specs/fixtures.js index 1ed5ed47f..50af1ded1 100644 --- a/app/javascript/dashboard/store/modules/helpCenterPortals/specs/fixtures.js +++ b/app/javascript/dashboard/store/modules/helpCenterPortals/specs/fixtures.js @@ -40,7 +40,6 @@ export default { 1: { isFetching: false, isUpdating: true, isDeleting: false }, }, }, - selectedPortalId: 1, }, uiFlags: { allFetched: false, diff --git a/app/javascript/dashboard/store/modules/helpCenterPortals/specs/getters.spec.js b/app/javascript/dashboard/store/modules/helpCenterPortals/specs/getters.spec.js index c7475812a..209d2351f 100644 --- a/app/javascript/dashboard/store/modules/helpCenterPortals/specs/getters.spec.js +++ b/app/javascript/dashboard/store/modules/helpCenterPortals/specs/getters.spec.js @@ -16,9 +16,9 @@ describe('#getters', () => { expect(getters.isFetchingPortals(state)).toEqual(true); }); - it('portalById', () => { + it('portalBySlug', () => { const state = portal; - expect(getters.portalById(state)(1)).toEqual({ + expect(getters.portalBySlug(state)(1)).toEqual({ id: 1, color: 'red', custom_domain: 'domain_for_help', diff --git a/app/javascript/dashboard/store/modules/helpCenterPortals/specs/mutations.spec.js b/app/javascript/dashboard/store/modules/helpCenterPortals/specs/mutations.spec.js index 6830e7d43..d20cacd1b 100644 --- a/app/javascript/dashboard/store/modules/helpCenterPortals/specs/mutations.spec.js +++ b/app/javascript/dashboard/store/modules/helpCenterPortals/specs/mutations.spec.js @@ -31,13 +31,13 @@ describe('#mutations', () => { 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 }); + mutations[types.ADD_PORTAL_ENTRY](state, { slug: 'new' }); + expect(state.portals.byId.new).toEqual({ slug: 'new' }); }); }); describe('[types.ADD_PORTAL_ID]', () => { - it('adds helpcenter id to state', () => { + it('adds helpcenter slug to state', () => { mutations[types.ADD_PORTAL_ID](state, 12); expect(state.portals.allIds).toEqual([1, 2, 12]); }); @@ -48,13 +48,13 @@ describe('#mutations', () => { 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 }); + it('does not updates if object slug is not present ', () => { + mutations[types.UPDATE_PORTAL_ENTRY](state, { slug: 5 }); expect(state).toEqual(portal); }); - it(' updates if object with id already present in the state', () => { + it(' updates if object with slug already present in the state', () => { mutations[types.UPDATE_PORTAL_ENTRY](state, { - id: 2, + slug: 2, name: 'Updated name', }); expect(state.portals.byId[2].name).toEqual('Updated name'); @@ -62,7 +62,7 @@ describe('#mutations', () => { }); describe('[types.REMOVE_PORTAL_ENTRY]', () => { - it('does not remove object entry if no id is passed', () => { + it('does not remove object entry if no slug is passed', () => { mutations[types.REMOVE_PORTAL_ENTRY](state, undefined); expect(state).toEqual({ ...portal }); }); @@ -73,7 +73,7 @@ describe('#mutations', () => { }); describe('[types.REMOVE_PORTAL_ID]', () => { - it('removes id from state', () => { + it('removes slug from state', () => { mutations[types.REMOVE_PORTAL_ID](state, 2); expect(state.portals.allIds).toEqual([1, 12]); }); @@ -82,12 +82,12 @@ describe('#mutations', () => { describe('[types.SET_HELP_PORTAL_UI_FLAG]', () => { it('sets correct flag in state', () => { mutations[types.SET_HELP_PORTAL_UI_FLAG](state, { - portalId: 1, + portalSlug: 'domain', uiFlags: { isFetching: true }, }); - expect(state.portals.uiFlags.byId[1]).toEqual({ + expect(state.portals.uiFlags.byId.domain).toEqual({ isFetching: true, - isUpdating: true, + isUpdating: false, isDeleting: false, }); }); @@ -99,9 +99,7 @@ describe('#mutations', () => { expect(state.portals.allIds).toEqual([]); expect(state.portals.byId).toEqual({}); expect(state.portals.uiFlags).toEqual({ - byId: { - '1': { isFetching: true, isUpdating: true, isDeleting: false }, - }, + byId: {}, }); }); }); @@ -118,11 +116,4 @@ describe('#mutations', () => { }); }); }); - - describe('#SET_SELECTED_PORTAL_ID', () => { - it('set selected portal id', () => { - mutations[types.SET_SELECTED_PORTAL_ID](state, 4); - expect(state.portals.selectedPortalId).toEqual(4); - }); - }); }); From c8d01a84ce96df45dc98f7291411bb8bac1c1abf Mon Sep 17 00:00:00 2001 From: Sojan Jose Date: Thu, 1 Sep 2022 00:31:43 +0530 Subject: [PATCH 2/3] feat: Ability to seed Demo Accounts (#5352) Introduces the ability to seed sample data into accounts in development and staging. fixes: #3429 --- .rubocop.yml | 1 + Gemfile | 5 +- .../super_admin/accounts_controller.rb | 5 + app/fields/avatar_field.rb | 4 +- .../super_admin/accounts/_seed_data.html.erb | 14 + app/views/super_admin/accounts/show.html.erb | 2 + config/routes.rb | 4 +- db/seeds.rb | 12 +- lib/seeders/account_seeder.rb | 150 +++---- lib/seeders/inbox_seeder.rb | 105 +++++ lib/seeders/seed_data.yml | 367 ++++++++++++++++++ public/assets/administrate/bot/avatar.png | Bin 0 -> 2165 bytes public/assets/administrate/user/avatar.png | Bin 0 -> 27270 bytes 13 files changed, 593 insertions(+), 76 deletions(-) create mode 100644 app/views/super_admin/accounts/_seed_data.html.erb create mode 100644 lib/seeders/inbox_seeder.rb create mode 100644 lib/seeders/seed_data.yml create mode 100644 public/assets/administrate/bot/avatar.png create mode 100644 public/assets/administrate/user/avatar.png diff --git a/.rubocop.yml b/.rubocop.yml index d63f0418d..dafd9a620 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -184,3 +184,4 @@ AllCops: - db/migrate/20200927135222_add_last_activity_at_to_conversation.rb - db/migrate/20210306170117_add_last_activity_at_to_contacts.rb - db/migrate/20220809104508_revert_cascading_indexes.rb + diff --git a/Gemfile b/Gemfile index ba83ce964..9c3590017 100644 --- a/Gemfile +++ b/Gemfile @@ -158,6 +158,10 @@ group :test do gem 'webmock' end +group :development, :test, :staging do + gem 'faker' +end + group :development, :test do gem 'active_record_query_trace' ##--- gems for debugging and error reporting ---## @@ -167,7 +171,6 @@ group :development, :test do gem 'byebug', platform: :mri gem 'climate_control' gem 'factory_bot_rails' - gem 'faker' gem 'listen' gem 'mock_redis' gem 'pry-rails' diff --git a/app/controllers/super_admin/accounts_controller.rb b/app/controllers/super_admin/accounts_controller.rb index 28cae96b0..3d038281c 100644 --- a/app/controllers/super_admin/accounts_controller.rb +++ b/app/controllers/super_admin/accounts_controller.rb @@ -41,4 +41,9 @@ class SuperAdmin::AccountsController < SuperAdmin::ApplicationController # See https://administrate-prototype.herokuapp.com/customizing_controller_actions # for more information + + def seed + Seeders::AccountSeeder.new(account: requested_resource).perform! + redirect_back(fallback_location: [namespace, requested_resource], notice: 'Account seeding triggered') + end end diff --git a/app/fields/avatar_field.rb b/app/fields/avatar_field.rb index a9674eb94..c443b7a21 100644 --- a/app/fields/avatar_field.rb +++ b/app/fields/avatar_field.rb @@ -2,6 +2,8 @@ require 'administrate/field/base' class AvatarField < Administrate::Field::Base def avatar_url - data.presence&.gsub('?d=404', '?d=mp') + return data.presence if data.presence + + resource.is_a?(User) ? '/assets/administrate/user/avatar.png' : '/assets/administrate/bot/avatar.png' end end diff --git a/app/views/super_admin/accounts/_seed_data.html.erb b/app/views/super_admin/accounts/_seed_data.html.erb new file mode 100644 index 000000000..9f8358228 --- /dev/null +++ b/app/views/super_admin/accounts/_seed_data.html.erb @@ -0,0 +1,14 @@ +<% if !Rails.env.production? %> +
+
+ <%= form_for([:seed, namespace, page.resource], method: :post, html: { class: "form" }) do |f| %> + +
+

Click the button to generate seed data into this account for demos.

+

Note: This will clear all the existing data in this account.

+
+ <%= f.submit 'Generate Seed Data' %> +
+ <% end %> +
+<% end %> diff --git a/app/views/super_admin/accounts/show.html.erb b/app/views/super_admin/accounts/show.html.erb index 93cd37945..1b37e4402 100644 --- a/app/views/super_admin/accounts/show.html.erb +++ b/app/views/super_admin/accounts/show.html.erb @@ -85,3 +85,5 @@ as well as a link to its edit page. <% end %> + +<%= render partial: "seed_data", locals: {page: page} %> diff --git a/config/routes.rb b/config/routes.rb index 44b5ce7ea..2b218bf76 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -340,7 +340,9 @@ Rails.application.routes.draw do resource :app_config, only: [:show, :create] # order of resources affect the order of sidebar navigation in super admin - resources :accounts + resources :accounts, only: [:index, :new, :create, :show, :edit, :update] do + post :seed, on: :member + end resources :users, only: [:index, :new, :create, :show, :edit, :update] resources :access_tokens, only: [:index, :show] resources :installation_configs, only: [:index, :new, :create, :show, :edit, :update] diff --git a/db/seeds.rb b/db/seeds.rb index 47ce3f6fb..c4c7eda71 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -11,6 +11,12 @@ end ## Seeds for Local Development unless Rails.env.production? + # Enables creating additional accounts from dashboard + installation_config = InstallationConfig.find_by(name: 'CREATE_NEW_ACCOUNT_FROM_DASHBOARD') + installation_config.value = true + installation_config.save! + GlobalConfig.clear_cache + account = Account.create!( name: 'Acme Inc' ) @@ -35,12 +41,6 @@ unless Rails.env.production? role: :administrator ) - # Enables creating additional accounts from dashboard - installation_config = InstallationConfig.find_by(name: 'CREATE_NEW_ACCOUNT_FROM_DASHBOARD') - installation_config.value = true - installation_config.save! - GlobalConfig.clear_cache - web_widget = Channel::WebWidget.create!(account: account, website_url: 'https://acme.inc') inbox = Inbox.create!(channel: web_widget, account: account, name: 'Acme Support') diff --git a/lib/seeders/account_seeder.rb b/lib/seeders/account_seeder.rb index 162bc4cd9..5302daefe 100644 --- a/lib/seeders/account_seeder.rb +++ b/lib/seeders/account_seeder.rb @@ -1,89 +1,105 @@ -## Class to generate sample data for a chatwoot test Account. +## Class to generate sample data for a chatwoot test @Account. ############################################################ ### Usage ##### # # # Seed an account with all data types in this class -# Seeders::AccountSeeder.new(account: account).seed! +# Seeders::AccountSeeder.new(account: @Account.find(1)).perform! # -# # When you want to seed only a specific type of data -# Seeders::AccountSeeder.new(account: account).seed_canned_responses -# # Seed specific number of objects -# Seeders::AccountSeeder.new(account: account).seed_canned_responses(count: 10) # ############################################################ class Seeders::AccountSeeder - pattr_initialize [:account!] + def initialize(account:) + raise 'Account Seeding is not allowed in production.' if Rails.env.production? - def seed! + @account_data = HashWithIndifferentAccess.new(YAML.safe_load(File.read(Rails.root.join('lib/seeders/seed_data.yml')))) + @account = account + end + + def perform! + set_up_account + seed_teams + set_up_users + seed_labels seed_canned_responses seed_inboxes + seed_contacts + end + + def set_up_account + @account.teams.destroy_all + @account.conversations.destroy_all + @account.labels.destroy_all + @account.inboxes.destroy_all + @account.contacts.destroy_all + end + + def seed_teams + @account_data['teams'].each do |team_name| + @account.teams.create!(name: team_name) + end + end + + def seed_labels + @account_data['labels'].each do |label| + @account.labels.create!(label) + end + end + + def set_up_users + @account_data['users'].each do |user| + user_record = User.create_with(name: user['name'], password: 'Password1!.').find_or_create_by!(email: (user['email']).to_s) + user_record.skip_confirmation! + user_record.save! + Avatar::AvatarFromUrlJob.perform_later(user_record, "https://xsgames.co/randomusers/avatar.php?g=#{user['gender']}") + AccountUser.create_with(role: (user['role'] || 'agent')).find_or_create_by!(account_id: @account.id, user_id: user_record.id) + next if user['team'].blank? + + add_user_to_teams(user: user_record, teams: user['team']) + end + end + + def add_user_to_teams(user:, teams:) + teams.each do |team| + team_record = @account.teams.where('name LIKE ?', "%#{team.downcase}%").first if team.present? + TeamMember.find_or_create_by!(team_id: team_record.id, user_id: user.id) unless team_record.nil? + end end def seed_canned_responses(count: 50) count.times do - account.canned_responses.create(content: Faker::Quote.fortune_cookie, short_code: Faker::Alphanumeric.alpha(number: 10)) + @account.canned_responses.create(content: Faker::Quote.fortune_cookie, short_code: Faker::Alphanumeric.alpha(number: 10)) + end + end + + def seed_contacts + @account_data['contacts'].each do |contact_data| + contact = @account.contacts.create!(contact_data.slice('name', 'email')) + Avatar::AvatarFromUrlJob.perform_later(contact, "https://xsgames.co/randomusers/avatar.php?g=#{contact_data['gender']}") + contact_data['conversations'].each do |conversation_data| + inbox = @account.inboxes.find_by(channel_type: conversation_data['channel']) + contact_inbox = inbox.contact_inboxes.create!(contact: contact, source_id: (conversation_data['source_id'] || SecureRandom.hex)) + create_conversation(contact_inbox: contact_inbox, conversation_data: conversation_data) + end + end + end + + def create_conversation(contact_inbox:, conversation_data:) + assignee = User.find_by(email: conversation_data['assignee']) if conversation_data['assignee'].present? + conversation = contact_inbox.conversations.create!(account: contact_inbox.inbox.account, contact: contact_inbox.contact, + inbox: contact_inbox.inbox, assignee: assignee) + create_messages(conversation: conversation, messages: conversation_data['messages']) + end + + def create_messages(conversation:, messages:) + messages.each do |message_data| + sender = User.find_by(email: message_data['sender']) if message_data['sender'].present? + conversation.messages.create!(message_data.slice('content', 'message_type').merge(account: conversation.inbox.account, sender: sender, + inbox: conversation.inbox)) end end def seed_inboxes - seed_website_inbox - seed_facebook_inbox - seed_twitter_inbox - seed_whatsapp_inbox - seed_sms_inbox - seed_email_inbox - seed_api_inbox - seed_telegram_inbox - seed_line_inbox - end - - def seed_website_inbox - channel = Channel::WebWidget.create!(account: account, website_url: 'https://acme.inc') - Inbox.create!(channel: channel, account: account, name: 'Acme Website') - end - - def seed_facebook_inbox - channel = Channel::FacebookPage.create!(account: account, user_access_token: 'test', page_access_token: 'test', page_id: 'test') - Inbox.create!(channel: channel, account: account, name: 'Acme Facebook') - end - - def seed_twitter_inbox - channel = Channel::TwitterProfile.create!(account: account, twitter_access_token: 'test', twitter_access_token_secret: 'test', profile_id: '123') - Inbox.create!(channel: channel, account: account, name: 'Acme Twitter') - end - - def seed_whatsapp_inbox - channel = Channel::Whatsapp.create!(account: account, phone_number: '+123456789') - Inbox.create!(channel: channel, account: account, name: 'Acme Whatsapp') - end - - def seed_sms_inbox - channel = Channel::Sms.create!(account: account, phone_number: '+123456789') - Inbox.create!(channel: channel, account: account, name: 'Acme SMS') - end - - def seed_email_inbox - channel = Channel::Email.create!(account: account, email: 'test@acme.inc', forward_to_email: 'test_fwd@acme.inc') - Inbox.create!(channel: channel, account: account, name: 'Acme Email') - end - - def seed_api_inbox - channel = Channel::Api.create!(account: account) - Inbox.create!(channel: channel, account: account, name: 'Acme API') - end - - def seed_telegram_inbox - # rubocop:disable Rails/SkipsModelValidations - Channel::Telegram.insert({ account_id: account.id, bot_name: 'Acme', bot_token: 'test', created_at: Time.now.utc, updated_at: Time.now.utc }, - returning: %w[id]) - channel = Channel::Telegram.find_by(bot_token: 'test') - Inbox.create!(channel: channel, account: account, name: 'Acme Telegram') - # rubocop:enable Rails/SkipsModelValidations - end - - def seed_line_inbox - channel = Channel::Line.create!(account: account, line_channel_id: 'test', line_channel_secret: 'test', line_channel_token: 'test') - Inbox.create!(channel: channel, account: account, name: 'Acme Line') + Seeders::InboxSeeder.new(account: @account, company_data: @account_data[:company]).perform! end end diff --git a/lib/seeders/inbox_seeder.rb b/lib/seeders/inbox_seeder.rb new file mode 100644 index 000000000..0c4bca669 --- /dev/null +++ b/lib/seeders/inbox_seeder.rb @@ -0,0 +1,105 @@ +## Class to generate sample inboxes for a chatwoot test @Account. +############################################################ +### Usage ##### +# +# # Seed an account with all data types in this class +# Seeders::InboxSeeder.new(account: @Account.find(1), company_data: {name: 'PaperLayer', doamin: 'paperlayer.test'}).perform! +# +# +############################################################ + +class Seeders::InboxSeeder + def initialize(account:, company_data:) + raise 'Inbox Seeding is not allowed in production.' if Rails.env.production? + + @account = account + @company_data = company_data + end + + def perform! + seed_website_inbox + seed_facebook_inbox + seed_twitter_inbox + seed_whatsapp_inbox + seed_sms_inbox + seed_email_inbox + seed_api_inbox + seed_telegram_inbox + seed_line_inbox + end + + def seed_website_inbox + channel = Channel::WebWidget.create!(account: @account, website_url: "https://#{@company_data['domain']}") + Inbox.create!(channel: channel, account: @account, name: "#{@company_data['name']} Website") + end + + def seed_facebook_inbox + channel = Channel::FacebookPage.create!(account: @account, user_access_token: SecureRandom.hex, page_access_token: SecureRandom.hex, + page_id: SecureRandom.hex) + Inbox.create!(channel: channel, account: @account, name: "#{@company_data['name']} Facebook") + end + + def seed_twitter_inbox + channel = Channel::TwitterProfile.create!(account: @account, twitter_access_token: SecureRandom.hex, + twitter_access_token_secret: SecureRandom.hex, profile_id: '123') + Inbox.create!(channel: channel, account: @account, name: "#{@company_data['name']} Twitter") + end + + def seed_whatsapp_inbox + # rubocop:disable Rails/SkipsModelValidations + Channel::Whatsapp.insert( + { + account_id: @account.id, + phone_number: Faker::PhoneNumber.cell_phone_in_e164, + created_at: Time.now.utc, + updated_at: Time.now.utc + }, + returning: %w[id] + ) + # rubocop:enable Rails/SkipsModelValidations + + channel = Channel::Whatsapp.find_by(account_id: @account.id) + + Inbox.create!(channel: channel, account: @account, name: "#{@company_data['name']} Whatsapp") + end + + def seed_sms_inbox + channel = Channel::Sms.create!(account: @account, phone_number: Faker::PhoneNumber.cell_phone_in_e164) + Inbox.create!(channel: channel, account: @account, name: "#{@company_data['name']} Mobile") + end + + def seed_email_inbox + channel = Channel::Email.create!(account: @account, email: "test#{SecureRandom.hex}@#{@company_data['domain']}", + forward_to_email: "test_fwd#{SecureRandom.hex}@#{@company_data['domain']}") + Inbox.create!(channel: channel, account: @account, name: "#{@company_data['name']} Email") + end + + def seed_api_inbox + channel = Channel::Api.create!(account: @account) + Inbox.create!(channel: channel, account: @account, name: "#{@company_data['name']} API") + end + + def seed_telegram_inbox + # rubocop:disable Rails/SkipsModelValidations + bot_token = SecureRandom.hex + Channel::Telegram.insert( + { + account_id: @account.id, + bot_name: (@company_data['name']).to_s, + bot_token: bot_token, + created_at: Time.now.utc, + updated_at: Time.now.utc + }, + returning: %w[id] + ) + channel = Channel::Telegram.find_by(bot_token: bot_token) + Inbox.create!(channel: channel, account: @account, name: "#{@company_data['name']} Telegram") + # rubocop:enable Rails/SkipsModelValidations + end + + def seed_line_inbox + channel = Channel::Line.create!(account: @account, line_channel_id: SecureRandom.hex, line_channel_secret: SecureRandom.hex, + line_channel_token: SecureRandom.hex) + Inbox.create!(channel: channel, account: @account, name: "#{@company_data['name']} Line") + end +end diff --git a/lib/seeders/seed_data.yml b/lib/seeders/seed_data.yml new file mode 100644 index 000000000..847ba5a8c --- /dev/null +++ b/lib/seeders/seed_data.yml @@ -0,0 +1,367 @@ +company: + name: 'PaperLayer' + domain: 'paperlayer.test' +users: + - name: 'Michael Scott' + gender: male + email: 'michale@paperlayer.test' + team: + - 'sales' + - 'management' + - 'administration' + - 'warehouse' + role: 'administrator' + - name: 'David Wallace' + gender: male + email: 'david@paperlayer.test' + team: + - 'Management' + - name: 'Deangelo Vickers' + gender: male + email: 'deangelo@paperlayer.test' + team: + - 'Management' + - name: 'Jo Bennett' + gender: female + email: 'jo@paperlayer.test' + team: + - 'Management' + - name: 'Josh Porter' + gender: male + email: 'josh@paperlayer.test' + team: + - 'Management' + - name: 'Charles Miner' + gender: male + email: 'charles@paperlayer.test' + team: + - 'Management' + - name: 'Ed Truck' + gender: male + email: 'ed@paperlayer.test' + team: + - 'Management' + - name: 'Dan Gore' + gender: male + email: 'dan@paperlayer.test' + team: + - 'Management' + - name: 'Craig D' + gender: male + email: 'craig@paperlayer.test' + team: + - 'Management' + - name: 'Troy Underbridge' + gender: male + email: 'troy@paperlayer.test' + team: + - 'Management' + - name: 'Karen Filippelli' + gender: female + email: 'karn@paperlayer.test' + team: + - 'Sales' + - name: 'Danny Cordray' + gender: female + email: 'danny@paperlayer.test' + team: + - 'Sales' + - name: 'Ben Nugent' + gender: male + email: 'ben@paperlayer.test' + team: + - 'Sales' + - name: 'Todd Packer' + gender: male + email: 'todd@paperlayer.test' + team: + - 'Sales' + - name: 'Cathy Simms' + gender: female + email: 'cathy@paperlayer.test' + team: + - 'Administration' + - name: 'Hunter Jo' + gender: male + email: 'hunter@paperlayer.test' + team: + - 'Administration' + - name: 'Rolando Silva' + gender: male + email: 'rolando@paperlayer.test' + team: + - 'Administration' + - name: 'Stephanie Wilson' + gender: female + email: 'stephanie@paperlayer.test' + team: + - 'Administration' + - name: 'Jordan Garfield' + gender: male + email: 'jorodan@paperlayer.test' + team: + - 'Administration' + - name: 'Ronni Carlo' + gender: male + email: 'ronni@paperlayer.test' + team: + - 'Administration' + - name: 'Lonny Collins' + gender: female + email: 'lonny@paperlayer.test' + team: + - 'Warehouse' + - name: 'Madge Madsen' + gender: female + email: 'madge@paperlayer.test' + team: + - 'Warehouse' + - name: 'Glenn Max' + gender: female + email: 'glenn@paperlayer.test' + team: + - 'Warehouse' + - name: 'Jerry DiCanio' + gender: male + email: 'jerry@paperlayer.test' + team: + - 'Warehouse' + - name: 'Phillip Martin' + gender: male + email: 'phillip@paperlayer.test' + team: + - 'Warehouse' + - name: 'Michael Josh' + gender: male + email: 'michale_josh@paperlayer.test' + team: + - 'Warehouse' + - name: 'Matt Hudson' + gender: male + email: 'matt@paperlayer.test' + team: + - 'Warehouse' + - name: 'Gideon' + gender: male + email: 'gideon@paperlayer.test' + team: + - 'Warehouse' + - name: 'Bruce' + gender: male + email: 'bruce@paperlayer.test' + team: + - 'Warehouse' + - name: 'Frank' + gender: male + email: 'frank@paperlayer.test' + team: + - 'Warehouse' + - name: 'Louanne Kelley' + gender: female + email: 'louanne@paperlayer.test' + - name: 'Devon White' + gender: male + email: 'devon@paperlayer.test' + - name: 'Kendall' + gender: male + email: 'kendall@paperlayer.test' + - email: 'sadiq@paperlayer.test' + name: 'Sadiq' + gender: male +teams: + - '💰 Sales' + - '💼 Management' + - '👩‍💼 Administration' + - '🚛 Warehouse' +labels: + - title: 'billing' + color: '#28AD21' + show_on_sidebar: true + - title: 'software' + color: '#8F6EF2' + show_on_sidebar: true + - title: 'delivery' + color: '#A2FDD5' + show_on_sidebar: true + - title: 'ops-handover' + color: '#A53326' + show_on_sidebar: true + - title: 'premium-customer' + color: '#6FD4EF' + show_on_sidebar: true + - title: 'lead' + color: '#F161C8' + show_on_sidebar: true +contacts: + - name: "Lorrie Trosdall" + email: "ltrosdall0@bravesites.test" + gender: 'female' + conversations: + - channel: Channel::WebWidget + messages: + - message_type: incoming + content: hello world + - name: "Tiffanie Cloughton" + email: "tcloughton1@newyorker.test" + gender: 'female' + conversations: + - channel: Channel::FacebookPage + messages: + - message_type: incoming + content: hello world + - name: "Melonie Keatch" + email: "mkeatch2@reuters.test" + gender: 'female' + conversations: + - channel: Channel::TwitterProfile + messages: + - message_type: incoming + content: hello world + - name: "Olin Canniffe" + email: "ocanniffe3@feedburner.test" + gender: 'male' + conversations: + - channel: Channel::Whatsapp + messages: + - message_type: incoming + content: hello world + - name: "Viviene Corp" + email: "vcorp4@instagram.test" + gender: 'female' + conversations: + - channel: Channel::Sms + source_id: "+1234567" + messages: + - message_type: incoming + content: hello world + - name: "Drake Pittway" + email: "dpittway5@chron.test" + gender: 'male' + conversations: + - channel: Channel::Line + messages: + - message_type: incoming + content: hello world + - name: "Klaus Crawley" + email: "kcrawley6@narod.ru" + gender: 'male' + conversations: + - channel: Channel::WebWidget + messages: + - message_type: incoming + content: hello world + - name: "Bing Cusworth" + email: "bcusworth7@arstechnica.test" + gender: 'male' + conversations: + - channel: Channel::TwitterProfile + messages: + - message_type: incoming + content: hello world + - name: "Claus Jira" + email: "cjira8@comcast.net" + gender: 'male' + conversations: + - channel: Channel::Whatsapp + messages: + - message_type: incoming + content: hello world + - name: "Quent Dalliston" + email: "qdalliston9@zimbio.test" + gender: 'male' + conversations: + - channel: Channel::Whatsapp + messages: + - message_type: incoming + content: hello world + - name: "Coreen Mewett" + email: "cmewetta@home.pl" + gender: 'female' + conversations: + - channel: Channel::FacebookPage + messages: + - message_type: incoming + content: hello world + - name: "Benyamin Janeway" + email: "bjanewayb@ustream.tv" + gender: 'male' + conversations: + - channel: Channel::Line + messages: + - message_type: incoming + content: hello world + - name: "Cordell Dalinder" + email: "cdalinderc@msn.test" + gender: 'male' + conversations: + - channel: Channel::Email + source_id: "cdalinderc@msn.test" + messages: + - message_type: incoming + content: hello world + - name: "Merrile Petruk" + email: "mpetrukd@wunderground.test" + gender: 'female' + conversations: + - channel: Channel::Email + source_id: "mpetrukd@wunderground.test" + messages: + - message_type: incoming + content: hello world + - name: "Nathaniel Vannuchi" + email: "nvannuchie@photobucket.test" + gender: 'male' + conversations: + - channel: Channel::FacebookPage + messages: + - message_type: incoming + content: "Hey there,I need some help with billing, my card is not working on the website." + - name: "Olia Olenchenko" + email: "oolenchenkof@bluehost.test" + gender: 'female' + conversations: + - channel: Channel::WebWidget + assignee: michael_scott@paperlayer.test + messages: + - message_type: incoming + content: "Billing section is not working, it throws some error." + - name: "Elisabeth Derington" + email: "ederingtong@printfriendly.test" + gender: 'female' + conversations: + - channel: Channel::Whatsapp + messages: + - message_type: incoming + content: "Hey \n I didn't get the product delivered, but it shows it is delivered to my address. Please check" + - name: "Willy Castelot" + email: "wcasteloth@exblog.jp" + gender: 'male' + conversations: + - channel: Channel::WebWidget + messages: + - message_type: incoming + content: "Hey there, \n I need some help with the product, my button is not working on the website." + - name: "Ophelia Folkard" + email: "ofolkardi@taobao.test" + gender: 'female' + conversations: + - channel: Channel::WebWidget + assignee: michael_scott@paperlayer.test + messages: + - message_type: incoming + content: "Hey, \n My card is not working on your website. Please help" + - name: "Candice Matherson" + email: "cmathersonj@va.gov" + gender: 'female' + conversations: + - channel: Channel::Email + source_id: "cmathersonj@va.gov" + assignee: michael_scott@paperlayer.test + messages: + - message_type: incoming + content: "Hey, \n I'm looking for some help to figure out if it is the right product for me." + - message_type: outgoing + content: Welcome to PaperLayer. Our Team will be getting back you shortly. + - message_type: outgoing + content: How may i help you ? + sender: michael_scott@paperlayer.test diff --git a/public/assets/administrate/bot/avatar.png b/public/assets/administrate/bot/avatar.png new file mode 100644 index 0000000000000000000000000000000000000000..4b5a2d686e7f86f29e4fac5142bffac3afaecb0a GIT binary patch literal 2165 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 YH^a_D&8eX1kNp8x<b+XA1cEx+W-In literal 0 HcmV?d00001 From 6e945dd61e613c7de752cf2a0f0b70ef19678c69 Mon Sep 17 00:00:00 2001 From: Muhsin Keloth Date: Thu, 1 Sep 2022 10:55:59 +0530 Subject: [PATCH 3/3] feat: Add the ability to delete/archive articles (#5319) --- .../dashboard/api/helpCenter/articles.js | 4 ++ .../dashboard/api/specs/article.spec.js | 11 ++++ .../dashboard/i18n/locale/en/helpCenter.json | 20 ++++++ .../helpcenter/components/PortalPopover.vue | 12 +++- .../pages/articles/ArticleSettings.vue | 2 +- .../helpcenter/pages/articles/EditArticle.vue | 64 ++++++++++++++++++- .../modules/helpCenterArticles/actions.js | 5 +- .../modules/helpCenterArticles/getters.js | 8 ++- .../helpCenterArticles/specs/action.spec.js | 11 +++- 9 files changed, 123 insertions(+), 14 deletions(-) diff --git a/app/javascript/dashboard/api/helpCenter/articles.js b/app/javascript/dashboard/api/helpCenter/articles.js index 8bc960267..62328e8eb 100644 --- a/app/javascript/dashboard/api/helpCenter/articles.js +++ b/app/javascript/dashboard/api/helpCenter/articles.js @@ -42,6 +42,10 @@ class ArticlesAPI extends PortalsAPI { category_id, }); } + + deleteArticle({ articleId, portalSlug }) { + return axios.delete(`${this.url}/${portalSlug}/articles/${articleId}`); + } } export default new ArticlesAPI(); diff --git a/app/javascript/dashboard/api/specs/article.spec.js b/app/javascript/dashboard/api/specs/article.spec.js index e20e2e222..51e90318f 100644 --- a/app/javascript/dashboard/api/specs/article.spec.js +++ b/app/javascript/dashboard/api/specs/article.spec.js @@ -52,4 +52,15 @@ describe('#PortalAPI', () => { ); }); }); + describeWithAPIMock('API calls', context => { + it('#deleteArticle', () => { + articlesAPI.deleteArticle({ + articleId: 1, + portalSlug: 'room-rental', + }); + expect(context.axiosMock.delete).toHaveBeenCalledWith( + '/api/v1/portals/room-rental/articles/1' + ); + }); + }); }); diff --git a/app/javascript/dashboard/i18n/locale/en/helpCenter.json b/app/javascript/dashboard/i18n/locale/en/helpCenter.json index 03a425a9b..1ee5bb640 100644 --- a/app/javascript/dashboard/i18n/locale/en/helpCenter.json +++ b/app/javascript/dashboard/i18n/locale/en/helpCenter.json @@ -224,6 +224,26 @@ "ERROR": "Error while saving article" } }, + "ARCHIVE_ARTICLE": { + "API": { + "ERROR": "Error while archiving article", + "SUCCESS": "Article archived successfully" + } + }, + "DELETE_ARTICLE": { + "MODAL": { + "CONFIRM": { + "TITLE": "Confirm Deletion", + "MESSAGE": "Are you sure to delete the article?", + "YES": "Yes, Delete", + "NO": "No, Keep it" + } + }, + "API": { + "SUCCESS_MESSAGE": "Article deleted successfully", + "ERROR_MESSAGE": "Error while deleting article" + } + }, "CREATE_ARTICLE": { "ERROR_MESSAGE": "Please add the article heading and content then only you can update the settings" }, diff --git a/app/javascript/dashboard/routes/dashboard/helpcenter/components/PortalPopover.vue b/app/javascript/dashboard/routes/dashboard/helpcenter/components/PortalPopover.vue index 539c15e7d..c8a4c785e 100644 --- a/app/javascript/dashboard/routes/dashboard/helpcenter/components/PortalPopover.vue +++ b/app/javascript/dashboard/routes/dashboard/helpcenter/components/PortalPopover.vue @@ -10,7 +10,7 @@ color-scheme="secondary" icon="settings" size="small" - @click="openPortalPage" + @click="openPortalArticles" > {{ $t('HELP_CENTER.PORTAL.POPOVER.PORTAL_SETTINGS') }}
@@ -62,10 +62,16 @@ export default { closePortalPopover() { this.$emit('close-popover'); }, - openPortalPage() { + openPortalArticles({ slug, locale }) { this.$emit('close-popover'); + const portal = this.portals.find(p => p.slug === slug); + this.$store.dispatch('portals/setPortalId', portal.id); this.$router.push({ - name: 'list_all_portals', + name: 'list_all_locale_articles', + params: { + portalSlug: slug, + locale: locale, + }, }); }, }, diff --git a/app/javascript/dashboard/routes/dashboard/helpcenter/pages/articles/ArticleSettings.vue b/app/javascript/dashboard/routes/dashboard/helpcenter/pages/articles/ArticleSettings.vue index 6817c175b..5dc08c5cb 100644 --- a/app/javascript/dashboard/routes/dashboard/helpcenter/pages/articles/ArticleSettings.vue +++ b/app/javascript/dashboard/routes/dashboard/helpcenter/pages/articles/ArticleSettings.vue @@ -127,7 +127,7 @@ export default { props: { article: { type: Object, - required: true, + default: () => ({}), }, }, data() { diff --git a/app/javascript/dashboard/routes/dashboard/helpcenter/pages/articles/EditArticle.vue b/app/javascript/dashboard/routes/dashboard/helpcenter/pages/articles/EditArticle.vue index f89e8b304..2d6402e62 100644 --- a/app/javascript/dashboard/routes/dashboard/helpcenter/pages/articles/EditArticle.vue +++ b/app/javascript/dashboard/routes/dashboard/helpcenter/pages/articles/EditArticle.vue @@ -27,6 +27,17 @@ v-if="showArticleSettings" :article="article" @save-article="saveArticle" + @delete-article="openDeletePopup" + @archive-article="archiveArticle" + /> + @@ -52,6 +63,7 @@ export default { isSaved: false, showArticleSettings: false, alertMessage: '', + showDeleteConfirmationPopup: false, }; }, computed: { @@ -82,6 +94,16 @@ export default { portalSlug: this.selectedPortalSlug, }); }, + openDeletePopup() { + this.showDeleteConfirmationPopup = true; + }, + closeDeletePopup() { + this.showDeleteConfirmationPopup = false; + }, + confirmDeletion() { + this.closeDeletePopup(); + this.deleteArticle(); + }, async saveArticle({ ...values }) { this.isUpdating = true; try { @@ -92,8 +114,7 @@ export default { }); } catch (error) { this.alertMessage = - error?.message || - this.$t('HELP_CENTER.EDIT_ARTICLE.API.ERROR_MESSAGE'); + error?.message || this.$t('HELP_CENTER.EDIT_ARTICLE.API.ERROR'); this.showAlert(this.alertMessage); } finally { setTimeout(() => { @@ -102,6 +123,45 @@ export default { }, 1500); } }, + async deleteArticle() { + try { + await this.$store.dispatch('articles/delete', { + portalSlug: this.selectedPortalSlug, + articleId: this.articleId, + }); + this.alertMessage = this.$t( + 'HELP_CENTER.DELETE_ARTICLE.API.SUCCESS_MESSAGE' + ); + this.$router.push({ + name: 'list_all_locale_articles', + params: { + portalSlug: this.selectedPortalSlug, + locale: this.locale, + }, + }); + } catch (error) { + this.alertMessage = + error?.message || + this.$t('HELP_CENTER.DELETE_ARTICLE.API.ERROR_MESSAGE'); + } finally { + this.showAlert(this.alertMessage); + } + }, + async archiveArticle() { + try { + await this.$store.dispatch('articles/update', { + portalSlug: this.selectedPortalSlug, + articleId: this.articleId, + status: 2, + }); + this.alertMessage = this.$t('HELP_CENTER.ARCHIVE_ARTICLE.API.SUCCESS'); + } catch (error) { + this.alertMessage = + error?.message || this.$t('HELP_CENTER.ARCHIVE_ARTICLE.API.ERROR'); + } finally { + this.showAlert(this.alertMessage); + } + }, openArticleSettings() { this.showArticleSettings = true; }, diff --git a/app/javascript/dashboard/store/modules/helpCenterArticles/actions.js b/app/javascript/dashboard/store/modules/helpCenterArticles/actions.js index b93195d40..e62901835 100644 --- a/app/javascript/dashboard/store/modules/helpCenterArticles/actions.js +++ b/app/javascript/dashboard/store/modules/helpCenterArticles/actions.js @@ -100,7 +100,7 @@ export const actions = { }); } }, - delete: async ({ commit }, articleId) => { + delete: async ({ commit }, { portalSlug, articleId }) => { commit(types.UPDATE_ARTICLE_FLAG, { uiFlags: { isDeleting: true, @@ -108,8 +108,7 @@ export const actions = { articleId, }); try { - await articlesAPI.delete(articleId); - + await articlesAPI.deleteArticle({ portalSlug, articleId }); commit(types.REMOVE_ARTICLE, articleId); commit(types.REMOVE_ARTICLE_ID, articleId); return articleId; diff --git a/app/javascript/dashboard/store/modules/helpCenterArticles/getters.js b/app/javascript/dashboard/store/modules/helpCenterArticles/getters.js index 35e32ee5e..edffc53c2 100644 --- a/app/javascript/dashboard/store/modules/helpCenterArticles/getters.js +++ b/app/javascript/dashboard/store/modules/helpCenterArticles/getters.js @@ -13,9 +13,11 @@ export const getters = { }, allArticles: (...getterArguments) => { const [state, _getters] = getterArguments; - const articles = state.articles.allIds.map(id => { - return _getters.articleById(id); - }); + const articles = state.articles.allIds + .map(id => { + return _getters.articleById(id); + }) + .filter(article => article !== undefined); return articles; }, getMeta: state => { diff --git a/app/javascript/dashboard/store/modules/helpCenterArticles/specs/action.spec.js b/app/javascript/dashboard/store/modules/helpCenterArticles/specs/action.spec.js index b0decf927..6fdfc5389 100644 --- a/app/javascript/dashboard/store/modules/helpCenterArticles/specs/action.spec.js +++ b/app/javascript/dashboard/store/modules/helpCenterArticles/specs/action.spec.js @@ -142,7 +142,11 @@ describe('#actions', () => { describe('#delete', () => { it('sends correct actions if API is success', async () => { axios.delete.mockResolvedValue({ data: articleList[0] }); - await actions.delete({ commit }, articleList[0].id); + await actions.delete( + { commit }, + { portalSlug: 'test', articleId: articleList[0].id } + ); + expect(commit.mock.calls).toEqual([ [ types.default.UPDATE_ARTICLE_FLAG, @@ -159,7 +163,10 @@ describe('#actions', () => { it('sends correct actions if API is error', async () => { axios.delete.mockRejectedValue({ message: 'Incorrect header' }); await expect( - actions.delete({ commit }, articleList[0].id) + actions.delete( + { commit }, + { portalSlug: 'test', articleId: articleList[0].id } + ) ).rejects.toThrow(Error); expect(commit.mock.calls).toEqual([ [