fix: Handle rich message signatures & attachment overflow (#13045)
This commit is contained in:
@@ -7,7 +7,7 @@ import { vOnClickOutside } from '@vueuse/components';
|
|||||||
import { ALLOWED_FILE_TYPES } from 'shared/constants/messages';
|
import { ALLOWED_FILE_TYPES } from 'shared/constants/messages';
|
||||||
import { useKeyboardEvents } from 'dashboard/composables/useKeyboardEvents';
|
import { useKeyboardEvents } from 'dashboard/composables/useKeyboardEvents';
|
||||||
import FileUpload from 'vue-upload-component';
|
import FileUpload from 'vue-upload-component';
|
||||||
import { extractTextFromMarkdown } from 'dashboard/helper/editorHelper';
|
import { INBOX_TYPES } from 'dashboard/helper/inbox';
|
||||||
|
|
||||||
import Button from 'dashboard/components-next/button/Button.vue';
|
import Button from 'dashboard/components-next/button/Button.vue';
|
||||||
import WhatsAppOptions from './WhatsAppOptions.vue';
|
import WhatsAppOptions from './WhatsAppOptions.vue';
|
||||||
@@ -50,12 +50,6 @@ const EmojiInput = defineAsyncComponent(
|
|||||||
() => import('shared/components/emoji/EmojiInput.vue')
|
() => import('shared/components/emoji/EmojiInput.vue')
|
||||||
);
|
);
|
||||||
|
|
||||||
const signatureToApply = computed(() =>
|
|
||||||
props.isEmailOrWebWidgetInbox
|
|
||||||
? props.messageSignature
|
|
||||||
: extractTextFromMarkdown(props.messageSignature)
|
|
||||||
);
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
fetchSignatureFlagFromUISettings,
|
fetchSignatureFlagFromUISettings,
|
||||||
setSignatureFlagForInbox,
|
setSignatureFlagForInbox,
|
||||||
@@ -80,12 +74,20 @@ const isRegularMessageMode = computed(() => {
|
|||||||
return !props.isWhatsappInbox && !props.isTwilioWhatsAppInbox;
|
return !props.isWhatsappInbox && !props.isTwilioWhatsAppInbox;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const isVoiceInbox = computed(() => props.channelType === INBOX_TYPES.VOICE);
|
||||||
|
|
||||||
|
const shouldShowSignatureButton = computed(() => {
|
||||||
|
return (
|
||||||
|
props.hasSelectedInbox && isRegularMessageMode.value && !isVoiceInbox.value
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
const setSignature = () => {
|
const setSignature = () => {
|
||||||
if (signatureToApply.value) {
|
if (props.messageSignature) {
|
||||||
if (sendWithSignature.value) {
|
if (sendWithSignature.value) {
|
||||||
emit('addSignature', signatureToApply.value);
|
emit('addSignature', props.messageSignature);
|
||||||
} else {
|
} else {
|
||||||
emit('removeSignature', signatureToApply.value);
|
emit('removeSignature', props.messageSignature);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -101,7 +103,7 @@ watch(
|
|||||||
() => props.hasSelectedInbox,
|
() => props.hasSelectedInbox,
|
||||||
newValue => {
|
newValue => {
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
if (newValue && props.isEmailOrWebWidgetInbox) setSignature();
|
if (newValue && !isVoiceInbox.value) setSignature();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
{ immediate: true }
|
{ immediate: true }
|
||||||
@@ -220,7 +222,7 @@ useKeyboardEvents(keyboardEvents);
|
|||||||
/>
|
/>
|
||||||
</FileUpload>
|
</FileUpload>
|
||||||
<Button
|
<Button
|
||||||
v-if="hasSelectedInbox && isRegularMessageMode"
|
v-if="shouldShowSignatureButton"
|
||||||
icon="i-lucide-signature"
|
icon="i-lucide-signature"
|
||||||
color="slate"
|
color="slate"
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ const removeAttachment = id => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col gap-4 p-4">
|
<div class="flex flex-col gap-4 p-4 max-h-48 overflow-y-auto">
|
||||||
<div
|
<div
|
||||||
v-if="filteredImageAttachments.length > 0"
|
v-if="filteredImageAttachments.length > 0"
|
||||||
class="flex flex-wrap gap-3"
|
class="flex flex-wrap gap-3"
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import { INBOX_TYPES } from 'dashboard/helper/inbox';
|
|||||||
import {
|
import {
|
||||||
appendSignature,
|
appendSignature,
|
||||||
removeSignature,
|
removeSignature,
|
||||||
extractTextFromMarkdown,
|
|
||||||
} from 'dashboard/helper/editorHelper';
|
} from 'dashboard/helper/editorHelper';
|
||||||
import {
|
import {
|
||||||
buildContactableInboxesList,
|
buildContactableInboxesList,
|
||||||
@@ -202,11 +201,8 @@ const handleInboxAction = ({ value, action, ...rest }) => {
|
|||||||
const removeSignatureFromMessage = () => {
|
const removeSignatureFromMessage = () => {
|
||||||
// Always remove the signature from message content when inbox/contact is removed
|
// Always remove the signature from message content when inbox/contact is removed
|
||||||
// to ensure no leftover signature content remains
|
// to ensure no leftover signature content remains
|
||||||
const signatureToRemove = inboxTypes.value.isEmailOrWebWidget
|
if (props.messageSignature) {
|
||||||
? props.messageSignature
|
state.message = removeSignature(state.message, props.messageSignature);
|
||||||
: extractTextFromMarkdown(props.messageSignature);
|
|
||||||
if (signatureToRemove) {
|
|
||||||
state.message = removeSignature(state.message, signatureToRemove);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -228,7 +224,11 @@ const onClickInsertEmoji = emoji => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleAddSignature = signature => {
|
const handleAddSignature = signature => {
|
||||||
state.message = appendSignature(state.message, signature);
|
state.message = appendSignature(
|
||||||
|
state.message,
|
||||||
|
signature,
|
||||||
|
inboxChannelType.value
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRemoveSignature = signature => {
|
const handleRemoveSignature = signature => {
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, watch, computed, nextTick } from 'vue';
|
import { ref, watch, nextTick } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import {
|
import {
|
||||||
appendSignature,
|
appendSignature,
|
||||||
extractTextFromMarkdown,
|
|
||||||
removeSignature,
|
removeSignature,
|
||||||
} from 'dashboard/helper/editorHelper';
|
} from 'dashboard/helper/editorHelper';
|
||||||
|
|
||||||
@@ -33,17 +32,13 @@ const state = ref({
|
|||||||
mentionSearchKey: '',
|
mentionSearchKey: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
const plainTextSignature = computed(() =>
|
|
||||||
extractTextFromMarkdown(props.messageSignature)
|
|
||||||
);
|
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
modelValue,
|
modelValue,
|
||||||
newValue => {
|
newValue => {
|
||||||
if (props.isEmailOrWebWidgetInbox) return;
|
if (props.isEmailOrWebWidgetInbox) return;
|
||||||
|
|
||||||
const bodyWithoutSignature = newValue
|
const bodyWithoutSignature = newValue
|
||||||
? removeSignature(newValue, plainTextSignature.value)
|
? removeSignature(newValue, props.messageSignature)
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
// Check if message starts with slash
|
// Check if message starts with slash
|
||||||
@@ -67,7 +62,7 @@ const hideMention = () => {
|
|||||||
const replaceText = async message => {
|
const replaceText = async message => {
|
||||||
// Only append signature on replace if sendWithSignature is true
|
// Only append signature on replace if sendWithSignature is true
|
||||||
const finalMessage = props.sendWithSignature
|
const finalMessage = props.sendWithSignature
|
||||||
? appendSignature(message, plainTextSignature.value)
|
? appendSignature(message, props.messageSignature, props.channelType)
|
||||||
: message;
|
: message;
|
||||||
|
|
||||||
await nextTick();
|
await nextTick();
|
||||||
|
|||||||
@@ -370,7 +370,7 @@ function addSignature() {
|
|||||||
// see if the content is empty, if it is before appending the signature
|
// see if the content is empty, if it is before appending the signature
|
||||||
// we need to add a paragraph node and move the cursor at the start of the editor
|
// we need to add a paragraph node and move the cursor at the start of the editor
|
||||||
const contentWasEmpty = isBodyEmpty(content);
|
const contentWasEmpty = isBodyEmpty(content);
|
||||||
content = appendSignature(content, props.signature);
|
content = appendSignature(content, props.signature, props.channelType);
|
||||||
// need to reload first, ensuring that the editorView is updated
|
// need to reload first, ensuring that the editorView is updated
|
||||||
reloadState(content);
|
reloadState(content);
|
||||||
|
|
||||||
|
|||||||
@@ -565,7 +565,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return this.sendWithSignature
|
return this.sendWithSignature
|
||||||
? appendSignature(message, this.messageSignature)
|
? appendSignature(message, this.messageSignature, this.channelType)
|
||||||
: removeSignature(message, this.messageSignature);
|
: removeSignature(message, this.messageSignature);
|
||||||
},
|
},
|
||||||
removeFromDraft() {
|
removeFromDraft() {
|
||||||
@@ -757,7 +757,11 @@ export default {
|
|||||||
// if signature is enabled, append it to the message
|
// if signature is enabled, append it to the message
|
||||||
// appendSignature ensures that the signature is not duplicated
|
// appendSignature ensures that the signature is not duplicated
|
||||||
// so we don't need to check if the signature is already present
|
// so we don't need to check if the signature is already present
|
||||||
message = appendSignature(message, this.messageSignature);
|
message = appendSignature(
|
||||||
|
message,
|
||||||
|
this.messageSignature,
|
||||||
|
this.channelType
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const updatedMessage = replaceVariablesInMessage({
|
const updatedMessage = replaceVariablesInMessage({
|
||||||
@@ -796,7 +800,11 @@ export default {
|
|||||||
this.message = '';
|
this.message = '';
|
||||||
if (this.sendWithSignature && !this.isPrivate) {
|
if (this.sendWithSignature && !this.isPrivate) {
|
||||||
// if signature is enabled, append it to the message
|
// if signature is enabled, append it to the message
|
||||||
this.message = appendSignature(this.message, this.messageSignature);
|
this.message = appendSignature(
|
||||||
|
this.message,
|
||||||
|
this.messageSignature,
|
||||||
|
this.channelType
|
||||||
|
);
|
||||||
}
|
}
|
||||||
this.attachedFiles = [];
|
this.attachedFiles = [];
|
||||||
this.isRecordingAudio = false;
|
this.isRecordingAudio = false;
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ export const FORMATTING = {
|
|||||||
// Channel formatting
|
// Channel formatting
|
||||||
'Channel::Email': {
|
'Channel::Email': {
|
||||||
marks: ['strong', 'em', 'code', 'link'],
|
marks: ['strong', 'em', 'code', 'link'],
|
||||||
nodes: ['bulletList', 'orderedList', 'codeBlock', 'blockquote'],
|
nodes: ['bulletList', 'orderedList', 'codeBlock', 'blockquote', 'image'],
|
||||||
menu: [
|
menu: [
|
||||||
'strong',
|
'strong',
|
||||||
'em',
|
'em',
|
||||||
@@ -19,7 +19,7 @@ export const FORMATTING = {
|
|||||||
},
|
},
|
||||||
'Channel::WebWidget': {
|
'Channel::WebWidget': {
|
||||||
marks: ['strong', 'em', 'code', 'link', 'strike'],
|
marks: ['strong', 'em', 'code', 'link', 'strike'],
|
||||||
nodes: ['bulletList', 'orderedList', 'codeBlock', 'blockquote'],
|
nodes: ['bulletList', 'orderedList', 'codeBlock', 'blockquote', 'image'],
|
||||||
menu: [
|
menu: [
|
||||||
'strong',
|
'strong',
|
||||||
'em',
|
'em',
|
||||||
@@ -127,7 +127,7 @@ export const FORMATTING = {
|
|||||||
},
|
},
|
||||||
'Context::MessageSignature': {
|
'Context::MessageSignature': {
|
||||||
marks: ['strong', 'em', 'link'],
|
marks: ['strong', 'em', 'link'],
|
||||||
nodes: [],
|
nodes: ['image'],
|
||||||
menu: ['strong', 'em', 'link', 'undo', 'redo', 'imageUpload'],
|
menu: ['strong', 'em', 'link', 'undo', 'redo', 'imageUpload'],
|
||||||
},
|
},
|
||||||
'Context::InboxSettings': {
|
'Context::InboxSettings': {
|
||||||
@@ -227,6 +227,11 @@ export const MARKDOWN_PATTERNS = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const CHANNEL_WITH_RICH_SIGNATURE = [
|
||||||
|
'Channel::Email',
|
||||||
|
'Channel::WebWidget',
|
||||||
|
];
|
||||||
|
|
||||||
// Editor image resize options for Message Editor
|
// Editor image resize options for Message Editor
|
||||||
export const MESSAGE_EDITOR_IMAGE_RESIZES = [
|
export const MESSAGE_EDITOR_IMAGE_RESIZES = [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -5,9 +5,36 @@ import {
|
|||||||
} from '@chatwoot/prosemirror-schema';
|
} from '@chatwoot/prosemirror-schema';
|
||||||
import { replaceVariablesInMessage } from '@chatwoot/utils';
|
import { replaceVariablesInMessage } from '@chatwoot/utils';
|
||||||
import * as Sentry from '@sentry/vue';
|
import * as Sentry from '@sentry/vue';
|
||||||
import { FORMATTING, MARKDOWN_PATTERNS } from 'dashboard/constants/editor';
|
import {
|
||||||
|
FORMATTING,
|
||||||
|
MARKDOWN_PATTERNS,
|
||||||
|
CHANNEL_WITH_RICH_SIGNATURE,
|
||||||
|
} from 'dashboard/constants/editor';
|
||||||
import camelcaseKeys from 'camelcase-keys';
|
import camelcaseKeys from 'camelcase-keys';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract text from markdown, and remove all images, code blocks, links, headers, bold, italic, lists etc.
|
||||||
|
* Links will be converted to text, and not removed.
|
||||||
|
*
|
||||||
|
* @param {string} markdown - markdown text to be extracted
|
||||||
|
* @returns {string} - The extracted text.
|
||||||
|
*/
|
||||||
|
export function extractTextFromMarkdown(markdown) {
|
||||||
|
if (!markdown) return '';
|
||||||
|
return markdown
|
||||||
|
.replace(/```[\s\S]*?```/g, '') // Remove code blocks
|
||||||
|
.replace(/`.*?`/g, '') // Remove inline code
|
||||||
|
.replace(/!\[.*?\]\(.*?\)/g, '') // Remove images before removing links
|
||||||
|
.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1') // Remove links but keep the text
|
||||||
|
.replace(/#+\s*|[*_-]{1,3}/g, '') // Remove headers, bold, italic, lists etc.
|
||||||
|
.split('\n')
|
||||||
|
.map(line => line.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
.join('\n') // Trim each line & remove any lines only having spaces
|
||||||
|
.replace(/\n{2,}/g, '\n') // Remove multiple consecutive newlines (blank lines)
|
||||||
|
.trim(); // Trim any extra space
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The delimiter used to separate the signature from the rest of the body.
|
* The delimiter used to separate the signature from the rest of the body.
|
||||||
* @type {string}
|
* @type {string}
|
||||||
@@ -69,15 +96,32 @@ export function findSignatureInBody(body, signature) {
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the channel supports image signatures.
|
||||||
|
*
|
||||||
|
* @param {string} channelType - The channel type.
|
||||||
|
* @returns {boolean} - True if the channel supports image signatures.
|
||||||
|
*/
|
||||||
|
export function supportsImageSignature(channelType) {
|
||||||
|
return CHANNEL_WITH_RICH_SIGNATURE.includes(channelType);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Appends the signature to the body, separated by the signature delimiter.
|
* Appends the signature to the body, separated by the signature delimiter.
|
||||||
|
* Automatically strips images for channels that don't support image signatures.
|
||||||
*
|
*
|
||||||
* @param {string} body - The body to append the signature to.
|
* @param {string} body - The body to append the signature to.
|
||||||
* @param {string} signature - The signature to append.
|
* @param {string} signature - The signature to append.
|
||||||
|
* @param {string} channelType - Optional. The channel type to determine if images should be stripped.
|
||||||
* @returns {string} - The body with the signature appended.
|
* @returns {string} - The body with the signature appended.
|
||||||
*/
|
*/
|
||||||
export function appendSignature(body, signature) {
|
export function appendSignature(body, signature, channelType) {
|
||||||
const cleanedSignature = cleanSignature(signature);
|
// For channels that don't support images, strip markdown formatting
|
||||||
|
const shouldStripImages = channelType && !supportsImageSignature(channelType);
|
||||||
|
const preparedSignature = shouldStripImages
|
||||||
|
? extractTextFromMarkdown(signature)
|
||||||
|
: signature;
|
||||||
|
const cleanedSignature = cleanSignature(preparedSignature);
|
||||||
// if signature is already present, return body
|
// if signature is already present, return body
|
||||||
if (findSignatureInBody(body, cleanedSignature) > -1) {
|
if (findSignatureInBody(body, cleanedSignature) > -1) {
|
||||||
return body;
|
return body;
|
||||||
@@ -88,16 +132,27 @@ export function appendSignature(body, signature) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes the signature from the body, along with the signature delimiter.
|
* Removes the signature from the body, along with the signature delimiter.
|
||||||
|
* Tries to find both the original signature and the stripped version (for non-image channels).
|
||||||
*
|
*
|
||||||
* @param {string} body - The body to remove the signature from.
|
* @param {string} body - The body to remove the signature from.
|
||||||
* @param {string} signature - The signature to remove.
|
* @param {string} signature - The signature to remove.
|
||||||
* @returns {string} - The body with the signature removed.
|
* @returns {string} - The body with the signature removed.
|
||||||
*/
|
*/
|
||||||
export function removeSignature(body, signature) {
|
export function removeSignature(body, signature) {
|
||||||
// this will find the index of the signature if it exists
|
// Build list of signatures to try: original first, then stripped version
|
||||||
// Regardless of extra spaces or new lines after the signature, the index will be the same if present
|
// Always try both to handle cases where channelType is unknown or inbox is being removed
|
||||||
const cleanedSignature = cleanSignature(signature);
|
const cleanedSignature = cleanSignature(signature);
|
||||||
const signatureIndex = findSignatureInBody(body, cleanedSignature);
|
const strippedSignature = cleanSignature(extractTextFromMarkdown(signature));
|
||||||
|
const signaturesToTry =
|
||||||
|
cleanedSignature === strippedSignature
|
||||||
|
? [cleanedSignature]
|
||||||
|
: [cleanedSignature, strippedSignature];
|
||||||
|
|
||||||
|
// Find the first matching signature
|
||||||
|
const signatureIndex = signaturesToTry.reduce(
|
||||||
|
(index, sig) => (index === -1 ? findSignatureInBody(body, sig) : index),
|
||||||
|
-1
|
||||||
|
);
|
||||||
|
|
||||||
// no need to trim the ends here, because it will simply be removed in the next method
|
// no need to trim the ends here, because it will simply be removed in the next method
|
||||||
let newBody = body;
|
let newBody = body;
|
||||||
@@ -138,28 +193,6 @@ export function replaceSignature(body, oldSignature, newSignature) {
|
|||||||
return appendSignature(withoutSignature, newSignature);
|
return appendSignature(withoutSignature, newSignature);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Extract text from markdown, and remove all images, code blocks, links, headers, bold, italic, lists etc.
|
|
||||||
* Links will be converted to text, and not removed.
|
|
||||||
*
|
|
||||||
* @param {string} markdown - markdown text to be extracted
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export function extractTextFromMarkdown(markdown) {
|
|
||||||
return markdown
|
|
||||||
.replace(/```[\s\S]*?```/g, '') // Remove code blocks
|
|
||||||
.replace(/`.*?`/g, '') // Remove inline code
|
|
||||||
.replace(/!\[.*?\]\(.*?\)/g, '') // Remove images before removing links
|
|
||||||
.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1') // Remove links but keep the text
|
|
||||||
.replace(/#+\s*|[*_-]{1,3}/g, '') // Remove headers, bold, italic, lists etc.
|
|
||||||
.split('\n')
|
|
||||||
.map(line => line.trim())
|
|
||||||
.filter(Boolean)
|
|
||||||
.join('\n') // Trim each line & remove any lines only having spaces
|
|
||||||
.replace(/\n{2,}/g, '\n') // Remove multiple consecutive newlines (blank lines)
|
|
||||||
.trim(); // Trim any extra space
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Scrolls the editor view into current cursor position
|
* Scrolls the editor view into current cursor position
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
replaceSignature,
|
replaceSignature,
|
||||||
cleanSignature,
|
cleanSignature,
|
||||||
extractTextFromMarkdown,
|
extractTextFromMarkdown,
|
||||||
|
supportsImageSignature,
|
||||||
insertAtCursor,
|
insertAtCursor,
|
||||||
findNodeToInsertImage,
|
findNodeToInsertImage,
|
||||||
setURLWithQueryAndSize,
|
setURLWithQueryAndSize,
|
||||||
@@ -144,6 +145,47 @@ describe('appendSignature', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('appendSignature with channelType', () => {
|
||||||
|
const signatureWithImage =
|
||||||
|
'Thanks\n';
|
||||||
|
const strippedSignature = 'Thanks';
|
||||||
|
|
||||||
|
it('keeps images for Email channel', () => {
|
||||||
|
const result = appendSignature(
|
||||||
|
'Hello',
|
||||||
|
signatureWithImage,
|
||||||
|
'Channel::Email'
|
||||||
|
);
|
||||||
|
expect(result).toContain(';
|
||||||
|
});
|
||||||
|
it('keeps images for WebWidget channel', () => {
|
||||||
|
const result = appendSignature(
|
||||||
|
'Hello',
|
||||||
|
signatureWithImage,
|
||||||
|
'Channel::WebWidget'
|
||||||
|
);
|
||||||
|
expect(result).toContain(';
|
||||||
|
});
|
||||||
|
it('strips images for Api channel', () => {
|
||||||
|
const result = appendSignature('Hello', signatureWithImage, 'Channel::Api');
|
||||||
|
expect(result).not.toContain(';
|
||||||
|
expect(result).toContain(strippedSignature);
|
||||||
|
});
|
||||||
|
it('strips images for WhatsApp channel', () => {
|
||||||
|
const result = appendSignature(
|
||||||
|
'Hello',
|
||||||
|
signatureWithImage,
|
||||||
|
'Channel::Whatsapp'
|
||||||
|
);
|
||||||
|
expect(result).not.toContain(';
|
||||||
|
expect(result).toContain(strippedSignature);
|
||||||
|
});
|
||||||
|
it('keeps images when channelType is not provided', () => {
|
||||||
|
const result = appendSignature('Hello', signatureWithImage);
|
||||||
|
expect(result).toContain(';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('cleanSignature', () => {
|
describe('cleanSignature', () => {
|
||||||
it('removes any instance of horizontal rule', () => {
|
it('removes any instance of horizontal rule', () => {
|
||||||
const options = [
|
const options = [
|
||||||
@@ -202,6 +244,37 @@ describe('removeSignature', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('removeSignature with stripped signature', () => {
|
||||||
|
const signatureWithImage =
|
||||||
|
'Thanks\n';
|
||||||
|
|
||||||
|
it('removes stripped signature from body', () => {
|
||||||
|
// Simulate a body where signature was added with images stripped
|
||||||
|
const bodyWithStrippedSignature = 'Hello\n\n--\n\nThanks';
|
||||||
|
const result = removeSignature(
|
||||||
|
bodyWithStrippedSignature,
|
||||||
|
signatureWithImage
|
||||||
|
);
|
||||||
|
expect(result).toBe('Hello\n\n');
|
||||||
|
});
|
||||||
|
it('removes original signature from body', () => {
|
||||||
|
// Simulate a body where signature was added with images (using cleanSignature format)
|
||||||
|
const cleanedSig = cleanSignature(signatureWithImage);
|
||||||
|
const bodyWithOriginalSignature = `Hello\n\n--\n\n${cleanedSig}`;
|
||||||
|
const result = removeSignature(
|
||||||
|
bodyWithOriginalSignature,
|
||||||
|
signatureWithImage
|
||||||
|
);
|
||||||
|
expect(result).toBe('Hello\n\n');
|
||||||
|
});
|
||||||
|
it('handles signature without images', () => {
|
||||||
|
const simpleSignature = 'Best regards';
|
||||||
|
const body = 'Hello\n\n--\n\nBest regards';
|
||||||
|
const result = removeSignature(body, simpleSignature);
|
||||||
|
expect(result).toBe('Hello\n\n');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('replaceSignature', () => {
|
describe('replaceSignature', () => {
|
||||||
it('appends the new signature if not present', () => {
|
it('appends the new signature if not present', () => {
|
||||||
Object.keys(DOES_NOT_HAVE_SIGNATURE).forEach(key => {
|
Object.keys(DOES_NOT_HAVE_SIGNATURE).forEach(key => {
|
||||||
@@ -258,6 +331,24 @@ describe('extractTextFromMarkdown', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('supportsImageSignature', () => {
|
||||||
|
it('returns true for Email channel', () => {
|
||||||
|
expect(supportsImageSignature('Channel::Email')).toBe(true);
|
||||||
|
});
|
||||||
|
it('returns true for WebWidget channel', () => {
|
||||||
|
expect(supportsImageSignature('Channel::WebWidget')).toBe(true);
|
||||||
|
});
|
||||||
|
it('returns false for Api channel', () => {
|
||||||
|
expect(supportsImageSignature('Channel::Api')).toBe(false);
|
||||||
|
});
|
||||||
|
it('returns false for WhatsApp channel', () => {
|
||||||
|
expect(supportsImageSignature('Channel::Whatsapp')).toBe(false);
|
||||||
|
});
|
||||||
|
it('returns false for Telegram channel', () => {
|
||||||
|
expect(supportsImageSignature('Channel::Telegram')).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('insertAtCursor', () => {
|
describe('insertAtCursor', () => {
|
||||||
it('should return undefined if editorView is not provided', () => {
|
it('should return undefined if editorView is not provided', () => {
|
||||||
const result = insertAtCursor(undefined, schema.text('Hello'), 0);
|
const result = insertAtCursor(undefined, schema.text('Hello'), 0);
|
||||||
|
|||||||
Reference in New Issue
Block a user