Merge branch 'develop' into feat/widget-v2
This commit is contained in:
@@ -24,7 +24,8 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
buttonClassName() {
|
||||
let className = 'text-white py-3 px-4 rounded shadow-sm leading-4';
|
||||
let className =
|
||||
'text-white py-3 px-4 rounded shadow-sm leading-4 cursor-pointer disabled:opacity-50';
|
||||
if (this.type === 'clear') {
|
||||
className = 'flex mx-auto mt-4 text-xs leading-3 w-auto text-black-600';
|
||||
}
|
||||
|
||||
@@ -87,7 +87,7 @@ export default {
|
||||
<template>
|
||||
<div
|
||||
role="dialog"
|
||||
class="emoji-dialog bg-white shadow-lg dark:bg-slate-900 rounded-md border border-solid border-slate-75 dark:border-slate-800/50 box-content h-[300px] absolute right-0 -top-[95px] w-80 z-20"
|
||||
class="emoji-dialog bg-white shadow-lg dark:bg-slate-900 rounded-md border border-solid border-slate-75 dark:border-slate-800/50 box-content h-[18.75rem] absolute right-0 -top-[95px] w-80 z-20"
|
||||
>
|
||||
<div class="flex flex-col">
|
||||
<div class="flex gap-2 emoji-search--wrap">
|
||||
@@ -230,7 +230,7 @@ export default {
|
||||
@apply box-border p-1;
|
||||
|
||||
.emoji--item {
|
||||
@apply h-[26px] w-[26px] leading-normal m-1;
|
||||
@apply h-[1.625rem] w-[1.625rem] leading-normal m-1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -243,7 +243,7 @@ export default {
|
||||
}
|
||||
|
||||
.empty-message {
|
||||
@apply items-center flex flex-col h-[212px] justify-center;
|
||||
@apply items-center flex flex-col h-[13.25rem] justify-center;
|
||||
|
||||
.emoji-icon {
|
||||
@apply text-slate-200 dark:text-slate-200 mb-2;
|
||||
@@ -255,7 +255,7 @@ export default {
|
||||
}
|
||||
|
||||
.emoji-item {
|
||||
@apply h-[212px] overflow-y-auto;
|
||||
@apply h-[13.25rem] overflow-y-auto;
|
||||
}
|
||||
|
||||
.emoji-category--title {
|
||||
@@ -263,7 +263,7 @@ export default {
|
||||
}
|
||||
|
||||
.emoji-dialog--footer {
|
||||
@apply relative w-[322px] -left-px rtl:left-[unset] rtl:-right-px bottom-0 py-0 rounded-b-md border-b border-solid border-slate-75 dark:border-slate-800/50 px-1 bg-slate-75 dark:bg-slate-800;
|
||||
@apply relative w-full py-0 rounded-b-[0.34rem] px-1 bg-slate-75 dark:bg-slate-800;
|
||||
|
||||
ul {
|
||||
@apply flex relative left-[2px] rtl:left-[unset] rtl:right-[2px] list-none m-0 overflow-auto py-1 px-0;
|
||||
|
||||
@@ -60,7 +60,7 @@ const hasValue = computed(() => {
|
||||
<woot-button
|
||||
variant="hollow"
|
||||
color-scheme="secondary"
|
||||
class="w-full border border-solid border-slate-100 dark:border-slate-700 px-2 hover:border-slate-75 dark:hover:border-slate-600"
|
||||
class="w-full px-2 border border-solid !border-n-weak dark:!border-n-weak hover:!border-n-strong dark:hover:!border-n-strong"
|
||||
@click="
|
||||
() => toggleDropdown() // ensure that the event is not passed to the button
|
||||
"
|
||||
@@ -73,25 +73,22 @@ const hasValue = computed(() => {
|
||||
:status="selectedItem.availability_status"
|
||||
:username="selectedItem.name"
|
||||
/>
|
||||
<div class="flex justify-between w-full min-w-0 items-center">
|
||||
<h4
|
||||
v-if="!hasValue"
|
||||
class="text-ellipsis text-sm text-slate-800 dark:text-slate-100"
|
||||
>
|
||||
<div class="flex items-center justify-between w-full min-w-0">
|
||||
<h4 v-if="!hasValue" class="text-sm text-ellipsis text-n-slate-12">
|
||||
{{ multiselectorPlaceholder }}
|
||||
</h4>
|
||||
<h4
|
||||
v-else
|
||||
class="items-center leading-tight overflow-hidden whitespace-nowrap text-ellipsis text-sm text-slate-800 dark:text-slate-100"
|
||||
class="items-center overflow-hidden text-sm leading-tight whitespace-nowrap text-ellipsis text-n-slate-12"
|
||||
:title="selectedItem.name"
|
||||
>
|
||||
{{ selectedItem.name }}
|
||||
</h4>
|
||||
<i
|
||||
v-if="showSearchDropdown"
|
||||
class="icon i-lucide-chevron-up text-slate-600 mr-1"
|
||||
class="mr-1 icon i-lucide-chevron-up text-n-slate-10"
|
||||
/>
|
||||
<i v-else class="icon i-lucide-chevron-down text-slate-600 mr-1" />
|
||||
<i v-else class="mr-1 icon i-lucide-chevron-down text-n-slate-10" />
|
||||
</div>
|
||||
</div>
|
||||
</woot-button>
|
||||
@@ -99,9 +96,9 @@ const hasValue = computed(() => {
|
||||
:class="{ 'dropdown-pane--open': showSearchDropdown }"
|
||||
class="dropdown-pane"
|
||||
>
|
||||
<div class="flex justify-between items-center mb-1">
|
||||
<div class="flex items-center justify-between mb-1">
|
||||
<h4
|
||||
class="text-sm text-slate-800 dark:text-slate-100 m-0 overflow-hidden whitespace-nowrap text-ellipsis"
|
||||
class="m-0 overflow-hidden text-sm text-n-slate-11 whitespace-nowrap text-ellipsis"
|
||||
>
|
||||
{{ multiselectorTitle }}
|
||||
</h4>
|
||||
|
||||
@@ -71,7 +71,7 @@ export default {
|
||||
|
||||
<template>
|
||||
<div class="dropdown-wrap">
|
||||
<div class="mb-2 flex-shrink-0 flex-grow-0 flex-auto max-h-8">
|
||||
<div class="flex-auto flex-grow-0 flex-shrink-0 mb-2 max-h-8">
|
||||
<input
|
||||
ref="searchbar"
|
||||
v-model="search"
|
||||
@@ -81,7 +81,7 @@ export default {
|
||||
:placeholder="inputPlaceholder"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex justify-start items-start flex-auto overflow-auto">
|
||||
<div class="flex items-start justify-start flex-auto overflow-auto mt-2">
|
||||
<div class="w-full max-h-[10rem]">
|
||||
<WootDropdownMenu>
|
||||
<WootDropdownItem v-for="option in filteredOptions" :key="option.id">
|
||||
@@ -107,7 +107,7 @@ export default {
|
||||
class="flex items-center justify-between w-full min-w-0 gap-2"
|
||||
>
|
||||
<span
|
||||
class="leading-4 my-0 overflow-hidden whitespace-nowrap text-ellipsis text-sm"
|
||||
class="my-0 overflow-hidden text-sm leading-4 whitespace-nowrap text-ellipsis"
|
||||
:title="option.name"
|
||||
>
|
||||
{{ option.name }}
|
||||
@@ -120,7 +120,7 @@ export default {
|
||||
</WootDropdownMenu>
|
||||
<h4
|
||||
v-if="noResult"
|
||||
class="w-full justify-center items-center flex text-slate-500 dark:text-slate-300 py-2 px-2.5 overflow-hidden whitespace-nowrap text-ellipsis text-sm"
|
||||
class="w-full justify-center items-center flex text-n-slate-10 py-2 px-2.5 overflow-hidden whitespace-nowrap text-ellipsis text-sm"
|
||||
>
|
||||
{{ noSearchResult }}
|
||||
</h4>
|
||||
@@ -142,15 +142,11 @@ export default {
|
||||
@apply justify-between w-full;
|
||||
|
||||
&.active {
|
||||
@apply bg-slate-25 dark:bg-slate-700 border-slate-50 dark:border-slate-900 font-medium;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
@apply bg-slate-25 dark:bg-slate-700;
|
||||
@apply bg-n-slate-2 dark:bg-n-solid-3 border-n-weak/50 dark:border-n-weak font-medium;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
@apply bg-slate-50 dark:bg-slate-800 text-slate-800 dark:text-slate-100;
|
||||
@apply bg-n-slate-2 dark:bg-n-solid-3 text-slate-800 dark:text-slate-100;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -30,6 +30,18 @@ describe('useMessageFormatter', () => {
|
||||
'<a href="https://twitter.com/hashtag/hashtag"'
|
||||
);
|
||||
});
|
||||
|
||||
it('should disable link formatting when linkify is false', () => {
|
||||
const message = 'Check https://example.com and {{user.id}}';
|
||||
const result = messageFormatter.formatMessage(
|
||||
message,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
);
|
||||
expect(result).not.toContain('<a href="https://example.com"');
|
||||
expect(result).toContain('{{user.id}}');
|
||||
});
|
||||
});
|
||||
|
||||
describe('truncateMessage', () => {
|
||||
|
||||
@@ -14,11 +14,13 @@ export const useMessageFormatter = () => {
|
||||
* @param {boolean} isAPrivateNote - Whether the message is a private note.
|
||||
* @returns {string} - The formatted message.
|
||||
*/
|
||||
const formatMessage = (message, isATweet, isAPrivateNote) => {
|
||||
// TODO: ref:https://github.com/chatwoot/chatwoot/pull/10725#discussion_r1925300874
|
||||
const formatMessage = (message, isATweet, isAPrivateNote, linkify) => {
|
||||
const messageFormatter = new MessageFormatter(
|
||||
message,
|
||||
isATweet,
|
||||
isAPrivateNote
|
||||
isAPrivateNote,
|
||||
linkify
|
||||
);
|
||||
return messageFormatter.formattedMessage;
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const DYTE_MEETING_LINK = 'https://app.dyte.in/meeting/stage/';
|
||||
const DYTE_MEETING_LINK = 'https://app.dyte.io/v2/meeting';
|
||||
|
||||
export const buildDyteURL = (roomName, dyteAuthToken) => {
|
||||
return `${DYTE_MEETING_LINK}${roomName}?authToken=${dyteAuthToken}&showSetupScreen=true&disableVideoBackground=true`;
|
||||
export const buildDyteURL = dyteAuthToken => {
|
||||
return `${DYTE_MEETING_LINK}?authToken=${dyteAuthToken}&showSetupScreen=true&disableVideoBackground=true`;
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import mila from 'markdown-it-link-attributes';
|
||||
import mentionPlugin from './markdownIt/link';
|
||||
import MarkdownIt from 'markdown-it';
|
||||
|
||||
const setImageHeight = inlineToken => {
|
||||
const imgSrc = inlineToken.attrGet('src');
|
||||
if (!imgSrc) return;
|
||||
@@ -30,25 +31,27 @@ const imgResizeManager = md => {
|
||||
});
|
||||
};
|
||||
|
||||
const md = MarkdownIt({
|
||||
html: false,
|
||||
xhtmlOut: true,
|
||||
breaks: true,
|
||||
langPrefix: 'language-',
|
||||
linkify: true,
|
||||
typographer: true,
|
||||
quotes: '\u201c\u201d\u2018\u2019',
|
||||
maxNesting: 20,
|
||||
})
|
||||
.use(mentionPlugin)
|
||||
.use(imgResizeManager)
|
||||
.use(mila, {
|
||||
attrs: {
|
||||
class: 'link',
|
||||
rel: 'noreferrer noopener nofollow',
|
||||
target: '_blank',
|
||||
},
|
||||
});
|
||||
const createMarkdownInstance = (linkify = true) => {
|
||||
return MarkdownIt({
|
||||
html: false,
|
||||
xhtmlOut: true,
|
||||
breaks: true,
|
||||
langPrefix: 'language-',
|
||||
linkify,
|
||||
typographer: true,
|
||||
quotes: '\u201c\u201d\u2018\u2019',
|
||||
maxNesting: 20,
|
||||
})
|
||||
.use(mentionPlugin)
|
||||
.use(imgResizeManager)
|
||||
.use(mila, {
|
||||
attrs: {
|
||||
class: 'link',
|
||||
rel: 'noreferrer noopener nofollow',
|
||||
target: '_blank',
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const TWITTER_USERNAME_REGEX = /(^|[^@\w])@(\w{1,15})\b/g;
|
||||
const TWITTER_USERNAME_REPLACEMENT = '$1[@$2](http://twitter.com/$2)';
|
||||
@@ -56,10 +59,17 @@ const TWITTER_HASH_REGEX = /(^|\s)#(\w+)/g;
|
||||
const TWITTER_HASH_REPLACEMENT = '$1[#$2](https://twitter.com/hashtag/$2)';
|
||||
|
||||
class MessageFormatter {
|
||||
constructor(message, isATweet = false, isAPrivateNote = false) {
|
||||
constructor(
|
||||
message,
|
||||
isATweet = false,
|
||||
isAPrivateNote = false,
|
||||
linkify = true
|
||||
) {
|
||||
this.message = message || '';
|
||||
this.isAPrivateNote = isAPrivateNote;
|
||||
this.isATweet = isATweet;
|
||||
this.linkify = linkify;
|
||||
this.md = createMarkdownInstance(linkify);
|
||||
}
|
||||
|
||||
formatMessage() {
|
||||
@@ -74,7 +84,7 @@ class MessageFormatter {
|
||||
TWITTER_HASH_REPLACEMENT
|
||||
);
|
||||
}
|
||||
return md.render(updatedMessage);
|
||||
return this.md.render(updatedMessage);
|
||||
}
|
||||
|
||||
get formattedMessage() {
|
||||
|
||||
@@ -16,6 +16,13 @@ describe('#MessageFormatter', () => {
|
||||
'<p>Chatwoot is an opensource tool. <a href="https://www.chatwoot.com" class="link" rel="noreferrer noopener nofollow" target="_blank">https://www.chatwoot.com</a></p>'
|
||||
);
|
||||
});
|
||||
it('should not convert template variables to links when linkify is disabled', () => {
|
||||
const message = 'Hey {{customer.name}}, check https://chatwoot.com';
|
||||
const formatter = new MessageFormatter(message, false, false, false);
|
||||
expect(formatter.formattedMessage).toMatch(
|
||||
'<p>Hey {{customer.name}}, check https://chatwoot.com</p>'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('parses heading to strong', () => {
|
||||
|
||||
@@ -24,7 +24,7 @@ export default {
|
||||
methods: {
|
||||
addEventHandler(keydownHandler) {
|
||||
const indexToAppend = taggedHandlers.push(keydownHandler) - 1;
|
||||
const root = this.$el;
|
||||
const root = this.getElementToBind();
|
||||
if (root && root.dataset) {
|
||||
// For the components with a top level v-if Vue renders it as an empty comment in the DOM
|
||||
// so we need to check if the root element has a dataset property to ensure it is a valid element
|
||||
@@ -32,6 +32,9 @@ export default {
|
||||
root.dataset.keydownHandlerIndex = indexToAppend;
|
||||
}
|
||||
},
|
||||
getElementToBind() {
|
||||
return this.$el;
|
||||
},
|
||||
getKeyboardEvents() {
|
||||
return null;
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user