diff --git a/app/controllers/api/v1/accounts/search_controller.rb b/app/controllers/api/v1/accounts/search_controller.rb
index 35979f70f..13e3a6a6c 100644
--- a/app/controllers/api/v1/accounts/search_controller.rb
+++ b/app/controllers/api/v1/accounts/search_controller.rb
@@ -15,6 +15,10 @@ class Api::V1::Accounts::SearchController < Api::V1::Accounts::BaseController
@result = search('Message')
end
+ def articles
+ @result = search('Article')
+ end
+
private
def search(search_type)
diff --git a/app/javascript/dashboard/api/search.js b/app/javascript/dashboard/api/search.js
index 7abb584c0..d533c2f28 100644
--- a/app/javascript/dashboard/api/search.js
+++ b/app/javascript/dashboard/api/search.js
@@ -40,6 +40,15 @@ class SearchAPI extends ApiClient {
},
});
}
+
+ articles({ q, page = 1 }) {
+ return axios.get(`${this.url}/articles`, {
+ params: {
+ q,
+ page: page,
+ },
+ });
+ }
}
export default new SearchAPI();
diff --git a/app/javascript/dashboard/i18n/locale/en/search.json b/app/javascript/dashboard/i18n/locale/en/search.json
index 3cb566813..e8510ab97 100644
--- a/app/javascript/dashboard/i18n/locale/en/search.json
+++ b/app/javascript/dashboard/i18n/locale/en/search.json
@@ -4,12 +4,14 @@
"ALL": "All",
"CONTACTS": "Contacts",
"CONVERSATIONS": "Conversations",
- "MESSAGES": "Messages"
+ "MESSAGES": "Messages",
+ "ARTICLES": "Articles"
},
"SECTION": {
"CONTACTS": "Contacts",
"CONVERSATIONS": "Conversations",
- "MESSAGES": "Messages"
+ "MESSAGES": "Messages",
+ "ARTICLES": "Articles"
},
"VIEW_MORE": "View more",
"LOAD_MORE": "Load more",
diff --git a/app/javascript/dashboard/modules/search/components/SearchResultArticleItem.vue b/app/javascript/dashboard/modules/search/components/SearchResultArticleItem.vue
new file mode 100644
index 000000000..7e2da950e
--- /dev/null
+++ b/app/javascript/dashboard/modules/search/components/SearchResultArticleItem.vue
@@ -0,0 +1,69 @@
+
+
+
+
+
+
+
+
+
+
+ {{ title }}
+
+
+ {{ category }}
+
+
+
+ {{ truncatedContent }}
+
+
+
+
diff --git a/app/javascript/dashboard/modules/search/components/SearchResultArticlesList.vue b/app/javascript/dashboard/modules/search/components/SearchResultArticlesList.vue
new file mode 100644
index 000000000..679e411c2
--- /dev/null
+++ b/app/javascript/dashboard/modules/search/components/SearchResultArticlesList.vue
@@ -0,0 +1,53 @@
+
+
+
+
+
+
+
diff --git a/app/javascript/dashboard/modules/search/components/SearchView.vue b/app/javascript/dashboard/modules/search/components/SearchView.vue
index 1b0a9e4d7..bd48a3078 100644
--- a/app/javascript/dashboard/modules/search/components/SearchView.vue
+++ b/app/javascript/dashboard/modules/search/components/SearchView.vue
@@ -8,6 +8,7 @@ import {
ROLES,
CONVERSATION_PERMISSIONS,
CONTACT_PERMISSIONS,
+ PORTAL_PERMISSIONS,
} from 'dashboard/constants/permissions.js';
import {
getUserPermissions,
@@ -22,6 +23,7 @@ import SearchTabs from './SearchTabs.vue';
import SearchResultConversationsList from './SearchResultConversationsList.vue';
import SearchResultMessagesList from './SearchResultMessagesList.vue';
import SearchResultContactsList from './SearchResultContactsList.vue';
+import SearchResultArticlesList from './SearchResultArticlesList.vue';
const router = useRouter();
const store = useStore();
@@ -34,6 +36,7 @@ const pages = ref({
contacts: 1,
conversations: 1,
messages: 1,
+ articles: 1,
});
const currentUser = useMapGetter('getCurrentUser');
@@ -43,6 +46,7 @@ const conversationRecords = useMapGetter(
'conversationSearch/getConversationRecords'
);
const messageRecords = useMapGetter('conversationSearch/getMessageRecords');
+const articleRecords = useMapGetter('conversationSearch/getArticleRecords');
const uiFlags = useMapGetter('conversationSearch/getUIFlags');
const addTypeToRecords = (records, type) =>
@@ -57,6 +61,9 @@ const mappedConversations = computed(() =>
const mappedMessages = computed(() =>
addTypeToRecords(messageRecords, 'message')
);
+const mappedArticles = computed(() =>
+ addTypeToRecords(articleRecords, 'article')
+);
const isSelectedTabAll = computed(() => selectedTab.value === 'all');
@@ -66,6 +73,7 @@ const sliceRecordsIfAllTab = items =>
const contacts = computed(() => sliceRecordsIfAllTab(mappedContacts));
const conversations = computed(() => sliceRecordsIfAllTab(mappedConversations));
const messages = computed(() => sliceRecordsIfAllTab(mappedMessages));
+const articles = computed(() => sliceRecordsIfAllTab(mappedArticles));
const filterByTab = tab =>
computed(() => selectedTab.value === tab || isSelectedTabAll.value);
@@ -73,6 +81,7 @@ const filterByTab = tab =>
const filterContacts = filterByTab('contacts');
const filterConversations = filterByTab('conversations');
const filterMessages = filterByTab('messages');
+const filterArticles = filterByTab('articles');
const userPermissions = computed(() =>
getUserPermissions(currentUser.value, currentAccountId.value)
@@ -80,7 +89,12 @@ const userPermissions = computed(() =>
const TABS_CONFIG = {
all: {
- permissions: [CONTACT_PERMISSIONS, ...ROLES, ...CONVERSATION_PERMISSIONS],
+ permissions: [
+ CONTACT_PERMISSIONS,
+ ...ROLES,
+ ...CONVERSATION_PERMISSIONS,
+ PORTAL_PERMISSIONS,
+ ],
count: () => null, // No count for all tab
},
contacts: {
@@ -95,6 +109,10 @@ const TABS_CONFIG = {
permissions: [...ROLES, ...CONVERSATION_PERMISSIONS],
count: () => mappedMessages.value.length,
},
+ articles: {
+ permissions: [...ROLES, PORTAL_PERMISSIONS],
+ count: () => mappedArticles.value.length,
+ },
};
const tabs = computed(() => {
@@ -123,6 +141,10 @@ const totalSearchResultsCount = computed(() => {
permissions: [...ROLES, ...CONVERSATION_PERMISSIONS],
count: () => conversations.value.length + messages.value.length,
},
+ articles: {
+ permissions: [...ROLES, PORTAL_PERMISSIONS],
+ count: () => articles.value.length,
+ },
};
return filterItemsByPermission(
permissionCounts,
@@ -138,12 +160,13 @@ const activeTabIndex = computed(() => {
});
const isFetchingAny = computed(() => {
- const { contact, message, conversation, isFetching } = uiFlags.value;
+ const { contact, message, conversation, article, isFetching } = uiFlags.value;
return (
isFetching ||
contact.isFetching ||
message.isFetching ||
- conversation.isFetching
+ conversation.isFetching ||
+ article.isFetching
);
});
@@ -171,6 +194,7 @@ const showLoadMore = computed(() => {
contacts: mappedContacts.value,
conversations: mappedConversations.value,
messages: mappedMessages.value,
+ articles: mappedArticles.value,
}[selectedTab.value];
return (
@@ -185,10 +209,11 @@ const showViewMore = computed(() => ({
conversations:
mappedConversations.value?.length > 5 && isSelectedTabAll.value,
messages: mappedMessages.value?.length > 5 && isSelectedTabAll.value,
+ articles: mappedArticles.value?.length > 5 && isSelectedTabAll.value,
}));
const clearSearchResult = () => {
- pages.value = { contacts: 1, conversations: 1, messages: 1 };
+ pages.value = { contacts: 1, conversations: 1, messages: 1, articles: 1 };
store.dispatch('conversationSearch/clearSearchResults');
};
@@ -214,6 +239,7 @@ const loadMore = () => {
contacts: 'conversationSearch/contactSearch',
conversations: 'conversationSearch/conversationSearch',
messages: 'conversationSearch/messageSearch',
+ articles: 'conversationSearch/articleSearch',
};
if (uiFlags.value.isFetching || selectedTab.value === 'all') return;
@@ -328,6 +354,28 @@ onUnmounted(() => {
/>
+
+
+
+
+
{
q: 'test',
});
expect(dispatch).toHaveBeenCalledWith('messageSearch', { q: 'test' });
+ expect(dispatch).toHaveBeenCalledWith('articleSearch', { q: 'test' });
});
});
@@ -150,6 +151,30 @@ describe('#actions', () => {
});
});
+ describe('#articleSearch', () => {
+ it('should handle successful article search', async () => {
+ axios.get.mockResolvedValue({
+ data: { payload: { articles: [{ id: 1 }] } },
+ });
+
+ await actions.articleSearch({ commit }, { q: 'test', page: 1 });
+ expect(commit.mock.calls).toEqual([
+ [types.ARTICLE_SEARCH_SET_UI_FLAG, { isFetching: true }],
+ [types.ARTICLE_SEARCH_SET, [{ id: 1 }]],
+ [types.ARTICLE_SEARCH_SET_UI_FLAG, { isFetching: false }],
+ ]);
+ });
+
+ it('should handle failed article search', async () => {
+ axios.get.mockRejectedValue({});
+ await actions.articleSearch({ commit }, { q: 'test' });
+ expect(commit.mock.calls).toEqual([
+ [types.ARTICLE_SEARCH_SET_UI_FLAG, { isFetching: true }],
+ [types.ARTICLE_SEARCH_SET_UI_FLAG, { isFetching: false }],
+ ]);
+ });
+ });
+
describe('#clearSearchResults', () => {
it('should commit clear search results mutation', () => {
actions.clearSearchResults({ commit });
diff --git a/app/javascript/dashboard/store/modules/specs/conversationSearch/getters.spec.js b/app/javascript/dashboard/store/modules/specs/conversationSearch/getters.spec.js
index ea3ca7048..efce6084a 100644
--- a/app/javascript/dashboard/store/modules/specs/conversationSearch/getters.spec.js
+++ b/app/javascript/dashboard/store/modules/specs/conversationSearch/getters.spec.js
@@ -37,6 +37,15 @@ describe('#getters', () => {
]);
});
+ it('getArticleRecords', () => {
+ const state = {
+ articleRecords: [{ id: 1, title: 'Article 1' }],
+ };
+ expect(getters.getArticleRecords(state)).toEqual([
+ { id: 1, title: 'Article 1' },
+ ]);
+ });
+
it('getUIFlags', () => {
const state = {
uiFlags: {
@@ -45,6 +54,7 @@ describe('#getters', () => {
contact: { isFetching: true },
message: { isFetching: false },
conversation: { isFetching: false },
+ article: { isFetching: false },
},
};
expect(getters.getUIFlags(state)).toEqual({
@@ -53,6 +63,7 @@ describe('#getters', () => {
contact: { isFetching: true },
message: { isFetching: false },
conversation: { isFetching: false },
+ article: { isFetching: false },
});
});
});
diff --git a/app/javascript/dashboard/store/modules/specs/conversationSearch/mutations.spec.js b/app/javascript/dashboard/store/modules/specs/conversationSearch/mutations.spec.js
index 7bef2e527..bf7e833d0 100644
--- a/app/javascript/dashboard/store/modules/specs/conversationSearch/mutations.spec.js
+++ b/app/javascript/dashboard/store/modules/specs/conversationSearch/mutations.spec.js
@@ -101,17 +101,39 @@ describe('#mutations', () => {
});
});
+ describe('#ARTICLE_SEARCH_SET', () => {
+ it('should append new article records to existing ones', () => {
+ const state = { articleRecords: [{ id: 1 }] };
+ mutations[types.ARTICLE_SEARCH_SET](state, [{ id: 2 }]);
+ expect(state.articleRecords).toEqual([{ id: 1 }, { id: 2 }]);
+ });
+ });
+
+ describe('#ARTICLE_SEARCH_SET_UI_FLAG', () => {
+ it('set article search UI flags correctly', () => {
+ const state = {
+ uiFlags: {
+ article: { isFetching: true },
+ },
+ };
+ mutations[types.ARTICLE_SEARCH_SET_UI_FLAG](state, { isFetching: false });
+ expect(state.uiFlags.article).toEqual({ isFetching: false });
+ });
+ });
+
describe('#CLEAR_SEARCH_RESULTS', () => {
it('should clear all search records', () => {
const state = {
contactRecords: [{ id: 1 }],
conversationRecords: [{ id: 1 }],
messageRecords: [{ id: 1 }],
+ articleRecords: [{ id: 1 }],
};
mutations[types.CLEAR_SEARCH_RESULTS](state);
expect(state.contactRecords).toEqual([]);
expect(state.conversationRecords).toEqual([]);
expect(state.messageRecords).toEqual([]);
+ expect(state.articleRecords).toEqual([]);
});
});
});
diff --git a/app/javascript/dashboard/store/mutation-types.js b/app/javascript/dashboard/store/mutation-types.js
index a74207e92..f3817c45a 100644
--- a/app/javascript/dashboard/store/mutation-types.js
+++ b/app/javascript/dashboard/store/mutation-types.js
@@ -317,8 +317,10 @@ export default {
CONVERSATION_SEARCH_SET: 'CONVERSATION_SEARCH_SET',
CONVERSATION_SEARCH_SET_UI_FLAG: 'CONVERSATION_SEARCH_SET_UI_FLAG',
MESSAGE_SEARCH_SET: 'MESSAGE_SEARCH_SET',
+ ARTICLE_SEARCH_SET: 'ARTICLE_SEARCH_SET',
CLEAR_SEARCH_RESULTS: 'CLEAR_SEARCH_RESULTS',
MESSAGE_SEARCH_SET_UI_FLAG: 'MESSAGE_SEARCH_SET_UI_FLAG',
+ ARTICLE_SEARCH_SET_UI_FLAG: 'ARTICLE_SEARCH_SET_UI_FLAG',
FULL_SEARCH_SET_UI_FLAG: 'FULL_SEARCH_SET_UI_FLAG',
SET_CONVERSATION_PARTICIPANTS_UI_FLAG:
'SET_CONVERSATION_PARTICIPANTS_UI_FLAG',
diff --git a/app/services/search_service.rb b/app/services/search_service.rb
index 5999c88a6..40d862b19 100644
--- a/app/services/search_service.rb
+++ b/app/services/search_service.rb
@@ -9,8 +9,10 @@ class SearchService
{ conversations: filter_conversations }
when 'Contact'
{ contacts: filter_contacts }
+ when 'Article'
+ { articles: filter_articles }
else
- { contacts: filter_contacts, messages: filter_messages, conversations: filter_conversations }
+ { contacts: filter_contacts, messages: filter_messages, conversations: filter_conversations, articles: filter_articles }
end
end
@@ -90,4 +92,12 @@ class SearchService
ILIKE :search OR identifier ILIKE :search", search: "%#{search_query}%"
).resolved_contacts.order_on_last_activity_at('desc').page(params[:page]).per(15)
end
+
+ def filter_articles
+ @articles = current_account.articles
+ .text_search(search_query)
+ .reorder('updated_at DESC')
+ .page(params[:page])
+ .per(15)
+ end
end
diff --git a/app/views/api/v1/accounts/search/_article.json.jbuilder b/app/views/api/v1/accounts/search/_article.json.jbuilder
new file mode 100644
index 000000000..a3cf94614
--- /dev/null
+++ b/app/views/api/v1/accounts/search/_article.json.jbuilder
@@ -0,0 +1,8 @@
+json.id article.id
+json.title article.title
+json.locale article.locale
+json.content article.content
+json.slug article.slug
+json.portal_slug article.portal.slug
+json.account_id article.account_id
+json.category_name article.category&.name
diff --git a/app/views/api/v1/accounts/search/_conversation_search_result.json.jbuilder b/app/views/api/v1/accounts/search/_conversation_search_result.json.jbuilder
new file mode 100644
index 000000000..a0b7e0203
--- /dev/null
+++ b/app/views/api/v1/accounts/search/_conversation_search_result.json.jbuilder
@@ -0,0 +1,15 @@
+json.id conversation.display_id
+json.account_id conversation.account_id
+json.created_at conversation.created_at.to_i
+json.message do
+ json.partial! 'message', formats: [:json], message: conversation.messages.try(:first)
+end
+json.contact do
+ json.partial! 'contact', formats: [:json], contact: conversation.contact if conversation.try(:contact).present?
+end
+json.inbox do
+ json.partial! 'inbox', formats: [:json], inbox: conversation.inbox if conversation.try(:inbox).present?
+end
+json.agent do
+ json.partial! 'agent', formats: [:json], agent: conversation.assignee if conversation.try(:assignee).present?
+end
diff --git a/app/views/api/v1/accounts/search/articles.json.jbuilder b/app/views/api/v1/accounts/search/articles.json.jbuilder
new file mode 100644
index 000000000..7d4fe031c
--- /dev/null
+++ b/app/views/api/v1/accounts/search/articles.json.jbuilder
@@ -0,0 +1,7 @@
+json.payload do
+ json.articles do
+ json.array! @result[:articles] do |article|
+ json.partial! 'article', formats: [:json], article: article
+ end
+ end
+end
\ No newline at end of file
diff --git a/app/views/api/v1/accounts/search/index.json.jbuilder b/app/views/api/v1/accounts/search/index.json.jbuilder
index 1c6e86284..a3d8f1858 100644
--- a/app/views/api/v1/accounts/search/index.json.jbuilder
+++ b/app/views/api/v1/accounts/search/index.json.jbuilder
@@ -1,21 +1,7 @@
json.payload do
json.conversations do
json.array! @result[:conversations] do |conversation|
- json.id conversation.display_id
- json.account_id conversation.account_id
- json.created_at conversation.created_at.to_i
- json.message do
- json.partial! 'message', formats: [:json], message: conversation.messages.try(:first)
- end
- json.contact do
- json.partial! 'contact', formats: [:json], contact: conversation.contact if conversation.try(:contact).present?
- end
- json.inbox do
- json.partial! 'inbox', formats: [:json], inbox: conversation.inbox if conversation.try(:inbox).present?
- end
- json.agent do
- json.partial! 'agent', formats: [:json], agent: conversation.assignee if conversation.try(:assignee).present?
- end
+ json.partial! 'conversation_search_result', formats: [:json], conversation: conversation
end
end
json.contacts do
@@ -23,10 +9,14 @@ json.payload do
json.partial! 'contact', formats: [:json], contact: contact
end
end
-
json.messages do
json.array! @result[:messages] do |message|
json.partial! 'message', formats: [:json], message: message
end
end
+ json.articles do
+ json.array! @result[:articles] do |article|
+ json.partial! 'article', formats: [:json], article: article
+ end
+ end
end
diff --git a/config/routes.rb b/config/routes.rb
index 4b4db7b6d..d1705d605 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -137,6 +137,7 @@ Rails.application.routes.draw do
get :conversations
get :messages
get :contacts
+ get :articles
end
end
diff --git a/spec/controllers/api/v1/accounts/search_controller_spec.rb b/spec/controllers/api/v1/accounts/search_controller_spec.rb
index b5644cebf..ea59bec9c 100644
--- a/spec/controllers/api/v1/accounts/search_controller_spec.rb
+++ b/spec/controllers/api/v1/accounts/search_controller_spec.rb
@@ -11,6 +11,11 @@ RSpec.describe 'Search', type: :request do
create(:message, conversation: conversation, account: account, content: 'test2')
create(:contact_inbox, contact_id: contact.id, inbox_id: conversation.inbox.id)
create(:inbox_member, user: agent, inbox: conversation.inbox)
+
+ # Create articles for testing
+ portal = create(:portal, account: account)
+ create(:article, title: 'Test Article Guide', content: 'This is a test article content',
+ account: account, portal: portal, author: agent, status: 'published')
end
describe 'GET /api/v1/accounts/{account.id}/search' do
@@ -33,10 +38,11 @@ RSpec.describe 'Search', type: :request do
response_data = JSON.parse(response.body, symbolize_names: true)
expect(response_data[:payload][:messages].first[:content]).to eq 'test2'
- expect(response_data[:payload].keys).to contain_exactly(:contacts, :conversations, :messages)
+ expect(response_data[:payload].keys).to contain_exactly(:contacts, :conversations, :messages, :articles)
expect(response_data[:payload][:messages].length).to eq 2
expect(response_data[:payload][:conversations].length).to eq 1
expect(response_data[:payload][:contacts].length).to eq 1
+ expect(response_data[:payload][:articles].length).to eq 1
end
end
end
@@ -115,4 +121,60 @@ RSpec.describe 'Search', type: :request do
end
end
end
+
+ describe 'GET /api/v1/accounts/{account.id}/search/articles' do
+ context 'when it is an unauthenticated user' do
+ it 'returns unauthorized' do
+ get "/api/v1/accounts/#{account.id}/search/articles", params: { q: 'test' }
+
+ expect(response).to have_http_status(:unauthorized)
+ end
+ end
+
+ context 'when it is an authenticated user' do
+ it 'returns all articles containing the search query' do
+ get "/api/v1/accounts/#{account.id}/search/articles",
+ headers: agent.create_new_auth_token,
+ params: { q: 'test' },
+ as: :json
+
+ expect(response).to have_http_status(:success)
+ response_data = JSON.parse(response.body, symbolize_names: true)
+
+ expect(response_data[:payload].keys).to contain_exactly(:articles)
+ expect(response_data[:payload][:articles].length).to eq 1
+ expect(response_data[:payload][:articles].first[:title]).to eq 'Test Article Guide'
+ end
+
+ it 'returns empty results when no articles match the search query' do
+ get "/api/v1/accounts/#{account.id}/search/articles",
+ headers: agent.create_new_auth_token,
+ params: { q: 'nonexistent' },
+ as: :json
+
+ expect(response).to have_http_status(:success)
+ response_data = JSON.parse(response.body, symbolize_names: true)
+
+ expect(response_data[:payload].keys).to contain_exactly(:articles)
+ expect(response_data[:payload][:articles].length).to eq 0
+ end
+
+ it 'supports pagination' do
+ portal = create(:portal, account: account)
+ 16.times do |i|
+ create(:article, title: "Test Article #{i}", account: account, portal: portal, author: agent, status: 'published')
+ end
+
+ get "/api/v1/accounts/#{account.id}/search/articles",
+ headers: agent.create_new_auth_token,
+ params: { q: 'test', page: 1 },
+ as: :json
+
+ expect(response).to have_http_status(:success)
+ response_data = JSON.parse(response.body, symbolize_names: true)
+
+ expect(response_data[:payload][:articles].length).to eq 15 # Default per_page is 15
+ end
+ end
+ end
end
diff --git a/spec/services/search_service_spec.rb b/spec/services/search_service_spec.rb
index af097a2c9..22809d042 100644
--- a/spec/services/search_service_spec.rb
+++ b/spec/services/search_service_spec.rb
@@ -10,6 +10,11 @@ describe SearchService do
let!(:harry) { create(:contact, name: 'Harry Potter', email: 'test@test.com', account_id: account.id) }
let!(:conversation) { create(:conversation, contact: harry, inbox: inbox, account: account) }
let!(:message) { create(:message, account: account, inbox: inbox, content: 'Harry Potter is a wizard') }
+ let!(:portal) { create(:portal, account: account) }
+ let(:article) do
+ create(:article, title: 'Harry Potter Magic Guide', content: 'Learn about wizardry', account: account, portal: portal, author: user,
+ status: 'published')
+ end
before do
create(:inbox_member, user: user, inbox: inbox)
@@ -27,7 +32,7 @@ describe SearchService do
it 'returns all for all' do
search_type = 'all'
search = described_class.new(current_user: user, current_account: account, params: params, search_type: search_type)
- expect(search.perform.keys).to match_array(%i[contacts messages conversations])
+ expect(search.perform.keys).to match_array(%i[contacts messages conversations articles])
end
it 'returns contacts for contacts' do
@@ -47,6 +52,12 @@ describe SearchService do
search = described_class.new(current_user: user, current_account: account, params: params, search_type: search_type)
expect(search.perform.keys).to match_array(%i[conversations])
end
+
+ it 'returns articles for articles' do
+ search_type = 'Article'
+ search = described_class.new(current_user: user, current_account: account, params: params, search_type: search_type)
+ expect(search.perform.keys).to match_array(%i[articles])
+ end
end
context 'when contact search' do
@@ -143,6 +154,50 @@ describe SearchService do
expect(search.perform[:conversations].map(&:id)).to include new_converstion.id
end
end
+
+ context 'when article search' do
+ it 'orders results by updated_at desc' do
+ # Create articles with explicit timestamps
+ older_time = 2.days.ago
+ newer_time = 1.hour.ago
+
+ article2 = create(:article, title: 'Spellcasting Guide',
+ account: account, portal: portal, author: user, status: 'published')
+ # rubocop:disable Rails/SkipsModelValidations
+ article2.update_column(:updated_at, older_time)
+ # rubocop:enable Rails/SkipsModelValidations
+
+ article3 = create(:article, title: 'Spellcasting Manual',
+ account: account, portal: portal, author: user, status: 'published')
+ # rubocop:disable Rails/SkipsModelValidations
+ article3.update_column(:updated_at, newer_time)
+ # rubocop:enable Rails/SkipsModelValidations
+
+ params = { q: 'Spellcasting' }
+ search = described_class.new(current_user: user, current_account: account, params: params, search_type: 'Article')
+ results = search.perform[:articles]
+
+ # Check the timestamps to understand ordering
+ results.map { |a| [a.id, a.updated_at] }
+
+ # Should be ordered by updated_at desc (newer first)
+ expect(results.length).to eq(2)
+ expect(results.first.updated_at).to be > results.second.updated_at
+ end
+
+ it 'returns paginated results' do
+ # Create many articles to test pagination
+ 16.times do |i|
+ create(:article, title: "Magic Article #{i}", account: account, portal: portal, author: user, status: 'published')
+ end
+
+ params = { q: 'Magic', page: 1 }
+ search = described_class.new(current_user: user, current_account: account, params: params, search_type: 'Article')
+ results = search.perform[:articles]
+
+ expect(results.length).to eq(15) # Default per_page is 15
+ end
+ end
end
describe '#use_gin_search' do