mirror of
https://github.com/lingble/chatwoot.git
synced 2025-11-02 12:08:01 +00:00
feat: Add support to uncategorized articles (#6912)
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
class Public::Api::V1::Portals::ArticlesController < Public::Api::V1::Portals::BaseController
|
||||
before_action :ensure_custom_domain_request, only: [:show, :index]
|
||||
before_action :portal
|
||||
before_action :set_category, except: [:index]
|
||||
before_action :set_category, except: [:index, :show]
|
||||
before_action :set_article, only: [:show]
|
||||
layout 'portal'
|
||||
|
||||
@@ -16,7 +16,7 @@ class Public::Api::V1::Portals::ArticlesController < Public::Api::V1::Portals::B
|
||||
private
|
||||
|
||||
def set_article
|
||||
@article = @category.articles.find(permitted_params[:id])
|
||||
@article = @portal.articles.find_by(slug: permitted_params[:article_slug])
|
||||
@article.increment_view_count
|
||||
@parsed_content = render_article_content(@article.content)
|
||||
end
|
||||
@@ -39,7 +39,7 @@ class Public::Api::V1::Portals::ArticlesController < Public::Api::V1::Portals::B
|
||||
end
|
||||
|
||||
def permitted_params
|
||||
params.permit(:slug, :category_slug, :locale, :id)
|
||||
params.permit(:slug, :category_slug, :locale, :id, :article_slug)
|
||||
end
|
||||
|
||||
def render_article_content(content)
|
||||
|
||||
@@ -5,6 +5,7 @@ class Public::Api::V1::Portals::BaseController < PublicController
|
||||
|
||||
def set_locale(&)
|
||||
switch_locale_with_portal(&) if params[:locale].present?
|
||||
switch_locale_with_article(&) if params[:article_slug].present?
|
||||
end
|
||||
|
||||
def switch_locale_with_portal(&)
|
||||
@@ -19,4 +20,16 @@ class Public::Api::V1::Portals::BaseController < PublicController
|
||||
|
||||
I18n.with_locale(@locale, &)
|
||||
end
|
||||
|
||||
def switch_locale_with_article(&)
|
||||
article = Article.find_by(slug: params[:article_slug])
|
||||
|
||||
@locale = if article.category.present?
|
||||
article.category.locale
|
||||
else
|
||||
'en'
|
||||
end
|
||||
|
||||
I18n.with_locale(@locale, &)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -8,8 +8,8 @@ export const buildPortalArticleURL = (
|
||||
portalSlug,
|
||||
categorySlug,
|
||||
locale,
|
||||
articleId
|
||||
articleSlug
|
||||
) => {
|
||||
const portalURL = buildPortalURL(portalSlug);
|
||||
return `${portalURL}/${locale}/${categorySlug}/${articleId}`;
|
||||
return `${portalURL}/articles/${articleSlug}`;
|
||||
};
|
||||
|
||||
@@ -20,9 +20,9 @@ describe('PortalHelper', () => {
|
||||
hostURL: 'https://app.chatwoot.com',
|
||||
helpCenterURL: 'https://help.chatwoot.com',
|
||||
};
|
||||
expect(buildPortalArticleURL('handbook', 'culture', 'fr', 1)).toEqual(
|
||||
'https://help.chatwoot.com/hc/handbook/fr/culture/1'
|
||||
);
|
||||
expect(
|
||||
buildPortalArticleURL('handbook', 'culture', 'fr', 'article-slug')
|
||||
).toEqual('https://help.chatwoot.com/hc/handbook/articles/article-slug');
|
||||
window.chatwootConfig = {};
|
||||
});
|
||||
});
|
||||
|
||||
@@ -93,7 +93,7 @@ export default {
|
||||
slug,
|
||||
this.article.category.slug,
|
||||
this.article.category.locale,
|
||||
this.article.id
|
||||
this.article.slug
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
@@ -86,7 +86,7 @@ export default {
|
||||
|
||||
methods: {
|
||||
generateArticleUrl(article) {
|
||||
return `/hc/${article.portal.slug}/${article.category.locale}/${article.category.slug}/${article.id}`;
|
||||
return `/hc/${article.portal.slug}/articles/${article.slug}`;
|
||||
},
|
||||
handleKeyboardEvent(e) {
|
||||
this.processKeyDownEvent(e);
|
||||
|
||||
@@ -41,7 +41,7 @@ class Article < ApplicationRecord
|
||||
inverse_of: :associated_articles,
|
||||
optional: true
|
||||
belongs_to :account
|
||||
belongs_to :category
|
||||
belongs_to :category, optional: true
|
||||
belongs_to :portal
|
||||
belongs_to :author, class_name: 'User'
|
||||
|
||||
@@ -49,7 +49,6 @@ class Article < ApplicationRecord
|
||||
before_validation :ensure_article_slug
|
||||
|
||||
validates :account_id, presence: true
|
||||
validates :category_id, presence: true
|
||||
validates :author_id, presence: true
|
||||
validates :title, presence: true
|
||||
validates :content, presence: true
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
json.id article.id
|
||||
json.slug article.slug
|
||||
json.title article.title
|
||||
json.content article.content
|
||||
json.description article.description
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<section class="bg-white lg:container w-full py-6 px-4 flex flex-col h-full">
|
||||
<div class="flex justify-between items-center w-full">
|
||||
<h3 class="text-xl text-slate-900 font-semibold subpixel-antialiased leading-relaxed hover:underline">
|
||||
<a href="/hc/<%= portal.slug %>/<%= category.locale %>/<%= category.slug %>">
|
||||
<a href="/hc/<%= portal.slug %>/<%= category.locale %>/categories/<%= category.slug %>">
|
||||
<%= category.name %>
|
||||
</a>
|
||||
</h3>
|
||||
@@ -18,7 +18,7 @@
|
||||
<% category.articles.published.order(position: :asc).take(5).each do |article| %>
|
||||
<a
|
||||
class="text-slate-800 hover:underline leading-8"
|
||||
href="/hc/<%= portal.slug %>/<%= category.locale %>/<%= category.slug %>/<%= article.id %>"
|
||||
href="/hc/<%= portal.slug %>/articles/<%= article.slug %>"
|
||||
>
|
||||
<div class="flex justify-between content-center my-1 -mx-1 p-1 rounded-lg hover:bg-slate-25">
|
||||
<%= article.title %>
|
||||
@@ -42,7 +42,7 @@
|
||||
</div>
|
||||
<div>
|
||||
<a
|
||||
href="/hc/<%= portal.slug %>/<%= category.locale %>/<%= category.slug %>"
|
||||
href="/hc/<%= portal.slug %>/<%= category.locale %>/categories/<%= category.slug %>"
|
||||
class="flex flex-row items-center text-base font-medium text-woot-500 hover:underline mt-4"
|
||||
style="color: <%= portal.color %>"
|
||||
>
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
<section class="bg-white lg:container w-full py-6 px-4 flex flex-col h-full">
|
||||
<div class="flex justify-between items-center w-full">
|
||||
<h3 class="text-xl text-slate-900 font-semibold subpixel-antialiased leading-relaxed hover:underline">
|
||||
<%= category %>
|
||||
</h3>
|
||||
<span class="text-slate-500">
|
||||
<%= render 'public/api/v1/portals/article_count', article_count: portal.articles.published.where(category_id: nil).size %>
|
||||
</span>
|
||||
</div>
|
||||
<div class="py-4 w-full mt-2 flex-grow">
|
||||
<% portal.articles.published.where(category_id: nil).order(position: :asc).take(5).each do |article| %>
|
||||
<a
|
||||
class="text-slate-800 hover:underline leading-8"
|
||||
href="/hc/<%= portal.slug %>/articles/<%= article.slug %>"
|
||||
>
|
||||
<div class="flex justify-between content-center my-1 -mx-1 p-1 rounded-lg hover:bg-slate-25">
|
||||
<%= article.title %>
|
||||
<span class="flex items-center">
|
||||
<svg
|
||||
class="w-4 h-4 fill-current text-slate-700"
|
||||
width="24"
|
||||
height="24"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M8.47 4.22a.75.75 0 0 0 0 1.06L15.19 12l-6.72 6.72a.75.75 0 1 0 1.06 1.06l7.25-7.25a.75.75 0 0 0 0-1.06L9.53 4.22a.75.75 0 0 0-1.06 0Z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
</a>
|
||||
<% end %>
|
||||
</div>
|
||||
</section>
|
||||
@@ -13,6 +13,7 @@
|
||||
|
||||
<div class="bg-gradient-to-b from-white to-slate-50">
|
||||
<div class="max-w-5xl px-8 lg:px-4 pt-8 pb-16 mx-auto space-y-4 w-full">
|
||||
<% if @article.category.present? %>
|
||||
<div>
|
||||
<a
|
||||
class="text-slate-700 hover:underline leading-8 text-sm font-medium"
|
||||
@@ -23,11 +24,12 @@
|
||||
<span class="text-xs text-slate-600 px-1">/</span>
|
||||
<a
|
||||
class="text-slate-700 hover:underline leading-8 text-sm font-medium"
|
||||
href="/hc/<%= @portal.slug %>/<%= @article.category.locale %>/<%= @article.category.slug %>"
|
||||
href="/hc/<%= @portal.slug %>/<%= @article.category.locale %>/categories/<%= @article.category.slug %>"
|
||||
>
|
||||
<%= @article.category.name %>
|
||||
</a>
|
||||
</div>
|
||||
<% end %>
|
||||
<h1 class="text-3xl font-bold md:tracking-normal leading-snug md:text-5xl text-slate-900">
|
||||
<%= @article.title %>
|
||||
</h1>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<section class="bg-white lg:container w-full py-6 px-4 flex flex-col h-full">
|
||||
<div class="flex justify-between items-center w-full">
|
||||
<h3 class="text-xl text-slate-900 font-semibold subpixel-antialiased leading-relaxed hover:underline"">
|
||||
<a href="/hc/<%= portal.slug %>/<%= category.locale %>/<%= category.slug %>">
|
||||
<a href="/hc/<%= portal.slug %>/<%= category.locale %>/categories/<%= category.slug %>">
|
||||
<%= category.name %>
|
||||
</a>
|
||||
</h3>
|
||||
@@ -20,7 +20,7 @@
|
||||
<div class="flex justify-between content-center h-8 my-1">
|
||||
<a
|
||||
class="text-slate-800 hover:underline leading-8"
|
||||
href="/hc/<%= portal.slug %>/<%= category.locale %>/<%= category.slug %>/<%= article.id %>"
|
||||
href="/hc/<%= portal.slug %>/articles/<%= article.slug %>"
|
||||
>
|
||||
<%= article.title %>
|
||||
</a>
|
||||
@@ -45,7 +45,7 @@
|
||||
</div>
|
||||
<div>
|
||||
<a
|
||||
href="/hc/<%= portal.slug %>/<%= category.locale %>/<%= category.slug %>"
|
||||
href="/hc/<%= portal.slug %>/<%= category.locale %>/categories/<%= category.slug %>"
|
||||
class="flex flex-row items-center text-base font-medium text-woot-600 hover:text-slate-900 hover:underline mt-4"
|
||||
>
|
||||
<%= I18n.t('public_portal.common.view_all_articles') %>
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
<% @category.articles.published.order(:position).each do |article| %>
|
||||
<a
|
||||
class="text-slate-800 flex justify-between content-center mb-4 py-2"
|
||||
href="/hc/<%= @portal.slug %>/<%= @category.locale %>/<%= @category.slug %>/<%= article.id %>"
|
||||
href="/hc/<%= @portal.slug %>/articles/<%= article.slug %>"
|
||||
>
|
||||
<div>
|
||||
<p class="font-medium mb-2 hover:underline"><%= article.title %></p>
|
||||
|
||||
@@ -5,4 +5,10 @@
|
||||
<%= render "public/api/v1/portals/category-block", category: category, portal: @portal %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-x-32 gap-y-0 lg:gap-y-12">
|
||||
<% if @portal.articles.where(status: :published, category_id: nil).order(position: :asc) %>
|
||||
<%= render "public/api/v1/portals/uncategorized-block", category: "Uncategorized", portal: @portal %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -338,9 +338,9 @@ Rails.application.routes.draw do
|
||||
get 'hc/:slug/:locale', to: 'public/api/v1/portals#show'
|
||||
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/:category_slug', to: 'public/api/v1/portals/categories#show'
|
||||
get 'hc/:slug/:locale/:category_slug/articles', to: 'public/api/v1/portals/articles#index'
|
||||
get 'hc/:slug/:locale/:category_slug/:id', to: 'public/api/v1/portals/articles#show'
|
||||
get 'hc/:slug/:locale/categories/:category_slug', to: 'public/api/v1/portals/categories#show'
|
||||
get 'hc/:slug/:locale/categories/:category_slug/articles', to: 'public/api/v1/portals/articles#index'
|
||||
get 'hc/:slug/articles/:article_slug', to: 'public/api/v1/portals/articles#show'
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# Used in mailer templates
|
||||
|
||||
@@ -9,6 +9,7 @@ RSpec.describe 'Public Articles API', type: :request do
|
||||
let!(:article) { create(:article, category: category, portal: portal, account_id: account.id, author_id: agent.id) }
|
||||
|
||||
before do
|
||||
ENV['HELPCENTER_URL'] = ENV.fetch('FRONTEND_URL', nil)
|
||||
create(:article, category: category, portal: portal, account_id: account.id, author_id: agent.id)
|
||||
create(:article, category: category, portal: portal, account_id: account.id, author_id: agent.id, associated_article_id: article.id)
|
||||
create(:article, category: category_2, portal: portal, account_id: account.id, author_id: agent.id, associated_article_id: article.id)
|
||||
@@ -17,7 +18,7 @@ RSpec.describe 'Public Articles API', type: :request do
|
||||
|
||||
describe 'GET /public/api/v1/portals/:slug/articles' do
|
||||
it 'Fetch all articles in the portal' do
|
||||
get "/hc/#{portal.slug}/#{category.locale}/#{category.slug}/articles"
|
||||
get "/hc/#{portal.slug}/#{category.locale}/categories/#{category.slug}/articles"
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
end
|
||||
@@ -31,7 +32,7 @@ RSpec.describe 'Public Articles API', type: :request do
|
||||
content: 'this is some test and funny content')
|
||||
expect(article2.id).not_to be_nil
|
||||
|
||||
get "/hc/#{portal.slug}/#{category.locale}/#{category.slug}/articles",
|
||||
get "/hc/#{portal.slug}/#{category.locale}/categories/#{category.slug}/articles",
|
||||
headers: agent.create_new_auth_token,
|
||||
params: { query: 'funny' }
|
||||
expect(response).to have_http_status(:success)
|
||||
@@ -40,14 +41,14 @@ RSpec.describe 'Public Articles API', type: :request do
|
||||
|
||||
describe 'GET /public/api/v1/portals/:slug/articles/:id' do
|
||||
it 'Fetch article with the id' do
|
||||
get "/hc/#{portal.slug}/#{category.locale}/#{category.slug}/#{article.id}"
|
||||
get "/hc/#{portal.slug}/articles/#{article.slug}"
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(article.reload.views).to eq 1
|
||||
end
|
||||
|
||||
it 'returns the article with the id with a different locale' do
|
||||
article_in_locale = create(:article, category: category_2, portal: portal, account_id: account.id, author_id: agent.id)
|
||||
get "/hc/#{portal.slug}/#{category_2.locale}/#{category_2.slug}/#{article_in_locale.id}"
|
||||
get "/hc/#{portal.slug}/articles/#{article_in_locale.slug}"
|
||||
expect(response).to have_http_status(:success)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -12,7 +12,9 @@ RSpec.describe 'Public Categories API', type: :request do
|
||||
|
||||
describe 'GET /public/api/v1/portals/:portal_slug/categories' do
|
||||
it 'Fetch all categories in the portal' do
|
||||
get "/hc/#{portal.slug}/categories"
|
||||
category = portal.categories.first
|
||||
|
||||
get "/hc/#{portal.slug}/#{category.locale}/categories"
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
end
|
||||
@@ -20,9 +22,9 @@ RSpec.describe 'Public Categories API', type: :request do
|
||||
|
||||
describe 'GET /public/api/v1/portals/:portal_slug/categories/:slug' do
|
||||
it 'Fetch category with the slug' do
|
||||
category_locale = 'en'
|
||||
category = portal.categories.first
|
||||
|
||||
get "/hc/#{portal.slug}/#{category_locale}/categories"
|
||||
get "/hc/#{portal.slug}/#{category.locale}/categories/#{category.slug}"
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
end
|
||||
|
||||
@@ -3,7 +3,6 @@ require 'rails_helper'
|
||||
RSpec.describe Article, type: :model do
|
||||
context 'with validations' do
|
||||
it { is_expected.to validate_presence_of(:account_id) }
|
||||
it { is_expected.to validate_presence_of(:category_id) }
|
||||
it { is_expected.to validate_presence_of(:author_id) }
|
||||
it { is_expected.to validate_presence_of(:title) }
|
||||
it { is_expected.to validate_presence_of(:content) }
|
||||
@@ -11,7 +10,6 @@ RSpec.describe Article, type: :model do
|
||||
|
||||
describe 'associations' do
|
||||
it { is_expected.to belong_to(:account) }
|
||||
it { is_expected.to belong_to(:category) }
|
||||
it { is_expected.to belong_to(:author) }
|
||||
end
|
||||
|
||||
|
||||
Reference in New Issue
Block a user