feat: Updated the search result fly-out menu design (#8203)

Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
This commit is contained in:
Sivin Varghese
2023-10-26 07:46:49 +05:30
committed by GitHub
parent b6831d464e
commit 3e54d3654b
13 changed files with 67 additions and 592 deletions

View File

@@ -71,18 +71,11 @@ export default {
},
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, '\\$&');
return this.highlightContent(
escapedText,
this.searchTerm,
'searchkey--highlight'
);
},
},
};

View File

@@ -1,80 +0,0 @@
<template>
<div class="relative">
<div
class="flex px-4 pb-1 flex-row gap-1 pt-2.5 border-b border-transparent"
>
<woot-sidemenu-icon
size="tiny"
class="relative top-0 ltr:-ml-1.5 rtl:-mr-1.5"
/>
<router-link
:to="searchUrl"
class="search-link flex-1 items-center gap-1 text-left h-6 rtl:mr-3 rtl:text-right rounded-md px-2 py-0 bg-slate-25 dark:bg-slate-800 inline-flex"
>
<div class="flex">
<fluent-icon
icon="search"
class="search--icon text-slate-800 dark:text-slate-200"
size="16"
/>
</div>
<p
class="search--label mb-0 overflow-hidden whitespace-nowrap text-ellipsis text-sm text-slate-800 dark:text-slate-200"
>
{{ $t('CONVERSATION.SEARCH_MESSAGES') }}
</p>
</router-link>
<switch-layout
:is-on-expanded-layout="isOnExpandedLayout"
@toggle="$emit('toggle-conversation-layout')"
/>
</div>
</div>
</template>
<script>
import { mixin as clickaway } from 'vue-clickaway';
import { mapGetters } from 'vuex';
import timeMixin from '../../../../mixins/time';
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
import SwitchLayout from './SwitchLayout.vue';
import { frontendURL } from 'dashboard/helper/URLHelper';
export default {
components: {
SwitchLayout,
},
directives: {
focus: {
inserted(el) {
el.focus();
},
},
},
mixins: [timeMixin, messageFormatterMixin, clickaway],
props: {
isOnExpandedLayout: {
type: Boolean,
required: true,
},
},
computed: {
...mapGetters({
accountId: 'getCurrentAccountId',
}),
searchUrl() {
return frontendURL(`accounts/${this.accountId}/search`);
},
},
};
</script>
<style lang="scss" scoped>
.search-link {
&:hover {
.search--icon,
.search--label {
@apply hover:text-woot-500 dark:hover:text-woot-500;
}
}
}
</style>

View File

@@ -1,41 +0,0 @@
import ResultItem from './ResultItem';
export default {
title: 'Components/Search/Result Items',
component: ResultItem,
argTypes: {
conversationId: {
defaultValue: '1',
control: {
type: 'number',
},
},
userName: {
defaultValue: 'John davies',
control: {
type: 'text',
},
},
inboxName: {
defaultValue: 'Support',
control: {
type: 'text',
},
},
timestamp: {
defaultValue: '1618046084',
control: {
type: 'number',
},
},
},
};
const Template = (args, { argTypes }) => ({
props: Object.keys(argTypes),
components: { ResultItem },
template: '<result-item v-bind="$props"></result-item>',
});
export const ResultItems = Template.bind({});
ResultItems.args = {};

View File

@@ -1,185 +0,0 @@
<template>
<div class="search-result" @click="onClick">
<div class="result-header">
<div class="conversation--block">
<fluent-icon icon="chat" class="icon--conversation-search-item" />
<div class="conversation">
<div class="user-wrap">
<div class="name-wrap">
<span class="sub-block-title">{{ userName }}</span>
</div>
<woot-label
:title="conversationsId"
:small="true"
color-scheme="secondary"
/>
</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',
}),
conversationsId() {
return `# ${this.conversationId}`;
},
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);
}
.icon--conversation-search-item {
color: var(--white);
}
}
}
.conversation--block {
align-items: center;
display: flex;
}
.icon--conversation-search-item {
align-items: center;
display: flex;
color: var(--w-500);
}
.conversation {
align-items: flex-start;
display: flex;
flex-direction: column;
padding: var(--space-smaller) var(--space-one);
}
.user-wrap {
display: flex;
.name-wrap {
max-width: 12.5rem;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
.sub-block-title {
font-weight: var(--font-weight-bold);
margin-right: var(--space-micro);
}
}
}
.inbox-name {
border-radius: var(--border-radius-normal);
color: var(--s-500);
font-size: var(--font-size-mini);
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>

View File

@@ -1,47 +0,0 @@
import SearchMessage from './SearchMessageItem';
export default {
title: 'Components/Search/Messages',
component: SearchMessage,
argTypes: {
userName: {
defaultValue: 'John davies',
control: {
type: 'text',
},
},
timestamp: {
defaultValue: '1618046084',
control: {
type: 'number',
},
},
messageType: {
control: {
type: 'number',
},
},
content: {
defaultValue:
'some designers and developers around the web know this and have put together a bunch of text generators',
control: {
type: 'text',
},
},
searchTerm: {
defaultValue: 'developers',
control: {
type: 'text',
},
},
},
};
const Template = (args, { argTypes }) => ({
props: Object.keys(argTypes),
components: { SearchMessage },
template: '<search-message v-bind="$props"></-item>',
});
export const Messages = Template.bind({});
Messages.args = {};

View File

@@ -1,163 +0,0 @@
<template>
<div class="message-item">
<div class="search-message">
<div class="user-wrap">
<div class="name-wrap">
<span class="text-block-title">{{ userName }}</span>
<div>
<fluent-icon
v-if="isOutgoingMessage"
icon="arrow-reply"
class="icon-outgoing"
/>
</div>
</div>
<span class="timestamp">{{ readableTime }} </span>
</div>
<p v-dompurify-html="prepareContent(content)" class="message-content" />
</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);
const escapedSearchTerm = this.escapeRegExp(this.searchTerm);
return plainTextContent.replace(
new RegExp(`(${escapedSearchTerm})`, 'ig'),
'<span class="searchkey--highlight">$1</span>'
);
},
// from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping
escapeRegExp(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
},
},
};
</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;
}
.icon-outgoing {
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;
max-width: 13.75rem;
.text-block-title {
font-weight: var(--font-weight-bold);
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
}
.icon-outgoing {
color: var(--w-500);
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);
margin-top: 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>

View File

@@ -1,36 +0,0 @@
<template>
<woot-button
v-tooltip.left="$t('CONVERSATION.SWITCH_VIEW_LAYOUT')"
icon="arrow-right-import"
size="tiny"
variant="smooth"
color-scheme="secondary"
class="layout-switch__container"
:class="{ expanded: isOnExpandedLayout }"
@click="toggle"
/>
</template>
<script>
export default {
props: {
isOnExpandedLayout: {
type: Boolean,
default: false,
},
},
methods: {
toggle() {
this.$emit('toggle');
},
},
};
</script>
<style lang="scss" soped>
.layout-switch__container {
&.expanded .icon {
transform: rotate(180deg);
}
}
</style>

View File

@@ -9,7 +9,7 @@
"
:show-new-button="false"
/>
<div>
<div class="overflow-auto max-h-[96%]">
<setting-intro-banner :header-title="portalName">
<woot-tabs
:index="activeTabIndex"
@@ -24,9 +24,9 @@
/>
</woot-tabs>
</setting-intro-banner>
</div>
<div class="overflow-auto p-4 max-w-full my-auto flex flex-wrap">
<router-view />
<div class="p-4 max-w-full my-auto flex flex-wrap">
<router-view />
</div>
</div>
</div>
</template>