diff --git a/apps/admin-x-design-system/src/utils/format-url.ts b/apps/admin-x-design-system/src/utils/format-url.ts index 68bf9ac4002..c9912b02a2b 100644 --- a/apps/admin-x-design-system/src/utils/format-url.ts +++ b/apps/admin-x-design-system/src/utils/format-url.ts @@ -1,4 +1,4 @@ -import isEmail from 'validator/es/lib/isEmail'; +import isEmail from 'validator/es/lib/isEmail.js'; export const formatUrl = (value: string, baseUrl?: string, nullable?: boolean) => { if (nullable && !value) { diff --git a/ghost/admin/app/components/koenig-lexical-editor.js b/ghost/admin/app/components/koenig-lexical-editor.js index 4cf5049021d..cbbb4502014 100644 --- a/ghost/admin/app/components/koenig-lexical-editor.js +++ b/ghost/admin/app/components/koenig-lexical-editor.js @@ -1,46 +1,13 @@ import * as Sentry from '@sentry/ember'; import Component from '@glimmer/component'; import React, {Suspense} from 'react'; -import ghostPaths from 'ghost-admin/utils/ghost-paths'; import moment from 'moment-timezone'; import {action} from '@ember/object'; import {didCancel, task} from 'ember-concurrency'; import {inject} from 'ghost-admin/decorators/inject'; +import {koenigFileUploadTypes, useKoenigFileUpload} from '@tryghost/admin-x-framework/hooks'; import {inject as service} from '@ember/service'; -// TODO(NY-1097) This should be DRYed out with what's in the `useFileUpload` hook. -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/', - requestMethod: 'put', - resourceName: 'media' - }, - file: { - endpoint: '/files/upload/', - resourceName: 'files' - } -}; - function LockIcon({...props}) { return ( @@ -469,202 +436,13 @@ export default class KoenigLexicalEditor extends Component { }; const cardConfig = Object.assign({}, defaultCardConfig, props.cardConfig, {pinturaConfig: this.pinturaConfig, visibilitySettings: defaultCardConfig.visibilitySettings}); - // TODO(NY-1097) This should be DRYed out with the other `useFileUpload` hook. - const useFileUpload = (type = 'image') => { - const [progress, setProgress] = React.useState(0); - const [isLoading, setLoading] = React.useState(false); - const [errors, setErrors] = React.useState([]); - const [filesNumber, setFilesNumber] = React.useState(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) { - // if type is file we don't need to validate since the card can accept any file type - if (type === 'file') { - return true; - } - const extensions = fileTypes[type].extensions; - const [, extension] = (/(?:\.([^.]+))?$/).exec(file.name) ?? []; - - // if extensions is falsy exit early and accept all files - if (!extensions) { - return true; - } - - 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, {formData = {}} = {}) => { - progressTracker.current.set(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 requestMethod = fileTypes[type].requestMethod || 'post'; - 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; - } - - // TODO: check for or expose known error types? - const errorResult = { - message, - context, - fileName: file.name - }; - - throw errorResult; - } - }; - - const upload = async (files = [], options = {}) => { - 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, options)); - } - - 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, error]); - setLoading(false); - setProgress(100); - progressTracker.current.clear(); - - return null; - } - }; - - return {progress, isLoading, upload, errors, filesNumber}; - }; - const KGEditorComponent = ({isInitInstance}) => { return (