feat: Revamps search to use new search API's (#6582)
* feat: Revamps search to use new search API's * Fixes search result spacing * Fixes message result * Fixes issue with empty search results * Remove console errors * Remove console errors * Fix console errors, canned responses * Fixes message rendering on results * Highlights search term * Fixes html rendering for emails * FIxes email rendering issues * Removes extra spaces and line breaks --------- Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
This commit is contained in:
committed by
GitHub
parent
2a385f377c
commit
88ed028a06
@@ -0,0 +1,110 @@
|
||||
<template>
|
||||
<blockquote ref="messageContainer" class="message">
|
||||
<p class="header">
|
||||
<strong class="author">
|
||||
{{ author }}
|
||||
</strong>
|
||||
{{ $t('SEARCH.WROTE') }}
|
||||
</p>
|
||||
<read-more :shrink="isOverflowing" @expand="isOverflowing = false">
|
||||
<div v-dompurify-html="prepareContent(content)" class="message-content" />
|
||||
</read-more>
|
||||
</blockquote>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
||||
import ReadMore from './ReadMore.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ReadMore,
|
||||
},
|
||||
mixins: [messageFormatterMixin],
|
||||
props: {
|
||||
author: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
content: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
searchTerm: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isOverflowing: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
messageContent() {
|
||||
return this.formatMessage(this.content);
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.$nextTick(() => {
|
||||
const wrap = this.$refs.messageContainer;
|
||||
const message = wrap.querySelector('.message-content');
|
||||
this.isOverflowing = message.offsetHeight > 150;
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
escapeHtml(html) {
|
||||
var text = document.createTextNode(html);
|
||||
var p = document.createElement('p');
|
||||
p.appendChild(text);
|
||||
return p.innerText;
|
||||
},
|
||||
prepareContent(content = '') {
|
||||
const escapedText = this.escapeHtml(content);
|
||||
const plainTextContent = this.getPlainText(escapedText);
|
||||
const escapedSearchTerm = this.escapeRegExp(this.searchTerm);
|
||||
return plainTextContent
|
||||
.replace(
|
||||
new RegExp(`(${escapedSearchTerm})`, 'ig'),
|
||||
'<span class="searchkey--highlight">$1</span>'
|
||||
)
|
||||
.replace(/\s{2,}|\n|\r/g, ' ');
|
||||
},
|
||||
// from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping
|
||||
escapeRegExp(string) {
|
||||
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.message {
|
||||
border-color: var(--s-100);
|
||||
border-width: var(--space-micro);
|
||||
padding: 0 var(--space-small);
|
||||
margin-top: var(--space-small);
|
||||
}
|
||||
.message-content::v-deep p,
|
||||
.message-content::v-deep li::marker {
|
||||
color: var(--s-700);
|
||||
margin-bottom: var(--space-smaller);
|
||||
}
|
||||
.author {
|
||||
color: var(--s-700);
|
||||
}
|
||||
.header {
|
||||
color: var(--s-500);
|
||||
margin-bottom: var(--space-smaller);
|
||||
}
|
||||
|
||||
.message-content {
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.message-content::v-deep .searchkey--highlight {
|
||||
color: var(--w-600);
|
||||
font-weight: var(--font-weight-bold);
|
||||
font-size: var(--font-size-small);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,56 @@
|
||||
<template>
|
||||
<div class="read-more">
|
||||
<div ref="content" :class="{ 'shrink-container': shrink }">
|
||||
<slot />
|
||||
<woot-button
|
||||
v-if="shrink"
|
||||
size="tiny"
|
||||
variant="smooth"
|
||||
color-scheme="primary"
|
||||
class="read-more-button"
|
||||
@click.prevent="$emit('expand')"
|
||||
>
|
||||
{{ $t('SEARCH.READ_MORE') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
shrink: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.shrink-container {
|
||||
max-height: 100px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
.shrink-container::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 50px;
|
||||
background: linear-gradient(to bottom, rgba(255, 255, 255, 0), #fff 100%);
|
||||
z-index: 4;
|
||||
}
|
||||
.read-more-button {
|
||||
max-width: max-content;
|
||||
position: absolute;
|
||||
bottom: var(--space-small);
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin: 0 auto;
|
||||
z-index: 5;
|
||||
box-shadow: var(--box-shadow);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,98 @@
|
||||
<template>
|
||||
<div class="input-container">
|
||||
<div class="icon-container">
|
||||
<fluent-icon icon="search" class="icon" aria-hidden="true" />
|
||||
</div>
|
||||
<input
|
||||
ref="searchInput"
|
||||
type="search"
|
||||
:placeholder="$t('SEARCH.INPUT_PLACEHOLDER')"
|
||||
:value="searchQuery"
|
||||
@input="debounceSearch"
|
||||
/>
|
||||
<div class="key-binding">
|
||||
<span>{{ $t('SEARCH.PLACEHOLDER_KEYBINDING') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
searchQuery: '',
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.$refs.searchInput.focus();
|
||||
document.addEventListener('keydown', this.handler);
|
||||
},
|
||||
beforeDestroy() {
|
||||
document.removeEventListener('keydown', this.handler);
|
||||
},
|
||||
methods: {
|
||||
handler(e) {
|
||||
if (e.key === '/' && document.activeElement.tagName !== 'INPUT') {
|
||||
e.preventDefault();
|
||||
this.$refs.searchInput.focus();
|
||||
}
|
||||
},
|
||||
debounceSearch(e) {
|
||||
this.searchQuery = e.target.value;
|
||||
clearTimeout(this.debounce);
|
||||
this.debounce = setTimeout(async () => {
|
||||
if (this.searchQuery.length > 2 || this.searchQuery.match(/^[0-9]+$/)) {
|
||||
this.$emit('search', this.searchQuery);
|
||||
}
|
||||
}, 500);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.input-container {
|
||||
position: relative;
|
||||
border-radius: var(--border-radius-normal);
|
||||
input[type='search'] {
|
||||
width: 100%;
|
||||
padding-left: calc(var(--space-large) + var(--space-small));
|
||||
margin-bottom: 0;
|
||||
padding-right: var(--space-mega);
|
||||
&:focus {
|
||||
.icon {
|
||||
color: var(--w-500) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.icon-container {
|
||||
padding-left: var(--space-small);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
.icon {
|
||||
color: var(--s-400);
|
||||
}
|
||||
}
|
||||
.key-binding {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
padding: var(--space-small) var(--space-small) 0 var(--space-small);
|
||||
span {
|
||||
color: var(--s-400);
|
||||
font-weight: var(--font-weight-medium);
|
||||
font-size: calc(var(--space-slab) + var(--space-micro));
|
||||
padding: 0 var(--space-small);
|
||||
border: 1px solid var(--s-100);
|
||||
border-radius: var(--border-radius-normal);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -19,8 +19,8 @@ import { frontendURL } from 'dashboard/helper/URLHelper';
|
||||
export default {
|
||||
props: {
|
||||
id: {
|
||||
type: String,
|
||||
default: '',
|
||||
type: [String, Number],
|
||||
default: 0,
|
||||
},
|
||||
email: {
|
||||
type: String,
|
||||
@@ -39,8 +39,8 @@ export default {
|
||||
default: '',
|
||||
},
|
||||
accountId: {
|
||||
type: String,
|
||||
default: '',
|
||||
type: [String, Number],
|
||||
default: 0,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
<template>
|
||||
<search-result-section
|
||||
:title="$t('SEARCH.SECTION.CONTACTS')"
|
||||
:empty="!contacts.length"
|
||||
:query="query"
|
||||
>
|
||||
<ul class="search-list">
|
||||
<li v-for="contact in contacts" :key="contact.id">
|
||||
<search-result-contact-item
|
||||
:id="contact.id"
|
||||
:name="contact.name"
|
||||
:email="contact.email"
|
||||
:phone="contact.phone_number"
|
||||
:account-id="accountId"
|
||||
:thumbnail="contact.thumbnail"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</search-result-section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
import SearchResultSection from './SearchResultSection.vue';
|
||||
import SearchResultContactItem from './SearchResultContactItem.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
SearchResultSection,
|
||||
SearchResultContactItem,
|
||||
},
|
||||
props: {
|
||||
contacts: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
query: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
accountId: 'getCurrentAccountId',
|
||||
}),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -17,10 +17,10 @@
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<span>{{ createdAtTime }}</span>
|
||||
<span class="created-at">{{ createdAtTime }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<h5 class="text-block-title name">
|
||||
<h5 v-if="name" class="text-block-title name">
|
||||
<span class="pre-text">from:</span>
|
||||
{{ name }}
|
||||
</h5>
|
||||
@@ -41,12 +41,8 @@ export default {
|
||||
mixins: [timeMixin],
|
||||
props: {
|
||||
id: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
email: {
|
||||
type: String,
|
||||
default: '',
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
inbox: {
|
||||
type: Object,
|
||||
@@ -56,12 +52,8 @@ export default {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
thumbnail: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
accountId: {
|
||||
type: String,
|
||||
type: [String, Number],
|
||||
default: '',
|
||||
},
|
||||
createdAt: {
|
||||
@@ -101,6 +93,7 @@ export default {
|
||||
.icon-wrap {
|
||||
width: var(--space-medium);
|
||||
height: var(--space-medium);
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -122,12 +115,13 @@ export default {
|
||||
.conversation-details {
|
||||
margin-left: var(--space-normal);
|
||||
flex-grow: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.conversation-id,
|
||||
.name {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.created-at,
|
||||
.pre-text {
|
||||
color: var(--s-600);
|
||||
font-size: var(--font-size-mini);
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
<template>
|
||||
<search-result-section
|
||||
:title="$t('SEARCH.SECTION.CONVERSATIONS')"
|
||||
:empty="!conversations.length"
|
||||
:query="query"
|
||||
>
|
||||
<ul class="search-list">
|
||||
<li v-for="conversation in conversations" :key="conversation.id">
|
||||
<search-result-conversation-item
|
||||
:id="conversation.id"
|
||||
:name="conversation.contact.name"
|
||||
:account-id="accountId"
|
||||
:inbox="conversation.inbox"
|
||||
:created-at="conversation.created_at"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</search-result-section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import SearchResultSection from './SearchResultSection.vue';
|
||||
import SearchResultConversationItem from './SearchResultConversationItem.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
SearchResultSection,
|
||||
SearchResultConversationItem,
|
||||
},
|
||||
props: {
|
||||
conversations: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
query: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
accountId: 'getCurrentAccountId',
|
||||
}),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,61 @@
|
||||
<template>
|
||||
<search-result-section
|
||||
:title="$t('SEARCH.SECTION.MESSAGES')"
|
||||
:empty="!messages.length"
|
||||
:query="query"
|
||||
>
|
||||
<ul class="search-list">
|
||||
<li v-for="message in messages" :key="message.id">
|
||||
<search-result-conversation-item
|
||||
:id="message.conversation_id"
|
||||
:account-id="accountId"
|
||||
:inbox="message.inbox"
|
||||
:created-at="message.created_at"
|
||||
>
|
||||
<message-content
|
||||
:author="getName(message)"
|
||||
:content="message.content"
|
||||
:search-term="query"
|
||||
/>
|
||||
</search-result-conversation-item>
|
||||
</li>
|
||||
</ul>
|
||||
</search-result-section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import SearchResultConversationItem from './SearchResultConversationItem.vue';
|
||||
import SearchResultSection from './SearchResultSection.vue';
|
||||
import MessageContent from './MessageContent';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
SearchResultConversationItem,
|
||||
SearchResultSection,
|
||||
MessageContent,
|
||||
},
|
||||
props: {
|
||||
messages: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
query: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
accountId: 'getCurrentAccountId',
|
||||
}),
|
||||
},
|
||||
methods: {
|
||||
getName(message) {
|
||||
return message && message.sender && message.sender.name
|
||||
? message.sender.name
|
||||
: this.$t('SEARCH.BOT_LABEL');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,70 @@
|
||||
<template>
|
||||
<section>
|
||||
<div class="label-container">
|
||||
<h3 class="text-block-title">{{ title }}</h3>
|
||||
</div>
|
||||
<slot />
|
||||
<div v-if="empty" class="empty">
|
||||
<fluent-icon icon="info" size="16px" class="icon" />
|
||||
<p class="empty-state__text">
|
||||
{{ $t('SEARCH.EMPTY_STATE', { item: titleCase, query }) }}
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
empty: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
query: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
titleCase() {
|
||||
return this.title.toLowerCase();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.search-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: var(--spacing-normal) 0;
|
||||
}
|
||||
.label-container {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
padding: var(--space-small) 0;
|
||||
z-index: 50;
|
||||
background: var(--white);
|
||||
}
|
||||
|
||||
.empty {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: var(--space-medium) var(--space-normal);
|
||||
background: var(--s-25);
|
||||
border-radius: var(--border-radius-medium);
|
||||
.icon {
|
||||
color: var(--s-500);
|
||||
}
|
||||
.empty-state__text {
|
||||
text-align: center;
|
||||
color: var(--s-500);
|
||||
margin: 0 var(--space-small);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,40 @@
|
||||
<template>
|
||||
<div class="tab-container">
|
||||
<woot-tabs :index="activeTab" :border="false" @change="onTabChange">
|
||||
<woot-tabs-item
|
||||
v-for="item in tabs"
|
||||
:key="item.key"
|
||||
:name="item.name"
|
||||
:count="item.count"
|
||||
/>
|
||||
</woot-tabs>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
tabs: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
activeTab: 0,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
onTabChange(index) {
|
||||
this.activeTab = index;
|
||||
this.$emit('tab-change', this.tabs[index].key);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.tab-container {
|
||||
margin-left: var(--space-minus-normal);
|
||||
margin-top: var(--space-small);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,202 @@
|
||||
<template>
|
||||
<section class="search-root">
|
||||
<woot-button
|
||||
color-scheme="secondary"
|
||||
size="large"
|
||||
icon="dismiss"
|
||||
variant="smooth"
|
||||
class="modal--close"
|
||||
@click="onBack"
|
||||
/>
|
||||
<header>
|
||||
<search-header @search="search" />
|
||||
<search-tabs :tabs="tabs" @tab-change="tab => (selectedTab = tab)" />
|
||||
</header>
|
||||
<div class="search-results">
|
||||
<woot-loading-state v-if="uiFlags.isFetching" :message="'Searching'" />
|
||||
<div v-else>
|
||||
<div v-if="all.length">
|
||||
<search-result-contacts-list
|
||||
v-if="filterContacts"
|
||||
:contacts="contacts"
|
||||
:query="query"
|
||||
/>
|
||||
<search-result-messages-list
|
||||
v-if="filterMessages"
|
||||
:messages="messages"
|
||||
:query="query"
|
||||
/>
|
||||
<search-result-conversations-list
|
||||
v-if="filterConversations"
|
||||
:conversations="conversations"
|
||||
:query="query"
|
||||
/>
|
||||
</div>
|
||||
<div v-else-if="showEmptySearchResults" class="empty">
|
||||
<fluent-icon icon="info" size="16px" class="icon" />
|
||||
<p class="empty-state__text">
|
||||
{{ $t('SEARCH.EMPTY_STATE_FULL', { query }) }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import SearchHeader from './SearchHeader.vue';
|
||||
import SearchTabs from './SearchTabs.vue';
|
||||
import SearchResultConversationsList from './SearchResultConversationsList.vue';
|
||||
import SearchResultMessagesList from './SearchResultMessagesList.vue';
|
||||
import SearchResultContactsList from './SearchResultContactsList.vue';
|
||||
import { isEmptyObject } from 'dashboard/helper/commons.js';
|
||||
|
||||
import { mapGetters } from 'vuex';
|
||||
import { CONVERSATION_EVENTS } from '../../../helper/AnalyticsHelper/events';
|
||||
export default {
|
||||
components: {
|
||||
SearchHeader,
|
||||
SearchTabs,
|
||||
SearchResultContactsList,
|
||||
SearchResultConversationsList,
|
||||
SearchResultMessagesList,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selectedTab: 'all',
|
||||
query: '',
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
fullSearchRecords: 'conversationSearch/getFullSearchRecords',
|
||||
uiFlags: 'conversationSearch/getUIFlags',
|
||||
}),
|
||||
contacts() {
|
||||
if (this.fullSearchRecords.contacts) {
|
||||
return this.fullSearchRecords.contacts.map(contact => ({
|
||||
...contact,
|
||||
type: 'contact',
|
||||
}));
|
||||
}
|
||||
return [];
|
||||
},
|
||||
conversations() {
|
||||
if (this.fullSearchRecords.conversations) {
|
||||
return this.fullSearchRecords.conversations.map(conversation => ({
|
||||
...conversation,
|
||||
type: 'conversation',
|
||||
}));
|
||||
}
|
||||
return [];
|
||||
},
|
||||
messages() {
|
||||
if (this.fullSearchRecords.messages) {
|
||||
return this.fullSearchRecords.messages.map(message => ({
|
||||
...message,
|
||||
type: 'message',
|
||||
}));
|
||||
}
|
||||
return [];
|
||||
},
|
||||
all() {
|
||||
return [...this.contacts, ...this.conversations, ...this.messages];
|
||||
},
|
||||
filterContacts() {
|
||||
return this.selectedTab === 'contacts' || this.selectedTab === 'all';
|
||||
},
|
||||
filterConversations() {
|
||||
return this.selectedTab === 'conversations' || this.selectedTab === 'all';
|
||||
},
|
||||
filterMessages() {
|
||||
return this.selectedTab === 'messages' || this.selectedTab === 'all';
|
||||
},
|
||||
totalSearchResultsCount() {
|
||||
return (
|
||||
this.contacts.length + this.conversations.length + this.messages.length
|
||||
);
|
||||
},
|
||||
tabs() {
|
||||
return [
|
||||
{
|
||||
key: 'all',
|
||||
name: this.$t('SEARCH.TABS.ALL'),
|
||||
count: this.totalSearchResultsCount,
|
||||
},
|
||||
{
|
||||
key: 'contacts',
|
||||
name: this.$t('SEARCH.TABS.CONTACTS'),
|
||||
count: this.contacts.length,
|
||||
},
|
||||
{
|
||||
key: 'conversations',
|
||||
name: this.$t('SEARCH.TABS.CONVERSATIONS'),
|
||||
count: this.conversations.length,
|
||||
},
|
||||
{
|
||||
key: 'messages',
|
||||
name: this.$t('SEARCH.TABS.MESSAGES'),
|
||||
count: this.messages.length,
|
||||
},
|
||||
];
|
||||
},
|
||||
showEmptySearchResults() {
|
||||
return (
|
||||
this.totalSearchResultsCount === 0 &&
|
||||
!isEmptyObject(this.fullSearchRecords)
|
||||
);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
search(q) {
|
||||
this.query = q;
|
||||
this.$track(CONVERSATION_EVENTS.SEARCH_CONVERSATION);
|
||||
this.$store.dispatch('conversationSearch/fullSearch', { q });
|
||||
},
|
||||
onBack() {
|
||||
this.$router.push({ name: 'home' });
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.search-root {
|
||||
max-width: 800px;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
position: relative;
|
||||
padding: var(--space-normal);
|
||||
flex-direction: column;
|
||||
.search-results {
|
||||
flex-grow: 1;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
margin-top: var(--space-normal);
|
||||
}
|
||||
}
|
||||
.modal--close {
|
||||
position: fixed;
|
||||
right: var(--space-small);
|
||||
top: var(--space-small);
|
||||
}
|
||||
|
||||
.empty {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: var(--space-medium) var(--space-normal);
|
||||
background: var(--s-25);
|
||||
border-radius: var(--border-radius-medium);
|
||||
.icon {
|
||||
color: var(--s-500);
|
||||
}
|
||||
.empty-state__text {
|
||||
text-align: center;
|
||||
color: var(--s-500);
|
||||
margin: 0 var(--space-small);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
12
app/javascript/dashboard/modules/search/search.routes.js
Normal file
12
app/javascript/dashboard/modules/search/search.routes.js
Normal file
@@ -0,0 +1,12 @@
|
||||
/* eslint-disable storybook/default-exports */
|
||||
import SearchView from './components/SearchView.vue';
|
||||
import { frontendURL } from '../../helper/URLHelper';
|
||||
|
||||
export const routes = [
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/search'),
|
||||
name: 'search',
|
||||
roles: ['administrator', 'agent'],
|
||||
component: SearchView,
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,28 @@
|
||||
import MessageContent from '../components/MessageContent.vue';
|
||||
|
||||
export default {
|
||||
title: 'Components/Search/MessageContent',
|
||||
component: MessageContent,
|
||||
argTypes: {
|
||||
content: {
|
||||
defaultValue: '123',
|
||||
control: {
|
||||
type: 'text',
|
||||
},
|
||||
},
|
||||
author: {
|
||||
defaultValue: 'John Doe',
|
||||
control: {
|
||||
type: 'text',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const Template = (args, { argTypes }) => ({
|
||||
props: Object.keys(argTypes),
|
||||
components: { MessageContent },
|
||||
template: '<message-content v-bind="$props"></message-content>',
|
||||
});
|
||||
|
||||
export const MessageResultItem = Template.bind({});
|
||||
Reference in New Issue
Block a user