mirror of
				https://github.com/lingble/chatwoot.git
				synced 2025-11-04 04:57:51 +00:00 
			
		
		
		
	fix: Avoid editor formatting issues when a canned response is edited (#5533)
This commit is contained in:
		
				
					committed by
					
						
						GitHub
					
				
			
			
				
	
			
			
			
						parent
						
							7b54990ae6
						
					
				
				
					commit
					705d06ac3c
				
			@@ -28,7 +28,7 @@ import {
 | 
				
			|||||||
  suggestionsPlugin,
 | 
					  suggestionsPlugin,
 | 
				
			||||||
  triggerCharacters,
 | 
					  triggerCharacters,
 | 
				
			||||||
} from '@chatwoot/prosemirror-schema/src/mentions/plugin';
 | 
					} from '@chatwoot/prosemirror-schema/src/mentions/plugin';
 | 
				
			||||||
import { EditorState } from 'prosemirror-state';
 | 
					import { EditorState, Selection } from 'prosemirror-state';
 | 
				
			||||||
import { defaultMarkdownParser } from 'prosemirror-markdown';
 | 
					import { defaultMarkdownParser } from 'prosemirror-markdown';
 | 
				
			||||||
import { wootWriterSetup } from '@chatwoot/prosemirror-schema';
 | 
					import { wootWriterSetup } from '@chatwoot/prosemirror-schema';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -61,23 +61,28 @@ export default {
 | 
				
			|||||||
  mixins: [eventListenerMixins],
 | 
					  mixins: [eventListenerMixins],
 | 
				
			||||||
  props: {
 | 
					  props: {
 | 
				
			||||||
    value: { type: String, default: '' },
 | 
					    value: { type: String, default: '' },
 | 
				
			||||||
 | 
					    editorId: { type: String, default: '' },
 | 
				
			||||||
    placeholder: { type: String, default: '' },
 | 
					    placeholder: { type: String, default: '' },
 | 
				
			||||||
    isPrivate: { type: Boolean, default: false },
 | 
					    isPrivate: { type: Boolean, default: false },
 | 
				
			||||||
    isFormatMode: { type: Boolean, default: false },
 | 
					 | 
				
			||||||
    enableSuggestions: { type: Boolean, default: true },
 | 
					    enableSuggestions: { type: Boolean, default: true },
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  data() {
 | 
					  data() {
 | 
				
			||||||
    return {
 | 
					    return {
 | 
				
			||||||
      lastValue: null,
 | 
					 | 
				
			||||||
      showUserMentions: false,
 | 
					      showUserMentions: false,
 | 
				
			||||||
      showCannedMenu: false,
 | 
					      showCannedMenu: false,
 | 
				
			||||||
      mentionSearchKey: '',
 | 
					      mentionSearchKey: '',
 | 
				
			||||||
      cannedSearchTerm: '',
 | 
					      cannedSearchTerm: '',
 | 
				
			||||||
      editorView: null,
 | 
					      editorView: null,
 | 
				
			||||||
      range: null,
 | 
					      range: null,
 | 
				
			||||||
 | 
					      state: undefined,
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  computed: {
 | 
					  computed: {
 | 
				
			||||||
 | 
					    contentFromEditor() {
 | 
				
			||||||
 | 
					      return addMentionsToMarkdownSerializer(
 | 
				
			||||||
 | 
					        defaultMarkdownSerializer
 | 
				
			||||||
 | 
					      ).serialize(this.editorView.state.doc);
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    plugins() {
 | 
					    plugins() {
 | 
				
			||||||
      if (!this.enableSuggestions) {
 | 
					      if (!this.enableSuggestions) {
 | 
				
			||||||
        return [];
 | 
					        return [];
 | 
				
			||||||
@@ -102,7 +107,6 @@ export default {
 | 
				
			|||||||
          onExit: () => {
 | 
					          onExit: () => {
 | 
				
			||||||
            this.mentionSearchKey = '';
 | 
					            this.mentionSearchKey = '';
 | 
				
			||||||
            this.showUserMentions = false;
 | 
					            this.showUserMentions = false;
 | 
				
			||||||
            this.editorView = null;
 | 
					 | 
				
			||||||
            return false;
 | 
					            return false;
 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
          onKeyDown: ({ event }) => {
 | 
					          onKeyDown: ({ event }) => {
 | 
				
			||||||
@@ -131,7 +135,6 @@ export default {
 | 
				
			|||||||
          onExit: () => {
 | 
					          onExit: () => {
 | 
				
			||||||
            this.cannedSearchTerm = '';
 | 
					            this.cannedSearchTerm = '';
 | 
				
			||||||
            this.showCannedMenu = false;
 | 
					            this.showCannedMenu = false;
 | 
				
			||||||
            this.editorView = null;
 | 
					 | 
				
			||||||
            return false;
 | 
					            return false;
 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
          onKeyDown: ({ event }) => {
 | 
					          onKeyDown: ({ event }) => {
 | 
				
			||||||
@@ -149,54 +152,57 @@ export default {
 | 
				
			|||||||
      this.$emit('toggle-canned-menu', !this.isPrivate && updatedValue);
 | 
					      this.$emit('toggle-canned-menu', !this.isPrivate && updatedValue);
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    value(newValue = '') {
 | 
					    value(newValue = '') {
 | 
				
			||||||
      if (newValue !== this.lastValue) {
 | 
					      if (newValue !== this.contentFromEditor) {
 | 
				
			||||||
        const { tr } = this.state;
 | 
					        this.reloadState();
 | 
				
			||||||
        if (this.isFormatMode) {
 | 
					 | 
				
			||||||
          this.state = createState(
 | 
					 | 
				
			||||||
            newValue,
 | 
					 | 
				
			||||||
            this.placeholder,
 | 
					 | 
				
			||||||
            this.plugins,
 | 
					 | 
				
			||||||
            this.isFormatMode
 | 
					 | 
				
			||||||
          );
 | 
					 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
          tr.insertText(newValue, 0, tr.doc.content.size);
 | 
					 | 
				
			||||||
          this.state = this.view.state.apply(tr);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        this.view.updateState(this.state);
 | 
					 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    editorId() {
 | 
				
			||||||
 | 
					      this.reloadState();
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    isPrivate() {
 | 
				
			||||||
 | 
					      this.reloadState();
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  created() {
 | 
					  created() {
 | 
				
			||||||
    this.state = createState(this.value, this.placeholder, this.plugins);
 | 
					    this.state = createState(this.value, this.placeholder, this.plugins);
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  mounted() {
 | 
					  mounted() {
 | 
				
			||||||
    this.view = new EditorView(this.$refs.editor, {
 | 
					    this.createEditorView();
 | 
				
			||||||
      state: this.state,
 | 
					    this.editorView.updateState(this.state);
 | 
				
			||||||
      dispatchTransaction: tx => {
 | 
					 | 
				
			||||||
        this.state = this.state.apply(tx);
 | 
					 | 
				
			||||||
        this.emitOnChange();
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      handleDOMEvents: {
 | 
					 | 
				
			||||||
        keyup: () => {
 | 
					 | 
				
			||||||
          this.onKeyup();
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        focus: () => {
 | 
					 | 
				
			||||||
          this.onFocus();
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        blur: () => {
 | 
					 | 
				
			||||||
          this.onBlur();
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        paste: (view, event) => {
 | 
					 | 
				
			||||||
          const data = event.clipboardData.files;
 | 
					 | 
				
			||||||
          if (data.length > 0) {
 | 
					 | 
				
			||||||
            event.preventDefault();
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
    this.focusEditorInputField();
 | 
					    this.focusEditorInputField();
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  methods: {
 | 
					  methods: {
 | 
				
			||||||
 | 
					    reloadState() {
 | 
				
			||||||
 | 
					      this.state = createState(this.value, this.placeholder, this.plugins);
 | 
				
			||||||
 | 
					      this.editorView.updateState(this.state);
 | 
				
			||||||
 | 
					      this.focusEditorInputField();
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    createEditorView() {
 | 
				
			||||||
 | 
					      this.editorView = new EditorView(this.$refs.editor, {
 | 
				
			||||||
 | 
					        state: this.state,
 | 
				
			||||||
 | 
					        dispatchTransaction: tx => {
 | 
				
			||||||
 | 
					          this.state = this.state.apply(tx);
 | 
				
			||||||
 | 
					          this.emitOnChange();
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        handleDOMEvents: {
 | 
				
			||||||
 | 
					          keyup: () => {
 | 
				
			||||||
 | 
					            this.onKeyup();
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          focus: () => {
 | 
				
			||||||
 | 
					            this.onFocus();
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          blur: () => {
 | 
				
			||||||
 | 
					            this.onBlur();
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          paste: (view, event) => {
 | 
				
			||||||
 | 
					            const data = event.clipboardData.files;
 | 
				
			||||||
 | 
					            if (data.length > 0) {
 | 
				
			||||||
 | 
					              event.preventDefault();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    handleKeyEvents(e) {
 | 
					    handleKeyEvents(e) {
 | 
				
			||||||
      if (hasPressedAltAndPKey(e)) {
 | 
					      if (hasPressedAltAndPKey(e)) {
 | 
				
			||||||
        this.focusEditorInputField();
 | 
					        this.focusEditorInputField();
 | 
				
			||||||
@@ -206,47 +212,59 @@ export default {
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    focusEditorInputField() {
 | 
					    focusEditorInputField() {
 | 
				
			||||||
      this.$refs.editor.querySelector('div.ProseMirror-woot-style').focus();
 | 
					      const { tr } = this.editorView.state;
 | 
				
			||||||
 | 
					      const selection = Selection.atEnd(tr.doc);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      this.editorView.dispatch(tr.setSelection(selection));
 | 
				
			||||||
 | 
					      this.editorView.focus();
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    insertMentionNode(mentionItem) {
 | 
					    insertMentionNode(mentionItem) {
 | 
				
			||||||
      if (!this.view) {
 | 
					      if (!this.editorView) {
 | 
				
			||||||
        return null;
 | 
					        return null;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      const node = this.view.state.schema.nodes.mention.create({
 | 
					      const node = this.editorView.state.schema.nodes.mention.create({
 | 
				
			||||||
        userId: mentionItem.key,
 | 
					        userId: mentionItem.key,
 | 
				
			||||||
        userFullName: mentionItem.label,
 | 
					        userFullName: mentionItem.label,
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      const tr = this.view.state.tr.replaceWith(
 | 
					      const tr = this.editorView.state.tr.replaceWith(
 | 
				
			||||||
        this.range.from,
 | 
					        this.range.from,
 | 
				
			||||||
        this.range.to,
 | 
					        this.range.to,
 | 
				
			||||||
        node
 | 
					        node
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
      this.state = this.view.state.apply(tr);
 | 
					      this.state = this.editorView.state.apply(tr);
 | 
				
			||||||
      return this.emitOnChange();
 | 
					      return this.emitOnChange();
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    insertCannedResponse(cannedItem) {
 | 
					    insertCannedResponse(cannedItem) {
 | 
				
			||||||
      if (!this.view) {
 | 
					      if (!this.editorView) {
 | 
				
			||||||
        return null;
 | 
					        return null;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      const tr = this.view.state.tr.insertText(
 | 
					      const tr = this.editorView.state.tr.insertText(
 | 
				
			||||||
        cannedItem,
 | 
					        cannedItem,
 | 
				
			||||||
        this.range.from,
 | 
					        this.range.from,
 | 
				
			||||||
        this.range.to
 | 
					        this.range.to
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
      this.state = this.view.state.apply(tr);
 | 
					      this.state = this.editorView.state.apply(tr);
 | 
				
			||||||
      return this.emitOnChange();
 | 
					      this.emitOnChange();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // Hacky fix for #5501
 | 
				
			||||||
 | 
					      this.state = createState(
 | 
				
			||||||
 | 
					        this.contentFromEditor,
 | 
				
			||||||
 | 
					        this.placeholder,
 | 
				
			||||||
 | 
					        this.plugins
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					      this.editorView.updateState(this.state);
 | 
				
			||||||
 | 
					      return false;
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    emitOnChange() {
 | 
					    emitOnChange() {
 | 
				
			||||||
      this.view.updateState(this.state);
 | 
					      this.editorView.updateState(this.state);
 | 
				
			||||||
      this.lastValue = addMentionsToMarkdownSerializer(
 | 
					
 | 
				
			||||||
        defaultMarkdownSerializer
 | 
					      this.$emit('input', this.contentFromEditor);
 | 
				
			||||||
      ).serialize(this.state.doc);
 | 
					 | 
				
			||||||
      this.$emit('input', this.lastValue);
 | 
					 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    hideMentions() {
 | 
					    hideMentions() {
 | 
				
			||||||
      this.showUserMentions = false;
 | 
					      this.showUserMentions = false;
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -56,6 +56,7 @@
 | 
				
			|||||||
      <woot-message-editor
 | 
					      <woot-message-editor
 | 
				
			||||||
        v-else
 | 
					        v-else
 | 
				
			||||||
        v-model="message"
 | 
					        v-model="message"
 | 
				
			||||||
 | 
					        :editor-id="editorStateId"
 | 
				
			||||||
        class="input"
 | 
					        class="input"
 | 
				
			||||||
        :is-private="isOnPrivateNote"
 | 
					        :is-private="isOnPrivateNote"
 | 
				
			||||||
        :placeholder="messagePlaceHolder"
 | 
					        :placeholder="messagePlaceHolder"
 | 
				
			||||||
@@ -429,6 +430,13 @@ export default {
 | 
				
			|||||||
    profilePath() {
 | 
					    profilePath() {
 | 
				
			||||||
      return frontendURL(`accounts/${this.accountId}/profile/settings`);
 | 
					      return frontendURL(`accounts/${this.accountId}/profile/settings`);
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    conversationId() {
 | 
				
			||||||
 | 
					      return this.currentChat.id;
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    editorStateId() {
 | 
				
			||||||
 | 
					      const key = `draft-${this.conversationIdByRoute}-${this.replyType}`;
 | 
				
			||||||
 | 
					      return key;
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  watch: {
 | 
					  watch: {
 | 
				
			||||||
    currentChat(conversation) {
 | 
					    currentChat(conversation) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,7 @@
 | 
				
			|||||||
export const LOCAL_STORAGE_KEYS = {
 | 
					export const LOCAL_STORAGE_KEYS = {
 | 
				
			||||||
  DISMISSED_UPDATES: 'dismissedUpdates',
 | 
					  DISMISSED_UPDATES: 'dismissedUpdates',
 | 
				
			||||||
  WIDGET_BUILDER: 'widgetBubble_',
 | 
					  WIDGET_BUILDER: 'widgetBubble_',
 | 
				
			||||||
 | 
					  DRAFT_MESSAGES: 'draftMessages',
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const LocalStorage = {
 | 
					export const LocalStorage = {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -46,6 +46,7 @@ export default {
 | 
				
			|||||||
    return {
 | 
					    return {
 | 
				
			||||||
      articleTitle: '',
 | 
					      articleTitle: '',
 | 
				
			||||||
      articleContent: '',
 | 
					      articleContent: '',
 | 
				
			||||||
 | 
					      saveArticle: () => {},
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  mounted() {
 | 
					  mounted() {
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user