feat: Add the ability to paste images to editor (#10072)
This commit is contained in:
@@ -8,6 +8,7 @@ import {
|
||||
EditorState,
|
||||
Selection,
|
||||
} from '@chatwoot/prosemirror-schema';
|
||||
import imagePastePlugin from '@chatwoot/prosemirror-schema/src/plugins/image';
|
||||
import { checkFileSizeLimit } from 'shared/helpers/FileHelper';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||
@@ -55,7 +56,7 @@ export default {
|
||||
return {
|
||||
editorView: null,
|
||||
state: undefined,
|
||||
plugins: [],
|
||||
plugins: [imagePastePlugin(this.handleImageUpload)],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -76,6 +77,7 @@ export default {
|
||||
this.reloadState();
|
||||
},
|
||||
},
|
||||
|
||||
created() {
|
||||
this.state = createState(
|
||||
this.value,
|
||||
@@ -95,6 +97,24 @@ export default {
|
||||
openFileBrowser() {
|
||||
this.$refs.imageUploadInput.click();
|
||||
},
|
||||
async handleImageUpload(url) {
|
||||
try {
|
||||
const fileUrl = await this.$store.dispatch(
|
||||
'articles/uploadExternalImage',
|
||||
{
|
||||
portalSlug: this.$route.params.portalSlug,
|
||||
url,
|
||||
}
|
||||
);
|
||||
|
||||
return fileUrl;
|
||||
} catch (error) {
|
||||
useAlert(
|
||||
this.$t('HELP_CENTER.ARTICLE_EDITOR.IMAGE_UPLOAD.UN_AUTHORIZED_ERROR')
|
||||
);
|
||||
return '';
|
||||
}
|
||||
},
|
||||
onFileChange() {
|
||||
const file = this.$refs.imageUploadInput.files[0];
|
||||
|
||||
|
||||
@@ -1,52 +1,93 @@
|
||||
import { uploadFile } from '../uploadHelper';
|
||||
import axios from 'axios';
|
||||
import { uploadExternalImage, uploadFile } from '../uploadHelper';
|
||||
|
||||
global.axios = axios;
|
||||
vi.mock('axios');
|
||||
|
||||
describe('#Upload Helpers', () => {
|
||||
describe('Upload Helpers', () => {
|
||||
afterEach(() => {
|
||||
// Cleaning up the mock after each test
|
||||
axios.post.mockReset();
|
||||
});
|
||||
|
||||
it('should send a POST request with correct data', async () => {
|
||||
const mockFile = new File(['dummy content'], 'example.png', {
|
||||
type: 'image/png',
|
||||
describe('uploadFile', () => {
|
||||
it('should send a POST request with correct data', async () => {
|
||||
const mockFile = new File(['dummy content'], 'example.png', {
|
||||
type: 'image/png',
|
||||
});
|
||||
const mockResponse = {
|
||||
data: {
|
||||
file_url: 'https://example.com/fileUrl',
|
||||
blob_key: 'blobKey123',
|
||||
blob_id: 'blobId456',
|
||||
},
|
||||
};
|
||||
|
||||
axios.post.mockResolvedValueOnce(mockResponse);
|
||||
|
||||
const result = await uploadFile(mockFile, '1602');
|
||||
|
||||
expect(axios.post).toHaveBeenCalledWith(
|
||||
'/api/v1/accounts/1602/upload',
|
||||
expect.any(FormData),
|
||||
{ headers: { 'Content-Type': 'multipart/form-data' } }
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
fileUrl: 'https://example.com/fileUrl',
|
||||
blobKey: 'blobKey123',
|
||||
blobId: 'blobId456',
|
||||
});
|
||||
});
|
||||
const mockResponse = {
|
||||
data: {
|
||||
file_url: 'https://example.com/fileUrl',
|
||||
blob_key: 'blobKey123',
|
||||
blob_id: 'blobId456',
|
||||
},
|
||||
};
|
||||
|
||||
axios.post.mockResolvedValueOnce(mockResponse);
|
||||
it('should handle errors', async () => {
|
||||
const mockFile = new File(['dummy content'], 'example.png', {
|
||||
type: 'image/png',
|
||||
});
|
||||
const mockError = new Error('Failed to upload');
|
||||
|
||||
const result = await uploadFile(mockFile, '1602');
|
||||
axios.post.mockRejectedValueOnce(mockError);
|
||||
|
||||
expect(axios.post).toHaveBeenCalledWith(
|
||||
'/api/v1/accounts/1602/upload',
|
||||
expect.any(FormData),
|
||||
{ headers: { 'Content-Type': 'multipart/form-data' } }
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
fileUrl: 'https://example.com/fileUrl',
|
||||
blobKey: 'blobKey123',
|
||||
blobId: 'blobId456',
|
||||
await expect(uploadFile(mockFile)).rejects.toThrow('Failed to upload');
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle errors', async () => {
|
||||
const mockFile = new File(['dummy content'], 'example.png', {
|
||||
type: 'image/png',
|
||||
describe('uploadExternalImage', () => {
|
||||
it('should send a POST request with correct data', async () => {
|
||||
const mockUrl = 'https://example.com/image.jpg';
|
||||
const mockResponse = {
|
||||
data: {
|
||||
file_url: 'https://example.com/fileUrl',
|
||||
blob_key: 'blobKey123',
|
||||
blob_id: 'blobId456',
|
||||
},
|
||||
};
|
||||
|
||||
axios.post.mockResolvedValueOnce(mockResponse);
|
||||
|
||||
const result = await uploadExternalImage(mockUrl, '1602');
|
||||
|
||||
expect(axios.post).toHaveBeenCalledWith(
|
||||
'/api/v1/accounts/1602/upload',
|
||||
{ external_url: mockUrl },
|
||||
{ headers: { 'Content-Type': 'application/json' } }
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
fileUrl: 'https://example.com/fileUrl',
|
||||
blobKey: 'blobKey123',
|
||||
blobId: 'blobId456',
|
||||
});
|
||||
});
|
||||
const mockError = new Error('Failed to upload');
|
||||
|
||||
axios.post.mockRejectedValueOnce(mockError);
|
||||
it('should handle errors', async () => {
|
||||
const mockUrl = 'https://example.com/image.jpg';
|
||||
const mockError = new Error('Failed to upload');
|
||||
|
||||
await expect(uploadFile(mockFile)).rejects.toThrow('Failed to upload');
|
||||
axios.post.mockRejectedValueOnce(mockError);
|
||||
|
||||
await expect(uploadExternalImage(mockUrl)).rejects.toThrow(
|
||||
'Failed to upload'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -19,26 +19,47 @@ const HEADERS = {
|
||||
* The function uses FormData to wrap the file and axios to send the request.
|
||||
*
|
||||
* @param {File} file - The file to be uploaded. It should be a File object (typically coming from a file input element).
|
||||
* @param {string} accountId - The account ID.
|
||||
* @returns {Promise} A promise that resolves with the server's response when the upload is successful, or rejects if there's an error.
|
||||
*/
|
||||
export async function uploadFile(file, accountId) {
|
||||
// Create a new FormData instance.
|
||||
let formData = new FormData();
|
||||
|
||||
if (!accountId) {
|
||||
accountId = window.location.pathname.split('/')[3];
|
||||
}
|
||||
|
||||
// Append the file to the FormData instance under the key 'attachment'.
|
||||
let formData = new FormData();
|
||||
formData.append('attachment', file);
|
||||
|
||||
// Use axios to send a POST request to the upload endpoint.
|
||||
const { data } = await axios.post(
|
||||
`/api/${API_VERSION}/accounts/${accountId}/upload`,
|
||||
formData,
|
||||
{
|
||||
headers: HEADERS,
|
||||
}
|
||||
{ headers: HEADERS }
|
||||
);
|
||||
|
||||
return {
|
||||
fileUrl: data.file_url,
|
||||
blobKey: data.blob_key,
|
||||
blobId: data.blob_id,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Uploads an image from an external URL.
|
||||
*
|
||||
* @param {string} url - The external URL of the image.
|
||||
* @param {string} accountId - The account ID.
|
||||
* @returns {Promise} A promise that resolves with the server's response.
|
||||
*/
|
||||
export async function uploadExternalImage(url, accountId) {
|
||||
if (!accountId) {
|
||||
accountId = window.location.pathname.split('/')[3];
|
||||
}
|
||||
|
||||
const { data } = await axios.post(
|
||||
`/api/${API_VERSION}/accounts/${accountId}/upload`,
|
||||
{ external_url: url },
|
||||
{ headers: { 'Content-Type': 'application/json' } }
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
"UPLOADING": "Uploading...",
|
||||
"SUCCESS": "Image uploaded successfully",
|
||||
"ERROR": "Error while uploading image",
|
||||
"UN_AUTHORIZED_ERROR": "You are not authorized to upload images",
|
||||
"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"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import articlesAPI from 'dashboard/api/helpCenter/articles';
|
||||
import { uploadFile } from 'dashboard/helper/uploadHelper';
|
||||
import { uploadExternalImage, uploadFile } from 'dashboard/helper/uploadHelper';
|
||||
import { throwErrorMessage } from 'dashboard/store/utils/api';
|
||||
|
||||
import types from '../../mutation-types';
|
||||
@@ -132,6 +132,11 @@ export const actions = {
|
||||
return fileUrl;
|
||||
},
|
||||
|
||||
uploadExternalImage: async (_, { url }) => {
|
||||
const { fileUrl } = await uploadExternalImage(url);
|
||||
return fileUrl;
|
||||
},
|
||||
|
||||
reorder: async (_, { portalSlug, categorySlug, reorderedGroup }) => {
|
||||
try {
|
||||
await articlesAPI.reorderArticles({
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import axios from 'axios';
|
||||
import { actions } from '../actions';
|
||||
import { uploadExternalImage, uploadFile } from 'dashboard/helper/uploadHelper';
|
||||
import * as types from '../../../mutation-types';
|
||||
import { uploadFile } from 'dashboard/helper/uploadHelper';
|
||||
import { actions } from '../actions';
|
||||
|
||||
vi.mock('dashboard/helper/uploadHelper');
|
||||
|
||||
@@ -211,4 +211,29 @@ describe('#actions', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('uploadExternalImage', () => {
|
||||
it('should upload the image from external URL and return the fileUrl', async () => {
|
||||
const mockUrl = 'https://example.com/image.jpg';
|
||||
const mockFileUrl = 'https://uploaded.example.com/image.jpg';
|
||||
uploadExternalImage.mockResolvedValueOnce({ fileUrl: mockFileUrl });
|
||||
|
||||
// When
|
||||
const result = await actions.uploadExternalImage({}, { url: mockUrl });
|
||||
|
||||
// Then
|
||||
expect(uploadExternalImage).toHaveBeenCalledWith(mockUrl);
|
||||
expect(result).toBe(mockFileUrl);
|
||||
});
|
||||
|
||||
it('should throw an error if the upload fails', async () => {
|
||||
const mockUrl = 'https://example.com/image.jpg';
|
||||
const mockError = new Error('Upload failed');
|
||||
uploadExternalImage.mockRejectedValueOnce(mockError);
|
||||
|
||||
await expect(
|
||||
actions.uploadExternalImage({}, { url: mockUrl })
|
||||
).rejects.toThrow('Upload failed');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user