mirror of
				https://github.com/lingble/chatwoot.git
				synced 2025-11-03 20:48:07 +00:00 
			
		
		
		
	feat: Adds image attachment for help center articles (#6426)
* Added one more endpoint to attach tempfile and get logo * Added one more endpoint to attach tempfile and get logo * spec fixes * Upload file for articles irrespective of the association * Upload file for articles irrespective of the association * Add multiple images with different keys * feat: Adds image attachment for help center articles * Adds validation for file upload * Fixes space above image after adding to doc * chore: Removed svg from file upload type * Update app/javascript/dashboard/components/widgets/WootWriter/FullEditor.vue Co-authored-by: Shivam Mishra <scm.mymail@gmail.com> * Update app/javascript/dashboard/components/widgets/WootWriter/FullEditor.vue Co-authored-by: Shivam Mishra <scm.mymail@gmail.com> * Removes caption for the image * Fixes woot prosemirror package version * Update yarn.lock * Update yarn.lock --------- Co-authored-by: Tejaswini Chile <tejaswini@chatwoot.com> Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com> Co-authored-by: iamsivin <iamsivin@gmail.com> Co-authored-by: Shivam Mishra <scm.mymail@gmail.com> Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
This commit is contained in:
		
				
					committed by
					
						
						GitHub
					
				
			
			
				
	
			
			
			
						parent
						
							80784e3cab
						
					
				
				
					commit
					2c8ecbeceb
				
			@@ -46,6 +46,20 @@ class ArticlesAPI extends PortalsAPI {
 | 
				
			|||||||
  deleteArticle({ articleId, portalSlug }) {
 | 
					  deleteArticle({ articleId, portalSlug }) {
 | 
				
			||||||
    return axios.delete(`${this.url}/${portalSlug}/articles/${articleId}`);
 | 
					    return axios.delete(`${this.url}/${portalSlug}/articles/${articleId}`);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  uploadImage({ portalSlug, file }) {
 | 
				
			||||||
 | 
					    let formData = new FormData();
 | 
				
			||||||
 | 
					    formData.append('background_image', file);
 | 
				
			||||||
 | 
					    return axios.post(
 | 
				
			||||||
 | 
					      `${this.url}/${portalSlug}/articles/attach_file`,
 | 
				
			||||||
 | 
					      formData,
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        headers: {
 | 
				
			||||||
 | 
					          'Content-Type': 'multipart/form-data',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default new ArticlesAPI();
 | 
					export default new ArticlesAPI();
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,13 @@
 | 
				
			|||||||
<template>
 | 
					<template>
 | 
				
			||||||
  <div>
 | 
					  <div>
 | 
				
			||||||
    <div class="editor-root editor--article">
 | 
					    <div class="editor-root editor--article">
 | 
				
			||||||
 | 
					      <input
 | 
				
			||||||
 | 
					        ref="imageUploadInput"
 | 
				
			||||||
 | 
					        type="file"
 | 
				
			||||||
 | 
					        accept="image/png, image/jpeg, image/jpg, image/gif, image/webp"
 | 
				
			||||||
 | 
					        hidden
 | 
				
			||||||
 | 
					        @change="onFileChange"
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
      <div ref="editor" />
 | 
					      <div ref="editor" />
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
@@ -16,23 +23,31 @@ import {
 | 
				
			|||||||
  EditorState,
 | 
					  EditorState,
 | 
				
			||||||
  Selection,
 | 
					  Selection,
 | 
				
			||||||
} from '@chatwoot/prosemirror-schema';
 | 
					} from '@chatwoot/prosemirror-schema';
 | 
				
			||||||
 | 
					import { checkFileSizeLimit } from 'shared/helpers/FileHelper';
 | 
				
			||||||
 | 
					import alertMixin from 'shared/mixins/alertMixin';
 | 
				
			||||||
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
 | 
					import eventListenerMixins from 'shared/mixins/eventListenerMixins';
 | 
				
			||||||
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
 | 
					import uiSettingsMixin from 'dashboard/mixins/uiSettings';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const createState = (content, placeholder, plugins = []) => {
 | 
					const MAXIMUM_FILE_UPLOAD_SIZE = 4; // in MB
 | 
				
			||||||
 | 
					const createState = (
 | 
				
			||||||
 | 
					  content,
 | 
				
			||||||
 | 
					  placeholder,
 | 
				
			||||||
 | 
					  plugins = [],
 | 
				
			||||||
 | 
					  onImageUpload = () => {}
 | 
				
			||||||
 | 
					) => {
 | 
				
			||||||
  return EditorState.create({
 | 
					  return EditorState.create({
 | 
				
			||||||
    doc: new ArticleMarkdownTransformer(fullSchema).parse(content),
 | 
					    doc: new ArticleMarkdownTransformer(fullSchema).parse(content),
 | 
				
			||||||
    plugins: wootArticleWriterSetup({
 | 
					    plugins: wootArticleWriterSetup({
 | 
				
			||||||
      schema: fullSchema,
 | 
					      schema: fullSchema,
 | 
				
			||||||
      placeholder,
 | 
					      placeholder,
 | 
				
			||||||
      plugins,
 | 
					      plugins,
 | 
				
			||||||
 | 
					      onImageUpload,
 | 
				
			||||||
    }),
 | 
					    }),
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default {
 | 
					export default {
 | 
				
			||||||
  mixins: [eventListenerMixins, uiSettingsMixin],
 | 
					  mixins: [eventListenerMixins, uiSettingsMixin, alertMixin],
 | 
				
			||||||
  props: {
 | 
					  props: {
 | 
				
			||||||
    value: { type: String, default: '' },
 | 
					    value: { type: String, default: '' },
 | 
				
			||||||
    editorId: { type: String, default: '' },
 | 
					    editorId: { type: String, default: '' },
 | 
				
			||||||
@@ -64,7 +79,12 @@ export default {
 | 
				
			|||||||
    },
 | 
					    },
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  created() {
 | 
					  created() {
 | 
				
			||||||
    this.state = createState(this.value, this.placeholder, this.plugins);
 | 
					    this.state = createState(
 | 
				
			||||||
 | 
					      this.value,
 | 
				
			||||||
 | 
					      this.placeholder,
 | 
				
			||||||
 | 
					      this.plugins,
 | 
				
			||||||
 | 
					      this.openFileBrowser
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  mounted() {
 | 
					  mounted() {
 | 
				
			||||||
    this.createEditorView();
 | 
					    this.createEditorView();
 | 
				
			||||||
@@ -73,8 +93,67 @@ export default {
 | 
				
			|||||||
    this.focusEditorInputField();
 | 
					    this.focusEditorInputField();
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  methods: {
 | 
					  methods: {
 | 
				
			||||||
 | 
					    openFileBrowser() {
 | 
				
			||||||
 | 
					      this.$refs.imageUploadInput.click();
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    onFileChange() {
 | 
				
			||||||
 | 
					      const file = this.$refs.imageUploadInput.files[0];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (checkFileSizeLimit(file, MAXIMUM_FILE_UPLOAD_SIZE)) {
 | 
				
			||||||
 | 
					        this.uploadImageToStorage(file);
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        this.showAlert(
 | 
				
			||||||
 | 
					          this.$t('HELP_CENTER.ARTICLE_EDITOR.IMAGE_UPLOAD.ERROR_FILE_SIZE', {
 | 
				
			||||||
 | 
					            size: MAXIMUM_FILE_UPLOAD_SIZE,
 | 
				
			||||||
 | 
					          })
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      this.$refs.imageUploadInput.value = '';
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    async uploadImageToStorage(file) {
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        const fileUrl = await this.$store.dispatch('articles/attachImage', {
 | 
				
			||||||
 | 
					          portalSlug: this.$route.params.portalSlug,
 | 
				
			||||||
 | 
					          file,
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (fileUrl) {
 | 
				
			||||||
 | 
					          this.onImageUploadStart(fileUrl);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        this.showAlert(
 | 
				
			||||||
 | 
					          this.$t('HELP_CENTER.ARTICLE_EDITOR.IMAGE_UPLOAD.SUCCESS')
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      } catch (error) {
 | 
				
			||||||
 | 
					        this.showAlert(
 | 
				
			||||||
 | 
					          this.$t('HELP_CENTER.ARTICLE_EDITOR.IMAGE_UPLOAD.ERROR')
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    onImageUploadStart(fileUrl) {
 | 
				
			||||||
 | 
					      const { selection } = this.editorView.state;
 | 
				
			||||||
 | 
					      const from = selection.from;
 | 
				
			||||||
 | 
					      const node = this.editorView.state.schema.nodes.image.create({
 | 
				
			||||||
 | 
					        src: fileUrl,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					      const paragraphNode = this.editorView.state.schema.node('paragraph');
 | 
				
			||||||
 | 
					      if (node) {
 | 
				
			||||||
 | 
					        // Insert the image and the caption wrapped inside a paragraph
 | 
				
			||||||
 | 
					        const tr = this.editorView.state.tr
 | 
				
			||||||
 | 
					          .replaceSelectionWith(paragraphNode)
 | 
				
			||||||
 | 
					          .insert(from + 1, node);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this.editorView.dispatch(tr.scrollIntoView());
 | 
				
			||||||
 | 
					        this.focusEditorInputField();
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    reloadState() {
 | 
					    reloadState() {
 | 
				
			||||||
      this.state = createState(this.value, this.placeholder, this.plugins);
 | 
					      this.state = createState(
 | 
				
			||||||
 | 
					        this.value,
 | 
				
			||||||
 | 
					        this.placeholder,
 | 
				
			||||||
 | 
					        this.plugins,
 | 
				
			||||||
 | 
					        this.openFileBrowser
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
      this.editorView.updateState(this.state);
 | 
					      this.editorView.updateState(this.state);
 | 
				
			||||||
      this.focusEditorInputField();
 | 
					      this.focusEditorInputField();
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -28,6 +28,17 @@
 | 
				
			|||||||
      "SAVING": "Saving...",
 | 
					      "SAVING": "Saving...",
 | 
				
			||||||
      "SAVED": "Saved"
 | 
					      "SAVED": "Saved"
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "ARTICLE_EDITOR": {
 | 
				
			||||||
 | 
					      "IMAGE_UPLOAD": {
 | 
				
			||||||
 | 
					        "TITLE": "Upload image",
 | 
				
			||||||
 | 
					        "UPLOADING": "Uploading...",
 | 
				
			||||||
 | 
					        "SUCCESS": "Image uploaded successfully",
 | 
				
			||||||
 | 
					        "ERROR": "Error while uploading image",
 | 
				
			||||||
 | 
					        "ERROR_FILE_SIZE": "Image size should be less than {size}MB",
 | 
				
			||||||
 | 
					        "ERROR_FILE_FORMAT": "Image format should be jpg, jpeg or png",
 | 
				
			||||||
 | 
					        "ERROR_FILE_DIMENSIONS": "Image dimensions should be less than 2000 x 2000"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "ARTICLE_SETTINGS": {
 | 
					    "ARTICLE_SETTINGS": {
 | 
				
			||||||
      "TITLE": "Article Settings",
 | 
					      "TITLE": "Article Settings",
 | 
				
			||||||
      "FORM": {
 | 
					      "FORM": {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -123,4 +123,19 @@ export const actions = {
 | 
				
			|||||||
      });
 | 
					      });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  attachImage: async (_, { portalSlug, file }) => {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const {
 | 
				
			||||||
 | 
					        data: { file_url: fileUrl },
 | 
				
			||||||
 | 
					      } = await articlesAPI.uploadImage({
 | 
				
			||||||
 | 
					        portalSlug,
 | 
				
			||||||
 | 
					        file,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					      return fileUrl;
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					      throwErrorMessage(error);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return '';
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -19,7 +19,7 @@
 | 
				
			|||||||
  },
 | 
					  },
 | 
				
			||||||
  "dependencies": {
 | 
					  "dependencies": {
 | 
				
			||||||
    "@braid/vue-formulate": "^2.5.2",
 | 
					    "@braid/vue-formulate": "^2.5.2",
 | 
				
			||||||
    "@chatwoot/prosemirror-schema": "https://github.com/chatwoot/prosemirror-schema.git#variable-mention",
 | 
					    "@chatwoot/prosemirror-schema": "https://github.com/chatwoot/prosemirror-schema.git",
 | 
				
			||||||
    "@chatwoot/utils": "^0.0.10",
 | 
					    "@chatwoot/utils": "^0.0.10",
 | 
				
			||||||
    "@hcaptcha/vue-hcaptcha": "^0.3.2",
 | 
					    "@hcaptcha/vue-hcaptcha": "^0.3.2",
 | 
				
			||||||
    "@june-so/analytics-next": "^1.36.5",
 | 
					    "@june-so/analytics-next": "^1.36.5",
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1391,9 +1391,9 @@
 | 
				
			|||||||
    is-url "^1.2.4"
 | 
					    is-url "^1.2.4"
 | 
				
			||||||
    nanoid "^2.1.11"
 | 
					    nanoid "^2.1.11"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"@chatwoot/prosemirror-schema@https://github.com/chatwoot/prosemirror-schema.git#variable-mention":
 | 
					"@chatwoot/prosemirror-schema@https://github.com/chatwoot/prosemirror-schema.git":
 | 
				
			||||||
  version "1.0.0"
 | 
					  version "1.0.0"
 | 
				
			||||||
  resolved "https://github.com/chatwoot/prosemirror-schema.git#2205f322e54517c415d54b013742838f2e5faf89"
 | 
					  resolved "https://github.com/chatwoot/prosemirror-schema.git#ebbf09d6ebd9138cdf2bb47257bb02ebbf01af53"
 | 
				
			||||||
  dependencies:
 | 
					  dependencies:
 | 
				
			||||||
    prosemirror-commands "^1.1.4"
 | 
					    prosemirror-commands "^1.1.4"
 | 
				
			||||||
    prosemirror-dropcursor "^1.3.2"
 | 
					    prosemirror-dropcursor "^1.3.2"
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user