mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-04-15 03:01:37 -05:00
Updated koenig-lexical upload function to handle multiple file types
refs https://github.com/TryGhost/Koenig/pull/491 - `<KoenigComposer>` now takes a `fileUpoader` object in place of `imageUploadFunction` - updated the upload functions in `koenig-lexical-editor.js` to match expected patterns, handle multiple files and file types, and return expected upload progress, result, and error details
This commit is contained in:
parent
9e42d1356a
commit
fb882e2e2a
1 changed files with 214 additions and 30 deletions
|
@ -4,6 +4,34 @@ import React, {Suspense} from 'react';
|
|||
import ghostPaths from 'ghost-admin/utils/ghost-paths';
|
||||
import {action} from '@ember/object';
|
||||
import {inject} from 'ghost-admin/decorators/inject';
|
||||
import {inject as service} from '@ember/service';
|
||||
|
||||
export const fileTypes = {
|
||||
image: {
|
||||
mimeTypes: ['image/gif', 'image/jpg', 'image/jpeg', 'image/png', 'image/svg+xml', 'image/webp'],
|
||||
extensions: ['gif', 'jpg', 'jpeg', 'png', 'svg', 'svgz', 'webp'],
|
||||
endpoint: '/images/upload/',
|
||||
resourceName: 'images'
|
||||
},
|
||||
video: {
|
||||
mimeTypes: ['video/mp4', 'video/webm', 'video/ogg'],
|
||||
extensions: ['mp4', 'webm', 'ogv'],
|
||||
endpoint: '/media/upload/',
|
||||
resourceName: 'media'
|
||||
},
|
||||
audio: {
|
||||
mimeTypes: ['audio/mp3', 'audio/mpeg', 'audio/ogg', 'audio/wav', 'audio/vnd.wav', 'audio/wave', 'audio/x-wav', 'audio/mp4', 'audio/x-m4a'],
|
||||
extensions: ['mp3', 'wav', 'ogg', 'm4a'],
|
||||
endpoint: '/media/upload/',
|
||||
resourceName: 'media'
|
||||
},
|
||||
mediaThumbnail: {
|
||||
mimeTypes: ['image/gif', 'image/jpg', 'image/jpeg', 'image/png', 'image/webp'],
|
||||
extensions: ['gif', 'jpg', 'jpeg', 'png', 'webp'],
|
||||
endpoint: '/media/thumbnail/upload/',
|
||||
resourceName: 'images'
|
||||
}
|
||||
};
|
||||
|
||||
class ErrorHandler extends React.Component {
|
||||
state = {
|
||||
|
@ -90,6 +118,8 @@ const KoenigEditor = (props) => {
|
|||
};
|
||||
|
||||
export default class KoenigLexicalEditor extends Component {
|
||||
@service ajax;
|
||||
|
||||
@inject config;
|
||||
|
||||
@action
|
||||
|
@ -121,39 +151,193 @@ export default class KoenigLexicalEditor extends Component {
|
|||
}
|
||||
};
|
||||
|
||||
const [uploadProgress, setUploadProgress] = React.useState(0);
|
||||
const useFileUpload = (type = 'image', {formData = {}, requestMethod = 'post'} = {}) => {
|
||||
const [progress, setProgress] = React.useState(0);
|
||||
const [isLoading, setLoading] = React.useState(false);
|
||||
const [errors, setErrors] = React.useState([]);
|
||||
const [filesNumber, setFilesNumber] = React.useState(0);
|
||||
|
||||
const uploadProgressHandler = (event) => {
|
||||
const percentComplete = (event.loaded / event.total) * 100;
|
||||
setUploadProgress(percentComplete);
|
||||
if (percentComplete === 100) {
|
||||
setUploadProgress(0);
|
||||
const progressTracker = React.useRef(new Map());
|
||||
|
||||
function updateProgress() {
|
||||
if (progressTracker.current.size === 0) {
|
||||
setProgress(0);
|
||||
return;
|
||||
}
|
||||
|
||||
let totalProgress = 0;
|
||||
|
||||
progressTracker.current.forEach(value => totalProgress += value);
|
||||
|
||||
setProgress(Math.round(totalProgress / progressTracker.current.size));
|
||||
}
|
||||
|
||||
// we only check the file extension by default because IE doesn't always
|
||||
// expose the mime-type, we'll rely on the API for final validation
|
||||
function defaultValidator(file) {
|
||||
let extensions = fileTypes[type].extensions;
|
||||
let [, extension] = (/(?:\.([^.]+))?$/).exec(file.name);
|
||||
|
||||
// if extensions is falsy exit early and accept all files
|
||||
if (!extensions) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!Array.isArray(extensions)) {
|
||||
extensions = extensions.split(',');
|
||||
}
|
||||
|
||||
if (!extension || extensions.indexOf(extension.toLowerCase()) === -1) {
|
||||
let validExtensions = `.${extensions.join(', .').toUpperCase()}`;
|
||||
return `The file type you uploaded is not supported. Please use ${validExtensions}`;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
const validate = (files = []) => {
|
||||
const validationResult = [];
|
||||
|
||||
for (let i = 0; i < files.length; i += 1) {
|
||||
let file = files[i];
|
||||
let result = defaultValidator(file);
|
||||
if (result === true) {
|
||||
continue;
|
||||
}
|
||||
|
||||
validationResult.push({fileName: file.name, message: result});
|
||||
}
|
||||
|
||||
return validationResult;
|
||||
};
|
||||
|
||||
const _uploadFile = async (file) => {
|
||||
progressTracker.current[file] = 0;
|
||||
|
||||
const fileFormData = new FormData();
|
||||
fileFormData.append('file', file, file.name);
|
||||
|
||||
Object.keys(formData || {}).forEach((key) => {
|
||||
fileFormData.append(key, formData[key]);
|
||||
});
|
||||
|
||||
const url = `${ghostPaths().apiRoot}${fileTypes[type].endpoint}`;
|
||||
|
||||
try {
|
||||
const response = await this.ajax[requestMethod](url, {
|
||||
data: fileFormData,
|
||||
processData: false,
|
||||
contentType: false,
|
||||
dataType: 'text',
|
||||
xhr: () => {
|
||||
const xhr = new window.XMLHttpRequest();
|
||||
|
||||
xhr.upload.addEventListener('progress', (event) => {
|
||||
if (event.lengthComputable) {
|
||||
progressTracker.current.set(file, (event.loaded / event.total) * 100);
|
||||
updateProgress();
|
||||
}
|
||||
}, false);
|
||||
|
||||
return xhr;
|
||||
}
|
||||
});
|
||||
|
||||
// force tracker progress to 100% in case we didn't get a final event
|
||||
progressTracker.current.set(file, 100);
|
||||
updateProgress();
|
||||
|
||||
let uploadResponse;
|
||||
let responseUrl;
|
||||
|
||||
try {
|
||||
uploadResponse = JSON.parse(response);
|
||||
} catch (error) {
|
||||
if (!(error instanceof SyntaxError)) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
if (uploadResponse) {
|
||||
const resource = uploadResponse[fileTypes[type].resourceName];
|
||||
if (resource && Array.isArray(resource) && resource[0]) {
|
||||
responseUrl = resource[0].url;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
url: responseUrl,
|
||||
fileName: file.name
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(error); // eslint-disable-line
|
||||
|
||||
// grab custom error message if present
|
||||
let message = error.payload?.errors?.[0]?.message || '';
|
||||
let context = error.payload?.errors?.[0]?.context || '';
|
||||
|
||||
// fall back to EmberData/ember-ajax default message for error type
|
||||
if (!message) {
|
||||
message = error.message;
|
||||
}
|
||||
|
||||
const errorResult = {
|
||||
message,
|
||||
context,
|
||||
fileName: file.name
|
||||
};
|
||||
|
||||
// TODO: check for or expose known error types?
|
||||
setErrors([...errors, errorResult]);
|
||||
}
|
||||
};
|
||||
|
||||
const upload = async (files = []) => {
|
||||
setFilesNumber(files.length);
|
||||
setLoading(true);
|
||||
|
||||
const validationResult = validate(files);
|
||||
|
||||
if (validationResult.length) {
|
||||
setErrors(validationResult);
|
||||
setLoading(false);
|
||||
setProgress(100);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
const uploadPromises = [];
|
||||
|
||||
for (let i = 0; i < files.length; i += 1) {
|
||||
const file = files[i];
|
||||
uploadPromises.push(_uploadFile(file));
|
||||
}
|
||||
|
||||
try {
|
||||
const uploadResult = await Promise.all(uploadPromises);
|
||||
setProgress(100);
|
||||
progressTracker.current.clear();
|
||||
|
||||
setLoading(false);
|
||||
|
||||
setErrors([]); // components expect array of objects: { fileName: string, message: string }[]
|
||||
|
||||
return uploadResult;
|
||||
} catch (error) {
|
||||
console.error(error); // eslint-disable-line no-console
|
||||
|
||||
setErrors([...errors, {message: error.message}]);
|
||||
setLoading(false);
|
||||
setProgress(100);
|
||||
progressTracker.current.clear();
|
||||
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return {progress, isLoading, upload, errors, filesNumber};
|
||||
};
|
||||
|
||||
async function imageUploader(files) {
|
||||
function uploadToUrl(formData, url) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', url);
|
||||
xhr.upload.onprogress = (event) => {
|
||||
uploadProgressHandler(event);
|
||||
};
|
||||
xhr.onload = () => resolve(xhr.response);
|
||||
xhr.onerror = () => reject(xhr.statusText);
|
||||
xhr.send(formData);
|
||||
});
|
||||
}
|
||||
const formData = new FormData();
|
||||
formData.append('file', files[0]);
|
||||
const url = `${ghostPaths().apiRoot}/images/upload/`;
|
||||
const response = await uploadToUrl(formData, url);
|
||||
const dataset = JSON.parse(response);
|
||||
const imageUrl = dataset?.images?.[0].url;
|
||||
return {
|
||||
src: imageUrl
|
||||
};
|
||||
}
|
||||
return (
|
||||
<div className={['koenig-react-editor', this.args.className].filter(Boolean).join(' ')}>
|
||||
<ErrorHandler>
|
||||
|
@ -162,7 +346,7 @@ export default class KoenigLexicalEditor extends Component {
|
|||
cardConfig={cardConfig}
|
||||
initialEditorState={this.args.lexical}
|
||||
onError={this.onError}
|
||||
imageUploadFunction={{imageUploader, uploadProgress}}
|
||||
fileUploader={{useFileUpload, fileTypes}}
|
||||
>
|
||||
<KoenigEditor
|
||||
onChange={this.args.onChange}
|
||||
|
|
Loading…
Add table
Reference in a new issue