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:
<img width="3840" height="2160" alt="classic-results"
src="https://github.com/user-attachments/assets/3bbb3272-33ca-4eb4-b80a-76ed77442088"
/>

Classic layout pagination:
<img width="3840" height="2160" alt="classic-page-two"
src="https://github.com/user-attachments/assets/062b09d3-7c58-4d3b-8611-b94375e7db51"
/>

Classic layout empty search:
<img width="3840" height="2160" alt="no-results"
src="https://github.com/user-attachments/assets/c5e3f47a-cd9a-4e14-ae92-ccba00c89e98"
/>

Documentation layout search results:
<img width="3840" height="2160" alt="documentation-results"
src="https://github.com/user-attachments/assets/9e45d8d9-c975-4589-b6c6-3bc7bb3c588e"
/>

Documentation layout dark theme:
<img width="3840" height="2160" alt="documentation-dark"
src="https://github.com/user-attachments/assets/cdb6ed63-4241-4b32-9f79-7d92ed479fc8"
/>

Plain embedded dark layout:
<img width="3840" height="2160" alt="plain-embedded-dark"
src="https://github.com/user-attachments/assets/7deb02b9-9f24-48fb-8979-a2ecd7002c05"
/>

---------

Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
Co-authored-by: Sojan Jose <sojan@pepalo.com>
Co-authored-by: Pranav <pranav@chatwoot.com>
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
Co-authored-by: Vinay Keerthi <11478411+stonecharioteer@users.noreply.github.com>
This commit is contained in:
Alex
2026-06-02 15:19:23 +05:30
committed by GitHub
co-authored by Shivam Mishra Sojan Jose Pranav Muhsin Keloth Vinay Keerthi
parent 88e2661ca6
commit ecd9c26c8c
24 changed files with 385 additions and 45 deletions
@@ -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')
+10 -3
View File
@@ -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
+3 -3
View File
@@ -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);
+1 -1
View File
@@ -55,6 +55,6 @@ body {
}
}
.turbolinks-progress-bar {
.turbo-progress-bar {
background-color: var(--dynamic-portal-color);
}
@@ -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()}`;
},
},
};
</script>
<template>
<div v-on-clickaway="closeSearch" class="relative w-full max-w-5xl my-4">
<PublicSearchInput
ref="searchInput"
:search-term="searchTerm"
:search-placeholder="searchTranslations.searchPlaceholder"
:size="size"
:kbd="kbdLabel"
@update:search-term="onUpdateSearchTerm"
@focus="openSearch"
/>
<form @submit.prevent="handleSubmit">
<PublicSearchInput
ref="searchInput"
:search-term="searchTerm"
:search-placeholder="searchTranslations.searchPlaceholder"
:size="size"
:kbd="kbdLabel"
@update:search-term="onUpdateSearchTerm"
@focus="openSearch"
/>
<button type="submit" class="sr-only">
{{ searchTranslations.submit }}
</button>
</form>
<div
v-if="shouldShowSearchBox"
class="absolute w-full top-14"
@@ -112,8 +112,8 @@ export default {
<p class="py-1 px-3" :class="getClassName(element)">
<a
:href="`#${element.slug}`"
data-turbolinks="false"
class="font-medium text-sm cursor-pointer"
data-turbo="false"
class="font-medium text-sm tracking-[0.28px] cursor-pointer"
:class="elementTextStyles(element)"
>
{{ element.title }}
+2 -3
View File
@@ -24,10 +24,9 @@ export const getHeadingsfromTheArticle = () => {
permalink.className = 'permalink text-slate-600 ml-3';
permalink.href = `#${slug}`;
permalink.title = headingText;
permalink.dataset.turbolinks = 'false';
permalink.dataset.turbo = 'false';
permalink.textContent = '#';
element.appendChild(permalink);
rows.push({
slug,
title: headingText,
@@ -188,7 +187,7 @@ export const InitializationHelpers = {
const a = document.createElement('a');
a.href = window.location.hash;
a['data-turbolinks'] = false;
a['data-turbo'] = false;
a.click();
}
},
+4
View File
@@ -55,10 +55,14 @@ class Article < ApplicationRecord
before_validation :ensure_article_slug
before_validation :ensure_locale_in_article
# Slugs that collide with help center routes (e.g. /hc/:slug/:locale/search)
RESERVED_SLUGS = %w[search articles categories].freeze
validates :account_id, presence: true
validates :author_id, presence: true
validates :title, presence: true
validates :content, presence: true, if: :published?
validates :slug, exclusion: { in: RESERVED_SLUGS }
# ensuring that the position is always set correctly
before_create :add_position_to_article
+1 -1
View File
@@ -1,6 +1,6 @@
<meta charset="utf-8">
<meta name="viewport" content="initial-scale=1">
<meta name="turbolinks-cache-control" content="no-cache">
<meta name="turbo-cache-control" content="no-cache">
<meta name="viewport" content="width=device-width, initial-scale=1">
<%= vite_client_tag %>
@@ -82,6 +82,7 @@ html.light {
emptyPlaceholder: '<%= I18n.t('public_portal.search.empty_placeholder') %>',
loadingPlaceholder: '<%= I18n.t('public_portal.search.loading_placeholder') %>',
resultsTitle: '<%= I18n.t('public_portal.search.results_title') %>',
submit: '<%= I18n.t('public_portal.search.submit') %>',
},
isPlainLayoutEnabled: '<%= @is_plain_layout_enabled %>',
tocHeader: '<%= I18n.t('public_portal.toc_header') %>'
@@ -11,9 +11,9 @@
<%= I18n.t('public_portal.hero.sub_title') %>
</p>
<form class="mt-8 group/herosearch relative z-30" onsubmit="return false">
<div class="mt-8 group/herosearch relative z-30">
<div id="search-wrap-hero" class="block w-full"></div>
</form>
</div>
<% if popular_topics.any? %>
<div class="mt-5 flex items-center gap-2 flex-wrap text-sm">
@@ -55,7 +55,7 @@
<% portal.public_locale_codes.each do |code| %>
<% is_current = code == locale %>
<a href="/hc/<%= portal.slug %>/<%= code %>/"
data-turbolinks="false"
data-turbo="false"
class="flex items-center gap-2.5 px-2.5 py-2 text-sm rounded-md transition <%= is_current ? 'bg-n-portal-soft text-n-portal' : 'text-n-slate-11 hover:bg-n-alpha-2 hover:text-n-slate-12' %>">
<span class="text-xs font-semibold uppercase min-w-9 text-center text-n-slate-11 bg-n-slate-2 border border-solid border-n-weak rounded px-1.5 py-0.5 flex-shrink-0"><%= code %></span>
<span class="flex-1 truncate <%= is_current ? 'font-medium' : '' %>"><%= language_name(code) %></span>
@@ -0,0 +1,20 @@
<%# locals: (input_class:) %>
<form class="w-full my-4" action="<%= request.path %>" method="GET" data-search-form>
<input type="hidden" name="locale" value="<%= params[:locale] %>">
<% if @theme_from_params.present? %>
<input type="hidden" name="theme" value="<%= @theme_from_params %>">
<% end %>
<% if @is_plain_layout_enabled %>
<input type="hidden" name="show_plain_layout" value="true">
<% end %>
<input type="text"
name="query"
value="<%= @query %>"
placeholder="<%= I18n.t('public_portal.search.search_placeholder') %>"
data-search-input
autofocus
class="<%= input_class %>">
<button type="submit" class="sr-only">
<%= I18n.t('public_portal.search.submit') %>
</button>
</form>
@@ -0,0 +1,33 @@
<script>
(function() {
const searchInputs = document.querySelectorAll('[data-search-input]');
searchInputs.forEach(function(input) {
let debounceTimer;
if (input.value.length > 0) {
setTimeout(function() {
input.setSelectionRange(input.value.length, input.value.length);
}, 0);
}
input.addEventListener('input', function() {
const form = input.closest('[data-search-form]');
const query = input.value.trim();
clearTimeout(debounceTimer);
debounceTimer = setTimeout(function() {
const currentQuery = new URLSearchParams(window.location.search).get('query') || '';
if (query !== currentQuery) {
form.requestSubmit();
}
}, 500);
});
input.closest('[data-search-form]').addEventListener('submit', function() {
clearTimeout(debounceTimer);
});
});
})();
</script>
@@ -0,0 +1,64 @@
<% content_for :head do %>
<title><%= I18n.t('public_portal.search.results_for', query: @query) %> | <%= @portal.name %></title>
<% end %>
<div class="px-6 md:px-10 py-8 md:py-10">
<% pagination_params = params.permit(:query, :locale, :theme, :show_plain_layout) %>
<nav class="flex items-center gap-2 text-sm text-n-slate-11 mb-8 flex-wrap">
<a
href="<%= public_portal_locale_path(@portal.slug, @locale) %>"
class="inline-flex items-center gap-1.5 hover:text-n-slate-12 transition"
>
<span class="i-lucide-house size-3.5" aria-hidden="true"></span>
<%= I18n.t('public_portal.common.home') %>
</a>
<span class="i-lucide-chevron-right size-3 text-n-slate-9" aria-hidden="true"></span>
<span class="text-n-slate-12 font-medium truncate"><%= I18n.t('public_portal.search.results') %></span>
</nav>
<div class="mb-8">
<h1 class="text-3xl md:text-4xl leading-snug font-620 tracking-tight text-n-slate-12 text-balance">
<%= I18n.t('public_portal.search.results_for', query: @query) %>
</h1>
<%= render 'public/api/v1/portals/search/form',
input_class: 'w-full px-4 py-3 border border-n-weak rounded-lg bg-n-alpha-1 text-n-slate-12 placeholder-n-slate-10 focus:outline-none focus:ring-2 focus:ring-n-portal focus:border-transparent' %>
</div>
<%= render 'public/api/v1/portals/search/search_handler' %>
<% if @articles.empty? %>
<%= render 'public/api/v1/portals/documentation_layout/empty_state',
message: I18n.t('public_portal.search.no_results', query: @query) %>
<% else %>
<p class="mb-6 text-sm text-n-slate-11">
<%= I18n.t('public_portal.search.found_results', count: @articles.total_count) %>
</p>
<div class="grid grid-cols-1 gap-4">
<% @articles.each do |article| %>
<%= render 'public/api/v1/portals/documentation_layout/article_card',
portal: @portal,
article: article %>
<% end %>
</div>
<% if @articles.respond_to?(:total_pages) && @articles.total_pages > 1 %>
<div class="flex justify-center mt-6">
<nav class="inline-flex">
<% if @articles.prev_page %>
<a href="<%= url_for(pagination_params.merge(page: @articles.prev_page)) %>" class="px-3 py-2 border border-n-weak rounded-l-md text-sm font-medium text-n-slate-11 hover:bg-n-alpha-2">
<%= I18n.t('public_portal.common.previous') %>
</a>
<% end %>
<% if @articles.next_page %>
<a href="<%= url_for(pagination_params.merge(page: @articles.next_page)) %>" class="px-3 py-2 border border-n-weak rounded-r-md text-sm font-medium text-n-slate-11 hover:bg-n-alpha-2">
<%= I18n.t('public_portal.common.next') %>
</a>
<% end %>
</nav>
</div>
<% end %>
<% end %>
</div>
@@ -0,0 +1,118 @@
<% content_for :head do %>
<title><%= I18n.t('public_portal.search.results_for', query: @query) %> | <%= @portal.name %></title>
<% end %>
<% search_input_class = 'w-full px-4 py-3 border border-slate-200 dark:border-slate-700 rounded-lg bg-white dark:bg-slate-800 text-slate-900 dark:text-slate-100 placeholder-slate-500 dark:placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent' %>
<% if !@is_plain_layout_enabled %>
<div id="portal-bg" class="bg-white dark:bg-slate-900 shadow-inner">
<div id="portal-bg-gradient" class="pt-8 pb-8 md:pt-14 md:pb-6">
<div class="max-w-5xl px-4 md:px-8 mx-auto flex flex-col">
<div class="flex flex-row items-center gap-px mb-6">
<a class="text-slate-500 dark:text-slate-200 text-sm gap-1 hover:cursor-pointer hover:underline leading-8 font-semibold"
href="<%= generate_home_link(@portal.slug, params[:locale], @theme_from_params, @is_plain_layout_enabled) %>">
<%= I18n.t('public_portal.common.home') %>
</a>
<span class="w-4 h-4 [&>svg]:w-3 [&>svg]:h-3 flex items-center justify-center text-xs text-slate-500 dark:text-slate-300">
<%= render partial: 'icons/chevron-right' %>
</span>
<span class="text-sm font-semibold text-slate-800 dark:text-slate-100">
<%= I18n.t('public_portal.search.results') %>
</span>
</div>
<h1 class="text-3xl font-semibold leading-normal md:tracking-normal md:text-4xl text-slate-900 dark:text-white">
<%= I18n.t('public_portal.search.results_for', query: @query) %>
</h1>
<%= render 'public/api/v1/portals/search/form', input_class: search_input_class %>
</div>
</div>
</div>
<% else %>
<div class="max-w-5xl px-4 md:px-8 mx-auto flex flex-col py-4">
<div class="flex flex-row items-center gap-px mb-6">
<a class="text-slate-500 dark:text-slate-200 text-sm gap-1 hover:cursor-pointer hover:underline leading-8 font-semibold"
href="<%= generate_home_link(@portal.slug, params[:locale], @theme_from_params, @is_plain_layout_enabled) %>">
<%= I18n.t('public_portal.common.home') %>
</a>
<span class="w-4 h-4 [&>svg]:w-3 [&>svg]:h-3 flex items-center justify-center text-xs text-slate-500 dark:text-slate-300">
<%= render partial: 'icons/chevron-right' %>
</span>
<span class="text-sm font-semibold text-slate-800 dark:text-slate-100">
<%= I18n.t('public_portal.search.results') %>
</span>
</div>
<h1 class="text-3xl font-semibold leading-normal md:tracking-normal md:text-4xl text-slate-900 dark:text-white">
<%= I18n.t('public_portal.search.results_for', query: @query) %>
</h1>
<%= render 'public/api/v1/portals/search/form', input_class: search_input_class %>
</div>
<% end %>
<% pagination_params = params.permit(:query, :locale, :theme, :show_plain_layout) %>
<%= render 'public/api/v1/portals/search/search_handler' %>
<section class="max-w-5xl w-full mx-auto px-4 md:px-8 py-6 flex flex-col items-center justify-center flex-grow">
<div class="w-full flex flex-col gap-6 flex-grow">
<% if @articles.empty? %>
<div class="h-full flex items-center justify-center bg-slate-50 dark:bg-slate-800 rounded-xl py-6">
<p class="text-sm text-slate-500"><%= I18n.t('public_portal.search.no_results', query: @query) %></p>
</div>
<% else %>
<p class="text-sm text-slate-600 dark:text-slate-400">
<%= I18n.t('public_portal.search.found_results', count: @articles.total_count) %>
</p>
<% @articles.each do |article| %>
<div class="border border-solid border-slate-100 dark:border-slate-800 rounded-lg">
<a class="p-4 text-slate-800 dark:text-slate-50 flex justify-between content-center hover:cursor-pointer"
href="<%= generate_article_link(@portal.slug, article.slug, @theme_from_params, @is_plain_layout_enabled) %>">
<div class="flex flex-col gap-5">
<div class="flex flex-col gap-1">
<h3 class="text-lg text-slate-900 tracking-[0.28px] dark:text-slate-50 font-semibold">
<%= article.title %>
</h3>
<p class="text-base font-normal text-slate-600 dark:text-slate-200 line-clamp-2 break-all">
<%= render_category_content(article.content) %>
</p>
</div>
<div class="flex flex-row items-center gap-2">
<% if article.category.present? %>
<span class="text-sm text-slate-600 dark:text-slate-400 font-medium">
<%= article.category.name %>
</span>
<span class="text-slate-600 dark:text-slate-400"></span>
<% end %>
<span class="text-sm text-slate-600 dark:text-slate-400 font-medium">
<%= I18n.t('public_portal.common.last_updated_on', last_updated_on: article.updated_at.strftime("%b %d, %Y")) %>
</span>
</div>
</div>
</a>
</div>
<% end %>
<% if @articles.respond_to?(:total_pages) && @articles.total_pages > 1 %>
<div class="flex justify-center mt-6">
<nav class="inline-flex">
<% if @articles.prev_page %>
<a href="<%= url_for(pagination_params.merge(page: @articles.prev_page)) %>" class="px-3 py-2 border border-slate-200 dark:border-slate-700 rounded-l-md text-sm font-medium text-slate-700 dark:text-slate-300 hover:bg-slate-50 dark:hover:bg-slate-800">
<%= I18n.t('public_portal.common.previous') %>
</a>
<% end %>
<% if @articles.next_page %>
<a href="<%= url_for(pagination_params.merge(page: @articles.next_page)) %>" class="px-3 py-2 border border-slate-200 dark:border-slate-700 rounded-r-md text-sm font-medium text-slate-700 dark:text-slate-300 hover:bg-slate-50 dark:hover:bg-slate-800">
<%= I18n.t('public_portal.common.next') %>
</a>
<% end %>
</nav>
</div>
<% end %>
<% end %>
</div>
</section>
+9
View File
@@ -427,6 +427,13 @@ en:
empty_placeholder: No results found.
loading_placeholder: Searching...
results_title: Search results
results: Search Results
results_for: "Search Results for '%{query}'"
no_results: "No results found for '%{query}'"
found_results:
one: Found 1 result
other: 'Found %{count} results'
submit: Search
toc_header: 'On this page'
sidebar:
help_center: Help Center
@@ -458,6 +465,8 @@ en:
others: others
by: By
no_articles: There are no articles here
previous: Previous
next: Next
article_actions:
label: Open in
view_markdown: View as Markdown
+1
View File
@@ -590,6 +590,7 @@ Rails.application.routes.draw do
get 'hc/:slug', to: 'public/api/v1/portals#show'
get 'hc/:slug/sitemap.xml', to: 'public/api/v1/portals#sitemap'
get 'hc/:slug/:locale', to: 'public/api/v1/portals#show', as: :public_portal_locale
get 'hc/:slug/:locale/search', to: 'public/api/v1/portals/search#index', as: :portal_search
get 'hc/:slug/:locale/articles', to: 'public/api/v1/portals/articles#index'
get 'hc/:slug/:locale/categories', to: 'public/api/v1/portals/categories#index'
get 'hc/:slug/:locale/categories/:category_slug', to: 'public/api/v1/portals/categories#show', as: :public_portal_category
@@ -0,0 +1,9 @@
module Enterprise::Public::Api::V1::Portals::SearchController
private
def search_articles
return super if @query.blank? || !@portal.account.feature_enabled?('help_center_embedding_search')
@articles = @articles.vector_search(search_params.merge(account_id: @portal.account_id, limit: nil))
end
end
@@ -26,13 +26,15 @@ module Enterprise::Concerns::Article
# if using add the filter block to the below query
# .filter { |ae| ae.neighbor_distance <= distance_threshold }
article_ids = ArticleEmbedding.where(article_id: filtered_article_ids)
.nearest_neighbors(:embedding, embedding, distance: 'cosine')
.limit(5)
.pluck(:article_id)
limit = params.key?(:limit) ? params[:limit] : 5
article_embeddings = ArticleEmbedding.where(article_id: filtered_article_ids)
.nearest_neighbors(:embedding, embedding, distance: 'cosine')
article_embeddings = article_embeddings.limit(limit) if limit.present?
article_ids = article_embeddings.pluck(:article_id)
# Fetch the articles by the IDs obtained from the nearest neighbors search
where(id: article_ids)
where(id: article_ids).in_order_of(:id, article_ids)
end
end
+1 -1
View File
@@ -40,6 +40,7 @@
"@formkit/vue": "^1.7.2",
"@hcaptcha/vue3-hcaptcha": "^1.3.0",
"@highlightjs/vue-plugin": "^2.1.0",
"@hotwired/turbo-rails": "^8.0.13",
"@iconify-json/fluent": "^1.2.32",
"@iconify-json/material-symbols": "^1.2.10",
"@lk77/vue3-color": "^3.0.6",
@@ -92,7 +93,6 @@
"snakecase-keys": "^8.0.1",
"timezone-phone-codes": "^0.0.2",
"tinykeys": "^3.0.0",
"turbolinks": "^5.2.0",
"urlpattern-polyfill": "^10.0.0",
"video.js": "7.21.1",
"videojs-record": "4.5.0",
+22 -8
View File
@@ -43,6 +43,9 @@ importers:
'@highlightjs/vue-plugin':
specifier: ^2.1.0
version: 2.1.0(highlight.js@11.10.0)(vue@3.5.12(typescript@5.6.2))
'@hotwired/turbo-rails':
specifier: ^8.0.13
version: 8.0.13
'@iconify-json/fluent':
specifier: ^1.2.32
version: 1.2.36
@@ -199,9 +202,6 @@ importers:
tinykeys:
specifier: ^3.0.0
version: 3.0.0
turbolinks:
specifier: ^5.2.0
version: 5.2.0
urlpattern-polyfill:
specifier: ^10.0.0
version: 10.0.0
@@ -942,6 +942,13 @@ packages:
'@histoire/vendors@0.17.17':
resolution: {integrity: sha512-QZvmffdoJlLuYftPIkOU5Q2FPAdG2JjMuQ5jF7NmEl0n1XnmbMqtRkdYTZ4eF6CO1KLZ0Zyf6gBQvoT1uWNcjA==}
'@hotwired/turbo-rails@8.0.13':
resolution: {integrity: sha512-6SCnnOSzhtaJ0pNkAjncZxjtKsK3sP/vPEkCnTXBXSHkr+vF7DTZkOlwjhms1DbbQNTsjCsBoKvzSMbh/omSCQ==}
'@hotwired/turbo@8.0.13':
resolution: {integrity: sha512-M7qXUqcGab6G5PKOiwhgbByTtrPgKPFCTMNQ52QhzUEXEqmp0/ApEguUesh/FPiUjrmFec+3lq98KsWnYY2C7g==}
engines: {node: '>= 14'}
'@humanwhocodes/config-array@0.11.14':
resolution: {integrity: sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==}
engines: {node: '>=10.10.0'}
@@ -1118,6 +1125,9 @@ packages:
'@rails/actioncable@6.1.3':
resolution: {integrity: sha512-m02524MR9cTnUNfGz39Lkx9jVvuL0tle4O7YgvouJ7H83FILxzG1nQ5jw8pAjLAr9XQGu+P1sY4SKE3zyhCNjw==}
'@rails/actioncable@7.2.201':
resolution: {integrity: sha512-wsTdWoZ5EfG5k3t7ORdyQF0ZmDEgN4aVPCanHAiNEwCROqibSZMXXmCbH7IDJUVri4FOeAVwwbPINI7HVHPKBw==}
'@rails/ujs@7.1.400':
resolution: {integrity: sha512-YwvXm3BR5tn+VCAKYGycLejMRVZE3Ionj5gFjEeGXCZnI0Rpi+7dKpmyu90kdUY7dRUFpHTdu9zZceEzFLl38w==}
@@ -4368,9 +4378,6 @@ packages:
tslib@2.8.1:
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
turbolinks@5.2.0:
resolution: {integrity: sha512-pMiez3tyBo6uRHFNNZoYMmrES/IaGgMhQQM+VFF36keryjb5ms0XkVpmKHkfW/4Vy96qiGW3K9bz0tF5sK9bBw==}
type-check@0.4.0:
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
engines: {node: '>= 0.8.0'}
@@ -5493,6 +5500,13 @@ snapshots:
'@histoire/vendors@0.17.17': {}
'@hotwired/turbo-rails@8.0.13':
dependencies:
'@hotwired/turbo': 8.0.13
'@rails/actioncable': 7.2.201
'@hotwired/turbo@8.0.13': {}
'@humanwhocodes/config-array@0.11.14':
dependencies:
'@humanwhocodes/object-schema': 2.0.3
@@ -5704,6 +5718,8 @@ snapshots:
'@rails/actioncable@6.1.3': {}
'@rails/actioncable@7.2.201': {}
'@rails/ujs@7.1.400': {}
'@rollup/plugin-yaml@4.1.2(rollup@4.59.0)':
@@ -9373,8 +9389,6 @@ snapshots:
tslib@2.8.1: {}
turbolinks@5.2.0: {}
type-check@0.4.0:
dependencies:
prelude-ls: 1.2.1
+6 -6
View File
@@ -140,7 +140,7 @@ describe PortalHelper do
context 'when theme is not present' do
it 'returns the correct link' do
expect(helper.generate_home_link('portal_slug', 'en', nil, true)).to eq(
'/hc/portal_slug/en'
'/hc/portal_slug/en?show_plain_layout=true'
)
end
end
@@ -148,7 +148,7 @@ describe PortalHelper do
context 'when theme is present and plain layout is enabled' do
it 'returns the correct link' do
expect(helper.generate_home_link('portal_slug', 'en', 'dark', true)).to eq(
'/hc/portal_slug/en?theme=dark'
'/hc/portal_slug/en?show_plain_layout=true&theme=dark'
)
end
end
@@ -172,7 +172,7 @@ describe PortalHelper do
theme: nil,
is_plain_layout_enabled: true
)).to eq(
'/hc/portal_slug/en/categories/category_slug'
'/hc/portal_slug/en/categories/category_slug?show_plain_layout=true'
)
end
end
@@ -186,7 +186,7 @@ describe PortalHelper do
theme: 'dark',
is_plain_layout_enabled: true
)).to eq(
'/hc/portal_slug/en/categories/category_slug?theme=dark'
'/hc/portal_slug/en/categories/category_slug?show_plain_layout=true&theme=dark'
)
end
end
@@ -210,7 +210,7 @@ describe PortalHelper do
context 'when theme is not present' do
it 'returns the correct link' do
expect(helper.generate_article_link('portal_slug', 'article_slug', nil, true)).to eq(
'/hc/portal_slug/articles/article_slug'
'/hc/portal_slug/articles/article_slug?show_plain_layout=true'
)
end
end
@@ -218,7 +218,7 @@ describe PortalHelper do
context 'when theme is present and plain layout is enabled' do
it 'returns the correct link' do
expect(helper.generate_article_link('portal_slug', 'article_slug', 'dark', true)).to eq(
'/hc/portal_slug/articles/article_slug?theme=dark'
'/hc/portal_slug/articles/article_slug?show_plain_layout=true&theme=dark'
)
end
end
+9
View File
@@ -20,6 +20,15 @@ RSpec.describe Article do
expect(article).not_to be_valid
expect(article.errors[:content]).to include("can't be blank")
end
it 'rejects reserved slugs that collide with help center routes' do
Article::RESERVED_SLUGS.each do |reserved_slug|
article = build(:article, portal_id: portal_1.id, author_id: user.id, category_id: category_1.id,
title: reserved_slug, slug: reserved_slug, content: 'content')
expect(article).not_to be_valid
expect(article.errors[:slug]).to include('is reserved')
end
end
end
describe 'associations' do