From ecd9c26c8cd2c334fa7e08e61ee77cb5252417d4 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 2 Jun 2026 11:49:23 +0200 Subject: [PATCH] feat: Implemented search results page functionality (#11086) # Pull Request Template ## Description Implemented search results page functionality. Now you can press "Enter" to search by term and display results in a results page. Also now you can link to /hc/{account}/en/search?query=XXXXXX to view search results for XXXXXX query. fixes: https://github.com/chatwoot/chatwoot/issues/10945 ## Screenshots Classic layout search results: classic-results Classic layout pagination: classic-page-two Classic layout empty search: no-results Documentation layout search results: documentation-results Documentation layout dark theme: documentation-dark Plain embedded dark layout: plain-embedded-dark --------- Co-authored-by: Shivam Mishra Co-authored-by: Sojan Jose Co-authored-by: Pranav Co-authored-by: Muhsin Keloth Co-authored-by: Vinay Keerthi <11478411+stonecharioteer@users.noreply.github.com> --- .../api/v1/portals/search_controller.rb | 31 +++++ app/helpers/portal_helper.rb | 13 +- app/javascript/entrypoints/portal.js | 6 +- app/javascript/portal/application.scss | 2 +- .../portal/components/PublicArticleSearch.vue | 37 ++++-- .../portal/components/TableOfContents.vue | 4 +- app/javascript/portal/portalHelpers.js | 5 +- app/models/article.rb | 4 + app/views/layouts/_portal_head.html.erb | 2 +- app/views/layouts/_portal_scripts.html.erb | 1 + .../documentation_layout/_hero.html.erb | 4 +- .../documentation_layout/_topbar.html.erb | 2 +- .../api/v1/portals/search/_form.html.erb | 20 +++ .../portals/search/_search_handler.html.erb | 33 +++++ .../search/index.html+documentation.erb | 64 ++++++++++ .../api/v1/portals/search/index.html.erb | 118 ++++++++++++++++++ config/locales/en.yml | 9 ++ config/routes.rb | 1 + .../api/v1/portals/search_controller.rb | 9 ++ .../app/models/enterprise/concerns/article.rb | 12 +- package.json | 2 +- pnpm-lock.yaml | 30 +++-- spec/helpers/portal_helper_spec.rb | 12 +- spec/models/article_spec.rb | 9 ++ 24 files changed, 385 insertions(+), 45 deletions(-) create mode 100644 app/controllers/public/api/v1/portals/search_controller.rb create mode 100644 app/views/public/api/v1/portals/search/_form.html.erb create mode 100644 app/views/public/api/v1/portals/search/_search_handler.html.erb create mode 100644 app/views/public/api/v1/portals/search/index.html+documentation.erb create mode 100644 app/views/public/api/v1/portals/search/index.html.erb create mode 100644 enterprise/app/controllers/enterprise/public/api/v1/portals/search_controller.rb diff --git a/app/controllers/public/api/v1/portals/search_controller.rb b/app/controllers/public/api/v1/portals/search_controller.rb new file mode 100644 index 000000000..104741773 --- /dev/null +++ b/app/controllers/public/api/v1/portals/search_controller.rb @@ -0,0 +1,31 @@ +class Public::Api::V1::Portals::SearchController < Public::Api::V1::Portals::BaseController + before_action :ensure_custom_domain_request, only: [:index] + before_action :portal + before_action :set_portal_layout + before_action :set_view_variant + before_action :ensure_portal_feature_enabled + layout 'portal' + + def index + @query = params[:query].to_s.strip + @articles = @portal.articles.published.includes(:category).where(locale: params[:locale]) + + search_articles + + @articles = @articles.page(params[:page]).per(10) + end + + private + + def search_articles + @articles = @query.present? ? @articles.search(search_params) : @articles.none + end + + def search_params + params.permit(:query, :locale, :sort, :status, :page).tap do |permitted| + permitted[:query] = @query + end + end +end + +Public::Api::V1::Portals::SearchController.prepend_mod_with('Public::Api::V1::Portals::SearchController') diff --git a/app/helpers/portal_helper.rb b/app/helpers/portal_helper.rb index 65166145c..0c993ec59 100644 --- a/app/helpers/portal_helper.rb +++ b/app/helpers/portal_helper.rb @@ -45,9 +45,16 @@ module PortalHelper theme.present? && theme != 'system' ? "?theme=#{theme}" : '' end + def portal_query_string(theme, is_plain_layout_enabled) + query_params = {} + query_params[:theme] = theme if theme.present? && theme != 'system' + query_params[:show_plain_layout] = true if is_plain_layout_enabled + query_params.present? ? "?#{query_params.to_query}" : '' + end + def generate_home_link(portal_slug, portal_locale, theme, is_plain_layout_enabled) if is_plain_layout_enabled - "/hc/#{portal_slug}/#{portal_locale}#{theme_query_string(theme)}" + "/hc/#{portal_slug}/#{portal_locale}#{portal_query_string(theme, is_plain_layout_enabled)}" else "/hc/#{portal_slug}/#{portal_locale}" end @@ -61,7 +68,7 @@ module PortalHelper is_plain_layout_enabled = params[:is_plain_layout_enabled] if is_plain_layout_enabled - "/hc/#{portal_slug}/#{category_locale}/categories/#{category_slug}#{theme_query_string(theme)}" + "/hc/#{portal_slug}/#{category_locale}/categories/#{category_slug}#{portal_query_string(theme, is_plain_layout_enabled)}" else "/hc/#{portal_slug}/#{category_locale}/categories/#{category_slug}" end @@ -69,7 +76,7 @@ module PortalHelper def generate_article_link(portal_slug, article_slug, theme, is_plain_layout_enabled) if is_plain_layout_enabled - "/hc/#{portal_slug}/articles/#{article_slug}#{theme_query_string(theme)}" + "/hc/#{portal_slug}/articles/#{article_slug}#{portal_query_string(theme, is_plain_layout_enabled)}" else "/hc/#{portal_slug}/articles/#{article_slug}" end diff --git a/app/javascript/entrypoints/portal.js b/app/javascript/entrypoints/portal.js index 42493615d..a1b58e390 100644 --- a/app/javascript/entrypoints/portal.js +++ b/app/javascript/entrypoints/portal.js @@ -1,9 +1,9 @@ import Rails from '@rails/ujs'; -import Turbolinks from 'turbolinks'; +import { Turbo } from '@hotwired/turbo-rails'; import '../portal/application.scss'; import { InitializationHelpers } from '../portal/portalHelpers'; Rails.start(); -Turbolinks.start(); +Turbo.start(); -document.addEventListener('turbolinks:load', InitializationHelpers.onLoad); +document.addEventListener('turbo:load', InitializationHelpers.onLoad); diff --git a/app/javascript/portal/application.scss b/app/javascript/portal/application.scss index 6d83f701a..00368d26a 100644 --- a/app/javascript/portal/application.scss +++ b/app/javascript/portal/application.scss @@ -55,6 +55,6 @@ body { } } -.turbolinks-progress-bar { +.turbo-progress-bar { background-color: var(--dynamic-portal-color); } diff --git a/app/javascript/portal/components/PublicArticleSearch.vue b/app/javascript/portal/components/PublicArticleSearch.vue index 086073fed..ebfe5b390 100644 --- a/app/javascript/portal/components/PublicArticleSearch.vue +++ b/app/javascript/portal/components/PublicArticleSearch.vue @@ -138,21 +138,40 @@ export default { this.isLoading = false; } }, + handleSubmit() { + const query = this.normalizedSearchTerm; + if (!query) return; + + const searchParams = new URLSearchParams({ query }); + const { theme, isPlainLayoutEnabled } = window.portalConfig; + + if (theme) searchParams.set('theme', theme); + if (isPlainLayoutEnabled === 'true') { + searchParams.set('show_plain_layout', 'true'); + } + + window.location.href = `/hc/${this.portalSlug}/${this.localeCode}/search?${searchParams.toString()}`; + }, }, };