feat: allow searching articles in omnisearch (#11558)
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
This commit is contained in:
@@ -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,
|
||||
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(() => {
|
||||
/>
|
||||
</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">
|
||||
<NextButton
|
||||
v-if="!isSelectedTabAll"
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
ROLES,
|
||||
CONVERSATION_PERMISSIONS,
|
||||
CONTACT_PERMISSIONS,
|
||||
PORTAL_PERMISSIONS,
|
||||
} from 'dashboard/constants/permissions.js';
|
||||
|
||||
import SearchView from './components/SearchView.vue';
|
||||
@@ -12,7 +13,12 @@ export const routes = [
|
||||
path: frontendURL('accounts/:accountId/search'),
|
||||
name: 'search',
|
||||
meta: {
|
||||
permissions: [...ROLES, ...CONVERSATION_PERMISSIONS, CONTACT_PERMISSIONS],
|
||||
permissions: [
|
||||
...ROLES,
|
||||
...CONVERSATION_PERMISSIONS,
|
||||
CONTACT_PERMISSIONS,
|
||||
PORTAL_PERMISSIONS,
|
||||
],
|
||||
},
|
||||
component: SearchView,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user