From 42a244369d4bd9c13de96ca2ac87ec0dd8e599a2 Mon Sep 17 00:00:00 2001 From: Sojan Jose Date: Wed, 4 Mar 2026 23:23:38 -0800 Subject: [PATCH] feat(help-center): enable drag-and-drop category reordering (#13706) --- .../api/v1/accounts/articles_controller.rb | 2 +- .../api/v1/accounts/categories_controller.rb | 9 ++- .../dashboard/api/helpCenter/categories.js | 6 ++ .../api/specs/helpCenter/categories.spec.js | 1 + .../Pages/ArticlePage/ArticleList.vue | 20 +++-- .../Pages/CategoryPage/CategoriesPage.vue | 12 +++ .../Pages/CategoryPage/CategoryList.vue | 76 ++++++++++++++---- .../dashboard/i18n/locale/en/helpCenter.json | 12 ++- .../pages/PortalsArticlesIndexPage.vue | 10 ++- .../pages/PortalsCategoriesIndexPage.vue | 2 +- .../modules/helpCenterArticles/actions.js | 17 +++- .../modules/helpCenterArticles/getters.js | 10 +++ .../modules/helpCenterArticles/mutations.js | 12 +++ .../helpCenterArticles/specs/action.spec.js | 59 ++++++++++++++ .../helpCenterArticles/specs/getters.spec.js | 78 +++++++++++++++++++ .../helpCenterArticles/specs/mutation.spec.js | 57 +++++++++++++- .../modules/helpCenterCategories/actions.js | 19 +++++ .../modules/helpCenterCategories/getters.js | 10 +++ .../modules/helpCenterCategories/mutations.js | 12 +++ .../specs/actions.spec.js | 59 ++++++++++++++ .../specs/getters.spec.js | 78 +++++++++++++++++++ .../specs/mutations.spec.js | 42 +++++++++- .../dashboard/store/mutation-types.js | 2 + app/models/article.rb | 12 +-- app/models/category.rb | 10 +++ app/models/portal.rb | 6 ++ app/policies/category_policy.rb | 4 + config/routes.rb | 4 +- .../policies/enterprise/category_policy.rb | 4 + .../v1/accounts/articles_controller_spec.rb | 32 ++++++++ .../v1/accounts/categories_controller_spec.rb | 41 ++++++++++ .../v1/accounts/articles_controller_spec.rb | 14 ++++ .../v1/accounts/categories_controller_spec.rb | 23 ++++++ 33 files changed, 708 insertions(+), 47 deletions(-) diff --git a/app/controllers/api/v1/accounts/articles_controller.rb b/app/controllers/api/v1/accounts/articles_controller.rb index 8a6fd61f8..5e1609b64 100644 --- a/app/controllers/api/v1/accounts/articles_controller.rb +++ b/app/controllers/api/v1/accounts/articles_controller.rb @@ -40,7 +40,7 @@ class Api::V1::Accounts::ArticlesController < Api::V1::Accounts::BaseController end def reorder - Article.update_positions(params[:positions_hash]) + Article.update_positions(portal: @portal, positions_hash: params[:positions_hash]) head :ok end diff --git a/app/controllers/api/v1/accounts/categories_controller.rb b/app/controllers/api/v1/accounts/categories_controller.rb index 834b19ed9..686ffaeec 100644 --- a/app/controllers/api/v1/accounts/categories_controller.rb +++ b/app/controllers/api/v1/accounts/categories_controller.rb @@ -1,7 +1,7 @@ class Api::V1::Accounts::CategoriesController < Api::V1::Accounts::BaseController before_action :portal before_action :check_authorization - before_action :fetch_category, except: [:index, :create] + before_action :fetch_category, except: [:index, :create, :reorder] before_action :set_current_page, only: [:index] def index @@ -32,6 +32,11 @@ class Api::V1::Accounts::CategoriesController < Api::V1::Accounts::BaseControlle head :ok end + def reorder + Category.update_positions(portal: @portal, positions_hash: params[:positions_hash]) + head :ok + end + private def fetch_category @@ -39,7 +44,7 @@ class Api::V1::Accounts::CategoriesController < Api::V1::Accounts::BaseControlle end def portal - @portal ||= Current.account.portals.find_by(slug: params[:portal_id]) + @portal ||= Current.account.portals.find_by!(slug: params[:portal_id]) end def related_categories_records diff --git a/app/javascript/dashboard/api/helpCenter/categories.js b/app/javascript/dashboard/api/helpCenter/categories.js index 01658497e..eda54aadb 100644 --- a/app/javascript/dashboard/api/helpCenter/categories.js +++ b/app/javascript/dashboard/api/helpCenter/categories.js @@ -25,6 +25,12 @@ class CategoriesAPI extends PortalsAPI { delete({ portalSlug, categoryId }) { return axios.delete(`${this.url}/${portalSlug}/categories/${categoryId}`); } + + reorder({ portalSlug, reorderedGroup }) { + return axios.post(`${this.url}/${portalSlug}/categories/reorder`, { + positions_hash: reorderedGroup, + }); + } } export default new CategoriesAPI(); diff --git a/app/javascript/dashboard/api/specs/helpCenter/categories.spec.js b/app/javascript/dashboard/api/specs/helpCenter/categories.spec.js index 2c56f4e00..febf6f7a1 100644 --- a/app/javascript/dashboard/api/specs/helpCenter/categories.spec.js +++ b/app/javascript/dashboard/api/specs/helpCenter/categories.spec.js @@ -8,5 +8,6 @@ describe('#BulkActionsAPI', () => { expect(categoriesAPI).toHaveProperty('create'); expect(categoriesAPI).toHaveProperty('update'); expect(categoriesAPI).toHaveProperty('delete'); + expect(categoriesAPI).toHaveProperty('reorder'); }); }); diff --git a/app/javascript/dashboard/components-next/HelpCenter/Pages/ArticlePage/ArticleList.vue b/app/javascript/dashboard/components-next/HelpCenter/Pages/ArticlePage/ArticleList.vue index 756857fa0..cc7c97000 100644 --- a/app/javascript/dashboard/components-next/HelpCenter/Pages/ArticlePage/ArticleList.vue +++ b/app/javascript/dashboard/components-next/HelpCenter/Pages/ArticlePage/ArticleList.vue @@ -58,18 +58,22 @@ const openArticle = id => { } }; -const onReorder = reorderedGroup => { - store.dispatch('articles/reorder', { - reorderedGroup, - portalSlug: route.params.portalSlug, - }); +const onReorder = async reorderedGroup => { + try { + await store.dispatch('articles/reorder', { + reorderedGroup, + portalSlug: route.params.portalSlug, + }); + } catch { + useAlert(t('HELP_CENTER.REORDER_ARTICLE.API.ERROR_MESSAGE')); + } }; const onDragEnd = () => { - // Reuse existing positions to maintain order within the current group + // Collect and sort existing positions, falling back to index+1 for null/0 values const sortedArticlePositions = localArticles.value - .map(article => article.position) - .sort((a, b) => a - b); // Use custom sort to handle numeric values correctly + .map((article, index) => article.position || index + 1) + .sort((a, b) => a - b); const orderedArticles = localArticles.value.map(article => article.id); diff --git a/app/javascript/dashboard/components-next/HelpCenter/Pages/CategoryPage/CategoriesPage.vue b/app/javascript/dashboard/components-next/HelpCenter/Pages/CategoryPage/CategoriesPage.vue index 12acd1bd8..1f4e4b347 100644 --- a/app/javascript/dashboard/components-next/HelpCenter/Pages/CategoryPage/CategoriesPage.vue +++ b/app/javascript/dashboard/components-next/HelpCenter/Pages/CategoryPage/CategoriesPage.vue @@ -98,6 +98,17 @@ const handleAction = ({ action, id, category: categoryData }) => { deleteCategory(categoryData); } }; + +const reorderCategories = async reorderedGroup => { + try { + await store.dispatch('categories/reorder', { + portalSlug: route.params.portalSlug, + reorderedGroup, + }); + } catch { + useAlert(t('HELP_CENTER.REORDER_CATEGORY.API.ERROR_MESSAGE')); + } +};