UNPKG

@craftercms/studio-ui

Version:

Services, components, models & utils to build CrafterCMS authoring extensions.

347 lines (345 loc) 11.8 kB
/* * Copyright (C) 2007-2022 Crafter Software Corporation. All Rights Reserved. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License version 3 as published by * the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ /* * Copyright (C) 2007-2022 Crafter Software Corporation. All Rights Reserved. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as published by * the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ import React, { useEffect, useMemo, useRef, useState } from 'react'; import Core from '@uppy/core'; import XHRUpload from '@uppy/xhr-upload'; import ProgressBar from '@uppy/progress-bar'; import Form from '@uppy/form'; import { defineMessages, useIntl } from 'react-intl'; import '@uppy/core/src/style.scss'; import '@uppy/progress-bar/src/style.scss'; import '@uppy/file-input/src/style.scss'; import { makeStyles } from 'tss-react/mui'; import { getGlobalHeaders } from '../../utils/ajax'; import { validateActionPolicy } from '../../services/sites'; import ConfirmDialog from '../ConfirmDialog/ConfirmDialog'; import { useDispatch } from 'react-redux'; import Typography from '@mui/material/Typography'; import Button from '@mui/material/Button'; import useSiteUIConfig from '../../hooks/useSiteUIConfig'; const messages = defineMessages({ chooseFile: { id: 'fileUpload.chooseFile', defaultMessage: 'Choose File' }, validatingFile: { id: 'fileUpload.validatingFile', defaultMessage: 'Validating File' }, uploadingFile: { id: 'fileUpload.uploadingFile', defaultMessage: 'Uploading File' }, uploadedFile: { id: 'fileUpload.uploadedFile', defaultMessage: 'Uploaded File' }, selectFileMessage: { id: 'fileUpload.selectFileMessage', defaultMessage: 'Please select a file to upload' }, createPolicy: { id: 'fileUpload.createPolicy', defaultMessage: 'The upload file name goes against project policies. Suggested modified file name is: "{name}". Would you like to use the suggested name?' }, policyError: { id: 'fileUpload.policyError', defaultMessage: 'The upload file name goes against project policies.' } }); const singleFileUploadStyles = makeStyles()(() => ({ fileNameTrimmed: { overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }, description: { margin: '10px 0' }, input: { display: 'none !important' }, inputContainer: { marginBottom: '10px' } })); export function SingleFileUpload(props) { const { url = '/studio/api/1/services/api/1/content/write-content.json', formTarget = '#asset_upload_form', onUploadStart, onComplete, onError, customFileName, fileTypes, path, site } = props; const { formatMessage } = useIntl(); const dispatch = useDispatch(); const [description, setDescription] = useState(formatMessage(messages.selectFileMessage)); const [file, setFile] = useState(null); const fileRef = useRef(null); const [suggestedName, setSuggestedName] = useState(null); const suggestedNameRef = useRef(null); const [fileNameErrorClass, setFileNameErrorClass] = useState(); const [disableInput, setDisableInput] = useState(false); const { upload } = useSiteUIConfig(); const [confirm, setConfirm] = useState(null); fileRef.current = file; suggestedNameRef.current = suggestedName; const { classes, cx } = singleFileUploadStyles(); const uppy = useMemo( () => new Core( Object.assign( Object.assign( Object.assign({ autoProceed: false }, fileTypes ? { restrictions: { allowedFileTypes: fileTypes } } : {}), customFileName ? { onBeforeFileAdded: (currentFile) => { return Object.assign(Object.assign({}, currentFile), { name: customFileName, meta: Object.assign(Object.assign({}, currentFile.meta), { name: customFileName }) }); } } : {} ), { onBeforeUpload: (files) => { if (suggestedNameRef.current) { const updatedFiles = Object.assign(Object.assign({}, files), { [fileRef.current.id]: Object.assign(Object.assign({}, files[fileRef.current.id]), { name: suggestedNameRef.current, meta: Object.assign(Object.assign({}, files[fileRef.current.id].meta), { name: suggestedNameRef.current }) }) }); setSuggestedName(null); return updatedFiles; } else { return files; } } } ) ), [fileTypes, customFileName] ); useEffect(() => { const instance = uppy .use(Form, { target: formTarget, getMetaFromForm: true, addResultToForm: true, submitOnSuccess: false, triggerUploadOnSubmit: false }) .use(ProgressBar, { target: '.uppy-progress-bar', hideAfterFinish: false }) .use(XHRUpload, { endpoint: url, formData: true, fieldName: 'file', timeout: upload.timeout, headers: getGlobalHeaders(), getResponseData: (responseText, response) => response }); return () => { // https://uppy.io/docs/uppy/#uppy-close instance.reset(); instance.close(); }; }, [uppy, formTarget, url, upload.timeout]); useEffect(() => { const onUploadSuccess = (file) => { setDescription(`${formatMessage(messages.uploadedFile)}:`); }; const onCompleteUpload = (result) => { onComplete === null || onComplete === void 0 ? void 0 : onComplete(result); setDisableInput(false); }; uppy.on('upload-success', onUploadSuccess); uppy.on('complete', onCompleteUpload); return () => { uppy.off('upload-success', onUploadSuccess); uppy.off('complete', onCompleteUpload); }; }, [onComplete, dispatch, formatMessage, path, uppy]); useEffect(() => { const onUploadError = (file, error, response) => { uppy.cancelAll(); setFileNameErrorClass('text-danger'); onError === null || onError === void 0 ? void 0 : onError({ file, error, response }); setDisableInput(false); }; uppy.on('upload-error', onUploadError); return () => { uppy.off('upload-error', onUploadError); }; }, [onError, uppy]); useEffect(() => { const onFileAdded = (file) => { setDescription(`${formatMessage(messages.validatingFile)}:`); setFile(file); setFileNameErrorClass(''); validateActionPolicy(site, { type: 'CREATE', target: path + file.name }).subscribe(({ allowed, modifiedValue }) => { if (allowed) { if (modifiedValue) { const modifiedName = modifiedValue.replace(path, ''); setConfirm({ body: formatMessage(messages.createPolicy, { name: modifiedName }) }); setSuggestedName(modifiedName); } else { setDisableInput(true); uppy.upload(); setDescription(`${formatMessage(messages.uploadingFile)}:`); onUploadStart === null || onUploadStart === void 0 ? void 0 : onUploadStart(); } } else { setConfirm({ error: true, body: formatMessage(messages.policyError) }); } }); }; uppy.on('file-added', onFileAdded); return () => { uppy.off('file-added', onFileAdded); }; }, [onUploadStart, formatMessage, path, site, uppy]); const onConfirm = () => { uppy.upload().then(() => {}); setSuggestedName(null); setDescription(`${formatMessage(messages.uploadingFile)}:`); onUploadStart === null || onUploadStart === void 0 ? void 0 : onUploadStart(); setConfirm(null); }; const onConfirmCancel = () => { var _a; (_a = document.querySelector('.uppy-FileInput-btn')) === null || _a === void 0 ? void 0 : _a.removeAttribute('disabled'); uppy.removeFile(file.id); setFile(null); setConfirm(null); setDescription(formatMessage(messages.selectFileMessage)); }; const onChange = ({ nativeEvent: event }) => { const files = Array.from(event.target.files); files.forEach((file) => { try { uppy.addFile({ source: 'file input', name: file.name, type: file.type, data: file }); } catch (err) { console.error(err); } }); }; // Clear input current value on click, so if you need to select the same file (in case of an error) it will re-trigger // the change/file selection. const onInputClick = (event) => { const element = event.target; element.value = ''; }; return React.createElement( React.Fragment, null, React.createElement( 'form', { id: 'asset_upload_form' }, React.createElement('input', { type: 'hidden', name: 'path', value: path }), React.createElement('input', { type: 'hidden', name: 'site', value: site }) ), React.createElement('div', { className: 'uppy-progress-bar' }), React.createElement( 'div', { className: 'uploaded-files' }, React.createElement( Typography, { variant: 'subtitle1', component: 'h2', className: classes.description }, description, file && React.createElement( 'em', { className: cx('single-file-upload--filename', fileNameErrorClass, classes.fileNameTrimmed), title: file.name }, file.name ) ), React.createElement( 'div', { className: classes.inputContainer }, React.createElement('input', { accept: fileTypes === null || fileTypes === void 0 ? void 0 : fileTypes.join(','), className: classes.input, id: 'contained-button-file', type: 'file', onChange: onChange, onClick: onInputClick }), React.createElement( 'label', { htmlFor: 'contained-button-file' }, React.createElement( Button, { variant: 'outlined', component: 'span', disabled: disableInput }, formatMessage(messages.chooseFile) ) ) ) ), React.createElement(ConfirmDialog, { open: Boolean(confirm), body: confirm === null || confirm === void 0 ? void 0 : confirm.body, onOk: (confirm === null || confirm === void 0 ? void 0 : confirm.error) ? onConfirmCancel : onConfirm, onCancel: (confirm === null || confirm === void 0 ? void 0 : confirm.error) ? null : onConfirmCancel, disableEnforceFocus: true }) ); } export default SingleFileUpload;