chore: Replace plain editor with advanced editor (#13071)
# Pull Request Template ## Description This PR reverts the plain text editor back to the **advanced editor**, which was previously removed in [https://github.com/chatwoot/chatwoot/pull/13058](https://github.com/chatwoot/chatwoot/pull/13058). All channels now use the **ProseMirror editor**, with formatting applied based on each channel’s configuration. This PR also fixes issues where **new lines were not properly preserved during Markdown serialization**, for both: * `Enter or CMD/Ctrl+enter` (new paragraph) * `Shift+Enter` (`hard_break`) Additionally, it resolves related **[Sentry issue](https://chatwoot-p3.sentry.io/issues/?environment=production&project=4507182691975168&query=is%3Aunresolved%20markdown&referrer=issue-list&statsPeriod=7d)**. With these changes: * Line breaks and spacing are now preserved correctly when saving canned responses. * When editing a canned response, the content retains the exact spacing and formatting as saved in editor. * Canned responses are now correctly converted to plain text where required and displayed consistently in the canned response list. ### https://github.com/chatwoot/prosemirror-schema/pull/38 --- ## Type of change - [x] New feature (non-breaking change which adds functionality) ## How Has This Been Tested? ## Checklist: - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my code - [x] I have commented on my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] Any dependent changes have been merged and published in downstream modules --------- Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
This commit is contained in:
parent
dcc72ee63c
commit
92422f979d
@ -55,6 +55,7 @@ import {
|
|||||||
getSelectionCoords,
|
getSelectionCoords,
|
||||||
calculateMenuPosition,
|
calculateMenuPosition,
|
||||||
getEffectiveChannelType,
|
getEffectiveChannelType,
|
||||||
|
stripUnsupportedFormatting,
|
||||||
} from 'dashboard/helper/editorHelper';
|
} from 'dashboard/helper/editorHelper';
|
||||||
import {
|
import {
|
||||||
hasPressedEnterAndNotCmdOrShift,
|
hasPressedEnterAndNotCmdOrShift,
|
||||||
@ -131,8 +132,10 @@ const editorMenuOptions = computed(() => {
|
|||||||
|
|
||||||
const createState = (content, placeholder, plugins = [], methods = {}) => {
|
const createState = (content, placeholder, plugins = [], methods = {}) => {
|
||||||
const schema = editorSchema.value;
|
const schema = editorSchema.value;
|
||||||
|
// Strip unsupported formatting before parsing to prevent "Token type not supported" errors
|
||||||
|
const sanitizedContent = stripUnsupportedFormatting(content, schema);
|
||||||
return EditorState.create({
|
return EditorState.create({
|
||||||
doc: new MessageMarkdownTransformer(schema).parse(content),
|
doc: new MessageMarkdownTransformer(schema).parse(sanitizedContent),
|
||||||
plugins: buildEditor({
|
plugins: buildEditor({
|
||||||
schema,
|
schema,
|
||||||
placeholder,
|
placeholder,
|
||||||
|
|||||||
@ -7,9 +7,7 @@ import { useTrack } from 'dashboard/composables';
|
|||||||
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
||||||
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
|
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
|
||||||
|
|
||||||
import CannedResponse from './CannedResponse.vue';
|
|
||||||
import ReplyToMessage from './ReplyToMessage.vue';
|
import ReplyToMessage from './ReplyToMessage.vue';
|
||||||
import ResizableTextArea from 'shared/components/ResizableTextArea.vue';
|
|
||||||
import AttachmentPreview from 'dashboard/components/widgets/AttachmentsPreview.vue';
|
import AttachmentPreview from 'dashboard/components/widgets/AttachmentsPreview.vue';
|
||||||
import ReplyTopPanel from 'dashboard/components/widgets/WootWriter/ReplyTopPanel.vue';
|
import ReplyTopPanel from 'dashboard/components/widgets/WootWriter/ReplyTopPanel.vue';
|
||||||
import ReplyEmailHead from './ReplyEmailHead.vue';
|
import ReplyEmailHead from './ReplyEmailHead.vue';
|
||||||
@ -46,7 +44,6 @@ import {
|
|||||||
appendSignature,
|
appendSignature,
|
||||||
removeSignature,
|
removeSignature,
|
||||||
getEffectiveChannelType,
|
getEffectiveChannelType,
|
||||||
extractTextFromMarkdown,
|
|
||||||
} from 'dashboard/helper/editorHelper';
|
} from 'dashboard/helper/editorHelper';
|
||||||
|
|
||||||
import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage';
|
import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage';
|
||||||
@ -72,8 +69,6 @@ export default {
|
|||||||
WhatsappTemplates,
|
WhatsappTemplates,
|
||||||
WootMessageEditor,
|
WootMessageEditor,
|
||||||
QuotedEmailPreview,
|
QuotedEmailPreview,
|
||||||
ResizableTextArea,
|
|
||||||
CannedResponse,
|
|
||||||
},
|
},
|
||||||
mixins: [inboxMixin, fileUploadMixin, keyboardEventListenerMixins],
|
mixins: [inboxMixin, fileUploadMixin, keyboardEventListenerMixins],
|
||||||
props: {
|
props: {
|
||||||
@ -114,8 +109,6 @@ export default {
|
|||||||
recordingAudioState: '',
|
recordingAudioState: '',
|
||||||
recordingAudioDurationText: '',
|
recordingAudioDurationText: '',
|
||||||
replyType: REPLY_EDITOR_MODES.REPLY,
|
replyType: REPLY_EDITOR_MODES.REPLY,
|
||||||
mentionSearchKey: '',
|
|
||||||
hasSlashCommand: false,
|
|
||||||
bccEmails: '',
|
bccEmails: '',
|
||||||
ccEmails: '',
|
ccEmails: '',
|
||||||
toEmails: '',
|
toEmails: '',
|
||||||
@ -148,9 +141,6 @@ export default {
|
|||||||
if (!senderId) return {};
|
if (!senderId) return {};
|
||||||
return this.$store.getters['contacts/getContact'](senderId);
|
return this.$store.getters['contacts/getContact'](senderId);
|
||||||
},
|
},
|
||||||
isRichEditorEnabled() {
|
|
||||||
return this.isAWebWidgetInbox || this.isAnEmailChannel || this.isAPIInbox;
|
|
||||||
},
|
|
||||||
shouldShowReplyToMessage() {
|
shouldShowReplyToMessage() {
|
||||||
return (
|
return (
|
||||||
this.inReplyTo?.id &&
|
this.inReplyTo?.id &&
|
||||||
@ -409,19 +399,6 @@ export default {
|
|||||||
!!this.quotedEmailText
|
!!this.quotedEmailText
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
showRichContentEditor() {
|
|
||||||
if (this.isOnPrivateNote || this.isRichEditorEnabled) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
// ensure that the signature is plain text depending on `showRichContentEditor`
|
|
||||||
signatureToApply() {
|
|
||||||
return this.showRichContentEditor
|
|
||||||
? this.messageSignature
|
|
||||||
: extractTextFromMarkdown(this.messageSignature);
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
currentChat(conversation, oldConversation) {
|
currentChat(conversation, oldConversation) {
|
||||||
@ -464,25 +441,7 @@ export default {
|
|||||||
this.resetRecorderAndClearAttachments();
|
this.resetRecorderAndClearAttachments();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
message(updatedMessage) {
|
message() {
|
||||||
// Check if the message starts with a slash.
|
|
||||||
const bodyWithoutSignature = removeSignature(
|
|
||||||
updatedMessage,
|
|
||||||
this.signatureToApply
|
|
||||||
);
|
|
||||||
const startsWithSlash = bodyWithoutSignature.startsWith('/');
|
|
||||||
|
|
||||||
// Determine if the user is potentially typing a slash command.
|
|
||||||
// This is true if the message starts with a slash and the rich content editor is not active.
|
|
||||||
this.hasSlashCommand = startsWithSlash && !this.showRichContentEditor;
|
|
||||||
this.showMentions = this.hasSlashCommand;
|
|
||||||
|
|
||||||
// If a slash command is active, extract the command text after the slash.
|
|
||||||
// If not, reset the mentionSearchKey.
|
|
||||||
this.mentionSearchKey = this.hasSlashCommand
|
|
||||||
? bodyWithoutSignature.substring(1)
|
|
||||||
: '';
|
|
||||||
|
|
||||||
// Autosave the current message draft.
|
// Autosave the current message draft.
|
||||||
this.doAutoSaveDraft();
|
this.doAutoSaveDraft();
|
||||||
},
|
},
|
||||||
@ -532,7 +491,6 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
handleInsert(article) {
|
handleInsert(article) {
|
||||||
const { url, title } = article;
|
const { url, title } = article;
|
||||||
if (this.isRichEditorEnabled) {
|
|
||||||
// Removing empty lines from the title
|
// Removing empty lines from the title
|
||||||
const lines = title.split('\n');
|
const lines = title.split('\n');
|
||||||
const nonEmptyLines = lines.filter(line => line.trim() !== '');
|
const nonEmptyLines = lines.filter(line => line.trim() !== '');
|
||||||
@ -541,11 +499,6 @@ export default {
|
|||||||
BUS_EVENTS.INSERT_INTO_RICH_EDITOR,
|
BUS_EVENTS.INSERT_INTO_RICH_EDITOR,
|
||||||
`[${filteredMarkdown}](${url})`
|
`[${filteredMarkdown}](${url})`
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
this.addIntoEditor(
|
|
||||||
`${this.$t('CONVERSATION.REPLYBOX.INSERT_READ_MORE')} ${url}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
useTrack(CONVERSATION_EVENTS.INSERT_ARTICLE_LINK);
|
useTrack(CONVERSATION_EVENTS.INSERT_ARTICLE_LINK);
|
||||||
},
|
},
|
||||||
@ -614,26 +567,14 @@ export default {
|
|||||||
if (this.isPrivate) {
|
if (this.isPrivate) {
|
||||||
return message;
|
return message;
|
||||||
}
|
}
|
||||||
if (this.showRichContentEditor) {
|
|
||||||
const effectiveChannelType = getEffectiveChannelType(
|
const effectiveChannelType = getEffectiveChannelType(
|
||||||
this.channelType,
|
this.channelType,
|
||||||
this.inbox?.medium || ''
|
this.inbox?.medium || ''
|
||||||
);
|
);
|
||||||
return this.sendWithSignature
|
return this.sendWithSignature
|
||||||
? appendSignature(
|
? appendSignature(message, this.messageSignature, effectiveChannelType)
|
||||||
message,
|
: removeSignature(message, this.messageSignature, effectiveChannelType);
|
||||||
this.messageSignature,
|
|
||||||
effectiveChannelType
|
|
||||||
)
|
|
||||||
: removeSignature(
|
|
||||||
message,
|
|
||||||
this.messageSignature,
|
|
||||||
effectiveChannelType
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return this.sendWithSignature
|
|
||||||
? appendSignature(message, this.signatureToApply)
|
|
||||||
: removeSignature(message, this.signatureToApply);
|
|
||||||
},
|
},
|
||||||
removeFromDraft() {
|
removeFromDraft() {
|
||||||
if (this.conversationIdByRoute) {
|
if (this.conversationIdByRoute) {
|
||||||
@ -649,7 +590,6 @@ export default {
|
|||||||
Escape: {
|
Escape: {
|
||||||
action: () => {
|
action: () => {
|
||||||
this.hideEmojiPicker();
|
this.hideEmojiPicker();
|
||||||
this.hideMentions();
|
|
||||||
},
|
},
|
||||||
allowOnFocusedInput: true,
|
allowOnFocusedInput: true,
|
||||||
},
|
},
|
||||||
@ -694,11 +634,6 @@ export default {
|
|||||||
// Don't handle paste if compose new conversation modal is open
|
// Don't handle paste if compose new conversation modal is open
|
||||||
if (this.newConversationModalActive) return;
|
if (this.newConversationModalActive) return;
|
||||||
|
|
||||||
const data = e.clipboardData.files;
|
|
||||||
if (!this.showRichContentEditor && data.length !== 0) {
|
|
||||||
this.$refs.messageInput?.$el?.blur();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter valid files (non-zero size)
|
// Filter valid files (non-zero size)
|
||||||
Array.from(e.clipboardData.files)
|
Array.from(e.clipboardData.files)
|
||||||
.filter(file => file.size > 0)
|
.filter(file => file.size > 0)
|
||||||
@ -832,7 +767,6 @@ 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
|
||||||
if (this.showRichContentEditor) {
|
|
||||||
const effectiveChannelType = getEffectiveChannelType(
|
const effectiveChannelType = getEffectiveChannelType(
|
||||||
this.channelType,
|
this.channelType,
|
||||||
this.inbox?.medium || ''
|
this.inbox?.medium || ''
|
||||||
@ -842,9 +776,6 @@ export default {
|
|||||||
this.messageSignature,
|
this.messageSignature,
|
||||||
effectiveChannelType
|
effectiveChannelType
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
message = appendSignature(message, this.signatureToApply);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const updatedMessage = replaceVariablesInMessage({
|
const updatedMessage = replaceVariablesInMessage({
|
||||||
@ -868,40 +799,21 @@ export default {
|
|||||||
});
|
});
|
||||||
if (canReply || this.isAWhatsAppChannel || this.isAPIInbox)
|
if (canReply || this.isAWhatsAppChannel || this.isAPIInbox)
|
||||||
this.replyType = mode;
|
this.replyType = mode;
|
||||||
if (this.showRichContentEditor) {
|
|
||||||
if (this.isRecordingAudio) {
|
if (this.isRecordingAudio) {
|
||||||
this.toggleAudioRecorder();
|
this.toggleAudioRecorder();
|
||||||
}
|
}
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.$nextTick(() => this.$refs.messageInput.focus());
|
|
||||||
},
|
},
|
||||||
clearEditorSelection() {
|
clearEditorSelection() {
|
||||||
this.updateEditorSelectionWith = '';
|
this.updateEditorSelectionWith = '';
|
||||||
},
|
},
|
||||||
insertIntoTextEditor(text, selectionStart, selectionEnd) {
|
|
||||||
const { message } = this;
|
|
||||||
const newMessage =
|
|
||||||
message.slice(0, selectionStart) +
|
|
||||||
text +
|
|
||||||
message.slice(selectionEnd, message.length);
|
|
||||||
this.message = newMessage;
|
|
||||||
},
|
|
||||||
addIntoEditor(content) {
|
addIntoEditor(content) {
|
||||||
if (this.showRichContentEditor) {
|
|
||||||
this.updateEditorSelectionWith = content;
|
this.updateEditorSelectionWith = content;
|
||||||
this.onFocus();
|
this.onFocus();
|
||||||
}
|
|
||||||
if (!this.showRichContentEditor) {
|
|
||||||
const { selectionStart, selectionEnd } = this.$refs.messageInput.$el;
|
|
||||||
this.insertIntoTextEditor(content, selectionStart, selectionEnd);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
clearMessage() {
|
clearMessage() {
|
||||||
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
|
||||||
if (this.showRichContentEditor) {
|
|
||||||
const effectiveChannelType = getEffectiveChannelType(
|
const effectiveChannelType = getEffectiveChannelType(
|
||||||
this.channelType,
|
this.channelType,
|
||||||
this.inbox?.medium || ''
|
this.inbox?.medium || ''
|
||||||
@ -911,9 +823,6 @@ export default {
|
|||||||
this.messageSignature,
|
this.messageSignature,
|
||||||
effectiveChannelType
|
effectiveChannelType
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
this.message = appendSignature(this.message, this.signatureToApply);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
this.attachedFiles = [];
|
this.attachedFiles = [];
|
||||||
this.isRecordingAudio = false;
|
this.isRecordingAudio = false;
|
||||||
@ -948,9 +857,6 @@ export default {
|
|||||||
this.toggleEmojiPicker();
|
this.toggleEmojiPicker();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
hideMentions() {
|
|
||||||
this.showMentions = false;
|
|
||||||
},
|
|
||||||
onTypingOn() {
|
onTypingOn() {
|
||||||
this.toggleTyping('on');
|
this.toggleTyping('on');
|
||||||
},
|
},
|
||||||
@ -1197,13 +1103,6 @@ export default {
|
|||||||
:message="inReplyTo"
|
:message="inReplyTo"
|
||||||
@dismiss="resetReplyToMessage"
|
@dismiss="resetReplyToMessage"
|
||||||
/>
|
/>
|
||||||
<CannedResponse
|
|
||||||
v-if="showMentions && hasSlashCommand"
|
|
||||||
v-on-clickaway="hideMentions"
|
|
||||||
class="normal-editor__canned-box"
|
|
||||||
:search-key="mentionSearchKey"
|
|
||||||
@replace="replaceText"
|
|
||||||
/>
|
|
||||||
<EmojiInput
|
<EmojiInput
|
||||||
v-if="showEmojiPicker"
|
v-if="showEmojiPicker"
|
||||||
v-on-clickaway="hideEmojiPicker"
|
v-on-clickaway="hideEmojiPicker"
|
||||||
@ -1227,23 +1126,7 @@ export default {
|
|||||||
@play="recordingAudioState = 'playing'"
|
@play="recordingAudioState = 'playing'"
|
||||||
@pause="recordingAudioState = 'paused'"
|
@pause="recordingAudioState = 'paused'"
|
||||||
/>
|
/>
|
||||||
<ResizableTextArea
|
|
||||||
v-else-if="!showRichContentEditor"
|
|
||||||
ref="messageInput"
|
|
||||||
v-model="message"
|
|
||||||
class="rounded-none input"
|
|
||||||
:placeholder="messagePlaceHolder"
|
|
||||||
:min-height="4"
|
|
||||||
:signature="signatureToApply"
|
|
||||||
allow-signature
|
|
||||||
:send-with-signature="sendWithSignature"
|
|
||||||
@typing-off="onTypingOff"
|
|
||||||
@typing-on="onTypingOn"
|
|
||||||
@focus="onFocus"
|
|
||||||
@blur="onBlur"
|
|
||||||
/>
|
|
||||||
<WootMessageEditor
|
<WootMessageEditor
|
||||||
v-else
|
|
||||||
v-model="message"
|
v-model="message"
|
||||||
:editor-id="editorStateId"
|
:editor-id="editorStateId"
|
||||||
class="input popover-prosemirror-menu"
|
class="input popover-prosemirror-menu"
|
||||||
@ -1369,10 +1252,6 @@ export default {
|
|||||||
|
|
||||||
.reply-box__top {
|
.reply-box__top {
|
||||||
@apply relative py-0 px-4 -mt-px;
|
@apply relative py-0 px-4 -mt-px;
|
||||||
|
|
||||||
textarea {
|
|
||||||
@apply shadow-none outline-none border-transparent bg-transparent m-0 max-h-60 min-h-[3rem] pt-4 pb-0 px-0 resize-none;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.emoji-dialog {
|
.emoji-dialog {
|
||||||
@ -1392,9 +1271,4 @@ export default {
|
|||||||
@apply ltr:left-1 rtl:right-1 -bottom-2;
|
@apply ltr:left-1 rtl:right-1 -bottom-2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.normal-editor__canned-box {
|
|
||||||
width: calc(100% - 2 * 1rem);
|
|
||||||
left: 1rem;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, watch, computed, nextTick } from 'vue';
|
import { ref, watch, computed, nextTick } from 'vue';
|
||||||
import { useKeyboardNavigableList } from 'dashboard/composables/useKeyboardNavigableList';
|
import { useKeyboardNavigableList } from 'dashboard/composables/useKeyboardNavigableList';
|
||||||
|
import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
items: {
|
items: {
|
||||||
@ -15,6 +16,8 @@ const props = defineProps({
|
|||||||
|
|
||||||
const emit = defineEmits(['mentionSelect']);
|
const emit = defineEmits(['mentionSelect']);
|
||||||
|
|
||||||
|
const { getPlainText } = useMessageFormatter();
|
||||||
|
|
||||||
const mentionsListContainerRef = ref(null);
|
const mentionsListContainerRef = ref(null);
|
||||||
const selectedIndex = ref(0);
|
const selectedIndex = ref(0);
|
||||||
|
|
||||||
@ -94,7 +97,7 @@ const variableKey = (item = {}) => {
|
|||||||
'text-n-slate-12': index === selectedIndex,
|
'text-n-slate-12': index === selectedIndex,
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
{{ item.description }}
|
{{ getPlainText(item.description) }}
|
||||||
</p>
|
</p>
|
||||||
<p
|
<p
|
||||||
class="max-w-full min-w-0 mb-0 overflow-hidden text-xs text-n-slate-11 group-hover:text-n-slate-12 text-ellipsis whitespace-nowrap"
|
class="max-w-full min-w-0 mb-0 overflow-hidden text-xs text-n-slate-11 group-hover:text-n-slate-12 text-ellipsis whitespace-nowrap"
|
||||||
|
|||||||
@ -34,6 +34,7 @@ export function extractTextFromMarkdown(markdown) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Strip unsupported markdown formatting based on channel capabilities.
|
* Strip unsupported markdown formatting based on channel capabilities.
|
||||||
|
* Uses MARKDOWN_PATTERNS from editor constants.
|
||||||
*
|
*
|
||||||
* @param {string} markdown - markdown text to process
|
* @param {string} markdown - markdown text to process
|
||||||
* @param {string} channelType - The channel type to check supported formatting
|
* @param {string} channelType - The channel type to check supported formatting
|
||||||
@ -43,35 +44,16 @@ export function stripUnsupportedSignatureMarkdown(markdown, channelType) {
|
|||||||
if (!markdown) return '';
|
if (!markdown) return '';
|
||||||
|
|
||||||
const { marks = [], nodes = [] } = FORMATTING[channelType] || {};
|
const { marks = [], nodes = [] } = FORMATTING[channelType] || {};
|
||||||
const has = (arr, key) => arr.includes(key);
|
const supported = [...marks, ...nodes];
|
||||||
|
|
||||||
// Define stripping rules: [condition, pattern, replacement]
|
// Apply patterns from MARKDOWN_PATTERNS for unsupported types
|
||||||
const rules = [
|
const result = MARKDOWN_PATTERNS.reduce((text, { type, patterns }) => {
|
||||||
[!has(nodes, 'image'), /!\[.*?\]\(.*?\)/g, ''],
|
if (supported.includes(type)) return text;
|
||||||
[!has(marks, 'link'), /\[([^\]]+)\]\([^)]+\)/g, '$1'],
|
return patterns.reduce(
|
||||||
[!has(nodes, 'codeBlock'), /```[\s\S]*?```/g, ''],
|
(t, { pattern, replacement }) => t.replace(pattern, replacement),
|
||||||
[!has(marks, 'code'), /`([^`]+)`/g, '$1'],
|
text
|
||||||
[!has(marks, 'strong'), /\*\*([^*]+)\*\*/g, '$1'],
|
|
||||||
[!has(marks, 'strong'), /__([^_]+)__/g, '$1'],
|
|
||||||
[!has(marks, 'em'), /\*([^*]+)\*/g, '$1'],
|
|
||||||
// Match _text_ only at word boundaries (whitespace/string start/end)
|
|
||||||
// Preserves underscores in URLs (e.g., https://example.com/path_name) and variable names
|
|
||||||
[
|
|
||||||
!has(marks, 'em'),
|
|
||||||
/(?<=^|[\s])_([^_\s][^_]*[^_\s]|[^_\s])_(?=$|[\s])/g,
|
|
||||||
'$1',
|
|
||||||
],
|
|
||||||
[!has(marks, 'strike'), /~~([^~]+)~~/g, '$1'],
|
|
||||||
[!has(nodes, 'blockquote'), /^>\s?/gm, ''],
|
|
||||||
[!has(nodes, 'bulletList'), /^[-*+]\s+/gm, ''],
|
|
||||||
[!has(nodes, 'orderedList'), /^\d+\.\s+/gm, ''],
|
|
||||||
];
|
|
||||||
|
|
||||||
const result = rules.reduce(
|
|
||||||
(text, [shouldStrip, pattern, replacement]) =>
|
|
||||||
shouldStrip ? text.replace(pattern, replacement) : text,
|
|
||||||
markdown
|
|
||||||
);
|
);
|
||||||
|
}, markdown);
|
||||||
|
|
||||||
return result
|
return result
|
||||||
.split('\n')
|
.split('\n')
|
||||||
@ -186,27 +168,22 @@ export function appendSignature(body, signature, channelType) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 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.
|
* Tries multiple signature variants: original, channel-stripped, and fully stripped.
|
||||||
*
|
*
|
||||||
* @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.
|
||||||
* @param {string} channelType - Optional. The effective channel type for channel-specific stripping.
|
* @param {string} channelType - Optional. The effective channel type for channel-specific stripping.
|
||||||
* For Twilio channels, pass the result of getEffectiveChannelType().
|
|
||||||
* @returns {string} - The body with the signature removed.
|
* @returns {string} - The body with the signature removed.
|
||||||
*/
|
*/
|
||||||
export function removeSignature(body, signature, channelType) {
|
export function removeSignature(body, signature, channelType) {
|
||||||
// Build list of signatures to try: original, channel-stripped, and fully stripped
|
// Build unique list of signature variants to try
|
||||||
const cleanedSignature = cleanSignature(signature);
|
|
||||||
const channelStripped = channelType
|
const channelStripped = channelType
|
||||||
? cleanSignature(stripUnsupportedSignatureMarkdown(signature, channelType))
|
? cleanSignature(stripUnsupportedSignatureMarkdown(signature, channelType))
|
||||||
: null;
|
: null;
|
||||||
const fullyStripped = cleanSignature(extractTextFromMarkdown(signature));
|
|
||||||
|
|
||||||
// Try signatures in order: original → channel-specific → fully stripped
|
|
||||||
const signaturesToTry = [
|
const signaturesToTry = [
|
||||||
cleanedSignature,
|
cleanSignature(signature),
|
||||||
channelStripped,
|
channelStripped,
|
||||||
fullyStripped,
|
cleanSignature(extractTextFromMarkdown(signature)),
|
||||||
].filter((sig, i, arr) => sig && arr.indexOf(sig) === i); // Remove nulls and duplicates
|
].filter((sig, i, arr) => sig && arr.indexOf(sig) === i); // Remove nulls and duplicates
|
||||||
|
|
||||||
// Find the first matching signature
|
// Find the first matching signature
|
||||||
@ -225,17 +202,12 @@ export function removeSignature(body, signature, channelType) {
|
|||||||
newBody = newBody.substring(0, signatureIndex).trimEnd();
|
newBody = newBody.substring(0, signatureIndex).trimEnd();
|
||||||
}
|
}
|
||||||
|
|
||||||
// let's find the delimiter and remove it
|
// Remove delimiter if it's at the end
|
||||||
const delimiterIndex = newBody.lastIndexOf(SIGNATURE_DELIMITER);
|
if (newBody.endsWith(SIGNATURE_DELIMITER)) {
|
||||||
if (
|
|
||||||
delimiterIndex !== -1 &&
|
|
||||||
delimiterIndex === newBody.length - SIGNATURE_DELIMITER.length // this will ensure the delimiter is at the end
|
|
||||||
) {
|
|
||||||
// if the delimiter is at the end, remove it
|
// if the delimiter is at the end, remove it
|
||||||
newBody = newBody.substring(0, delimiterIndex);
|
newBody = newBody.slice(0, -SIGNATURE_DELIMITER.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
// return the value
|
|
||||||
return newBody;
|
return newBody;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import BaseSettingsHeader from '../components/BaseSettingsHeader.vue';
|
|||||||
import { computed, onMounted, ref, defineOptions } from 'vue';
|
import { computed, onMounted, ref, defineOptions } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { useStoreGetters, useStore } from 'dashboard/composables/store';
|
import { useStoreGetters, useStore } from 'dashboard/composables/store';
|
||||||
|
import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
|
||||||
|
|
||||||
import Button from 'dashboard/components-next/button/Button.vue';
|
import Button from 'dashboard/components-next/button/Button.vue';
|
||||||
|
|
||||||
@ -17,6 +18,8 @@ const getters = useStoreGetters();
|
|||||||
const store = useStore();
|
const store = useStore();
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const { getPlainText } = useMessageFormatter();
|
||||||
|
|
||||||
const showAddPopup = ref(false);
|
const showAddPopup = ref(false);
|
||||||
const loading = ref({});
|
const loading = ref({});
|
||||||
const showEditPopup = ref(false);
|
const showEditPopup = ref(false);
|
||||||
@ -182,7 +185,7 @@ const tableHeaders = computed(() => {
|
|||||||
{{ cannedItem.short_code }}
|
{{ cannedItem.short_code }}
|
||||||
</td>
|
</td>
|
||||||
<td class="py-4 ltr:pr-4 rtl:pl-4 md:break-all whitespace-normal">
|
<td class="py-4 ltr:pr-4 rtl:pl-4 md:break-all whitespace-normal">
|
||||||
{{ cannedItem.content }}
|
{{ getPlainText(cannedItem.content) }}
|
||||||
</td>
|
</td>
|
||||||
<td class="py-4 flex justify-end gap-1">
|
<td class="py-4 flex justify-end gap-1">
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@ -33,7 +33,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@breezystack/lamejs": "^1.2.7",
|
"@breezystack/lamejs": "^1.2.7",
|
||||||
"@chatwoot/ninja-keys": "1.2.3",
|
"@chatwoot/ninja-keys": "1.2.3",
|
||||||
"@chatwoot/prosemirror-schema": "1.2.6",
|
"@chatwoot/prosemirror-schema": "1.3.4",
|
||||||
"@chatwoot/utils": "^0.0.51",
|
"@chatwoot/utils": "^0.0.51",
|
||||||
"@formkit/core": "^1.6.7",
|
"@formkit/core": "^1.6.7",
|
||||||
"@formkit/vue": "^1.6.7",
|
"@formkit/vue": "^1.6.7",
|
||||||
|
|||||||
10
pnpm-lock.yaml
generated
10
pnpm-lock.yaml
generated
@ -20,8 +20,8 @@ importers:
|
|||||||
specifier: 1.2.3
|
specifier: 1.2.3
|
||||||
version: 1.2.3
|
version: 1.2.3
|
||||||
'@chatwoot/prosemirror-schema':
|
'@chatwoot/prosemirror-schema':
|
||||||
specifier: 1.2.6
|
specifier: 1.3.4
|
||||||
version: 1.2.6
|
version: 1.3.4
|
||||||
'@chatwoot/utils':
|
'@chatwoot/utils':
|
||||||
specifier: ^0.0.51
|
specifier: ^0.0.51
|
||||||
version: 0.0.51
|
version: 0.0.51
|
||||||
@ -430,8 +430,8 @@ packages:
|
|||||||
'@chatwoot/ninja-keys@1.2.3':
|
'@chatwoot/ninja-keys@1.2.3':
|
||||||
resolution: {integrity: sha512-xM8d9P5ikDMZm2WbaCTk/TW5HFauylrU3cJ75fq5je6ixKwyhl/0kZbVN/vbbZN4+AUX/OaSIn6IJbtCgIF67g==}
|
resolution: {integrity: sha512-xM8d9P5ikDMZm2WbaCTk/TW5HFauylrU3cJ75fq5je6ixKwyhl/0kZbVN/vbbZN4+AUX/OaSIn6IJbtCgIF67g==}
|
||||||
|
|
||||||
'@chatwoot/prosemirror-schema@1.2.6':
|
'@chatwoot/prosemirror-schema@1.3.4':
|
||||||
resolution: {integrity: sha512-ej60kU3m/tP0VoGkOJhj0X+Mxt7fEX5DSEE4IibCZnTM4kMewkMxSYyPu0AXqaA4nKX1hTMrwcbv1t7gVQttWQ==}
|
resolution: {integrity: sha512-XY1UyG9topVy7fNCt45YgiU/QzlJJRhrcrSWcvAIIcDYKomnEcGOyeAfM3WeFD6GgOuLzRUyIHrW+WQGpHaZvA==}
|
||||||
|
|
||||||
'@chatwoot/utils@0.0.51':
|
'@chatwoot/utils@0.0.51':
|
||||||
resolution: {integrity: sha512-WlEmWfOTzR7YZRUWzn5Wpm15/BRudpwqoNckph8TohyDbiim1CP4UZGa+qjajxTbNGLLhtKlm0Xl+X16+5Wceg==}
|
resolution: {integrity: sha512-WlEmWfOTzR7YZRUWzn5Wpm15/BRudpwqoNckph8TohyDbiim1CP4UZGa+qjajxTbNGLLhtKlm0Xl+X16+5Wceg==}
|
||||||
@ -4909,7 +4909,7 @@ snapshots:
|
|||||||
hotkeys-js: 3.8.7
|
hotkeys-js: 3.8.7
|
||||||
lit: 2.2.6
|
lit: 2.2.6
|
||||||
|
|
||||||
'@chatwoot/prosemirror-schema@1.2.6':
|
'@chatwoot/prosemirror-schema@1.3.4':
|
||||||
dependencies:
|
dependencies:
|
||||||
markdown-it-sup: 2.0.0
|
markdown-it-sup: 2.0.0
|
||||||
prosemirror-commands: 1.6.0
|
prosemirror-commands: 1.6.0
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user