@craftercms/studio-ui
Version:
Services, components, models & utils to build CrafterCMS authoring extensions.
347 lines (345 loc) • 11.8 kB
JavaScript
/*
* 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;