From 32b63ad0c06f715b163879bf1a7f8a27005381a1 Mon Sep 17 00:00:00 2001
From: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Date: Fri, 19 Jun 2026 12:41:21 +0530
Subject: [PATCH] feat: add search to Help Center admin pages (#14772)
---
.../dashboard/api/helpCenter/articles.js | 2 +
.../HelpCenter/ArticleCard/ArticleCard.vue | 16 +++---
.../HelpCenter/HelpCenterLayout.vue | 7 ++-
.../Pages/ArticlePage/ArticleList.vue | 5 ++
.../Pages/ArticlePage/ArticlesPage.vue | 34 ++++++++++++-
.../Pages/CategoryPage/CategoriesPage.vue | 23 ++++++++-
.../CategoryPage/CategoryHeaderControls.vue | 51 ++++++++++++-------
.../Pages/CategoryPage/CategoryList.vue | 6 ++-
.../Pages/LocalePage/LocalesPage.vue | 49 +++++++++++++++---
.../dashboard/i18n/locale/en/helpCenter.json | 15 ++++++
.../pages/PortalsArticlesIndexPage.vue | 15 +++++-
.../modules/helpCenterArticles/actions.js | 3 +-
12 files changed, 184 insertions(+), 42 deletions(-)
diff --git a/app/javascript/dashboard/api/helpCenter/articles.js b/app/javascript/dashboard/api/helpCenter/articles.js
index bab45bcb5..55b620d1a 100644
--- a/app/javascript/dashboard/api/helpCenter/articles.js
+++ b/app/javascript/dashboard/api/helpCenter/articles.js
@@ -16,6 +16,7 @@ class ArticlesAPI extends PortalsAPI {
authorId,
categorySlug,
sort,
+ query,
}) {
const url = getArticleSearchURL({
pageNumber,
@@ -25,6 +26,7 @@ class ArticlesAPI extends PortalsAPI {
authorId,
categorySlug,
sort,
+ query,
host: this.url,
});
diff --git a/app/javascript/dashboard/components-next/HelpCenter/ArticleCard/ArticleCard.vue b/app/javascript/dashboard/components-next/HelpCenter/ArticleCard/ArticleCard.vue
index 9b1c48a90..564888f0d 100644
--- a/app/javascript/dashboard/components-next/HelpCenter/ArticleCard/ArticleCard.vue
+++ b/app/javascript/dashboard/components-next/HelpCenter/ArticleCard/ArticleCard.vue
@@ -210,9 +210,9 @@ const handleClick = id => {
-
-
-
+
+
+
{
{{ authorName || '-' }}
-
+
- {{ categoryName }}
+ {{ categoryName }}
@@ -247,7 +245,7 @@ const handleClick = id => {
-
+
{{ lastUpdatedAt }}
diff --git a/app/javascript/dashboard/components-next/HelpCenter/HelpCenterLayout.vue b/app/javascript/dashboard/components-next/HelpCenter/HelpCenterLayout.vue
index 5b6399da7..fb564622a 100644
--- a/app/javascript/dashboard/components-next/HelpCenter/HelpCenterLayout.vue
+++ b/app/javascript/dashboard/components-next/HelpCenter/HelpCenterLayout.vue
@@ -67,11 +67,11 @@ const togglePortalSwitcher = () => {
>
{{ activePortalName }}
-
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 575eaa828..75aeb86d1 100644
--- a/app/javascript/dashboard/components-next/HelpCenter/Pages/ArticlePage/ArticleList.vue
+++ b/app/javascript/dashboard/components-next/HelpCenter/Pages/ArticlePage/ArticleList.vue
@@ -24,6 +24,10 @@ const props = defineProps({
type: Set,
default: () => new Set(),
},
+ isSearching: {
+ type: Boolean,
+ default: false,
+ },
});
const emit = defineEmits(['translateArticle', 'toggleSelect']);
@@ -41,6 +45,7 @@ const hoveredArticleId = ref(null);
const dragEnabled = computed(() => {
return (
props.isCategoryArticles &&
+ !props.isSearching &&
localArticles.value?.length > 1 &&
props.selectedArticleIds.size === 0
);
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 68c93c007..66bcf6831 100644
--- a/app/javascript/dashboard/components-next/HelpCenter/Pages/ArticlePage/ArticlesPage.vue
+++ b/app/javascript/dashboard/components-next/HelpCenter/Pages/ArticlePage/ArticlesPage.vue
@@ -5,6 +5,7 @@ import { useI18n } from 'vue-i18n';
import { OnClickOutside } from '@vueuse/components';
import { useMapGetter } from 'dashboard/composables/store.js';
import { useConfig } from 'dashboard/composables/useConfig';
+import { debounce } from '@chatwoot/utils';
import { ARTICLE_TABS, CATEGORY_ALL } from 'dashboard/helper/portalHelper';
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
import { useAlert } from 'dashboard/composables';
@@ -18,6 +19,7 @@ import Spinner from 'dashboard/components-next/spinner/Spinner.vue';
import ArticleEmptyState from 'dashboard/components-next/HelpCenter/EmptyState/Article/ArticleEmptyState.vue';
import BulkSelectBar from 'dashboard/components-next/captain/assistant/BulkSelectBar.vue';
import Button from 'dashboard/components-next/button/Button.vue';
+import Input from 'dashboard/components-next/input/Input.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';
@@ -49,7 +51,12 @@ const props = defineProps({
},
});
-const emit = defineEmits(['pageChange', 'fetchPortal', 'refreshArticles']);
+const emit = defineEmits([
+ 'pageChange',
+ 'fetchPortal',
+ 'refreshArticles',
+ 'search',
+]);
const router = useRouter();
const route = useRoute();
@@ -65,6 +72,9 @@ const isFeatureEnabledonAccount = useMapGetter(
const selectedArticleIds = ref(new Set());
const deleteConfirmDialogRef = ref(null);
const isCategoryMenuOpen = ref(false);
+const searchQuery = ref(route.query.search || '');
+
+const debouncedSearch = debounce(() => emit('search', searchQuery.value), 500);
const { isEnterprise } = useConfig();
@@ -122,6 +132,7 @@ const updateRoute = newParams => {
categorySlug: newParams.categorySlug ?? categorySlug,
...newParams,
},
+ query: route.query,
});
};
@@ -145,7 +156,12 @@ const showCategoryHeaderControls = computed(
() => props.isCategoryArticles && !isSwitchingPortal.value
);
+const isSearching = computed(() => Boolean(searchQuery.value?.trim()));
+
const getEmptyStateText = type => {
+ if (isSearching.value) {
+ return t(`HELP_CENTER.ARTICLES_PAGE.EMPTY_STATE.SEARCH.${type}`);
+ }
if (props.isCategoryArticles) {
return t(`HELP_CENTER.ARTICLES_PAGE.EMPTY_STATE.CATEGORY.${type}`);
}
@@ -291,6 +307,19 @@ watch(
:show-pagination-footer="shouldShowPaginationFooter"
@update:current-page="handlePageChange"
>
+
+
+
props.isFetching || isSwitchingPortal.value);
-const hasCategories = computed(() => props.categories?.length > 0);
+
+const filteredCategories = computed(() => {
+ const query = searchQuery.value.trim().toLowerCase();
+ if (!query) return props.categories;
+ return props.categories.filter(category =>
+ category.name?.toLowerCase().includes(query)
+ );
+});
+
+const hasCategories = computed(() => filteredCategories.value?.length > 0);
+const isSearching = computed(() => searchQuery.value.trim().length > 0);
const updateRoute = (newParams, routeName) => {
const { accountId, portalSlug, locale } = route.params;
@@ -115,6 +126,7 @@ const reorderCategories = async reorderedGroup => {
{
+
{
:items="breadcrumbItems"
@click="handleBreadcrumbClick"
/>
-
-
-
-
-
+
diff --git a/app/javascript/dashboard/components-next/HelpCenter/Pages/CategoryPage/CategoryList.vue b/app/javascript/dashboard/components-next/HelpCenter/Pages/CategoryPage/CategoryList.vue
index 45e783246..a306c54e5 100644
--- a/app/javascript/dashboard/components-next/HelpCenter/Pages/CategoryPage/CategoryList.vue
+++ b/app/javascript/dashboard/components-next/HelpCenter/Pages/CategoryPage/CategoryList.vue
@@ -8,6 +8,10 @@ const props = defineProps({
type: Array,
required: true,
},
+ disableDrag: {
+ type: Boolean,
+ default: false,
+ },
});
const emit = defineEmits(['click', 'action', 'reorder']);
@@ -15,7 +19,7 @@ const emit = defineEmits(['click', 'action', 'reorder']);
const localCategories = ref(props.categories);
const dragEnabled = computed(() => {
- return localCategories.value?.length > 1;
+ return !props.disableDrag && localCategories.value?.length > 1;
});
const handleClick = slug => {
diff --git a/app/javascript/dashboard/components-next/HelpCenter/Pages/LocalePage/LocalesPage.vue b/app/javascript/dashboard/components-next/HelpCenter/Pages/LocalePage/LocalesPage.vue
index 22dd42edd..fc3b428aa 100644
--- a/app/javascript/dashboard/components-next/HelpCenter/Pages/LocalePage/LocalesPage.vue
+++ b/app/javascript/dashboard/components-next/HelpCenter/Pages/LocalePage/LocalesPage.vue
@@ -2,8 +2,12 @@
import { computed, ref } from 'vue';
import { useMapGetter } from 'dashboard/composables/store.js';
+import { useI18n } from 'vue-i18n';
+
import HelpCenterLayout from 'dashboard/components-next/HelpCenter/HelpCenterLayout.vue';
import Button from 'dashboard/components-next/button/Button.vue';
+import Input from 'dashboard/components-next/input/Input.vue';
+import EmptyStateLayout from 'dashboard/components-next/EmptyStateLayout.vue';
import Spinner from 'dashboard/components-next/spinner/Spinner.vue';
import LocaleList from 'dashboard/components-next/HelpCenter/Pages/LocalePage/LocaleList.vue';
import AddLocaleDialog from 'dashboard/components-next/HelpCenter/Pages/LocalePage/AddLocaleDialog.vue';
@@ -19,7 +23,10 @@ const props = defineProps({
},
});
+const { t } = useI18n();
+
const addLocaleDialogRef = ref(null);
+const searchQuery = ref('');
const isSwitchingPortal = useMapGetter('portals/isSwitchingPortal');
@@ -28,6 +35,19 @@ const openAddLocaleDialog = () => {
};
const localeCount = computed(() => props.locales?.length);
+
+const filteredLocales = computed(() => {
+ const query = searchQuery.value.trim().toLowerCase();
+ if (!query) return props.locales;
+ return props.locales.filter(
+ locale =>
+ locale.name?.toLowerCase().includes(query) ||
+ locale.code?.toLowerCase().includes(query)
+ );
+});
+
+const isSearching = computed(() => searchQuery.value.trim().length > 0);
+const hasResults = computed(() => filteredLocales.value?.length > 0);
@@ -39,12 +59,21 @@ const localeCount = computed(() => props.locales?.length);
{{ $t('HELP_CENTER.LOCALES_PAGE.LOCALES_COUNT', localeCount) }}
-
+
+
+
+
@@ -54,7 +83,13 @@ const localeCount = computed(() => props.locales?.length);
>
-
+
+
diff --git a/app/javascript/dashboard/i18n/locale/en/helpCenter.json b/app/javascript/dashboard/i18n/locale/en/helpCenter.json
index dfaec8119..ae099a7a9 100644
--- a/app/javascript/dashboard/i18n/locale/en/helpCenter.json
+++ b/app/javascript/dashboard/i18n/locale/en/helpCenter.json
@@ -552,6 +552,7 @@
"LOCALE": {
"ALL": "All locales"
},
+ "SEARCH_PLACEHOLDER": "Search articles...",
"NEW_ARTICLE": "New article"
},
"EMPTY_STATE": {
@@ -579,6 +580,10 @@
"CATEGORY": {
"TITLE": "There are no articles in this category",
"SUBTITLE": "Articles in this category will appear here"
+ },
+ "SEARCH": {
+ "TITLE": "No matching articles",
+ "SUBTITLE": "We couldn't find any articles matching your search. Try a different term."
}
},
"BULK_TRANSLATE": {
@@ -624,6 +629,7 @@
"CATEGORY_HEADER": {
"NEW_CATEGORY": "New category",
"EDIT_CATEGORY": "Edit category",
+ "SEARCH_PLACEHOLDER": "Search categories...",
"CATEGORIES_COUNT": "{n} category | {n} categories",
"BREADCRUMB": {
"CATEGORY_LOCALE": "Categories ({localeCode})",
@@ -634,6 +640,10 @@
"TITLE": "No categories found",
"SUBTITLE": "Categories will appear here. You can add a category by clicking the 'New Category' button."
},
+ "SEARCH_EMPTY_STATE": {
+ "TITLE": "No matching categories",
+ "SUBTITLE": "We couldn't find any categories matching your search. Try a different term."
+ },
"CATEGORY_CARD": {
"ARTICLES_COUNT": "{count} article | {count} articles"
},
@@ -691,6 +701,11 @@
"LOCALES_PAGE": {
"LOCALES_COUNT": "No locales available | {n} locale | {n} locales",
"NEW_LOCALE_BUTTON_TEXT": "New locale",
+ "SEARCH_PLACEHOLDER": "Search locales...",
+ "SEARCH_EMPTY_STATE": {
+ "TITLE": "No matching locales",
+ "SUBTITLE": "We couldn't find any locales matching your search. Try a different term."
+ },
"LOCALE_CARD": {
"ARTICLES_COUNT": "{count} article | {count} articles",
"CATEGORIES_COUNT": "{count} category | {count} categories",
diff --git a/app/javascript/dashboard/routes/dashboard/helpcenter/pages/PortalsArticlesIndexPage.vue b/app/javascript/dashboard/routes/dashboard/helpcenter/pages/PortalsArticlesIndexPage.vue
index c0fc800e6..5b563203d 100644
--- a/app/javascript/dashboard/routes/dashboard/helpcenter/pages/PortalsArticlesIndexPage.vue
+++ b/app/javascript/dashboard/routes/dashboard/helpcenter/pages/PortalsArticlesIndexPage.vue
@@ -1,15 +1,17 @@