feat: Improve email rendering, introduce a new layout for emails (#5039)
Co-authored-by: Fayaz Ahmed <15716057+fayazara@users.noreply.github.com>
This commit is contained in:
parent
ef9ea99b91
commit
2c372fe315
19 changed files with 282 additions and 71 deletions
|
@ -296,7 +296,7 @@
|
|||
@include margin($zero $space-normal);
|
||||
|
||||
--bubble-max-width: 49.6rem;
|
||||
max-width: Min(var(--bubble-max-width), 85%);
|
||||
max-width: Min(var(--bubble-max-width), 84%);
|
||||
|
||||
.sender--name {
|
||||
font-size: $font-size-mini;
|
||||
|
|
|
@ -1,5 +1,11 @@
|
|||
<template>
|
||||
<div class="conversations-list-wrap">
|
||||
<div
|
||||
class="conversations-list-wrap"
|
||||
:class="{
|
||||
hide: !showConversationList,
|
||||
'list--full-width': isOnExpandedLayout,
|
||||
}"
|
||||
>
|
||||
<slot />
|
||||
<div
|
||||
class="chat-list__top"
|
||||
|
@ -46,7 +52,7 @@
|
|||
|
||||
<woot-button
|
||||
v-else
|
||||
v-tooltip.top-end="$t('FILTER.TOOLTIP_LABEL')"
|
||||
v-tooltip.right="$t('FILTER.TOOLTIP_LABEL')"
|
||||
variant="clear"
|
||||
color-scheme="secondary"
|
||||
icon="filter"
|
||||
|
@ -210,6 +216,14 @@ export default {
|
|||
type: [String, Number],
|
||||
default: 0,
|
||||
},
|
||||
showConversationList: {
|
||||
default: true,
|
||||
type: Boolean,
|
||||
},
|
||||
isOnExpandedLayout: {
|
||||
default: false,
|
||||
type: Boolean,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
@ -696,6 +710,17 @@ export default {
|
|||
@include breakpoint(xxxlarge up) {
|
||||
flex-basis: 46rem;
|
||||
}
|
||||
|
||||
&.hide {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.list--full-width {
|
||||
width: 100%;
|
||||
@include breakpoint(xxxlarge up) {
|
||||
flex-basis: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
.filter--actions {
|
||||
display: flex;
|
||||
|
|
|
@ -1,9 +1,13 @@
|
|||
<template>
|
||||
<div class="conversation-details-wrap">
|
||||
<div
|
||||
class="conversation-details-wrap"
|
||||
:class="{ 'with-border-left': !isOnExpandedLayout }"
|
||||
>
|
||||
<conversation-header
|
||||
v-if="currentChat.id"
|
||||
:chat="currentChat"
|
||||
:is-contact-panel-open="isContactPanelOpen"
|
||||
:show-back-button="isOnExpandedLayout"
|
||||
@contact-panel-toggle="onToggleContactPanel"
|
||||
/>
|
||||
<woot-tabs
|
||||
|
@ -26,7 +30,7 @@
|
|||
:is-contact-panel-open="isContactPanelOpen"
|
||||
@contact-panel-toggle="onToggleContactPanel"
|
||||
/>
|
||||
<empty-state v-else />
|
||||
<empty-state v-else :is-on-expanded-layout="isOnExpandedLayout" />
|
||||
<div v-show="showContactPanel" class="conversation-sidebar-wrap">
|
||||
<contact-panel
|
||||
v-if="showContactPanel"
|
||||
|
@ -71,6 +75,10 @@ export default {
|
|||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
isOnExpandedLayout: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return { activeIndex: 0 };
|
||||
|
@ -134,8 +142,11 @@ export default {
|
|||
flex-direction: column;
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
border-left: 1px solid var(--color-border);
|
||||
background: var(--color-background-light);
|
||||
|
||||
&.with-border-left {
|
||||
border-left: 1px solid var(--color-border);
|
||||
}
|
||||
}
|
||||
|
||||
.dashboard-app--tabs {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<template>
|
||||
<div class="conv-header">
|
||||
<div class="user">
|
||||
<back-button v-if="showBackButton" :back-url="backButtonUrl" />
|
||||
<Thumbnail
|
||||
:src="currentContact.thumbnail"
|
||||
size="40px"
|
||||
|
@ -47,19 +48,21 @@
|
|||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { hasPressedAltAndOKey } from 'shared/helpers/KeyboardHelpers';
|
||||
import { mapGetters } from 'vuex';
|
||||
import MoreActions from './MoreActions';
|
||||
import Thumbnail from '../Thumbnail';
|
||||
import agentMixin from '../../../mixins/agentMixin.js';
|
||||
import BackButton from '../BackButton';
|
||||
import differenceInHours from 'date-fns/differenceInHours';
|
||||
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
||||
import inboxMixin from 'shared/mixins/inboxMixin';
|
||||
import { hasPressedAltAndOKey } from 'shared/helpers/KeyboardHelpers';
|
||||
import wootConstants from '../../../constants';
|
||||
import differenceInHours from 'date-fns/differenceInHours';
|
||||
import InboxName from '../InboxName';
|
||||
|
||||
import MoreActions from './MoreActions';
|
||||
import Thumbnail from '../Thumbnail';
|
||||
import wootConstants from '../../../constants';
|
||||
import { conversationListPageURL } from 'dashboard/helper/URLHelper';
|
||||
export default {
|
||||
components: {
|
||||
BackButton,
|
||||
InboxName,
|
||||
MoreActions,
|
||||
Thumbnail,
|
||||
|
@ -74,6 +77,10 @@ export default {
|
|||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
showBackButton: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
|
@ -83,6 +90,19 @@ export default {
|
|||
chatMetadata() {
|
||||
return this.chat.meta;
|
||||
},
|
||||
backButtonUrl() {
|
||||
const {
|
||||
params: { accountId, inbox_id: inboxId, label, teamId },
|
||||
name,
|
||||
} = this.$route;
|
||||
return conversationListPageURL({
|
||||
accountId,
|
||||
inboxId,
|
||||
label,
|
||||
teamId,
|
||||
conversationType: name === 'conversation_mentions' ? 'mention' : '',
|
||||
});
|
||||
},
|
||||
isHMACVerified() {
|
||||
if (!this.isAWebWidgetInbox) {
|
||||
return true;
|
||||
|
|
|
@ -35,7 +35,7 @@
|
|||
<!-- No conversation selected -->
|
||||
<div v-else-if="allConversations.length && !currentChat.id">
|
||||
<img src="~dashboard/assets/images/chat.svg" alt="No Chat" />
|
||||
<span>{{ $t('CONVERSATION.404') }}</span>
|
||||
<span>{{ conversationMissingMessage }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -51,6 +51,12 @@ export default {
|
|||
OnboardingView,
|
||||
},
|
||||
mixins: [accountMixin, adminMixin],
|
||||
props: {
|
||||
isOnExpandedLayout: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
currentChat: 'getSelectedChat',
|
||||
|
@ -65,6 +71,12 @@ export default {
|
|||
}
|
||||
return this.$t('CONVERSATION.LOADING_CONVERSATIONS');
|
||||
},
|
||||
conversationMissingMessage() {
|
||||
if (!this.isOnExpandedLayout) {
|
||||
return this.$t('CONVERSATION.SELECT_A_CONVERSATION');
|
||||
}
|
||||
return this.$t('CONVERSATION.404');
|
||||
},
|
||||
newInboxURL() {
|
||||
return this.addAccountScoping('settings/inboxes/new');
|
||||
},
|
||||
|
|
|
@ -170,7 +170,7 @@ export default {
|
|||
};
|
||||
},
|
||||
computed: {
|
||||
contentToBeParsed() {
|
||||
emailMessageContent() {
|
||||
const {
|
||||
html_content: { full: fullHTMLContent } = {},
|
||||
text_content: { full: fullTextContent } = {},
|
||||
|
@ -182,13 +182,19 @@ export default {
|
|||
return false;
|
||||
}
|
||||
|
||||
if (this.contentToBeParsed.includes('<blockquote')) {
|
||||
if (this.emailMessageContent.includes('<blockquote')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
message() {
|
||||
// If the message is an email, emailMessageContent would be present
|
||||
// In that case, we would use letter package to render the email
|
||||
if (this.emailMessageContent && this.isIncoming) {
|
||||
return this.emailMessageContent;
|
||||
}
|
||||
|
||||
const botMessageContent = generateBotMessageContent(
|
||||
this.contentType,
|
||||
this.contentAttributes,
|
||||
|
@ -200,21 +206,6 @@ export default {
|
|||
},
|
||||
}
|
||||
);
|
||||
|
||||
const {
|
||||
email: { content_type: contentType = '' } = {},
|
||||
} = this.contentAttributes;
|
||||
if (this.contentToBeParsed && this.isIncoming) {
|
||||
const parsedContent = this.stripStyleCharacters(this.contentToBeParsed);
|
||||
if (parsedContent) {
|
||||
// This is a temporary fix for line-breaks in text/plain emails
|
||||
// Now, It is not rendered properly in the email preview.
|
||||
// FIXME: Remove this once we have a better solution for rendering text/plain emails
|
||||
return contentType.includes('text/plain')
|
||||
? parsedContent.replace(/\n/g, '<br />')
|
||||
: parsedContent;
|
||||
}
|
||||
}
|
||||
return (
|
||||
this.formatMessage(
|
||||
this.data.content,
|
||||
|
@ -331,6 +322,7 @@ export default {
|
|||
'activity-wrap': !this.isBubble,
|
||||
'is-pending': this.isPending,
|
||||
'is-failed': this.isFailed,
|
||||
'is-email': this.isEmailContentType,
|
||||
};
|
||||
},
|
||||
bubbleClass() {
|
||||
|
@ -342,6 +334,7 @@ export default {
|
|||
'is-text': this.hasText,
|
||||
'is-from-bot': this.isSentByBot,
|
||||
'is-failed': this.isFailed,
|
||||
'is-email': this.isEmailContentType,
|
||||
};
|
||||
},
|
||||
isPending() {
|
||||
|
@ -518,6 +511,10 @@ export default {
|
|||
}
|
||||
}
|
||||
|
||||
.wrap.is-email {
|
||||
--bubble-max-width: 84% !important;
|
||||
}
|
||||
|
||||
.sender--info {
|
||||
align-items: center;
|
||||
color: var(--b-700);
|
||||
|
|
|
@ -6,7 +6,8 @@
|
|||
'hide--quoted': !showQuotedContent,
|
||||
}"
|
||||
>
|
||||
<div v-dompurify-html="message" class="text-content" />
|
||||
<div v-if="!isEmail" v-dompurify-html="message" class="text-content" />
|
||||
<letter v-else class="text-content" :html="message" />
|
||||
<button
|
||||
v-if="displayQuotedButton"
|
||||
class="quoted-text--button"
|
||||
|
@ -25,7 +26,10 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import Letter from 'vue-letter';
|
||||
|
||||
export default {
|
||||
components: { Letter },
|
||||
props: {
|
||||
message: {
|
||||
type: String,
|
||||
|
@ -65,14 +69,16 @@ export default {
|
|||
padding-left: var(--space-two);
|
||||
}
|
||||
table {
|
||||
all: revert;
|
||||
margin: 0;
|
||||
border: 0;
|
||||
|
||||
td {
|
||||
all: revert;
|
||||
margin: 0;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
tr {
|
||||
all: revert;
|
||||
border-bottom: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -12,6 +12,10 @@ export default {
|
|||
SNOOZED: 'snoozed',
|
||||
ALL: 'all',
|
||||
},
|
||||
LAYOUT_TYPES: {
|
||||
CONDENSED: 'condensed',
|
||||
EXPANDED: 'expanded',
|
||||
},
|
||||
DOCS_URL: '//www.chatwoot.com/docs/product/',
|
||||
};
|
||||
export const DEFAULT_REDIRECT_URL = '/app/';
|
||||
|
|
|
@ -44,6 +44,26 @@ export const conversationUrl = ({
|
|||
return url;
|
||||
};
|
||||
|
||||
export const conversationListPageURL = ({
|
||||
accountId,
|
||||
conversationType = '',
|
||||
inboxId,
|
||||
label,
|
||||
teamId,
|
||||
}) => {
|
||||
let url = `accounts/${accountId}/dashboard`;
|
||||
if (label) {
|
||||
url = `accounts/${accountId}/label/${label}`;
|
||||
} else if (teamId) {
|
||||
url = `accounts/${accountId}/team/${teamId}`;
|
||||
} else if (conversationType === 'mention') {
|
||||
url = `accounts/${accountId}/mentions/conversations`;
|
||||
} else if (inboxId) {
|
||||
url = `accounts/${accountId}/inbox/${inboxId}`;
|
||||
}
|
||||
return frontendURL(url);
|
||||
};
|
||||
|
||||
export const isValidURL = value => {
|
||||
/* eslint-disable no-useless-escape */
|
||||
const URL_REGEX = /^https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)$/gm;
|
||||
|
|
|
@ -3,9 +3,33 @@ import {
|
|||
conversationUrl,
|
||||
isValidURL,
|
||||
getLoginRedirectURL,
|
||||
conversationListPageURL,
|
||||
} from '../URLHelper';
|
||||
|
||||
describe('#URL Helpers', () => {
|
||||
describe('conversationListPageURL', () => {
|
||||
it('should return url to dashboard', () => {
|
||||
expect(conversationListPageURL({ accountId: 1 })).toBe(
|
||||
'/app/accounts/1/dashboard'
|
||||
);
|
||||
});
|
||||
it('should return url to inbox', () => {
|
||||
expect(conversationListPageURL({ accountId: 1, inboxId: 1 })).toBe(
|
||||
'/app/accounts/1/inbox/1'
|
||||
);
|
||||
});
|
||||
it('should return url to label', () => {
|
||||
expect(conversationListPageURL({ accountId: 1, label: 'support' })).toBe(
|
||||
'/app/accounts/1/label/support'
|
||||
);
|
||||
});
|
||||
|
||||
it('should return url to team', () => {
|
||||
expect(conversationListPageURL({ accountId: 1, teamId: 1 })).toBe(
|
||||
'/app/accounts/1/team/1'
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('conversationUrl', () => {
|
||||
it('should return direct conversation URL if activeInbox is nil', () => {
|
||||
expect(conversationUrl({ accountId: 1, id: 1 })).toBe(
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
{
|
||||
"CONVERSATION": {
|
||||
"404": "Please select a conversation from left pane",
|
||||
"SELECT_A_CONVERSATION": "Please select a conversation from left pane",
|
||||
"404": "Sorry, we cannot find the conversation. Please try again",
|
||||
"SWITCH_VIEW_LAYOUT": "Switch the layout",
|
||||
"DASHBOARD_APP_TAB_MESSAGES": "Messages",
|
||||
"UNVERIFIED_SESSION": "The identity of this user is not verified",
|
||||
"NO_MESSAGE_1": "Uh oh! Looks like there are no messages from customers in your inbox.",
|
||||
|
|
|
@ -1,18 +1,25 @@
|
|||
<template>
|
||||
<section class="conversation-page">
|
||||
<chat-list
|
||||
:show-conversation-list="showConversationList"
|
||||
:conversation-inbox="inboxId"
|
||||
:label="label"
|
||||
:team-id="teamId"
|
||||
:conversation-type="conversationType"
|
||||
:folders-id="foldersId"
|
||||
:is-on-expanded-layout="isOnExpandedLayout"
|
||||
@conversation-load="onConversationLoad"
|
||||
>
|
||||
<pop-over-search />
|
||||
<pop-over-search
|
||||
:is-on-expanded-layout="isOnExpandedLayout"
|
||||
@toggle-conversation-layout="toggleConversationLayout"
|
||||
/>
|
||||
</chat-list>
|
||||
<conversation-box
|
||||
v-if="showMessageView"
|
||||
:inbox-id="inboxId"
|
||||
:is-contact-panel-open="isContactPanelOpen"
|
||||
:is-on-expanded-layout="isOnExpandedLayout"
|
||||
@contact-panel-toggle="onToggleContactPanel"
|
||||
/>
|
||||
</section>
|
||||
|
@ -25,6 +32,7 @@ import ConversationBox from '../../../components/widgets/conversation/Conversati
|
|||
import PopOverSearch from './search/PopOverSearch';
|
||||
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
||||
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
||||
import wootConstants from 'dashboard/constants';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
@ -69,6 +77,21 @@ export default {
|
|||
chatList: 'getAllConversations',
|
||||
currentChat: 'getSelectedChat',
|
||||
}),
|
||||
showConversationList() {
|
||||
return this.isOnExpandedLayout ? !this.conversationId : true;
|
||||
},
|
||||
showMessageView() {
|
||||
return this.conversationId ? true : !this.isOnExpandedLayout;
|
||||
},
|
||||
isOnExpandedLayout() {
|
||||
const {
|
||||
LAYOUT_TYPES: { CONDENSED },
|
||||
} = wootConstants;
|
||||
const {
|
||||
conversation_display_type: conversationDisplayType = CONDENSED,
|
||||
} = this.uiSettings;
|
||||
return conversationDisplayType !== CONDENSED;
|
||||
},
|
||||
isContactPanelOpen() {
|
||||
if (this.currentChat.id) {
|
||||
const {
|
||||
|
@ -101,6 +124,17 @@ export default {
|
|||
this.$store.dispatch('setActiveInbox', this.inboxId);
|
||||
this.setActiveChat();
|
||||
},
|
||||
toggleConversationLayout() {
|
||||
const { LAYOUT_TYPES } = wootConstants;
|
||||
const {
|
||||
conversation_display_type: conversationDisplayType = LAYOUT_TYPES.CONDENSED,
|
||||
} = this.uiSettings;
|
||||
const newViewType =
|
||||
conversationDisplayType === LAYOUT_TYPES.CONDENSED
|
||||
? LAYOUT_TYPES.EXPANDED
|
||||
: LAYOUT_TYPES.CONDENSED;
|
||||
this.updateUISettings({ conversation_display_type: newViewType });
|
||||
},
|
||||
fetchConversationIfUnavailable() {
|
||||
if (!this.conversationId) {
|
||||
return;
|
||||
|
|
|
@ -11,6 +11,10 @@
|
|||
:placeholder="$t('CONVERSATION.SEARCH_MESSAGES')"
|
||||
@focus="onSearch"
|
||||
/>
|
||||
<switch-layout
|
||||
:is-on-expanded-layout="isOnExpandedLayout"
|
||||
@toggle="$emit('toggle-conversation-layout')"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="showSearchBox" class="results-wrap">
|
||||
<div class="show-results">
|
||||
|
@ -55,10 +59,11 @@ import { mapGetters } from 'vuex';
|
|||
import timeMixin from '../../../../mixins/time';
|
||||
import ResultItem from './ResultItem';
|
||||
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
||||
|
||||
import SwitchLayout from './SwitchLayout.vue';
|
||||
export default {
|
||||
components: {
|
||||
ResultItem,
|
||||
SwitchLayout,
|
||||
},
|
||||
|
||||
directives: {
|
||||
|
@ -71,6 +76,13 @@ export default {
|
|||
|
||||
mixins: [timeMixin, messageFormatterMixin, clickaway],
|
||||
|
||||
props: {
|
||||
isOnExpandedLayout: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
searchTerm: '',
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
<template>
|
||||
<button
|
||||
v-tooltip.left="$t('CONVERSATION.SWITCH_VIEW_LAYOUT')"
|
||||
class="layout-switch__container"
|
||||
@click="toggle"
|
||||
>
|
||||
<div
|
||||
class="layout-switch__btn left"
|
||||
:class="{ active: !isOnExpandedLayout }"
|
||||
>
|
||||
<fluent-icon icon="panel-sidebar" :size="16" />
|
||||
</div>
|
||||
<div
|
||||
class="layout-switch__btn right"
|
||||
:class="{ active: isOnExpandedLayout }"
|
||||
>
|
||||
<fluent-icon icon="panel-contract" :size="16" />
|
||||
</div>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
isOnExpandedLayout: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
toggle() {
|
||||
this.$emit('toggle');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" soped>
|
||||
.layout-switch__container {
|
||||
align-items: center;
|
||||
background-color: var(--s-100);
|
||||
border-radius: var(--border-radius-medium);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
padding: var(--space-micro);
|
||||
|
||||
.layout-switch__btn {
|
||||
border-radius: var(--border-radius-normal);
|
||||
color: var(--s-400);
|
||||
display: flex;
|
||||
padding: var(--space-micro) var(--space-smaller);
|
||||
|
||||
&.active {
|
||||
background-color: var(--white);
|
||||
color: var(--w-500);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -103,6 +103,8 @@
|
|||
"microphone-play-outline": "M8,5.14V19.14L19,12.14L8,5.14Z",
|
||||
"number-symbol-outline": "M10.987 2.89a.75.75 0 1 0-1.474-.28L8.494 7.999 3.75 8a.75.75 0 1 0 0 1.5l4.46-.002-.946 5-4.514.002a.75.75 0 0 0 0 1.5l4.23-.002-.967 5.116a.75.75 0 1 0 1.474.278l1.02-5.395 5.474-.002-.968 5.119a.75.75 0 1 0 1.474.278l1.021-5.398 4.742-.002a.75.75 0 1 0 0-1.5l-4.458.002.946-5 4.512-.002a.75.75 0 1 0 0-1.5l-4.229.002.966-5.104a.75.75 0 0 0-1.474-.28l-1.018 5.385-5.474.002.966-5.107Zm-1.25 6.608 5.474-.003-.946 5-5.474.002.946-5Z",
|
||||
"open-outline": "M6.25 4.5A1.75 1.75 0 0 0 4.5 6.25v11.5c0 .966.783 1.75 1.75 1.75h11.5a1.75 1.75 0 0 0 1.75-1.75v-4a.75.75 0 0 1 1.5 0v4A3.25 3.25 0 0 1 17.75 21H6.25A3.25 3.25 0 0 1 3 17.75V6.25A3.25 3.25 0 0 1 6.25 3h4a.75.75 0 0 1 0 1.5h-4ZM13 3.75a.75.75 0 0 1 .75-.75h6.5a.75.75 0 0 1 .75.75v6.5a.75.75 0 0 1-1.5 0V5.56l-5.22 5.22a.75.75 0 0 1-1.06-1.06l5.22-5.22h-4.69a.75.75 0 0 1-.75-.75Z",
|
||||
"panel-sidebar-outline": "M4.75 4A2.75 2.75 0 0 0 2 6.75v10.5A2.75 2.75 0 0 0 4.75 20h14.5A2.75 2.75 0 0 0 22 17.25V6.75A2.75 2.75 0 0 0 19.25 4H4.75ZM9 18.5v-13h10.25c.69 0 1.25.56 1.25 1.25v10.5c0 .69-.56 1.25-1.25 1.25H9ZM5.5 3.5h4M5.5 5.5h4M5.5 7.5h4M5.5 9.5h4",
|
||||
"panel-contract-outline": "M14.193 14.751a.75.75 0 0 0 1.059.056l2.5-2.25a.75.75 0 0 0 0-1.114l-2.5-2.25a.75.75 0 0 0-1.004 1.115l1.048.942H11.75a.75.75 0 1 0 0 1.5h3.546l-1.048.942a.75.75 0 0 0-.055 1.06ZM2 6.75A2.75 2.75 0 0 1 4.75 4h14.5A2.75 2.75 0 0 1 22 6.75v10.5A2.75 2.75 0 0 1 19.25 20H4.75A2.75 2.75 0 0 1 2 17.25V6.75ZM9 5.5v13h10.25c.69 0 1.25-.56 1.25-1.25V6.75c0-.69-.56-1.25-1.25-1.25H9Z",
|
||||
"pen-outline": "M7.5 2.75a.75.75 0 0 0-1.5 0v3a1.75 1.75 0 0 0 1.543 1.738L6.527 9.993a3.868 3.868 0 0 0 .119 3.143l3.99 7.95c.283.566.803.914 1.364.914s1.08-.348 1.365-.913l3.99-7.951c.481-.96.526-2.137.118-3.143l-1.016-2.505A1.75 1.75 0 0 0 18 5.75v-3a.75.75 0 0 0-1.5 0v3a.25.25 0 0 1-.25.25h-8.5a.25.25 0 0 1-.25-.25v-3Zm7.343 4.75 1.24 3.057c.247.61.217 1.336-.07 1.906l-3.263 6.504v-6.668a1.5 1.5 0 1 0-1.5 0v6.668l-3.264-6.504a2.368 2.368 0 0 1-.069-1.906L9.157 7.5h5.686Z",
|
||||
"people-outline": "M4 13.999 13 14a2 2 0 0 1 1.995 1.85L15 16v1.5C14.999 21 11.284 22 8.5 22c-2.722 0-6.335-.956-6.495-4.27L2 17.5v-1.501c0-1.054.816-1.918 1.85-1.995L4 14ZM15.22 14H20c1.054 0 1.918.816 1.994 1.85L22 16v1c-.001 3.062-2.858 4-5 4a7.16 7.16 0 0 1-2.14-.322c.336-.386.607-.827.802-1.327A6.19 6.19 0 0 0 17 19.5l.267-.006c.985-.043 3.086-.363 3.226-2.289L20.5 17v-1a.501.501 0 0 0-.41-.492L20 15.5h-4.051a2.957 2.957 0 0 0-.595-1.34L15.22 14H20h-4.78ZM4 15.499l-.1.01a.51.51 0 0 0-.254.136.506.506 0 0 0-.136.253l-.01.101V17.5c0 1.009.45 1.722 1.417 2.242.826.445 2.003.714 3.266.753l.317.005.317-.005c1.263-.039 2.439-.308 3.266-.753.906-.488 1.359-1.145 1.412-2.057l.005-.186V16a.501.501 0 0 0-.41-.492L13 15.5l-9-.001ZM8.5 3a4.5 4.5 0 1 1 0 9 4.5 4.5 0 0 1 0-9Zm9 2a3.5 3.5 0 1 1 0 7 3.5 3.5 0 0 1 0-7Zm-9-.5c-1.654 0-3 1.346-3 3s1.346 3 3 3 3-1.346 3-3-1.346-3-3-3Zm9 2c-1.103 0-2 .897-2 2s.897 2 2 2 2-.897 2-2-.897-2-2-2Z",
|
||||
"people-team-outline": "M14.754 10c.966 0 1.75.784 1.75 1.75v4.749a4.501 4.501 0 0 1-9.002 0V11.75c0-.966.783-1.75 1.75-1.75h5.502Zm0 1.5H9.252a.25.25 0 0 0-.25.25v4.749a3.001 3.001 0 0 0 6.002 0V11.75a.25.25 0 0 0-.25-.25ZM3.75 10h3.381a2.738 2.738 0 0 0-.618 1.5H3.75a.25.25 0 0 0-.25.25v3.249a2.501 2.501 0 0 0 3.082 2.433c.085.504.24.985.453 1.432A4.001 4.001 0 0 1 2 14.999V11.75c0-.966.784-1.75 1.75-1.75Zm13.125 0h3.375c.966 0 1.75.784 1.75 1.75V15a4 4 0 0 1-5.03 3.866c.214-.448.369-.929.455-1.433A2.5 2.5 0 0 0 20.5 15v-3.25a.25.25 0 0 0-.25-.25h-2.757a2.738 2.738 0 0 0-.618-1.5ZM12 3a3 3 0 1 1 0 6 3 3 0 0 1 0-6Zm6.5 1a2.5 2.5 0 1 1 0 5 2.5 2.5 0 0 1 0-5Zm-13 0a2.5 2.5 0 1 1 0 5 2.5 2.5 0 0 1 0-5Zm6.5.5a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3Zm6.5 1a1 1 0 1 0 0 2 1 1 0 0 0 0-2Zm-13 0a1 1 0 1 0 0 2 1 1 0 0 0 0-2Z",
|
||||
|
@ -145,6 +147,10 @@
|
|||
"subtract-solid": "M3.997 13H20a1 1 0 1 0 0-2H3.997a1 1 0 1 0 0 2Z",
|
||||
"drag-outline": "M15 3.707V8.5a.5.5 0 0 0 1 0V3.707l1.146 1.147a.5.5 0 0 0 .708-.708l-2-2a.499.499 0 0 0-.708 0l-2 2a.5.5 0 0 0 .708.708L15 3.707ZM2 4.5a.5.5 0 0 1 .5-.5h6a.5.5 0 0 1 0 1h-6a.5.5 0 0 1-.5-.5Zm0 5a.5.5 0 0 1 .5-.5h9a.5.5 0 0 1 0 1h-9a.5.5 0 0 1-.5-.5Zm.5 4.5a.5.5 0 0 0 0 1h6a.5.5 0 0 0 0-1h-6ZM15 16.293V11.5a.5.5 0 0 1 1 0v4.793l1.146-1.147a.5.5 0 0 1 .708.708l-2 2a.5.5 0 0 1-.708 0l-2-2a.5.5 0 0 1 .708-.708L15 16.293Z",
|
||||
"pane-open-outline": "M14.807 9.249a.75.75 0 0 0-1.059-.056l-2.5 2.25a.75.75 0 0 0 0 1.114l2.5 2.25a.75.75 0 0 0 1.004-1.115l-1.048-.942h3.546a.75.75 0 1 0 0-1.5h-3.546l1.048-.942a.75.75 0 0 0 .055-1.059ZM2 17.251A2.75 2.75 0 0 0 4.75 20h14.5A2.75 2.75 0 0 0 22 17.25V6.75A2.75 2.75 0 0 0 19.25 4H4.75A2.75 2.75 0 0 0 2 6.75v10.5Zm2.75 1.25c-.69 0-1.25-.56-1.25-1.25V6.749c0-.69.56-1.25 1.25-1.25h3.254V18.5H4.75Zm4.754 0V5.5h9.746c.69 0 1.25.56 1.25 1.25v10.5c0 .69-.56 1.25-1.25 1.25H9.504Z",
|
||||
"table-switch-outline": [
|
||||
"M8.78 1.22a.75.75 0 0 0-1.06 1.06l.72.72H6.25A3.25 3.25 0 0 0 3 6.25v2.19l-.72-.72a.75.75 0 0 0-1.06 1.06l2 2a.748.748 0 0 0 1.06 0l2-2a.747.747 0 0 0 0-1.06.75.75 0 0 0-1.06 0l-.72.72V6.25c0-.966.784-1.75 1.75-1.75h2.19l-.72.72a.75.75 0 0 0 .78 1.237.75.75 0 0 0 .28-.177l2-2a.75.75 0 0 0 0-1.06l-2-2Z",
|
||||
"M11.832 3a1.755 1.755 0 0 1 0 1.5H14v4h-4V6.475l-.513.512a1.742 1.742 0 0 1-.987.495V8.5H7.482c-.052.361-.217.71-.495.987L6.475 10H8.5v4h-4v-2.168a1.755 1.755 0 0 1-1.5 0v5.918A3.25 3.25 0 0 0 6.25 21h11.5A3.25 3.25 0 0 0 21 17.75V6.25A3.25 3.25 0 0 0 17.75 3h-5.918ZM10 14v-4h4v4h-4Zm5.5 0v-4h4v4h-4ZM14 15.5v4h-4v-4h4Zm1.5 4v-4h4v2.25a1.75 1.75 0 0 1-1.75 1.75H15.5Zm0-11v-4h2.25c.966 0 1.75.784 1.75 1.75V8.5h-4Zm-11 7h4v4H6.25a1.75 1.75 0 0 1-1.75-1.75V15.5Z"
|
||||
],
|
||||
"pane-close-outline": "M9.193 9.249a.75.75 0 0 1 1.059-.056l2.5 2.25a.75.75 0 0 1 0 1.114l-2.5 2.25a.75.75 0 0 1-1.004-1.115l1.048-.942H6.75a.75.75 0 1 1 0-1.5h3.546l-1.048-.942a.75.75 0 0 1-.055-1.06ZM22 17.25A2.75 2.75 0 0 1 19.25 20H4.75A2.75 2.75 0 0 1 2 17.25V6.75A2.75 2.75 0 0 1 4.75 4h14.5A2.75 2.75 0 0 1 22 6.75v10.5Zm-2.75 1.25c.69 0 1.25-.56 1.25-1.25V6.749c0-.69-.56-1.25-1.25-1.25h-3.254V18.5h3.254Zm-4.754 0V5.5H4.75c-.69 0-1.25.56-1.25 1.25v10.5c0 .69.56 1.25 1.25 1.25h9.746Z",
|
||||
"chevron-left-solid": "M15.707 4.293a1 1 0 0 1 0 1.414L9.414 12l6.293 6.293a1 1 0 0 1-1.414 1.414l-7-7a1 1 0 0 1 0-1.414l7-7a1 1 0 0 1 1.414 0Z",
|
||||
"chevron-right-solid": "M8.293 4.293a1 1 0 0 0 0 1.414L14.586 12l-6.293 6.293a1 1 0 1 0 1.414 1.414l7-7a1 1 0 0 0 0-1.414l-7-7a1 1 0 0 0-1.414 0Z"
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import MessageFormatter from '../helpers/MessageFormatter';
|
||||
import DOMPurify from 'dompurify';
|
||||
|
||||
export default {
|
||||
methods: {
|
||||
|
@ -22,25 +21,5 @@ export default {
|
|||
|
||||
return `${description.slice(0, 97)}...`;
|
||||
},
|
||||
stripStyleCharacters(message) {
|
||||
return DOMPurify.sanitize(message, {
|
||||
FORBID_TAGS: ['style'],
|
||||
FORBID_ATTR: [
|
||||
'id',
|
||||
'class',
|
||||
'style',
|
||||
'bgcolor',
|
||||
'valign',
|
||||
'width',
|
||||
'face',
|
||||
'color',
|
||||
'height',
|
||||
'lang',
|
||||
'align',
|
||||
'size',
|
||||
'border',
|
||||
],
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -14,17 +14,4 @@ describe('messageFormatterMixin', () => {
|
|||
'Chatwoot is an opensource tool. https://www.chatwoot.com'
|
||||
);
|
||||
});
|
||||
|
||||
it('stripStyleCharacters returns message without style tags', () => {
|
||||
const Component = {
|
||||
render() {},
|
||||
mixins: [messageFormatterMixin],
|
||||
};
|
||||
const wrapper = shallowMount(Component);
|
||||
const message =
|
||||
'<b style="max-width:100%">Chatwoot is an opensource tool. https://www.chatwoot.com</b><style type="css">.message{}</style>';
|
||||
expect(wrapper.vm.stripStyleCharacters(message)).toMatch(
|
||||
'<b>Chatwoot is an opensource tool. https://www.chatwoot.com</b>'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -67,6 +67,7 @@
|
|||
"vue-dompurify-html": "^2.5.2",
|
||||
"vue-easytable": "2.5.5",
|
||||
"vue-i18n": "8.24.3",
|
||||
"vue-letter": "^0.1.3",
|
||||
"vue-loader": "15.9.6",
|
||||
"vue-multiselect": "~2.1.6",
|
||||
"vue-router": "~3.5.2",
|
||||
|
|
12
yarn.lock
12
yarn.lock
|
@ -9941,6 +9941,11 @@ lazy-universal-dotenv@^3.0.1:
|
|||
dotenv "^8.0.0"
|
||||
dotenv-expand "^5.1.0"
|
||||
|
||||
lettersanitizer@^1.0.0:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/lettersanitizer/-/lettersanitizer-1.0.2.tgz#902b7b4deea0e52f0400e0d268921f489f5ce100"
|
||||
integrity sha512-f035TAen0M3Oh5Nhe8hp2uA5zQwN1rIDdSfC1VNLXd1XoeIm1r/whVCB1YPQH/uN1KhRrjCJcHl0moCpXj0X8A==
|
||||
|
||||
leven@^3.1.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2"
|
||||
|
@ -15743,6 +15748,13 @@ vue-jest@4:
|
|||
extract-from-css "^0.4.4"
|
||||
source-map "0.5.6"
|
||||
|
||||
vue-letter@^0.1.3:
|
||||
version "0.1.3"
|
||||
resolved "https://registry.yarnpkg.com/vue-letter/-/vue-letter-0.1.3.tgz#29cfb1768e80382bdebbaeada3d012fd37c47f52"
|
||||
integrity sha512-tobFN1Ri3QJuPJGG+/6ITZuW9DnJ0trIF8AZqhexVKS49lMI560J/DYryTWrITjb7xYicJfDEMXtAFDxxcG3qQ==
|
||||
dependencies:
|
||||
lettersanitizer "^1.0.0"
|
||||
|
||||
vue-loader@15.9.6:
|
||||
version "15.9.6"
|
||||
resolved "https://registry.yarnpkg.com/vue-loader/-/vue-loader-15.9.6.tgz#f4bb9ae20c3a8370af3ecf09b8126d38ffdb6b8b"
|
||||
|
|
Loading…
Reference in a new issue