mirror of
https://github.com/lingble/chatwoot.git
synced 2025-11-03 12:37:56 +00:00
feat: allow searching articles in omnisearch (#11558)
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
This commit is contained in:
@@ -15,6 +15,10 @@ class Api::V1::Accounts::SearchController < Api::V1::Accounts::BaseController
|
|||||||
@result = search('Message')
|
@result = search('Message')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def articles
|
||||||
|
@result = search('Article')
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def search(search_type)
|
def search(search_type)
|
||||||
|
|||||||
@@ -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();
|
export default new SearchAPI();
|
||||||
|
|||||||
@@ -4,12 +4,14 @@
|
|||||||
"ALL": "All",
|
"ALL": "All",
|
||||||
"CONTACTS": "Contacts",
|
"CONTACTS": "Contacts",
|
||||||
"CONVERSATIONS": "Conversations",
|
"CONVERSATIONS": "Conversations",
|
||||||
"MESSAGES": "Messages"
|
"MESSAGES": "Messages",
|
||||||
|
"ARTICLES": "Articles"
|
||||||
},
|
},
|
||||||
"SECTION": {
|
"SECTION": {
|
||||||
"CONTACTS": "Contacts",
|
"CONTACTS": "Contacts",
|
||||||
"CONVERSATIONS": "Conversations",
|
"CONVERSATIONS": "Conversations",
|
||||||
"MESSAGES": "Messages"
|
"MESSAGES": "Messages",
|
||||||
|
"ARTICLES": "Articles"
|
||||||
},
|
},
|
||||||
"VIEW_MORE": "View more",
|
"VIEW_MORE": "View more",
|
||||||
"LOAD_MORE": "Load more",
|
"LOAD_MORE": "Load more",
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
<script setup>
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import Icon from 'next/icon/Icon.vue';
|
||||||
|
import { frontendURL } from 'dashboard/helper/URLHelper';
|
||||||
|
import MessageFormatter from 'shared/helpers/MessageFormatter';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
id: { type: [String, Number], default: 0 },
|
||||||
|
title: { type: String, default: '' },
|
||||||
|
description: { type: String, default: '' },
|
||||||
|
category: { type: String, default: '' },
|
||||||
|
locale: { type: String, default: '' },
|
||||||
|
content: { type: String, default: '' },
|
||||||
|
portalSlug: { type: String, required: true },
|
||||||
|
accountId: { type: [String, Number], default: 0 },
|
||||||
|
});
|
||||||
|
|
||||||
|
const MAX_LENGTH = 300;
|
||||||
|
|
||||||
|
const navigateTo = computed(() => {
|
||||||
|
return frontendURL(
|
||||||
|
`accounts/${props.accountId}/portals/${props.portalSlug}/${props.locale}/articles/edit/${props.id}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const truncatedContent = computed(() => {
|
||||||
|
if (!props.content) return props.description || '';
|
||||||
|
|
||||||
|
// Use MessageFormatter to properly convert markdown to plain text
|
||||||
|
const formatter = new MessageFormatter(props.content);
|
||||||
|
const plainText = formatter.plainText.trim();
|
||||||
|
|
||||||
|
return plainText.length > MAX_LENGTH
|
||||||
|
? `${plainText.substring(0, MAX_LENGTH)}...`
|
||||||
|
: plainText;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<router-link
|
||||||
|
:to="navigateTo"
|
||||||
|
class="flex items-start p-2 rounded-xl cursor-pointer hover:bg-n-slate-2"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="flex items-center justify-center w-6 h-6 mt-0.5 rounded bg-n-slate-3"
|
||||||
|
>
|
||||||
|
<Icon icon="i-lucide-library-big" class="text-n-slate-10" />
|
||||||
|
</div>
|
||||||
|
<div class="ltr:ml-2 rtl:mr-2 min-w-0 flex-1">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<h5 class="text-sm font-medium truncate min-w-0 text-n-slate-12">
|
||||||
|
{{ title }}
|
||||||
|
</h5>
|
||||||
|
<span
|
||||||
|
v-if="category"
|
||||||
|
class="text-xs font-medium whitespace-nowrap capitalize bg-n-slate-3 px-1 py-0.5 rounded text-n-slate-10"
|
||||||
|
>
|
||||||
|
{{ category }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p
|
||||||
|
v-if="truncatedContent"
|
||||||
|
class="mt-1 text-sm text-n-slate-11 line-clamp-2"
|
||||||
|
>
|
||||||
|
{{ truncatedContent }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</router-link>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
<script setup>
|
||||||
|
import { useMapGetter } from 'dashboard/composables/store.js';
|
||||||
|
|
||||||
|
import SearchResultSection from './SearchResultSection.vue';
|
||||||
|
import SearchResultArticleItem from './SearchResultArticleItem.vue';
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
articles: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
query: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
isFetching: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
showTitle: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const accountId = useMapGetter('getCurrentAccountId');
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<SearchResultSection
|
||||||
|
:title="$t('SEARCH.SECTION.ARTICLES')"
|
||||||
|
:empty="!articles.length"
|
||||||
|
:query="query"
|
||||||
|
:show-title="showTitle"
|
||||||
|
:is-fetching="isFetching"
|
||||||
|
>
|
||||||
|
<ul v-if="articles.length" class="space-y-1.5 list-none">
|
||||||
|
<li v-for="article in articles" :key="article.id">
|
||||||
|
<SearchResultArticleItem
|
||||||
|
:id="article.id"
|
||||||
|
:title="article.title"
|
||||||
|
:description="article.description"
|
||||||
|
:content="article.content"
|
||||||
|
:portal-slug="article.portal_slug"
|
||||||
|
:locale="article.locale"
|
||||||
|
:account-id="accountId"
|
||||||
|
:category="article.category_name"
|
||||||
|
:status="article.status"
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</SearchResultSection>
|
||||||
|
</template>
|
||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
ROLES,
|
ROLES,
|
||||||
CONVERSATION_PERMISSIONS,
|
CONVERSATION_PERMISSIONS,
|
||||||
CONTACT_PERMISSIONS,
|
CONTACT_PERMISSIONS,
|
||||||
|
PORTAL_PERMISSIONS,
|
||||||
} from 'dashboard/constants/permissions.js';
|
} from 'dashboard/constants/permissions.js';
|
||||||
import {
|
import {
|
||||||
getUserPermissions,
|
getUserPermissions,
|
||||||
@@ -22,6 +23,7 @@ import SearchTabs from './SearchTabs.vue';
|
|||||||
import SearchResultConversationsList from './SearchResultConversationsList.vue';
|
import SearchResultConversationsList from './SearchResultConversationsList.vue';
|
||||||
import SearchResultMessagesList from './SearchResultMessagesList.vue';
|
import SearchResultMessagesList from './SearchResultMessagesList.vue';
|
||||||
import SearchResultContactsList from './SearchResultContactsList.vue';
|
import SearchResultContactsList from './SearchResultContactsList.vue';
|
||||||
|
import SearchResultArticlesList from './SearchResultArticlesList.vue';
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const store = useStore();
|
const store = useStore();
|
||||||
@@ -34,6 +36,7 @@ const pages = ref({
|
|||||||
contacts: 1,
|
contacts: 1,
|
||||||
conversations: 1,
|
conversations: 1,
|
||||||
messages: 1,
|
messages: 1,
|
||||||
|
articles: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
const currentUser = useMapGetter('getCurrentUser');
|
const currentUser = useMapGetter('getCurrentUser');
|
||||||
@@ -43,6 +46,7 @@ const conversationRecords = useMapGetter(
|
|||||||
'conversationSearch/getConversationRecords'
|
'conversationSearch/getConversationRecords'
|
||||||
);
|
);
|
||||||
const messageRecords = useMapGetter('conversationSearch/getMessageRecords');
|
const messageRecords = useMapGetter('conversationSearch/getMessageRecords');
|
||||||
|
const articleRecords = useMapGetter('conversationSearch/getArticleRecords');
|
||||||
const uiFlags = useMapGetter('conversationSearch/getUIFlags');
|
const uiFlags = useMapGetter('conversationSearch/getUIFlags');
|
||||||
|
|
||||||
const addTypeToRecords = (records, type) =>
|
const addTypeToRecords = (records, type) =>
|
||||||
@@ -57,6 +61,9 @@ const mappedConversations = computed(() =>
|
|||||||
const mappedMessages = computed(() =>
|
const mappedMessages = computed(() =>
|
||||||
addTypeToRecords(messageRecords, 'message')
|
addTypeToRecords(messageRecords, 'message')
|
||||||
);
|
);
|
||||||
|
const mappedArticles = computed(() =>
|
||||||
|
addTypeToRecords(articleRecords, 'article')
|
||||||
|
);
|
||||||
|
|
||||||
const isSelectedTabAll = computed(() => selectedTab.value === 'all');
|
const isSelectedTabAll = computed(() => selectedTab.value === 'all');
|
||||||
|
|
||||||
@@ -66,6 +73,7 @@ const sliceRecordsIfAllTab = items =>
|
|||||||
const contacts = computed(() => sliceRecordsIfAllTab(mappedContacts));
|
const contacts = computed(() => sliceRecordsIfAllTab(mappedContacts));
|
||||||
const conversations = computed(() => sliceRecordsIfAllTab(mappedConversations));
|
const conversations = computed(() => sliceRecordsIfAllTab(mappedConversations));
|
||||||
const messages = computed(() => sliceRecordsIfAllTab(mappedMessages));
|
const messages = computed(() => sliceRecordsIfAllTab(mappedMessages));
|
||||||
|
const articles = computed(() => sliceRecordsIfAllTab(mappedArticles));
|
||||||
|
|
||||||
const filterByTab = tab =>
|
const filterByTab = tab =>
|
||||||
computed(() => selectedTab.value === tab || isSelectedTabAll.value);
|
computed(() => selectedTab.value === tab || isSelectedTabAll.value);
|
||||||
@@ -73,6 +81,7 @@ const filterByTab = tab =>
|
|||||||
const filterContacts = filterByTab('contacts');
|
const filterContacts = filterByTab('contacts');
|
||||||
const filterConversations = filterByTab('conversations');
|
const filterConversations = filterByTab('conversations');
|
||||||
const filterMessages = filterByTab('messages');
|
const filterMessages = filterByTab('messages');
|
||||||
|
const filterArticles = filterByTab('articles');
|
||||||
|
|
||||||
const userPermissions = computed(() =>
|
const userPermissions = computed(() =>
|
||||||
getUserPermissions(currentUser.value, currentAccountId.value)
|
getUserPermissions(currentUser.value, currentAccountId.value)
|
||||||
@@ -80,7 +89,12 @@ const userPermissions = computed(() =>
|
|||||||
|
|
||||||
const TABS_CONFIG = {
|
const TABS_CONFIG = {
|
||||||
all: {
|
all: {
|
||||||
permissions: [CONTACT_PERMISSIONS, ...ROLES, ...CONVERSATION_PERMISSIONS],
|
permissions: [
|
||||||
|
CONTACT_PERMISSIONS,
|
||||||
|
...ROLES,
|
||||||
|
...CONVERSATION_PERMISSIONS,
|
||||||
|
PORTAL_PERMISSIONS,
|
||||||
|
],
|
||||||
count: () => null, // No count for all tab
|
count: () => null, // No count for all tab
|
||||||
},
|
},
|
||||||
contacts: {
|
contacts: {
|
||||||
@@ -95,6 +109,10 @@ const TABS_CONFIG = {
|
|||||||
permissions: [...ROLES, ...CONVERSATION_PERMISSIONS],
|
permissions: [...ROLES, ...CONVERSATION_PERMISSIONS],
|
||||||
count: () => mappedMessages.value.length,
|
count: () => mappedMessages.value.length,
|
||||||
},
|
},
|
||||||
|
articles: {
|
||||||
|
permissions: [...ROLES, PORTAL_PERMISSIONS],
|
||||||
|
count: () => mappedArticles.value.length,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const tabs = computed(() => {
|
const tabs = computed(() => {
|
||||||
@@ -123,6 +141,10 @@ const totalSearchResultsCount = computed(() => {
|
|||||||
permissions: [...ROLES, ...CONVERSATION_PERMISSIONS],
|
permissions: [...ROLES, ...CONVERSATION_PERMISSIONS],
|
||||||
count: () => conversations.value.length + messages.value.length,
|
count: () => conversations.value.length + messages.value.length,
|
||||||
},
|
},
|
||||||
|
articles: {
|
||||||
|
permissions: [...ROLES, PORTAL_PERMISSIONS],
|
||||||
|
count: () => articles.value.length,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
return filterItemsByPermission(
|
return filterItemsByPermission(
|
||||||
permissionCounts,
|
permissionCounts,
|
||||||
@@ -138,12 +160,13 @@ const activeTabIndex = computed(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const isFetchingAny = computed(() => {
|
const isFetchingAny = computed(() => {
|
||||||
const { contact, message, conversation, isFetching } = uiFlags.value;
|
const { contact, message, conversation, article, isFetching } = uiFlags.value;
|
||||||
return (
|
return (
|
||||||
isFetching ||
|
isFetching ||
|
||||||
contact.isFetching ||
|
contact.isFetching ||
|
||||||
message.isFetching ||
|
message.isFetching ||
|
||||||
conversation.isFetching
|
conversation.isFetching ||
|
||||||
|
article.isFetching
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -171,6 +194,7 @@ const showLoadMore = computed(() => {
|
|||||||
contacts: mappedContacts.value,
|
contacts: mappedContacts.value,
|
||||||
conversations: mappedConversations.value,
|
conversations: mappedConversations.value,
|
||||||
messages: mappedMessages.value,
|
messages: mappedMessages.value,
|
||||||
|
articles: mappedArticles.value,
|
||||||
}[selectedTab.value];
|
}[selectedTab.value];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -185,10 +209,11 @@ const showViewMore = computed(() => ({
|
|||||||
conversations:
|
conversations:
|
||||||
mappedConversations.value?.length > 5 && isSelectedTabAll.value,
|
mappedConversations.value?.length > 5 && isSelectedTabAll.value,
|
||||||
messages: mappedMessages.value?.length > 5 && isSelectedTabAll.value,
|
messages: mappedMessages.value?.length > 5 && isSelectedTabAll.value,
|
||||||
|
articles: mappedArticles.value?.length > 5 && isSelectedTabAll.value,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const clearSearchResult = () => {
|
const clearSearchResult = () => {
|
||||||
pages.value = { contacts: 1, conversations: 1, messages: 1 };
|
pages.value = { contacts: 1, conversations: 1, messages: 1, articles: 1 };
|
||||||
store.dispatch('conversationSearch/clearSearchResults');
|
store.dispatch('conversationSearch/clearSearchResults');
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -214,6 +239,7 @@ const loadMore = () => {
|
|||||||
contacts: 'conversationSearch/contactSearch',
|
contacts: 'conversationSearch/contactSearch',
|
||||||
conversations: 'conversationSearch/conversationSearch',
|
conversations: 'conversationSearch/conversationSearch',
|
||||||
messages: 'conversationSearch/messageSearch',
|
messages: 'conversationSearch/messageSearch',
|
||||||
|
articles: 'conversationSearch/articleSearch',
|
||||||
};
|
};
|
||||||
|
|
||||||
if (uiFlags.value.isFetching || selectedTab.value === 'all') return;
|
if (uiFlags.value.isFetching || selectedTab.value === 'all') return;
|
||||||
@@ -328,6 +354,28 @@ onUnmounted(() => {
|
|||||||
/>
|
/>
|
||||||
</Policy>
|
</Policy>
|
||||||
|
|
||||||
|
<Policy
|
||||||
|
:permissions="[...ROLES, PORTAL_PERMISSIONS]"
|
||||||
|
class="flex flex-col justify-center"
|
||||||
|
>
|
||||||
|
<SearchResultArticlesList
|
||||||
|
v-if="filterArticles"
|
||||||
|
:is-fetching="uiFlags.article.isFetching"
|
||||||
|
:articles="articles"
|
||||||
|
:query="query"
|
||||||
|
:show-title="isSelectedTabAll"
|
||||||
|
/>
|
||||||
|
<NextButton
|
||||||
|
v-if="showViewMore.articles"
|
||||||
|
:label="t(`SEARCH.VIEW_MORE`)"
|
||||||
|
icon="i-lucide-eye"
|
||||||
|
slate
|
||||||
|
sm
|
||||||
|
outline
|
||||||
|
@click="selectedTab = 'articles'"
|
||||||
|
/>
|
||||||
|
</Policy>
|
||||||
|
|
||||||
<div v-if="showLoadMore" class="flex justify-center mt-4 mb-6">
|
<div v-if="showLoadMore" class="flex justify-center mt-4 mb-6">
|
||||||
<NextButton
|
<NextButton
|
||||||
v-if="!isSelectedTabAll"
|
v-if="!isSelectedTabAll"
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {
|
|||||||
ROLES,
|
ROLES,
|
||||||
CONVERSATION_PERMISSIONS,
|
CONVERSATION_PERMISSIONS,
|
||||||
CONTACT_PERMISSIONS,
|
CONTACT_PERMISSIONS,
|
||||||
|
PORTAL_PERMISSIONS,
|
||||||
} from 'dashboard/constants/permissions.js';
|
} from 'dashboard/constants/permissions.js';
|
||||||
|
|
||||||
import SearchView from './components/SearchView.vue';
|
import SearchView from './components/SearchView.vue';
|
||||||
@@ -12,7 +13,12 @@ export const routes = [
|
|||||||
path: frontendURL('accounts/:accountId/search'),
|
path: frontendURL('accounts/:accountId/search'),
|
||||||
name: 'search',
|
name: 'search',
|
||||||
meta: {
|
meta: {
|
||||||
permissions: [...ROLES, ...CONVERSATION_PERMISSIONS, CONTACT_PERMISSIONS],
|
permissions: [
|
||||||
|
...ROLES,
|
||||||
|
...CONVERSATION_PERMISSIONS,
|
||||||
|
CONTACT_PERMISSIONS,
|
||||||
|
PORTAL_PERMISSIONS,
|
||||||
|
],
|
||||||
},
|
},
|
||||||
component: SearchView,
|
component: SearchView,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5,12 +5,14 @@ export const initialState = {
|
|||||||
contactRecords: [],
|
contactRecords: [],
|
||||||
conversationRecords: [],
|
conversationRecords: [],
|
||||||
messageRecords: [],
|
messageRecords: [],
|
||||||
|
articleRecords: [],
|
||||||
uiFlags: {
|
uiFlags: {
|
||||||
isFetching: false,
|
isFetching: false,
|
||||||
isSearchCompleted: false,
|
isSearchCompleted: false,
|
||||||
contact: { isFetching: false },
|
contact: { isFetching: false },
|
||||||
conversation: { isFetching: false },
|
conversation: { isFetching: false },
|
||||||
message: { isFetching: false },
|
message: { isFetching: false },
|
||||||
|
article: { isFetching: false },
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -27,6 +29,9 @@ export const getters = {
|
|||||||
getMessageRecords(state) {
|
getMessageRecords(state) {
|
||||||
return state.messageRecords;
|
return state.messageRecords;
|
||||||
},
|
},
|
||||||
|
getArticleRecords(state) {
|
||||||
|
return state.articleRecords;
|
||||||
|
},
|
||||||
getUIFlags(state) {
|
getUIFlags(state) {
|
||||||
return state.uiFlags;
|
return state.uiFlags;
|
||||||
},
|
},
|
||||||
@@ -65,6 +70,7 @@ export const actions = {
|
|||||||
dispatch('contactSearch', { q }),
|
dispatch('contactSearch', { q }),
|
||||||
dispatch('conversationSearch', { q }),
|
dispatch('conversationSearch', { q }),
|
||||||
dispatch('messageSearch', { q }),
|
dispatch('messageSearch', { q }),
|
||||||
|
dispatch('articleSearch', { q }),
|
||||||
]);
|
]);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Ignore error
|
// Ignore error
|
||||||
@@ -108,6 +114,17 @@ export const actions = {
|
|||||||
commit(types.MESSAGE_SEARCH_SET_UI_FLAG, { isFetching: false });
|
commit(types.MESSAGE_SEARCH_SET_UI_FLAG, { isFetching: false });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
async articleSearch({ commit }, { q, page = 1 }) {
|
||||||
|
commit(types.ARTICLE_SEARCH_SET_UI_FLAG, { isFetching: true });
|
||||||
|
try {
|
||||||
|
const { data } = await SearchAPI.articles({ q, page });
|
||||||
|
commit(types.ARTICLE_SEARCH_SET, data.payload.articles);
|
||||||
|
} catch (error) {
|
||||||
|
// Ignore error
|
||||||
|
} finally {
|
||||||
|
commit(types.ARTICLE_SEARCH_SET_UI_FLAG, { isFetching: false });
|
||||||
|
}
|
||||||
|
},
|
||||||
async clearSearchResults({ commit }) {
|
async clearSearchResults({ commit }) {
|
||||||
commit(types.CLEAR_SEARCH_RESULTS);
|
commit(types.CLEAR_SEARCH_RESULTS);
|
||||||
},
|
},
|
||||||
@@ -126,6 +143,9 @@ export const mutations = {
|
|||||||
[types.MESSAGE_SEARCH_SET](state, records) {
|
[types.MESSAGE_SEARCH_SET](state, records) {
|
||||||
state.messageRecords = [...state.messageRecords, ...records];
|
state.messageRecords = [...state.messageRecords, ...records];
|
||||||
},
|
},
|
||||||
|
[types.ARTICLE_SEARCH_SET](state, records) {
|
||||||
|
state.articleRecords = [...state.articleRecords, ...records];
|
||||||
|
},
|
||||||
[types.SEARCH_CONVERSATIONS_SET_UI_FLAG](state, uiFlags) {
|
[types.SEARCH_CONVERSATIONS_SET_UI_FLAG](state, uiFlags) {
|
||||||
state.uiFlags = { ...state.uiFlags, ...uiFlags };
|
state.uiFlags = { ...state.uiFlags, ...uiFlags };
|
||||||
},
|
},
|
||||||
@@ -141,10 +161,14 @@ export const mutations = {
|
|||||||
[types.MESSAGE_SEARCH_SET_UI_FLAG](state, uiFlags) {
|
[types.MESSAGE_SEARCH_SET_UI_FLAG](state, uiFlags) {
|
||||||
state.uiFlags.message = { ...state.uiFlags.message, ...uiFlags };
|
state.uiFlags.message = { ...state.uiFlags.message, ...uiFlags };
|
||||||
},
|
},
|
||||||
|
[types.ARTICLE_SEARCH_SET_UI_FLAG](state, uiFlags) {
|
||||||
|
state.uiFlags.article = { ...state.uiFlags.article, ...uiFlags };
|
||||||
|
},
|
||||||
[types.CLEAR_SEARCH_RESULTS](state) {
|
[types.CLEAR_SEARCH_RESULTS](state) {
|
||||||
state.contactRecords = [];
|
state.contactRecords = [];
|
||||||
state.conversationRecords = [];
|
state.conversationRecords = [];
|
||||||
state.messageRecords = [];
|
state.messageRecords = [];
|
||||||
|
state.articleRecords = [];
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ describe('#actions', () => {
|
|||||||
q: 'test',
|
q: 'test',
|
||||||
});
|
});
|
||||||
expect(dispatch).toHaveBeenCalledWith('messageSearch', { 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', () => {
|
describe('#clearSearchResults', () => {
|
||||||
it('should commit clear search results mutation', () => {
|
it('should commit clear search results mutation', () => {
|
||||||
actions.clearSearchResults({ commit });
|
actions.clearSearchResults({ commit });
|
||||||
|
|||||||
@@ -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', () => {
|
it('getUIFlags', () => {
|
||||||
const state = {
|
const state = {
|
||||||
uiFlags: {
|
uiFlags: {
|
||||||
@@ -45,6 +54,7 @@ describe('#getters', () => {
|
|||||||
contact: { isFetching: true },
|
contact: { isFetching: true },
|
||||||
message: { isFetching: false },
|
message: { isFetching: false },
|
||||||
conversation: { isFetching: false },
|
conversation: { isFetching: false },
|
||||||
|
article: { isFetching: false },
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
expect(getters.getUIFlags(state)).toEqual({
|
expect(getters.getUIFlags(state)).toEqual({
|
||||||
@@ -53,6 +63,7 @@ describe('#getters', () => {
|
|||||||
contact: { isFetching: true },
|
contact: { isFetching: true },
|
||||||
message: { isFetching: false },
|
message: { isFetching: false },
|
||||||
conversation: { isFetching: false },
|
conversation: { isFetching: false },
|
||||||
|
article: { isFetching: false },
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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', () => {
|
describe('#CLEAR_SEARCH_RESULTS', () => {
|
||||||
it('should clear all search records', () => {
|
it('should clear all search records', () => {
|
||||||
const state = {
|
const state = {
|
||||||
contactRecords: [{ id: 1 }],
|
contactRecords: [{ id: 1 }],
|
||||||
conversationRecords: [{ id: 1 }],
|
conversationRecords: [{ id: 1 }],
|
||||||
messageRecords: [{ id: 1 }],
|
messageRecords: [{ id: 1 }],
|
||||||
|
articleRecords: [{ id: 1 }],
|
||||||
};
|
};
|
||||||
mutations[types.CLEAR_SEARCH_RESULTS](state);
|
mutations[types.CLEAR_SEARCH_RESULTS](state);
|
||||||
expect(state.contactRecords).toEqual([]);
|
expect(state.contactRecords).toEqual([]);
|
||||||
expect(state.conversationRecords).toEqual([]);
|
expect(state.conversationRecords).toEqual([]);
|
||||||
expect(state.messageRecords).toEqual([]);
|
expect(state.messageRecords).toEqual([]);
|
||||||
|
expect(state.articleRecords).toEqual([]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -317,8 +317,10 @@ export default {
|
|||||||
CONVERSATION_SEARCH_SET: 'CONVERSATION_SEARCH_SET',
|
CONVERSATION_SEARCH_SET: 'CONVERSATION_SEARCH_SET',
|
||||||
CONVERSATION_SEARCH_SET_UI_FLAG: 'CONVERSATION_SEARCH_SET_UI_FLAG',
|
CONVERSATION_SEARCH_SET_UI_FLAG: 'CONVERSATION_SEARCH_SET_UI_FLAG',
|
||||||
MESSAGE_SEARCH_SET: 'MESSAGE_SEARCH_SET',
|
MESSAGE_SEARCH_SET: 'MESSAGE_SEARCH_SET',
|
||||||
|
ARTICLE_SEARCH_SET: 'ARTICLE_SEARCH_SET',
|
||||||
CLEAR_SEARCH_RESULTS: 'CLEAR_SEARCH_RESULTS',
|
CLEAR_SEARCH_RESULTS: 'CLEAR_SEARCH_RESULTS',
|
||||||
MESSAGE_SEARCH_SET_UI_FLAG: 'MESSAGE_SEARCH_SET_UI_FLAG',
|
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',
|
FULL_SEARCH_SET_UI_FLAG: 'FULL_SEARCH_SET_UI_FLAG',
|
||||||
SET_CONVERSATION_PARTICIPANTS_UI_FLAG:
|
SET_CONVERSATION_PARTICIPANTS_UI_FLAG:
|
||||||
'SET_CONVERSATION_PARTICIPANTS_UI_FLAG',
|
'SET_CONVERSATION_PARTICIPANTS_UI_FLAG',
|
||||||
|
|||||||
@@ -9,8 +9,10 @@ class SearchService
|
|||||||
{ conversations: filter_conversations }
|
{ conversations: filter_conversations }
|
||||||
when 'Contact'
|
when 'Contact'
|
||||||
{ contacts: filter_contacts }
|
{ contacts: filter_contacts }
|
||||||
|
when 'Article'
|
||||||
|
{ articles: filter_articles }
|
||||||
else
|
else
|
||||||
{ contacts: filter_contacts, messages: filter_messages, conversations: filter_conversations }
|
{ contacts: filter_contacts, messages: filter_messages, conversations: filter_conversations, articles: filter_articles }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -90,4 +92,12 @@ class SearchService
|
|||||||
ILIKE :search OR identifier ILIKE :search", search: "%#{search_query}%"
|
ILIKE :search OR identifier ILIKE :search", search: "%#{search_query}%"
|
||||||
).resolved_contacts.order_on_last_activity_at('desc').page(params[:page]).per(15)
|
).resolved_contacts.order_on_last_activity_at('desc').page(params[:page]).per(15)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def filter_articles
|
||||||
|
@articles = current_account.articles
|
||||||
|
.text_search(search_query)
|
||||||
|
.reorder('updated_at DESC')
|
||||||
|
.page(params[:page])
|
||||||
|
.per(15)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
8
app/views/api/v1/accounts/search/_article.json.jbuilder
Normal file
8
app/views/api/v1/accounts/search/_article.json.jbuilder
Normal file
@@ -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
|
||||||
@@ -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
|
||||||
7
app/views/api/v1/accounts/search/articles.json.jbuilder
Normal file
7
app/views/api/v1/accounts/search/articles.json.jbuilder
Normal file
@@ -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
|
||||||
@@ -1,21 +1,7 @@
|
|||||||
json.payload do
|
json.payload do
|
||||||
json.conversations do
|
json.conversations do
|
||||||
json.array! @result[:conversations] do |conversation|
|
json.array! @result[:conversations] do |conversation|
|
||||||
json.id conversation.display_id
|
json.partial! 'conversation_search_result', formats: [:json], conversation: conversation
|
||||||
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
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
json.contacts do
|
json.contacts do
|
||||||
@@ -23,10 +9,14 @@ json.payload do
|
|||||||
json.partial! 'contact', formats: [:json], contact: contact
|
json.partial! 'contact', formats: [:json], contact: contact
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
json.messages do
|
json.messages do
|
||||||
json.array! @result[:messages] do |message|
|
json.array! @result[:messages] do |message|
|
||||||
json.partial! 'message', formats: [:json], message: message
|
json.partial! 'message', formats: [:json], message: message
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
json.articles do
|
||||||
|
json.array! @result[:articles] do |article|
|
||||||
|
json.partial! 'article', formats: [:json], article: article
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -137,6 +137,7 @@ Rails.application.routes.draw do
|
|||||||
get :conversations
|
get :conversations
|
||||||
get :messages
|
get :messages
|
||||||
get :contacts
|
get :contacts
|
||||||
|
get :articles
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,11 @@ RSpec.describe 'Search', type: :request do
|
|||||||
create(:message, conversation: conversation, account: account, content: 'test2')
|
create(:message, conversation: conversation, account: account, content: 'test2')
|
||||||
create(:contact_inbox, contact_id: contact.id, inbox_id: conversation.inbox.id)
|
create(:contact_inbox, contact_id: contact.id, inbox_id: conversation.inbox.id)
|
||||||
create(:inbox_member, user: agent, inbox: conversation.inbox)
|
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
|
end
|
||||||
|
|
||||||
describe 'GET /api/v1/accounts/{account.id}/search' do
|
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)
|
response_data = JSON.parse(response.body, symbolize_names: true)
|
||||||
|
|
||||||
expect(response_data[:payload][:messages].first[:content]).to eq 'test2'
|
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][:messages].length).to eq 2
|
||||||
expect(response_data[:payload][:conversations].length).to eq 1
|
expect(response_data[:payload][:conversations].length).to eq 1
|
||||||
expect(response_data[:payload][:contacts].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
|
end
|
||||||
end
|
end
|
||||||
@@ -115,4 +121,60 @@ RSpec.describe 'Search', type: :request do
|
|||||||
end
|
end
|
||||||
end
|
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
|
end
|
||||||
|
|||||||
@@ -10,6 +10,11 @@ describe SearchService do
|
|||||||
let!(:harry) { create(:contact, name: 'Harry Potter', email: 'test@test.com', account_id: account.id) }
|
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!(:conversation) { create(:conversation, contact: harry, inbox: inbox, account: account) }
|
||||||
let!(:message) { create(:message, account: account, inbox: inbox, content: 'Harry Potter is a wizard') }
|
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
|
before do
|
||||||
create(:inbox_member, user: user, inbox: inbox)
|
create(:inbox_member, user: user, inbox: inbox)
|
||||||
@@ -27,7 +32,7 @@ describe SearchService do
|
|||||||
it 'returns all for all' do
|
it 'returns all for all' do
|
||||||
search_type = 'all'
|
search_type = 'all'
|
||||||
search = described_class.new(current_user: user, current_account: account, params: params, search_type: search_type)
|
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
|
end
|
||||||
|
|
||||||
it 'returns contacts for contacts' do
|
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)
|
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])
|
expect(search.perform.keys).to match_array(%i[conversations])
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
context 'when contact search' do
|
context 'when contact search' do
|
||||||
@@ -143,6 +154,50 @@ describe SearchService do
|
|||||||
expect(search.perform[:conversations].map(&:id)).to include new_converstion.id
|
expect(search.perform[:conversations].map(&:id)).to include new_converstion.id
|
||||||
end
|
end
|
||||||
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
|
end
|
||||||
|
|
||||||
describe '#use_gin_search' do
|
describe '#use_gin_search' do
|
||||||
|
|||||||
Reference in New Issue
Block a user