feat: Redesigned search UI (#1845)
This commit is contained in:
parent
c99c63cd79
commit
36f086c5cb
7 changed files with 591 additions and 215 deletions
|
@ -11,7 +11,7 @@
|
||||||
"TITLE": "Search messages",
|
"TITLE": "Search messages",
|
||||||
"LOADING_MESSAGE": "Crunching data...",
|
"LOADING_MESSAGE": "Crunching data...",
|
||||||
"PLACEHOLDER": "Type any text to search messages",
|
"PLACEHOLDER": "Type any text to search messages",
|
||||||
"NO_MATCHING_RESULTS": "There are no messages matching the search parameters."
|
"NO_MATCHING_RESULTS": "No results found."
|
||||||
},
|
},
|
||||||
"UNREAD_MESSAGES": "Unread Messages",
|
"UNREAD_MESSAGES": "Unread Messages",
|
||||||
"UNREAD_MESSAGE": "Unread Message",
|
"UNREAD_MESSAGE": "Unread Message",
|
||||||
|
|
|
@ -6,17 +6,7 @@
|
||||||
:active-team="activeTeam"
|
:active-team="activeTeam"
|
||||||
@conversation-load="onConversationLoad"
|
@conversation-load="onConversationLoad"
|
||||||
>
|
>
|
||||||
<button class="search--button" @click="onSearch">
|
<pop-over-search />
|
||||||
<i class="ion-ios-search-strong search--icon" />
|
|
||||||
<div class="text-truncate">
|
|
||||||
{{ $t('CONVERSATION.SEARCH_MESSAGES') }}
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
<search
|
|
||||||
v-if="showSearchModal"
|
|
||||||
:show="showSearchModal"
|
|
||||||
:on-close="closeSearch"
|
|
||||||
/>
|
|
||||||
</chat-list>
|
</chat-list>
|
||||||
<conversation-box
|
<conversation-box
|
||||||
:inbox-id="inboxId"
|
:inbox-id="inboxId"
|
||||||
|
@ -29,17 +19,16 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
|
|
||||||
import ChatList from '../../../components/ChatList';
|
import ChatList from '../../../components/ChatList';
|
||||||
import ConversationBox from '../../../components/widgets/conversation/ConversationBox';
|
import ConversationBox from '../../../components/widgets/conversation/ConversationBox';
|
||||||
import Search from './search/Search.vue';
|
import PopOverSearch from './search/PopOverSearch';
|
||||||
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
ChatList,
|
ChatList,
|
||||||
ConversationBox,
|
ConversationBox,
|
||||||
Search,
|
PopOverSearch,
|
||||||
},
|
},
|
||||||
mixins: [uiSettingsMixin],
|
mixins: [uiSettingsMixin],
|
||||||
props: {
|
props: {
|
||||||
|
@ -146,31 +135,6 @@ export default {
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.search--button {
|
|
||||||
align-items: center;
|
|
||||||
border: 0;
|
|
||||||
color: var(--s-400);
|
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
|
||||||
font-size: var(--font-size-small);
|
|
||||||
font-weight: 400;
|
|
||||||
padding: var(--space-normal) var(--space-normal) var(--space-slab);
|
|
||||||
text-align: left;
|
|
||||||
line-height: var(--font-size-large);
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
.search--icon {
|
|
||||||
color: var(--w-500);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.search--icon {
|
|
||||||
color: var(--s-600);
|
|
||||||
font-size: var(--font-size-large);
|
|
||||||
padding-right: var(--space-small);
|
|
||||||
}
|
|
||||||
|
|
||||||
.conversation-page {
|
.conversation-page {
|
||||||
display: flex;
|
display: flex;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
|
@ -0,0 +1,228 @@
|
||||||
|
<template>
|
||||||
|
<div v-on-clickaway="closeSearch" class="search-wrap">
|
||||||
|
<div class="search" :class="{ 'is-active': showSearchBox }">
|
||||||
|
<div class="icon">
|
||||||
|
<i class="ion-ios-search-strong search--icon" />
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
v-model="searchTerm"
|
||||||
|
class="search--input"
|
||||||
|
:placeholder="$t('CONVERSATION.SEARCH_MESSAGES')"
|
||||||
|
@focus="onSearch"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div v-if="showSearchBox" class="results-wrap">
|
||||||
|
<div class="show-results">
|
||||||
|
<div>
|
||||||
|
<div class="result-view">
|
||||||
|
<div class="result">
|
||||||
|
Search Results
|
||||||
|
<span v-if="resultsCount" class="message-counter">
|
||||||
|
({{ resultsCount }})
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="uiFlags.isFetching" class="search--activity-message">
|
||||||
|
<woot-spinner size="" />
|
||||||
|
{{ $t('CONVERSATION.SEARCH.LOADING_MESSAGE') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="showSearchResult" class="search-results--container">
|
||||||
|
<result-item
|
||||||
|
v-for="conversation in conversations"
|
||||||
|
:key="conversation.messageId"
|
||||||
|
:conversation-id="conversation.id"
|
||||||
|
:user-name="conversation.contact.name"
|
||||||
|
:timestamp="conversation.created_at"
|
||||||
|
:messages="conversation.messages"
|
||||||
|
:search-term="searchTerm"
|
||||||
|
:inbox-name="conversation.inbox.name"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="showEmptyResult" class="search--activity-no-message">
|
||||||
|
{{ $t('CONVERSATION.SEARCH.NO_MATCHING_RESULTS') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mixin as clickaway } from 'vue-clickaway';
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
import timeMixin from '../../../../mixins/time';
|
||||||
|
import ResultItem from './ResultItem';
|
||||||
|
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
ResultItem,
|
||||||
|
},
|
||||||
|
|
||||||
|
directives: {
|
||||||
|
focus: {
|
||||||
|
inserted(el) {
|
||||||
|
el.focus();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
mixins: [timeMixin, messageFormatterMixin, clickaway],
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
searchTerm: '',
|
||||||
|
showSearchBox: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
...mapGetters({
|
||||||
|
conversations: 'conversationSearch/getConversations',
|
||||||
|
uiFlags: 'conversationSearch/getUIFlags',
|
||||||
|
}),
|
||||||
|
resultsCount() {
|
||||||
|
return this.conversations.length;
|
||||||
|
},
|
||||||
|
showSearchResult() {
|
||||||
|
return (
|
||||||
|
this.searchTerm && this.conversations.length && !this.uiFlags.isFetching
|
||||||
|
);
|
||||||
|
},
|
||||||
|
showEmptyResult() {
|
||||||
|
return (
|
||||||
|
this.searchTerm &&
|
||||||
|
!this.conversations.length &&
|
||||||
|
!this.uiFlags.isFetching
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
searchTerm(newValue) {
|
||||||
|
if (this.typingTimer) {
|
||||||
|
clearTimeout(this.typingTimer);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.typingTimer = setTimeout(() => {
|
||||||
|
this.hasSearched = true;
|
||||||
|
this.$store.dispatch('conversationSearch/get', { q: newValue });
|
||||||
|
}, 1000);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.$store.dispatch('conversationSearch/get', { q: '' });
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
onSearch() {
|
||||||
|
this.showSearchBox = true;
|
||||||
|
},
|
||||||
|
closeSearch() {
|
||||||
|
this.showSearchBox = false;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.search-wrap {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search {
|
||||||
|
display: flex;
|
||||||
|
padding: 0;
|
||||||
|
border-bottom: 1px solid transparent;
|
||||||
|
padding: var(--space-one) var(--space-normal) var(--space-smaller)
|
||||||
|
var(--space-normal);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.search--icon {
|
||||||
|
color: var(--w-500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.search--input {
|
||||||
|
align-items: center;
|
||||||
|
border: 0;
|
||||||
|
color: var(--color-body);
|
||||||
|
cursor: pointer;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
font-size: var(--font-size-small);
|
||||||
|
font-weight: var(--font-weight-normal);
|
||||||
|
text-align: left;
|
||||||
|
line-height: var(--font-size-large);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search--icon {
|
||||||
|
color: var(--s-600);
|
||||||
|
font-size: var(--font-size-large);
|
||||||
|
padding: 0 var(--space-small) 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
input::placeholder {
|
||||||
|
color: var(--color-body);
|
||||||
|
font-size: var(--font-size-small);
|
||||||
|
}
|
||||||
|
|
||||||
|
.results-wrap {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 9999;
|
||||||
|
box-shadow: var(--shadow-large);
|
||||||
|
background: white;
|
||||||
|
width: 100%;
|
||||||
|
max-height: 70vh;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.show-results {
|
||||||
|
list-style-type: none;
|
||||||
|
font-size: var(--font-size-small);
|
||||||
|
font-weight: var(--font-weight-normal);
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-view {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result {
|
||||||
|
padding: var(--space-smaller) var(--space-smaller) var(--space-smaller)
|
||||||
|
var(--space-normal);
|
||||||
|
color: var(--s-700);
|
||||||
|
font-size: var(--font-size-medium);
|
||||||
|
font-weight: var(--font-weight-bold);
|
||||||
|
|
||||||
|
.message-counter {
|
||||||
|
color: var(--s-500);
|
||||||
|
font-size: var(--font-size-small);
|
||||||
|
font-weight: var(--font-weight-bold);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.search--activity-message {
|
||||||
|
padding: var(--space-small) var(--space-normal) var(--space-small)
|
||||||
|
var(--space-zero);
|
||||||
|
font-size: var(--font-size-small);
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
color: var(--s-500);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search--activity-no-message {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
padding: var(--space-one) var(--space-zero) var(--space-two) var(--space-zero);
|
||||||
|
font-size: var(--font-size-small);
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
color: var(--s-500);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,191 @@
|
||||||
|
<template>
|
||||||
|
<div class="search-result" @click="onClick">
|
||||||
|
<div class="result-header">
|
||||||
|
<div class="message">
|
||||||
|
<i class="ion-ios-chatboxes-outline" />
|
||||||
|
<div class="conversation">
|
||||||
|
<div class="name-wrap">
|
||||||
|
<span class="user-name">{{ userName }}</span>
|
||||||
|
<div>
|
||||||
|
<span class="conversation-id"># {{ conversationId }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span class="inbox-name">{{ inboxName }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span class="timestamp">{{ readableTime }} </span>
|
||||||
|
</div>
|
||||||
|
<search-message-item
|
||||||
|
v-for="message in messages"
|
||||||
|
:key="message.created_at"
|
||||||
|
:user-name="message.sender_name"
|
||||||
|
:timestamp="message.created_at"
|
||||||
|
:message-type="message.message_type"
|
||||||
|
:content="message.content"
|
||||||
|
:search-term="searchTerm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
import { frontendURL, conversationUrl } from 'dashboard/helper/URLHelper';
|
||||||
|
import timeMixin from 'dashboard/mixins/time';
|
||||||
|
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
||||||
|
import SearchMessageItem from './SearchMessageItem.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: { SearchMessageItem },
|
||||||
|
mixins: [timeMixin, messageFormatterMixin],
|
||||||
|
|
||||||
|
props: {
|
||||||
|
conversationId: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
userName: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
inboxName: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
timestamp: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
messages: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
searchTerm: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
...mapGetters({
|
||||||
|
accountId: 'getCurrentAccountId',
|
||||||
|
}),
|
||||||
|
readableTime() {
|
||||||
|
if (!this.timestamp) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.dynamicTime(this.timestamp);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
onClick() {
|
||||||
|
const path = conversationUrl({
|
||||||
|
accountId: this.accountId,
|
||||||
|
id: this.conversationId,
|
||||||
|
});
|
||||||
|
window.location = frontendURL(path);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.search-result {
|
||||||
|
display: block;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--color-body);
|
||||||
|
padding: var(--space-smaller) var(--space-two) 0 var(--space-normal);
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
padding-bottom: var(--space-normal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
background: var(--color-background);
|
||||||
|
padding: var(--space-smaller) var(--space-slab);
|
||||||
|
margin-bottom: var(--space-small);
|
||||||
|
border-radius: var(--border-radius-medium);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--w-400);
|
||||||
|
color: var(--white);
|
||||||
|
.inbox-name {
|
||||||
|
color: var(--white);
|
||||||
|
}
|
||||||
|
.timestamp {
|
||||||
|
color: var(--white);
|
||||||
|
}
|
||||||
|
.ion-ios-chatboxes-outline {
|
||||||
|
color: var(--white);
|
||||||
|
}
|
||||||
|
.conversation-id {
|
||||||
|
background: var(--w-50);
|
||||||
|
color: var(--s-500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.message {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ion-ios-chatboxes-outline {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
font-size: var(--font-size-large);
|
||||||
|
color: var(--w-500);
|
||||||
|
}
|
||||||
|
|
||||||
|
.conversation {
|
||||||
|
align-items: flex-start;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: var(--space-smaller) var(--space-one);
|
||||||
|
}
|
||||||
|
|
||||||
|
.name-wrap {
|
||||||
|
display: flex;
|
||||||
|
width: 20rem;
|
||||||
|
|
||||||
|
.user-name {
|
||||||
|
font-size: var(--font-size-default);
|
||||||
|
font-weight: var(--font-weight-bold);
|
||||||
|
margin-right: var(--space-micro);
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.conversation-id {
|
||||||
|
background: var(--w-400);
|
||||||
|
border-radius: var(--border-radius-normal);
|
||||||
|
color: var(--w-50);
|
||||||
|
align-items: center;
|
||||||
|
font-size: var(--font-size-mini);
|
||||||
|
font-weight: var(--font-weight-bold);
|
||||||
|
padding: 0 var(--space-smaller);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.inbox-name {
|
||||||
|
border-radius: var(--border-radius-normal);
|
||||||
|
color: var(--s-500);
|
||||||
|
font-size: var(--font-size-normal);
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
}
|
||||||
|
|
||||||
|
.timestamp {
|
||||||
|
color: var(--s-500);
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
font-size: var(--font-size-mini);
|
||||||
|
margin-top: var(--space-smaller);
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,175 +0,0 @@
|
||||||
<template>
|
|
||||||
<woot-modal
|
|
||||||
class="message-search--modal"
|
|
||||||
:show.sync="show"
|
|
||||||
:on-close="onClose"
|
|
||||||
>
|
|
||||||
<woot-modal-header :header-title="$t('CONVERSATION.SEARCH.TITLE')" />
|
|
||||||
<div class="search--container">
|
|
||||||
<input
|
|
||||||
v-model="searchTerm"
|
|
||||||
v-focus
|
|
||||||
:placeholder="$t('CONVERSATION.SEARCH.PLACEHOLDER')"
|
|
||||||
type="text"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div v-if="uiFlags.isFetching" class="search--activity-message">
|
|
||||||
<woot-spinner size="" />
|
|
||||||
{{ $t('CONVERSATION.SEARCH.LOADING_MESSAGE') }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
v-if="searchTerm && conversations.length && !uiFlags.isFetching"
|
|
||||||
class="search-results--container"
|
|
||||||
>
|
|
||||||
<div v-for="conversation in conversations" :key="conversation.id">
|
|
||||||
<button
|
|
||||||
v-for="message in conversation.messages"
|
|
||||||
:key="message.id"
|
|
||||||
class="search--messages"
|
|
||||||
@click="() => onClick(conversation)"
|
|
||||||
>
|
|
||||||
<div class="search--messages__metadata">
|
|
||||||
<span>#{{ conversation.id }}</span>
|
|
||||||
<span>{{ dynamicTime(message.created_at) }}</span>
|
|
||||||
</div>
|
|
||||||
<div v-html="prepareContent(message.content)" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
v-else-if="
|
|
||||||
searchTerm &&
|
|
||||||
!conversations.length &&
|
|
||||||
!uiFlags.isFetching &&
|
|
||||||
hasSearched
|
|
||||||
"
|
|
||||||
class="search--activity-message"
|
|
||||||
>
|
|
||||||
{{ $t('CONVERSATION.SEARCH.NO_MATCHING_RESULTS') }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</woot-modal>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import { mapGetters } from 'vuex';
|
|
||||||
import { frontendURL, conversationUrl } from '../../../../helper/URLHelper';
|
|
||||||
import timeMixin from '../../../../mixins/time';
|
|
||||||
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
directives: {
|
|
||||||
focus: {
|
|
||||||
inserted(el) {
|
|
||||||
el.focus();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
mixins: [timeMixin, messageFormatterMixin],
|
|
||||||
props: {
|
|
||||||
show: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
onClose: {
|
|
||||||
type: Function,
|
|
||||||
default: () => {},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
searchTerm: '',
|
|
||||||
hasSearched: false,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
...mapGetters({
|
|
||||||
conversations: 'conversationSearch/getConversations',
|
|
||||||
uiFlags: 'conversationSearch/getUIFlags',
|
|
||||||
accountId: 'getCurrentAccountId',
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
searchTerm(newValue) {
|
|
||||||
if (this.typingTimer) {
|
|
||||||
clearTimeout(this.typingTimer);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.typingTimer = setTimeout(() => {
|
|
||||||
this.hasSearched = true;
|
|
||||||
this.$store.dispatch('conversationSearch/get', { q: newValue });
|
|
||||||
}, 1000);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
this.$store.dispatch('conversationSearch/get', { q: '' });
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
prepareContent(content = '') {
|
|
||||||
const plainTextContent = this.getPlainText(content);
|
|
||||||
return plainTextContent.replace(
|
|
||||||
new RegExp(`(${this.searchTerm})`, 'ig'),
|
|
||||||
'<span class="searchkey--highlight">$1</span>'
|
|
||||||
);
|
|
||||||
},
|
|
||||||
onClick(conversation) {
|
|
||||||
const path = conversationUrl({
|
|
||||||
accountId: this.accountId,
|
|
||||||
id: conversation.id,
|
|
||||||
});
|
|
||||||
window.location = frontendURL(path);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
.search--container {
|
|
||||||
font-size: var(--font-size-default);
|
|
||||||
padding: var(--space-normal) var(--space-large);
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-results--container {
|
|
||||||
max-height: 300px;
|
|
||||||
overflow: scroll;
|
|
||||||
}
|
|
||||||
|
|
||||||
.searchkey--highlight {
|
|
||||||
background: var(--w-500);
|
|
||||||
color: var(--white);
|
|
||||||
}
|
|
||||||
|
|
||||||
.search--activity-message {
|
|
||||||
color: var(--s-800);
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search--messages {
|
|
||||||
border-bottom: 1px solid var(--b-100);
|
|
||||||
color: var(--color-body);
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: var(--font-size-small);
|
|
||||||
line-height: 1.5;
|
|
||||||
padding: var(--space-normal);
|
|
||||||
text-align: left;
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: var(--w-50);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-search--modal .modal-container {
|
|
||||||
width: 800px;
|
|
||||||
min-height: 460px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search--messages__metadata {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
margin-bottom: var(--space-small);
|
|
||||||
color: var(--s-500);
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -0,0 +1,155 @@
|
||||||
|
<template>
|
||||||
|
<div class="message-item">
|
||||||
|
<div class="search-message">
|
||||||
|
<div class="user-wrap">
|
||||||
|
<div class="name-wrap">
|
||||||
|
<span class="user-name">{{ userName }}</span>
|
||||||
|
<div>
|
||||||
|
<i v-if="isOutgoingMessage" class="ion-headphone" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span class="timestamp">{{ readableTime }} </span>
|
||||||
|
</div>
|
||||||
|
<p class="message-content" v-html="prepareContent(content)"></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { MESSAGE_TYPE } from 'shared/constants/messages';
|
||||||
|
import timeMixin from 'dashboard/mixins/time';
|
||||||
|
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
mixins: [timeMixin, messageFormatterMixin],
|
||||||
|
|
||||||
|
props: {
|
||||||
|
userName: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
timestamp: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
messageType: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
searchTerm: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
isOutgoingMessage() {
|
||||||
|
return this.messageType === MESSAGE_TYPE.OUTGOING;
|
||||||
|
},
|
||||||
|
readableTime() {
|
||||||
|
if (!this.timestamp) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return this.dynamicTime(this.timestamp);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
prepareContent(content = '') {
|
||||||
|
const plainTextContent = this.getPlainText(content);
|
||||||
|
return plainTextContent.replace(
|
||||||
|
new RegExp(`(${this.searchTerm})`, 'ig'),
|
||||||
|
'<span class="searchkey--highlight">$1</span>'
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.message-item {
|
||||||
|
background: var(--color-background-light);
|
||||||
|
border-radius: var(--border-radius-medium);
|
||||||
|
color: var(--color-body);
|
||||||
|
margin-bottom: var(--space-small);
|
||||||
|
margin-left: var(--space-one);
|
||||||
|
padding: 0 var(--space-small);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--w-400);
|
||||||
|
color: var(--white);
|
||||||
|
.message-content::v-deep .searchkey--highlight {
|
||||||
|
color: var(--white);
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
.ion-headphone {
|
||||||
|
color: var(--white);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&:last-child {
|
||||||
|
.search-message {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-message {
|
||||||
|
padding: var(--space-smaller) var(--space-smaller);
|
||||||
|
&:hover {
|
||||||
|
color: var(--white);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-wrap {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name-wrap {
|
||||||
|
display: flex;
|
||||||
|
width: 22rem;
|
||||||
|
|
||||||
|
.user-name {
|
||||||
|
font-size: var(--font-size-small);
|
||||||
|
font-weight: var(--font-weight-bold);
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ion-headphone {
|
||||||
|
color: var(--w-500);
|
||||||
|
font-size: var(--font-size-mini);
|
||||||
|
padding: var(--space-micro);
|
||||||
|
padding-right: var(--space-smaller);
|
||||||
|
}
|
||||||
|
|
||||||
|
.timestamp {
|
||||||
|
font-size: var(--font-size-mini);
|
||||||
|
top: var(--space-micro);
|
||||||
|
position: relative;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-content {
|
||||||
|
font-size: var(--font-size-small);
|
||||||
|
margin-bottom: var(--space-micro);
|
||||||
|
padding: 0;
|
||||||
|
line-height: 1.35;
|
||||||
|
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);
|
||||||
|
padding: (var(--space-zero) var(--space-zero));
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -6,9 +6,22 @@ end
|
||||||
json.payload do
|
json.payload do
|
||||||
json.array! @conversations do |conversation|
|
json.array! @conversations do |conversation|
|
||||||
json.id conversation.display_id
|
json.id conversation.display_id
|
||||||
|
json.created_at conversation.created_at.to_i
|
||||||
|
json.contact do
|
||||||
|
json.id conversation.contact.id
|
||||||
|
json.name conversation.contact.name
|
||||||
|
end
|
||||||
|
json.inbox do
|
||||||
|
json.id conversation.inbox.id
|
||||||
|
json.name conversation.inbox.name
|
||||||
|
json.channel_type conversation.inbox.channel_type
|
||||||
|
end
|
||||||
json.messages do
|
json.messages do
|
||||||
json.array! conversation.messages do |message|
|
json.array! conversation.messages do |message|
|
||||||
json.content message.content
|
json.content message.content
|
||||||
|
json.id message.id
|
||||||
|
json.sender_name message.sender.name if message.sender
|
||||||
|
json.message_type message.message_type_before_type_cast
|
||||||
json.created_at message.created_at.to_i
|
json.created_at message.created_at.to_i
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue