mirror of
https://github.com/lingble/chatwoot.git
synced 2025-11-01 03:27:52 +00:00
feat: add a common upload endpoint (#7806)
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
This commit is contained in:
13
app/controllers/api/v1/upload_controller.rb
Normal file
13
app/controllers/api/v1/upload_controller.rb
Normal file
@@ -0,0 +1,13 @@
|
||||
class Api::V1::UploadController < Api::BaseController
|
||||
def create
|
||||
file_blob = ActiveStorage::Blob.create_and_upload!(
|
||||
key: nil,
|
||||
io: params[:attachment].tempfile,
|
||||
filename: params[:attachment].original_filename,
|
||||
content_type: params[:attachment].content_type
|
||||
)
|
||||
file_blob.save!
|
||||
|
||||
render json: { file_url: url_for(file_blob), blob_key: file_blob.key, blob_id: file_blob.id }
|
||||
end
|
||||
end
|
||||
@@ -9,14 +9,6 @@ class AutomationsAPI extends ApiClient {
|
||||
clone(automationId) {
|
||||
return axios.post(`${this.url}/${automationId}/clone`);
|
||||
}
|
||||
|
||||
attachment(file) {
|
||||
return axios.post(`${this.url}/attach_file`, file, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default new AutomationsAPI();
|
||||
|
||||
@@ -47,20 +47,6 @@ class ArticlesAPI extends PortalsAPI {
|
||||
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',
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
reorderArticles({ portalSlug, reorderedGroup, categorySlug }) {
|
||||
return axios.post(`${this.url}/${portalSlug}/articles/reorder`, {
|
||||
positions_hash: reorderedGroup,
|
||||
|
||||
@@ -61,11 +61,9 @@ export default {
|
||||
this.label = this.$t('AUTOMATION.ATTACHMENT.LABEL_UPLOADING');
|
||||
try {
|
||||
const file = event.target.files[0];
|
||||
const formData = new FormData();
|
||||
formData.append('attachment', file, file.name);
|
||||
const id = await this.$store.dispatch(
|
||||
'automations/uploadAttachment',
|
||||
formData
|
||||
file
|
||||
);
|
||||
this.$emit('input', [id]);
|
||||
this.uploadState = 'uploaded';
|
||||
|
||||
53
app/javascript/dashboard/helper/specs/uploadHelper.spec.js
Normal file
53
app/javascript/dashboard/helper/specs/uploadHelper.spec.js
Normal file
@@ -0,0 +1,53 @@
|
||||
import { uploadFile } from '../uploadHelper';
|
||||
import axios from 'axios';
|
||||
|
||||
// Mocking axios using jest-mock-axios
|
||||
global.axios = axios;
|
||||
jest.mock('axios');
|
||||
|
||||
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',
|
||||
});
|
||||
const mockResponse = {
|
||||
data: {
|
||||
file_url: 'https://example.com/fileUrl',
|
||||
blob_key: 'blobKey123',
|
||||
blob_id: 'blobId456',
|
||||
},
|
||||
};
|
||||
|
||||
axios.post.mockResolvedValueOnce(mockResponse);
|
||||
|
||||
const result = await uploadFile(mockFile);
|
||||
|
||||
expect(axios.post).toHaveBeenCalledWith(
|
||||
'/api/v1/upload',
|
||||
expect.any(FormData),
|
||||
{ headers: { 'Content-Type': 'multipart/form-data' } }
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
fileUrl: 'https://example.com/fileUrl',
|
||||
blobKey: 'blobKey123',
|
||||
blobId: 'blobId456',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle errors', async () => {
|
||||
const mockFile = new File(['dummy content'], 'example.png', {
|
||||
type: 'image/png',
|
||||
});
|
||||
const mockError = new Error('Failed to upload');
|
||||
|
||||
axios.post.mockRejectedValueOnce(mockError);
|
||||
|
||||
await expect(uploadFile(mockFile)).rejects.toThrow('Failed to upload');
|
||||
});
|
||||
});
|
||||
41
app/javascript/dashboard/helper/uploadHelper.js
Normal file
41
app/javascript/dashboard/helper/uploadHelper.js
Normal file
@@ -0,0 +1,41 @@
|
||||
/* global axios */
|
||||
|
||||
/**
|
||||
* Constants and Configuration
|
||||
*/
|
||||
|
||||
// Version for the API endpoint.
|
||||
const API_VERSION = 'v1';
|
||||
|
||||
// Default headers to be used in the axios request.
|
||||
const HEADERS = {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
};
|
||||
|
||||
/**
|
||||
* Uploads a file to the server.
|
||||
*
|
||||
* This function sends a POST request to a given API endpoint and uploads the specified file.
|
||||
* 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).
|
||||
* @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) {
|
||||
// Create a new FormData instance.
|
||||
let formData = new FormData();
|
||||
|
||||
// Append the file to the FormData instance under the key 'attachment'.
|
||||
formData.append('attachment', file);
|
||||
|
||||
// Use axios to send a POST request to the upload endpoint.
|
||||
const { data } = await axios.post(`/api/${API_VERSION}/upload`, formData, {
|
||||
headers: HEADERS,
|
||||
});
|
||||
|
||||
return {
|
||||
fileUrl: data.file_url,
|
||||
blobKey: data.blob_key,
|
||||
blobId: data.blob_id,
|
||||
};
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as MutationHelpers from 'shared/helpers/vuex/mutationHelpers';
|
||||
import types from '../mutation-types';
|
||||
import { uploadFile } from 'dashboard/helper/uploadHelper';
|
||||
import AutomationAPI from '../../api/automation';
|
||||
|
||||
export const state = {
|
||||
@@ -77,12 +78,8 @@ export const actions = {
|
||||
}
|
||||
},
|
||||
uploadAttachment: async (_, file) => {
|
||||
try {
|
||||
const { data } = await AutomationAPI.attachment(file);
|
||||
return data.blob_id;
|
||||
} catch (error) {
|
||||
throw new Error(error);
|
||||
}
|
||||
const { blobId } = await uploadFile(file);
|
||||
return blobId;
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import articlesAPI from 'dashboard/api/helpCenter/articles';
|
||||
import { uploadFile } from 'dashboard/helper/uploadHelper';
|
||||
import { throwErrorMessage } from 'dashboard/store/utils/api';
|
||||
|
||||
import types from '../../mutation-types';
|
||||
@@ -126,19 +127,9 @@ 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 '';
|
||||
attachImage: async (_, { file }) => {
|
||||
const { fileUrl } = await uploadFile(file);
|
||||
return fileUrl;
|
||||
},
|
||||
|
||||
reorder: async (_, { portalSlug, categorySlug, reorderedGroup }) => {
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import axios from 'axios';
|
||||
import { actions } from '../actions';
|
||||
import * as types from '../../../mutation-types';
|
||||
import { uploadFile } from 'dashboard/helper/uploadHelper';
|
||||
|
||||
jest.mock('dashboard/helper/uploadHelper');
|
||||
|
||||
const articleList = [
|
||||
{
|
||||
id: 1,
|
||||
@@ -180,4 +184,36 @@ describe('#actions', () => {
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('attachImage', () => {
|
||||
it('should upload the file and return the fileUrl', async () => {
|
||||
// Given
|
||||
const mockFile = new Blob(['test'], { type: 'image/png' });
|
||||
mockFile.name = 'test.png';
|
||||
|
||||
const mockFileUrl = 'https://test.com/test.png';
|
||||
uploadFile.mockResolvedValueOnce({ fileUrl: mockFileUrl });
|
||||
|
||||
// When
|
||||
const result = await actions.attachImage({}, { file: mockFile });
|
||||
|
||||
// Then
|
||||
expect(uploadFile).toHaveBeenCalledWith(mockFile);
|
||||
expect(result).toBe(mockFileUrl);
|
||||
});
|
||||
|
||||
it('should throw an error if the upload fails', async () => {
|
||||
// Given
|
||||
const mockFile = new Blob(['test'], { type: 'image/png' });
|
||||
mockFile.name = 'test.png';
|
||||
|
||||
const mockError = new Error('Upload failed');
|
||||
uploadFile.mockRejectedValueOnce(mockError);
|
||||
|
||||
// When & Then
|
||||
await expect(actions.attachImage({}, { file: mockFile })).rejects.toThrow(
|
||||
'Upload failed'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -226,6 +226,8 @@ Rails.application.routes.draw do
|
||||
# end of account scoped api routes
|
||||
# ----------------------------------
|
||||
|
||||
resources :upload, only: [:create]
|
||||
|
||||
namespace :integrations do
|
||||
resources :webhooks, only: [:create]
|
||||
end
|
||||
|
||||
42
spec/controllers/api/v1/upload_controller_spec.rb
Normal file
42
spec/controllers/api/v1/upload_controller_spec.rb
Normal file
@@ -0,0 +1,42 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'Api::V1::UploadController', type: :request do
|
||||
describe 'POST /api/v1/upload/' do
|
||||
let(:account) { create(:account) }
|
||||
let(:user) { create(:user, account: account) }
|
||||
|
||||
it 'uploads the image when authorized' do
|
||||
file = fixture_file_upload(Rails.root.join('spec/assets/avatar.png'), 'image/png')
|
||||
|
||||
post '/api/v1/upload/',
|
||||
headers: user.create_new_auth_token,
|
||||
params: { attachment: file }
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
blob = response.parsed_body
|
||||
|
||||
expect(blob['errors']).to be_nil
|
||||
|
||||
expect(blob['file_url']).to be_present
|
||||
expect(blob['blob_key']).to be_present
|
||||
expect(blob['blob_id']).to be_present
|
||||
end
|
||||
|
||||
it 'does not upload when un-authorized' do
|
||||
file = fixture_file_upload(Rails.root.join('spec/assets/avatar.png'), 'image/png')
|
||||
|
||||
post '/api/v1/upload/',
|
||||
headers: {},
|
||||
params: { attachment: file }
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
blob = response.parsed_body
|
||||
|
||||
expect(blob['errors']).to be_present
|
||||
|
||||
expect(blob['file_url']).to be_nil
|
||||
expect(blob['blob_key']).to be_nil
|
||||
expect(blob['blob_id']).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user