mirror of
				https://github.com/lingble/chatwoot.git
				synced 2025-10-31 11:08:04 +00:00 
			
		
		
		
	fix: Update article count in portal admin dashboard (#5647)
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
This commit is contained in:
		| @@ -5,9 +5,10 @@ class Api::V1::Accounts::ArticlesController < Api::V1::Accounts::BaseController | ||||
|   before_action :set_current_page, only: [:index] | ||||
|  | ||||
|   def index | ||||
|     @articles_count = @portal.articles.count | ||||
|     @articles = @portal.articles | ||||
|     @articles = @articles.search(list_params) if list_params.present? | ||||
|     @portal_articles = @portal.articles | ||||
|     @all_articles = @portal_articles.search(list_params) | ||||
|     @articles_count = @all_articles.count | ||||
|     @articles = @all_articles.page(@current_page) | ||||
|   end | ||||
|  | ||||
|   def create | ||||
|   | ||||
| @@ -5,6 +5,7 @@ class Api::V1::Accounts::CategoriesController < Api::V1::Accounts::BaseControlle | ||||
|   before_action :set_current_page, only: [:index] | ||||
|  | ||||
|   def index | ||||
|     @current_locale = params[:locale] | ||||
|     @categories = @portal.categories.search(params) | ||||
|   end | ||||
|  | ||||
|   | ||||
| @@ -14,7 +14,10 @@ class Api::V1::Accounts::PortalsController < Api::V1::Accounts::BaseController | ||||
|     @portal.members << agents | ||||
|   end | ||||
|  | ||||
|   def show; end | ||||
|   def show | ||||
|     @all_articles = @portal.articles | ||||
|     @articles = @all_articles.search(locale: params[:locale]) | ||||
|   end | ||||
|  | ||||
|   def create | ||||
|     @portal = Current.account.portals.build(portal_params) | ||||
|   | ||||
| @@ -7,8 +7,8 @@ class CategoriesAPI extends PortalsAPI { | ||||
|     super('categories', { accountScoped: true }); | ||||
|   } | ||||
|  | ||||
|   get({ portalSlug }) { | ||||
|     return axios.get(`${this.url}/${portalSlug}/categories`); | ||||
|   get({ portalSlug, locale }) { | ||||
|     return axios.get(`${this.url}/${portalSlug}/categories?locale=${locale}`); | ||||
|   } | ||||
|  | ||||
|   create({ portalSlug, categoryObj }) { | ||||
|   | ||||
| @@ -6,6 +6,10 @@ class PortalsAPI extends ApiClient { | ||||
|     super('portals', { accountScoped: true }); | ||||
|   } | ||||
|  | ||||
|   getPortal({ portalSlug, locale }) { | ||||
|     return axios.get(`${this.url}/${portalSlug}?locale=${locale}`); | ||||
|   } | ||||
|  | ||||
|   updatePortal({ portalSlug, portalObj }) { | ||||
|     return axios.patch(`${this.url}/${portalSlug}`, portalObj); | ||||
|   } | ||||
|   | ||||
| @@ -16,7 +16,7 @@ | ||||
|       :accessible-menu-items="accessibleMenuItems" | ||||
|       :additional-secondary-menu-items="additionalSecondaryMenuItems" | ||||
|       @open-popover="openPortalPopover" | ||||
|       @open-modal="onClickOpenAddCatogoryModal" | ||||
|       @open-modal="onClickOpenAddCategoryModal" | ||||
|     /> | ||||
|     <section class="app-content columns" :class="contentClassName"> | ||||
|       <router-view /> | ||||
| @@ -134,14 +134,14 @@ export default { | ||||
|     }, | ||||
|     accessibleMenuItems() { | ||||
|       if (!this.selectedPortal) return []; | ||||
|  | ||||
|       const { | ||||
|         meta: { | ||||
|           all_articles_count: allArticlesCount, | ||||
|           mine_articles_count: mineArticlesCount, | ||||
|           draft_articles_count: draftArticlesCount, | ||||
|           archived_articles_count: archivedArticlesCount, | ||||
|         } = {}, | ||||
|       } = this.selectedPortal; | ||||
|         allArticlesCount, | ||||
|         mineArticlesCount, | ||||
|         draftArticlesCount, | ||||
|         archivedArticlesCount, | ||||
|       } = this.meta; | ||||
|  | ||||
|       return [ | ||||
|         { | ||||
|           icon: 'book', | ||||
| @@ -216,6 +216,13 @@ export default { | ||||
|       return this.selectedPortal ? this.selectedPortal.name : ''; | ||||
|     }, | ||||
|   }, | ||||
|  | ||||
|   watch: { | ||||
|     selectedPortal() { | ||||
|       this.fetchPortalsAndItsCategories(); | ||||
|     }, | ||||
|   }, | ||||
|  | ||||
|   mounted() { | ||||
|     window.addEventListener('resize', this.handleResize); | ||||
|     this.handleResize(); | ||||
| @@ -251,12 +258,15 @@ export default { | ||||
|     toggleSidebar() { | ||||
|       this.isSidebarOpen = !this.isSidebarOpen; | ||||
|     }, | ||||
|     fetchPortalsAndItsCategories() { | ||||
|       this.$store.dispatch('portals/index').then(() => { | ||||
|         this.$store.dispatch('categories/index', { | ||||
|           portalSlug: this.selectedPortalSlug, | ||||
|         }); | ||||
|       }); | ||||
|     async fetchPortalsAndItsCategories() { | ||||
|       await this.$store.dispatch('portals/index'); | ||||
|  | ||||
|       const selectedPortalParam = { | ||||
|         portalSlug: this.selectedPortalSlug, | ||||
|         locale: this.selectedLocaleInPortal, | ||||
|       }; | ||||
|       this.$store.dispatch('portals/show', selectedPortalParam); | ||||
|       this.$store.dispatch('categories/index', selectedPortalParam); | ||||
|       this.$store.dispatch('agents/get'); | ||||
|     }, | ||||
|     toggleKeyShortcutModal() { | ||||
| @@ -277,7 +287,7 @@ export default { | ||||
|     closePortalPopover() { | ||||
|       this.showPortalPopover = false; | ||||
|     }, | ||||
|     onClickOpenAddCatogoryModal() { | ||||
|     onClickOpenAddCategoryModal() { | ||||
|       this.showAddCategoryModal = true; | ||||
|     }, | ||||
|     onClickCloseAddCategoryModal() { | ||||
|   | ||||
| @@ -112,16 +112,8 @@ export default { | ||||
|     this.selectedLocale = this.locale || this.portal?.meta?.default_locale; | ||||
|   }, | ||||
|   methods: { | ||||
|     fetchPortalsAndItsCategories() { | ||||
|       this.$store.dispatch('portals/index').then(() => { | ||||
|         this.$store.dispatch('categories/index', { | ||||
|           portalSlug: this.portal.slug, | ||||
|         }); | ||||
|       }); | ||||
|     }, | ||||
|     onClick(event, code, portal) { | ||||
|       event.preventDefault(); | ||||
|       this.fetchPortalsAndItsCategories(); | ||||
|       this.$router.push({ | ||||
|         name: 'list_all_locale_articles', | ||||
|         params: { | ||||
|   | ||||
| @@ -104,9 +104,6 @@ export default { | ||||
|       } | ||||
|       return null; | ||||
|     }, | ||||
|     articleCount() { | ||||
|       return this.articles ? this.articles.length : 0; | ||||
|     }, | ||||
|     headerTitleInCategoryView() { | ||||
|       return this.categories && this.categories.length | ||||
|         ? this.selectedCategory.name | ||||
|   | ||||
| @@ -2,13 +2,13 @@ import categoriesAPI from 'dashboard/api/helpCenter/categories.js'; | ||||
| import { throwErrorMessage } from 'dashboard/store/utils/api'; | ||||
| import types from '../../mutation-types'; | ||||
| export const actions = { | ||||
|   index: async ({ commit }, { portalSlug }) => { | ||||
|   index: async ({ commit }, { portalSlug, locale }) => { | ||||
|     try { | ||||
|       commit(types.SET_UI_FLAG, { isFetching: true }); | ||||
|       if (portalSlug) { | ||||
|         const { | ||||
|           data: { payload }, | ||||
|         } = await categoriesAPI.get({ portalSlug }); | ||||
|         } = await categoriesAPI.get({ portalSlug, locale }); | ||||
|         commit(types.CLEAR_CATEGORIES); | ||||
|         const categoryIds = payload.map(category => category.id); | ||||
|         commit(types.ADD_MANY_CATEGORIES, payload); | ||||
|   | ||||
| @@ -7,14 +7,12 @@ export const actions = { | ||||
|     try { | ||||
|       commit(types.SET_UI_FLAG, { isFetching: true }); | ||||
|       const { | ||||
|         data: { payload, meta }, | ||||
|         data: { payload }, | ||||
|       } = await portalAPIs.get(); | ||||
|       commit(types.CLEAR_PORTALS); | ||||
|       const portalSlugs = payload.map(portal => portal.slug); | ||||
|       commit(types.ADD_MANY_PORTALS_ENTRY, payload); | ||||
|       commit(types.ADD_MANY_PORTALS_IDS, portalSlugs); | ||||
|  | ||||
|       commit(types.SET_PORTALS_META, meta); | ||||
|     } catch (error) { | ||||
|       throwErrorMessage(error); | ||||
|     } finally { | ||||
| @@ -22,6 +20,21 @@ export const actions = { | ||||
|     } | ||||
|   }, | ||||
|  | ||||
|   show: async ({ commit }, { portalSlug, locale }) => { | ||||
|     commit(types.SET_UI_FLAG, { isFetchingItem: true }); | ||||
|     try { | ||||
|       const response = await portalAPIs.getPortal({ portalSlug, locale }); | ||||
|       const { | ||||
|         data: { meta }, | ||||
|       } = response; | ||||
|       commit(types.SET_PORTALS_META, meta); | ||||
|     } catch (error) { | ||||
|       // Ignore error | ||||
|     } finally { | ||||
|       commit(types.SET_UI_FLAG, { isFetchingItem: false }); | ||||
|     } | ||||
|   }, | ||||
|  | ||||
|   create: async ({ commit }, params) => { | ||||
|     commit(types.SET_UI_FLAG, { isCreating: true }); | ||||
|     try { | ||||
|   | ||||
| @@ -20,7 +20,5 @@ export const getters = { | ||||
|     return portals; | ||||
|   }, | ||||
|   count: state => state.portals.allIds.length || 0, | ||||
|   getMeta: state => { | ||||
|     return state.meta; | ||||
|   }, | ||||
|   getMeta: state => state.meta, | ||||
| }; | ||||
|   | ||||
| @@ -10,8 +10,10 @@ export const defaultPortalFlags = { | ||||
|  | ||||
| const state = { | ||||
|   meta: { | ||||
|     count: 0, | ||||
|     currentPage: 1, | ||||
|     allArticlesCount: 0, | ||||
|     mineArticlesCount: 0, | ||||
|     draftArticlesCount: 0, | ||||
|     archivedArticlesCount: 0, | ||||
|   }, | ||||
|  | ||||
|   portals: { | ||||
|   | ||||
| @@ -44,9 +44,16 @@ export const mutations = { | ||||
|   }, | ||||
|  | ||||
|   [types.SET_PORTALS_META]: ($state, data) => { | ||||
|     const { portals_count: count, current_page: currentPage } = data; | ||||
|     Vue.set($state.meta, 'count', count); | ||||
|     Vue.set($state.meta, 'currentPage', currentPage); | ||||
|     const { | ||||
|       all_articles_count: allArticlesCount, | ||||
|       mine_articles_count: mineArticlesCount, | ||||
|       draft_articles_count: draftArticlesCount, | ||||
|       archived_articles_count: archivedArticlesCount, | ||||
|     } = data; | ||||
|     Vue.set($state.meta, 'allArticlesCount', allArticlesCount); | ||||
|     Vue.set($state.meta, 'archivedArticlesCount', archivedArticlesCount); | ||||
|     Vue.set($state.meta, 'mineArticlesCount', mineArticlesCount); | ||||
|     Vue.set($state.meta, 'draftArticlesCount', draftArticlesCount); | ||||
|   }, | ||||
|  | ||||
|   [types.ADD_PORTAL_ID]($state, portalSlug) { | ||||
|   | ||||
| @@ -22,7 +22,6 @@ describe('#actions', () => { | ||||
|         [types.CLEAR_PORTALS], | ||||
|         [types.ADD_MANY_PORTALS_ENTRY, apiResponse.payload], | ||||
|         [types.ADD_MANY_PORTALS_IDS, ['domain', 'campaign']], | ||||
|         [types.SET_PORTALS_META, { current_page: 1, portals_count: 1 }], | ||||
|         [types.SET_UI_FLAG, { isFetching: false }], | ||||
|       ]); | ||||
|     }); | ||||
| @@ -66,6 +65,36 @@ describe('#actions', () => { | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   describe('#show', () => { | ||||
|     it('sends correct actions if API is success', async () => { | ||||
|       axios.get.mockResolvedValue({ | ||||
|         data: { meta: { all_articles_count: 1 } }, | ||||
|       }); | ||||
|       await actions.show( | ||||
|         { commit }, | ||||
|         { | ||||
|           portalSlug: 'handbook', | ||||
|           locale: 'en', | ||||
|         } | ||||
|       ); | ||||
|       expect(commit.mock.calls).toEqual([ | ||||
|         [types.SET_UI_FLAG, { isFetchingItem: true }], | ||||
|         [types.SET_PORTALS_META, { all_articles_count: 1 }], | ||||
|         [types.SET_UI_FLAG, { isFetchingItem: false }], | ||||
|       ]); | ||||
|     }); | ||||
|     it('sends correct actions if API is error', async () => { | ||||
|       axios.post.mockRejectedValue({ message: 'Incorrect header' }); | ||||
|       await expect( | ||||
|         actions.create({ commit, dispatch, state: { portals: {} } }, {}) | ||||
|       ).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.payload[1] }); | ||||
|   | ||||
| @@ -107,12 +107,20 @@ describe('#mutations', () => { | ||||
|   describe('#SET_PORTALS_META', () => { | ||||
|     it('add meta to state', () => { | ||||
|       mutations[types.SET_PORTALS_META](state, { | ||||
|         portals_count: 10, | ||||
|         current_page: 1, | ||||
|       }); | ||||
|       expect(state.meta).toEqual({ | ||||
|         count: 10, | ||||
|         currentPage: 1, | ||||
|         all_articles_count: 10, | ||||
|         archived_articles_count: 10, | ||||
|         draft_articles_count: 10, | ||||
|         mine_articles_count: 10, | ||||
|       }); | ||||
|       expect(state.meta).toEqual({ | ||||
|         count: 0, | ||||
|         currentPage: 1, | ||||
|         allArticlesCount: 10, | ||||
|         archivedArticlesCount: 10, | ||||
|         draftArticlesCount: 10, | ||||
|         mineArticlesCount: 10, | ||||
|       }); | ||||
|     }); | ||||
|   }); | ||||
|   | ||||
| @@ -83,11 +83,7 @@ class Article < ApplicationRecord | ||||
|     ).search_by_category_locale(params[:locale]).search_by_author(params[:author_id]).search_by_status(params[:status]) | ||||
|  | ||||
|     records = records.text_search(params[:query]) if params[:query].present? | ||||
|     records.page(current_page(params)) | ||||
|   end | ||||
|  | ||||
|   def self.current_page(params) | ||||
|     params[:page] || 1 | ||||
|     records | ||||
|   end | ||||
|  | ||||
|   def associate_root_article(associated_article_id) | ||||
|   | ||||
| @@ -13,12 +13,6 @@ json.category do | ||||
|   json.locale article.category.locale | ||||
| end | ||||
|  | ||||
| if article.portal.present? | ||||
|   json.portal do | ||||
|     json.partial! 'api/v1/accounts/portals/portal', formats: [:json], portal: article.portal | ||||
|   end | ||||
| end | ||||
|  | ||||
| json.views article.views | ||||
|  | ||||
| if article.author.present? | ||||
|   | ||||
| @@ -8,7 +8,7 @@ json.account_id article.account_id | ||||
|  | ||||
| if article.portal.present? | ||||
|   json.portal do | ||||
|     json.partial! 'api/v1/accounts/portals/portal', formats: [:json], portal: article.portal | ||||
|     json.partial! 'api/v1/accounts/portals/portal', formats: [:json], portal: article.portal, articles: [] | ||||
|   end | ||||
| end | ||||
|  | ||||
|   | ||||
| @@ -3,10 +3,11 @@ json.payload do | ||||
| end | ||||
|  | ||||
| json.meta do | ||||
|   json.current_page @current_page | ||||
|   json.articles_count @articles_count | ||||
|   json.all_articles_count @portal_articles.size | ||||
|   json.archived_articles_count @articles.archived.size | ||||
|   json.articles_count @articles_count | ||||
|   json.current_page @current_page | ||||
|   json.draft_articles_count @all_articles.draft.size | ||||
|   json.mine_articles_count @all_articles.search_by_author(current_user.id).size if current_user.present? | ||||
|   json.published_count @articles.published.size | ||||
|   json.draft_articles_count @articles.draft.size | ||||
|   json.mine_articles_count @articles.search_by_author(current_user.id).size if current_user.present? | ||||
| end | ||||
|   | ||||
| @@ -27,5 +27,5 @@ if category.root_category.present? | ||||
| end | ||||
|  | ||||
| json.meta do | ||||
|   json.articles_count category.articles.size | ||||
|   json.articles_count category.articles.search(locale: @current_locale).size | ||||
| end | ||||
|   | ||||
| @@ -28,11 +28,11 @@ json.portal_members do | ||||
| end | ||||
|  | ||||
| json.meta do | ||||
|   json.all_articles_count portal.articles.size | ||||
|   json.archived_articles_count portal.articles.archived.size | ||||
|   json.published_count portal.articles.published.size | ||||
|   json.draft_articles_count portal.articles.draft.size | ||||
|   json.mine_articles_count portal.articles.search_by_author(current_user.id).size if current_user.present? | ||||
|   json.categories_count portal.categories.size | ||||
|   json.all_articles_count articles.try(:size) | ||||
|   json.archived_articles_count articles.try(:archived).try(:size) | ||||
|   json.published_count articles.try(:published).try(:size) | ||||
|   json.draft_articles_count articles.try(:draft).try(:size) | ||||
|   json.mine_articles_count articles.search_by_author(current_user.id).try(:size) if current_user.present? && articles.any? | ||||
|   json.categories_count portal.categories.try(:size) | ||||
|   json.default_locale portal.default_locale | ||||
| end | ||||
|   | ||||
| @@ -1 +1 @@ | ||||
| json.partial! 'portal', portal: @portal | ||||
| json.partial! 'portal', portal: @portal, articles: [] | ||||
|   | ||||
| @@ -1 +1 @@ | ||||
| json.partial! 'portal', portal: @portal | ||||
| json.partial! 'portal', portal: @portal, articles: [] | ||||
|   | ||||
| @@ -1,5 +1,7 @@ | ||||
| json.payload do | ||||
|   json.array! @portals, partial: 'portal', as: :portal | ||||
|   json.array! @portals.each do |portal| | ||||
|     json.partial! 'portal', formats: [:json], portal: portal, articles: [] | ||||
|   end | ||||
| end | ||||
|  | ||||
| json.meta do | ||||
|   | ||||
| @@ -1 +1 @@ | ||||
| json.partial! 'portal', portal: @portal | ||||
| json.partial! 'portal', portal: @portal, articles: @articles | ||||
|   | ||||
| @@ -1 +1 @@ | ||||
| json.partial! 'portal', portal: @portal | ||||
| json.partial! 'portal', portal: @portal, articles: [] | ||||
|   | ||||
| @@ -194,7 +194,8 @@ RSpec.describe 'Api::V1::Accounts::Articles', type: :request do | ||||
|         expect(response).to have_http_status(:success) | ||||
|         json_response = JSON.parse(response.body) | ||||
|         expect(json_response['payload'].count).to be 1 | ||||
|         expect(json_response['meta']['articles_count']).to be 2 | ||||
|         expect(json_response['meta']['all_articles_count']).to be 2 | ||||
|         expect(json_response['meta']['articles_count']).to be 1 | ||||
|         expect(json_response['meta']['mine_articles_count']).to be 1 | ||||
|       end | ||||
|     end | ||||
|   | ||||
| @@ -50,6 +50,25 @@ RSpec.describe 'Api::V1::Accounts::Portals', type: :request do | ||||
|         expect(response).to have_http_status(:success) | ||||
|         json_response = JSON.parse(response.body) | ||||
|         expect(json_response['name']).to eq portal.name | ||||
|         expect(json_response['meta']['all_articles_count']).to eq 0 | ||||
|       end | ||||
|  | ||||
|       it 'returns portal articles metadata' do | ||||
|         portal.update(config: { allowed_locales: %w[en es], default_locale: 'en' }) | ||||
|         en_cat = create(:category, locale: :en, portal_id: portal.id, slug: 'en-cat') | ||||
|         es_cat = create(:category, locale: :es, portal_id: portal.id, slug: 'es-cat') | ||||
|         create(:article, category_id: en_cat.id, portal_id: portal.id, author_id: agent.id) | ||||
|         create(:article, category_id: en_cat.id, portal_id: portal.id, author_id: admin.id) | ||||
|         create(:article, category_id: es_cat.id, portal_id: portal.id, author_id: agent.id) | ||||
|  | ||||
|         get "/api/v1/accounts/#{account.id}/portals/#{portal.slug}?locale=en", | ||||
|             headers: agent.create_new_auth_token | ||||
|  | ||||
|         expect(response).to have_http_status(:success) | ||||
|         json_response = JSON.parse(response.body) | ||||
|         expect(json_response['name']).to eq portal.name | ||||
|         expect(json_response['meta']['all_articles_count']).to eq 2 | ||||
|         expect(json_response['meta']['mine_articles_count']).to eq 1 | ||||
|       end | ||||
|     end | ||||
|   end | ||||
|   | ||||
| @@ -126,20 +126,5 @@ RSpec.describe Article, type: :model do | ||||
|         expect(article.slug).to include('the-awesome-article-1') | ||||
|       end | ||||
|     end | ||||
|  | ||||
|     context 'with pagination' do | ||||
|       it 'returns paginated articles' do | ||||
|         build_list(:article, 30) do |record, i| | ||||
|           record.category_id = category_2.id | ||||
|           record.title = "title #{i}" | ||||
|           record.portal_id = portal_2.id | ||||
|           record.author_id = user.id | ||||
|           record.save! | ||||
|         end | ||||
|         params = { category_slug: 'category_2' } | ||||
|         records = portal_2.articles.search(params) | ||||
|         expect(records.count).to eq(25) | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| end | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Tejaswini Chile
					Tejaswini Chile