From b33eecaff7d6060b96f11140e9930350ce900f87 Mon Sep 17 00:00:00 2001 From: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Date: Tue, 26 May 2026 17:32:58 +0530 Subject: [PATCH] feat: bulk change category for articles (#14443) --- .../articles/bulk_actions_controller.rb | 18 +++++- .../dashboard/api/helpCenter/articles.js | 7 +++ .../dashboard/api/specs/article.spec.js | 29 ++++++++++ .../Pages/ArticlePage/ArticlesPage.vue | 55 +++++++++++++++++++ .../Pages/CategoryPage/CategoryForm.vue | 2 +- .../dashboard/i18n/locale/en/helpCenter.json | 3 + config/routes.rb | 1 + 7 files changed, 113 insertions(+), 2 deletions(-) diff --git a/app/controllers/api/v1/accounts/articles/bulk_actions_controller.rb b/app/controllers/api/v1/accounts/articles/bulk_actions_controller.rb index b45c16828..7ea9cab5d 100644 --- a/app/controllers/api/v1/accounts/articles/bulk_actions_controller.rb +++ b/app/controllers/api/v1/accounts/articles/bulk_actions_controller.rb @@ -1,7 +1,7 @@ class Api::V1::Accounts::Articles::BulkActionsController < Api::V1::Accounts::BaseController before_action :portal before_action :check_authorization - before_action :set_articles, only: [:update_status, :delete_articles] + before_action :set_articles, only: [:update_status, :update_category, :delete_articles] def translate head :not_implemented @@ -19,6 +19,18 @@ class Api::V1::Accounts::Articles::BulkActionsController < Api::V1::Accounts::Ba render_could_not_create_error(e.message) end + def update_category + return render_could_not_create_error(I18n.t('portals.articles.no_articles_found')) if @articles.none? + return render_could_not_create_error(I18n.t('portals.articles.category_not_found')) unless category_valid? + + ActiveRecord::Base.transaction do + @articles.find_each { |article| article.update!(category_id: params[:category_id]) } + end + head :ok + rescue ActiveRecord::RecordInvalid => e + render_could_not_create_error(e.message) + end + def delete_articles return render_could_not_create_error(I18n.t('portals.articles.no_articles_found')) if @articles.none? @@ -39,5 +51,9 @@ class Api::V1::Accounts::Articles::BulkActionsController < Api::V1::Accounts::Ba def set_articles @articles = @portal.articles.where(id: params[:ids]) end + + def category_valid? + @portal.categories.exists?(id: params[:category_id]) + end end Api::V1::Accounts::Articles::BulkActionsController.prepend_mod_with('Api::V1::Accounts::Articles::BulkActionsController') diff --git a/app/javascript/dashboard/api/helpCenter/articles.js b/app/javascript/dashboard/api/helpCenter/articles.js index c79aa5da7..bab45bcb5 100644 --- a/app/javascript/dashboard/api/helpCenter/articles.js +++ b/app/javascript/dashboard/api/helpCenter/articles.js @@ -87,6 +87,13 @@ class ArticlesAPI extends PortalsAPI { ); } + bulkUpdateCategory({ portalSlug, articleIds, categoryId }) { + return axios.patch( + `${this.url}/${portalSlug}/articles/bulk_actions/update_category`, + { ids: articleIds, category_id: categoryId } + ); + } + bulkDelete({ portalSlug, articleIds }) { return axios.delete( `${this.url}/${portalSlug}/articles/bulk_actions/delete_articles`, diff --git a/app/javascript/dashboard/api/specs/article.spec.js b/app/javascript/dashboard/api/specs/article.spec.js index 71128682c..b40613739 100644 --- a/app/javascript/dashboard/api/specs/article.spec.js +++ b/app/javascript/dashboard/api/specs/article.spec.js @@ -153,4 +153,33 @@ describe('#PortalAPI', () => { ); }); }); + describe('API calls', () => { + const originalAxios = window.axios; + const axiosMock = { + post: vi.fn(() => Promise.resolve()), + get: vi.fn(() => Promise.resolve()), + patch: vi.fn(() => Promise.resolve()), + delete: vi.fn(() => Promise.resolve()), + }; + + beforeEach(() => { + window.axios = axiosMock; + }); + + afterEach(() => { + window.axios = originalAxios; + }); + + it('#bulkUpdateCategory', () => { + articlesAPI.bulkUpdateCategory({ + portalSlug: 'room-rental', + articleIds: [1, 2, 3], + categoryId: 7, + }); + expect(axiosMock.patch).toHaveBeenCalledWith( + '/api/v1/portals/room-rental/articles/bulk_actions/update_category', + { ids: [1, 2, 3], category_id: 7 } + ); + }); + }); }); diff --git a/app/javascript/dashboard/components-next/HelpCenter/Pages/ArticlePage/ArticlesPage.vue b/app/javascript/dashboard/components-next/HelpCenter/Pages/ArticlePage/ArticlesPage.vue index fde215824..c15578533 100644 --- a/app/javascript/dashboard/components-next/HelpCenter/Pages/ArticlePage/ArticlesPage.vue +++ b/app/javascript/dashboard/components-next/HelpCenter/Pages/ArticlePage/ArticlesPage.vue @@ -2,6 +2,7 @@ import { ref, computed, watch } from 'vue'; import { useRouter, useRoute } from 'vue-router'; import { useI18n } from 'vue-i18n'; +import { OnClickOutside } from '@vueuse/components'; import { useMapGetter } from 'dashboard/composables/store.js'; import { useConfig } from 'dashboard/composables/useConfig'; import { ARTICLE_TABS, CATEGORY_ALL } from 'dashboard/helper/portalHelper'; @@ -18,6 +19,7 @@ import ArticleEmptyState from 'dashboard/components-next/HelpCenter/EmptyState/A import BulkSelectBar from 'dashboard/components-next/captain/assistant/BulkSelectBar.vue'; import Button from 'dashboard/components-next/button/Button.vue'; import Dialog from 'dashboard/components-next/dialog/Dialog.vue'; +import DropdownMenu from 'dashboard/components-next/dropdown-menu/DropdownMenu.vue'; import BulkTranslateDialog from './BulkTranslateDialog.vue'; const props = defineProps({ @@ -62,6 +64,7 @@ const isFeatureEnabledonAccount = useMapGetter( const selectedArticleIds = ref(new Set()); const deleteConfirmDialogRef = ref(null); +const isCategoryMenuOpen = ref(false); const { isEnterprise } = useConfig(); @@ -221,6 +224,34 @@ const bulkUpdateStatus = async status => { } }; +const categoryMenuItems = computed(() => + props.categories.map(category => ({ + label: category.name, + value: category.id, + action: 'move', + emoji: category.icon, + })) +); + +const handleBulkUpdateCategory = async ({ value }) => { + isCategoryMenuOpen.value = false; + try { + await articlesAPI.bulkUpdateCategory({ + portalSlug: route.params.portalSlug, + articleIds: [...selectedArticleIds.value], + categoryId: value, + }); + onBulkActionSuccess( + t('HELP_CENTER.ARTICLES_PAGE.BULK_ACTIONS.CATEGORY_SUCCESS') + ); + } catch (error) { + useAlert( + error?.message || + t('HELP_CENTER.ARTICLES_PAGE.BULK_ACTIONS.CATEGORY_ERROR') + ); + } +}; + const confirmBulkDelete = () => { deleteConfirmDialogRef.value?.open(); }; @@ -340,6 +371,30 @@ watch( class="[&>span:nth-child(2)]:hidden sm:[&>span:nth-child(2)]:inline w-fit" @click="bulkUpdateStatus('archived')" /> +