feat: Add the ability to paste images to editor (#10072)
This commit is contained in:
parent
bb74c621b5
commit
2c17c95eab
@ -8,6 +8,7 @@ import {
|
|||||||
EditorState,
|
EditorState,
|
||||||
Selection,
|
Selection,
|
||||||
} from '@chatwoot/prosemirror-schema';
|
} from '@chatwoot/prosemirror-schema';
|
||||||
|
import imagePastePlugin from '@chatwoot/prosemirror-schema/src/plugins/image';
|
||||||
import { checkFileSizeLimit } from 'shared/helpers/FileHelper';
|
import { checkFileSizeLimit } from 'shared/helpers/FileHelper';
|
||||||
import { useAlert } from 'dashboard/composables';
|
import { useAlert } from 'dashboard/composables';
|
||||||
import { useUISettings } from 'dashboard/composables/useUISettings';
|
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||||
@ -55,7 +56,7 @@ export default {
|
|||||||
return {
|
return {
|
||||||
editorView: null,
|
editorView: null,
|
||||||
state: undefined,
|
state: undefined,
|
||||||
plugins: [],
|
plugins: [imagePastePlugin(this.handleImageUpload)],
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@ -76,6 +77,7 @@ export default {
|
|||||||
this.reloadState();
|
this.reloadState();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
created() {
|
created() {
|
||||||
this.state = createState(
|
this.state = createState(
|
||||||
this.value,
|
this.value,
|
||||||
@ -95,6 +97,24 @@ export default {
|
|||||||
openFileBrowser() {
|
openFileBrowser() {
|
||||||
this.$refs.imageUploadInput.click();
|
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() {
|
onFileChange() {
|
||||||
const file = this.$refs.imageUploadInput.files[0];
|
const file = this.$refs.imageUploadInput.files[0];
|
||||||
|
|
||||||
|
|||||||
@ -1,15 +1,15 @@
|
|||||||
import { uploadFile } from '../uploadHelper';
|
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
import { uploadExternalImage, uploadFile } from '../uploadHelper';
|
||||||
|
|
||||||
global.axios = axios;
|
global.axios = axios;
|
||||||
vi.mock('axios');
|
vi.mock('axios');
|
||||||
|
|
||||||
describe('#Upload Helpers', () => {
|
describe('Upload Helpers', () => {
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
// Cleaning up the mock after each test
|
|
||||||
axios.post.mockReset();
|
axios.post.mockReset();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('uploadFile', () => {
|
||||||
it('should send a POST request with correct data', async () => {
|
it('should send a POST request with correct data', async () => {
|
||||||
const mockFile = new File(['dummy content'], 'example.png', {
|
const mockFile = new File(['dummy content'], 'example.png', {
|
||||||
type: 'image/png',
|
type: 'image/png',
|
||||||
@ -49,4 +49,45 @@ describe('#Upload Helpers', () => {
|
|||||||
|
|
||||||
await expect(uploadFile(mockFile)).rejects.toThrow('Failed to upload');
|
await expect(uploadFile(mockFile)).rejects.toThrow('Failed to upload');
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle errors', async () => {
|
||||||
|
const mockUrl = 'https://example.com/image.jpg';
|
||||||
|
const mockError = new Error('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.
|
* 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 {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.
|
* @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) {
|
export async function uploadFile(file, accountId) {
|
||||||
// Create a new FormData instance.
|
|
||||||
let formData = new FormData();
|
|
||||||
|
|
||||||
if (!accountId) {
|
if (!accountId) {
|
||||||
accountId = window.location.pathname.split('/')[3];
|
accountId = window.location.pathname.split('/')[3];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Append the file to the FormData instance under the key 'attachment'.
|
// Append the file to the FormData instance under the key 'attachment'.
|
||||||
|
let formData = new FormData();
|
||||||
formData.append('attachment', file);
|
formData.append('attachment', file);
|
||||||
|
|
||||||
// Use axios to send a POST request to the upload endpoint.
|
|
||||||
const { data } = await axios.post(
|
const { data } = await axios.post(
|
||||||
`/api/${API_VERSION}/accounts/${accountId}/upload`,
|
`/api/${API_VERSION}/accounts/${accountId}/upload`,
|
||||||
formData,
|
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 {
|
return {
|
||||||
|
|||||||
@ -41,6 +41,7 @@
|
|||||||
"UPLOADING": "Uploading...",
|
"UPLOADING": "Uploading...",
|
||||||
"SUCCESS": "Image uploaded successfully",
|
"SUCCESS": "Image uploaded successfully",
|
||||||
"ERROR": "Error while uploading image",
|
"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_SIZE": "Image size should be less than {size}MB",
|
||||||
"ERROR_FILE_FORMAT": "Image format should be jpg, jpeg or png",
|
"ERROR_FILE_FORMAT": "Image format should be jpg, jpeg or png",
|
||||||
"ERROR_FILE_DIMENSIONS": "Image dimensions should be less than 2000 x 2000"
|
"ERROR_FILE_DIMENSIONS": "Image dimensions should be less than 2000 x 2000"
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import articlesAPI from 'dashboard/api/helpCenter/articles';
|
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 { throwErrorMessage } from 'dashboard/store/utils/api';
|
||||||
|
|
||||||
import types from '../../mutation-types';
|
import types from '../../mutation-types';
|
||||||
@ -132,6 +132,11 @@ export const actions = {
|
|||||||
return fileUrl;
|
return fileUrl;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
uploadExternalImage: async (_, { url }) => {
|
||||||
|
const { fileUrl } = await uploadExternalImage(url);
|
||||||
|
return fileUrl;
|
||||||
|
},
|
||||||
|
|
||||||
reorder: async (_, { portalSlug, categorySlug, reorderedGroup }) => {
|
reorder: async (_, { portalSlug, categorySlug, reorderedGroup }) => {
|
||||||
try {
|
try {
|
||||||
await articlesAPI.reorderArticles({
|
await articlesAPI.reorderArticles({
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { actions } from '../actions';
|
import { uploadExternalImage, uploadFile } from 'dashboard/helper/uploadHelper';
|
||||||
import * as types from '../../../mutation-types';
|
import * as types from '../../../mutation-types';
|
||||||
import { uploadFile } from 'dashboard/helper/uploadHelper';
|
import { actions } from '../actions';
|
||||||
|
|
||||||
vi.mock('dashboard/helper/uploadHelper');
|
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');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -31,7 +31,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@braid/vue-formulate": "^2.5.2",
|
"@braid/vue-formulate": "^2.5.2",
|
||||||
"@chatwoot/ninja-keys": "1.2.3",
|
"@chatwoot/ninja-keys": "1.2.3",
|
||||||
"@chatwoot/prosemirror-schema": "1.0.16",
|
"@chatwoot/prosemirror-schema": "1.0.17",
|
||||||
"@chatwoot/utils": "^0.0.25",
|
"@chatwoot/utils": "^0.0.25",
|
||||||
"@hcaptcha/vue-hcaptcha": "^0.3.2",
|
"@hcaptcha/vue-hcaptcha": "^0.3.2",
|
||||||
"@june-so/analytics-next": "^2.0.0",
|
"@june-so/analytics-next": "^2.0.0",
|
||||||
|
|||||||
@ -2914,10 +2914,10 @@
|
|||||||
hotkeys-js "3.8.7"
|
hotkeys-js "3.8.7"
|
||||||
lit "2.2.6"
|
lit "2.2.6"
|
||||||
|
|
||||||
"@chatwoot/prosemirror-schema@1.0.16":
|
"@chatwoot/prosemirror-schema@1.0.17":
|
||||||
version "1.0.16"
|
version "1.0.17"
|
||||||
resolved "https://registry.yarnpkg.com/@chatwoot/prosemirror-schema/-/prosemirror-schema-1.0.16.tgz#63cfd4cff0f138466a95fedba5d237409a0b418e"
|
resolved "https://registry.yarnpkg.com/@chatwoot/prosemirror-schema/-/prosemirror-schema-1.0.17.tgz#5c04a94c8200b303d943369b2534730d5ba15a64"
|
||||||
integrity sha512-qFPHPrfkG14vB0C/8+kF6huAvz4c2h6s07WKznZqUZ5CgJYd+fESQ49CScOYwOTO6luV4ghILv9kAVRjiaenOQ==
|
integrity sha512-3hwBSMdb5a+Ilct0fNSlAvQu/9+pnyI/FoGshVRC9Hn1tCdZdS7LUlxOuDweta3HneVclhE+SxJN5lM33v5WGw==
|
||||||
dependencies:
|
dependencies:
|
||||||
markdown-it-sup "^1.0.0"
|
markdown-it-sup "^1.0.0"
|
||||||
prosemirror-commands "^1.1.4"
|
prosemirror-commands "^1.1.4"
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user