UNPKG

@craftercms/studio-ui

Version:

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

487 lines (485 loc) 15.9 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 { Dashboard } from '@craftercms/uppy'; import React, { useCallback, useEffect, useRef } from 'react'; import { makeStyles } from 'tss-react/mui'; import palette from '../../styles/palette'; import { validateActionPolicy } from '../../services/sites'; import { defineMessages, useIntl } from 'react-intl'; import { showSystemNotification } from '../../state/actions/system'; import { useDispatch } from 'react-redux'; import { alpha } from '@mui/material'; import { Subject } from 'rxjs'; import { debounceTime } from 'rxjs/operators'; const useStyles = makeStyles()((theme) => ({ dashboard: { paddingBottom: 0, '& .uppy-Dashboard-inner': { border: 0, borderRadius: 0, backgroundColor: theme.palette.background.default }, '& .uppy-Dashboard-files, & .uppy-size--md .uppy-Dashboard-files': { padding: '15px' }, '& .uppy-Dashboard-browse': { color: theme.palette.primary.main, '&:hover': { textDecoration: 'underline' } }, '& .uppy-Dashboard-dropFilesHereHint': { border: `1px dashed ${theme.palette.primary.main}`, color: theme.palette.primary.main }, '& [data-uppy-drag-drop-supported=true] .uppy-Dashboard-AddFiles': { border: 0 }, // region header '& .uppy-dashboard-header': { backgroundColor: theme.palette.background.paper, minHeight: '64px', color: theme.palette.text.primary, padding: '8px', display: 'flex', alignItems: 'center', borderBottom: '1px solid rgba(0, 0, 0, 0.12)', '& .uppy-dashboard-header-actions': { marginLeft: 'auto' }, '& .uppy-dashboard-header-title': { padding: '0 8px', margin: 0, fontSize: '1.25rem', fontFamily: 'Source Sans Pro, Open Sans, sans-serif', fontWeight: 600, lineHeight: '1.6' } }, // endregion // region item card '& .uppy-dashboard-item-card': { display: 'flex', position: 'relative' }, '& .uppy-dashboard-item-validating': { color: 'rgba(0, 0, 0, 0.54)' }, '& .uppy-dashboard-item-preview': { width: '120px', height: '120px', position: 'relative' }, '& .uppy-Dashboard-Item-name': { display: 'flex', alignItems: 'center', '& .suggested-file-name': { display: 'flex', alignItems: 'center' }, '& .suggested-icon': { fill: palette.gray.medium6, width: '20px', height: '20px', marginRight: '5px', marginLeft: '5px' } }, '& .uppy-dashboard-site-policy-warning': { color: theme.palette.error.main, display: 'flex', alignItems: 'center', fontSize: '0.875rem', marginBottom: '5px', '& .warning-icon': { fill: theme.palette.error.main, width: '20px', height: '20px', marginRight: '5px' } }, '& .uppy-Dashboard-Item-previewInnerWrap': { backgroundColor: `${theme.palette.divider} !important`, borderTopRightRadius: 0, borderBottomRightRadius: 0 }, '& .uppy-dashboard-item-statusType': { display: 'inline-block' }, '& .uppy-dashboard-item-fileInfoAndButtons': { padding: '15px', display: 'flex', alignItems: 'center', flexGrow: 1 }, '& .uppy-dashboard-item-progress': { position: 'absolute', bottom: 0, left: 0, right: 0, '& .uppy-file-progress-bar': { backgroundColor: theme.palette.primary.main, height: '2px', transition: 'background-color, width 0.3s ease-out', borderBottomRightRadius: '5px', borderBottomLeftRadius: '5px', '&.complete': { backgroundColor: theme.palette.success.main }, '&.error': { backgroundColor: theme.palette.error.main } } }, '& .item-name-valid': { color: theme.palette.success.main }, '& .item-name-invalid': { textDecoration: 'line-through', color: theme.palette.text.primary }, // endregion // region File list '& .uppy-Dashboard-files': {}, '& .uppy-Dashboard-AddFiles-title': { color: theme.palette.text.primary, position: 'relative', zIndex: 1 }, '& .uppy-dashboard-files-list-row': { marginBottom: '20px', '&:last-child': { marginBottom: 0 } }, // endregion // region Footer '& .uppy-DashboardContent-bar': { position: 'relative', borderTop: `1px solid ${theme.palette.divider}`, backgroundColor: theme.palette.background.paper, borderBottom: 0 }, '& .uppy-dashboard-progress-indicator': { position: 'absolute', top: 0, left: 0, right: 0, '& .uppy-file-progress-bar': { backgroundColor: theme.palette.primary.main, height: '2px', transition: 'background-color, width 0.3s ease-out', '&.complete': { backgroundColor: theme.palette.success.main }, '&.error': { backgroundColor: theme.palette.error.main } } }, '& .uppy-dashboard-validation-buttons': { marginLeft: 'auto', '& button:first-child': { marginRight: '10px' } }, '& .uppy-dashboard-right-buttons': { marginLeft: 'auto', '& button:last-child': { marginLeft: '10px' } }, '& .uppy-dashboard-button-base': { display: 'inline-flex', alignItems: 'center', justifyContent: 'center', position: 'relative', boxSizing: 'border-box', WebkitTapHighlightColor: 'transparent', backgroundColor: 'transparent', outline: 0, border: 0, margin: 0, borderRadius: 0, padding: 0, cursor: 'pointer', userSelect: 'none', verticalAlign: 'middle', MozAppearance: 'none', WebkitAppearance: 'none', textDecoration: 'none', color: 'inherit', '&::-moz-focus-inner': { borderStyle: 'none' }, '&:disabled': { pointerEvents: 'none', cursor: 'default' }, '@media print': { colorAdjust: 'exact' } }, '& .uppy-dashboard-text-button': Object.assign(Object.assign({}, theme.typography.button), { minWidth: 64, padding: '6px 8px', borderRadius: theme.shape.borderRadius, transition: theme.transitions.create(['background-color', 'box-shadow', 'border-color', 'color'], { duration: theme.transitions.duration.short }), color: theme.palette.primary.main, '&:hover': { textDecoration: 'none', // backgroundColor: alpha(theme.palette.text.primary, theme.palette.action.hoverOpacity), // Reset on touch devices, it doesn't add specificity '@media (hover: none)': { backgroundColor: 'transparent' }, backgroundColor: alpha(theme.palette.primary.main, theme.palette.action.hoverOpacity) }, '&:disabled': { color: theme.palette.action.disabled, pointerEvents: 'none', cursor: 'default' } }), '& .uppy-dashboard-icon-button': { textAlign: 'center', flex: '0 0 auto', borderRadius: '50%', overflow: 'visible', color: theme.palette.action.active, transition: theme.transitions.create('background-color', { duration: theme.transitions.duration.shortest }), padding: 12, fontSize: theme.typography.pxToRem(28), '&:hover': { backgroundColor: alpha(theme.palette.action.active, theme.palette.action.hoverOpacity), '@media (hover: none)': { backgroundColor: 'transparent' } }, '&.edgeEnd': { marginRight: '-12px' } }, '& .uppy-dashboard-svg-icon': { userSelect: 'none', width: '1em', height: '1em', display: 'inline-block', fill: 'currentColor', flexShrink: 0, fontSize: theme.typography.pxToRem(24), transition: theme.transitions.create('fill', { duration: theme.transitions.duration.shorter }) } // endregion } })); const translations = defineMessages({ cancelPending: { id: 'uppyDashboard.cancelPending', defaultMessage: 'Cancel pending' }, clearCompleted: { id: 'uppyDashboard.clearCompleted', defaultMessage: 'Clear completed' }, clear: { id: 'words.clear', defaultMessage: 'Clear' }, addMore: { id: 'uppyDashboard.addMore', defaultMessage: 'Add more' }, rejectAll: { id: 'uppyDashboard.rejectAll', defaultMessage: 'Reject all changes' }, acceptAll: { id: 'uppyDashboard.acceptAll', defaultMessage: 'Accept all changes' }, validating: { id: 'words.validating', defaultMessage: 'Validating' }, validateAndRetry: { id: 'uppyDashboard.validateAndRetry', defaultMessage: 'Accept changes and upload' }, removeFile: { id: 'uppyDashboard.removeFile', defaultMessage: 'Remove file' }, back: { id: 'words.back', defaultMessage: 'Back' }, addingMoreFiles: { id: 'uppyDashboard.addingMoreFiles', defaultMessage: 'Adding more files' }, renamingFromTo: { id: 'uppyDashboard.renamingFromTo', defaultMessage: "Renaming from %'{from}' to %'{to}'" }, close: { id: 'words.close', defaultMessage: 'Close' }, minimize: { id: 'words.minimize', defaultMessage: 'Minimize' }, maxFiles: { id: 'uppyDashboard.maxFiles', defaultMessage: '{maxFiles} max.' }, maxActiveUploadsReached: { id: 'uppyDashboard.maxActiveUploadsReached', defaultMessage: '{maxFiles} maximum active uploads reached. Excess has been discarded.' }, projectPoliciesChangeRequired: { id: 'uppyDashboard.projectPoliciesChangeRequired', defaultMessage: 'File name "{fileName}" requires changes to comply with project policies.' }, projectPoliciesNoComply: { id: 'uppyDashboard.projectPoliciesNoComply', defaultMessage: 'File name "{fileName}" doesn\'t comply with project policies and can\'t be uploaded.' } }); export function UppyDashboard(props) { const { uppy, site, path, onClose, onMinimized, title, onPendingChanges, maxActiveUploads } = props; const options = Object.assign( { replaceTargetContent: true, width: '100%', height: '60vh', fileManagerSelectionType: 'both' }, props.options ); const { classes } = useStyles(); const ref = useRef(); const { formatMessage } = useIntl(); const dispatch = useDispatch(); const targetsRef = useRef([]); // onItemsUploaded will be called every 1000ms and it will use the targetsRef.current list to dispatch itemsUploaded system event // then next time onItemsUploaded will be called with a new list of targetsRef.current const onItemsUploaded = useCallback(() => { targetsRef.current = []; }, []); const functionsRef = useRef({ onItemsUploaded: null, onPendingChanges: null, onMinimized: null, onClose: null }); functionsRef.current.onItemsUploaded = onItemsUploaded; functionsRef.current.onPendingChanges = onPendingChanges; functionsRef.current.onMinimized = onMinimized; functionsRef.current.onClose = onClose; useEffect(() => { const onItemsUploaded$ = new Subject(); const subscription = onItemsUploaded$.pipe(debounceTime(1000)).subscribe(() => { functionsRef.current.onItemsUploaded(); }); if (uppy.getPlugin('craftercms:Dashboard')) { uppy.removePlugin(uppy.getPlugin('craftercms:Dashboard')); } uppy.use( Dashboard, Object.assign(Object.assign({}, options), { inline: true, target: ref.current, validateActionPolicy, onPendingChanges: function () { functionsRef.current.onPendingChanges.apply(null, arguments); }, onClose: function () { functionsRef.current.onClose.apply(null, arguments); }, onMinimized: function () { functionsRef.current.onMinimized.apply(null, arguments); }, title, id: 'craftercms:Dashboard', site, path, locale: { strings: { // @ts-ignore - TODO: find substitution(s) cancelPending: formatMessage(translations.cancelPending), clearCompleted: formatMessage(translations.clearCompleted), clear: formatMessage(translations.clear), addMore: formatMessage(translations.addMore), acceptAll: formatMessage(translations.acceptAll), rejectAll: formatMessage(translations.rejectAll), validating: formatMessage(translations.validating), validateAndRetry: formatMessage(translations.validateAndRetry), removeFile: formatMessage(translations.removeFile), back: formatMessage(translations.back), addingMoreFiles: formatMessage(translations.addingMoreFiles), renamingFromTo: formatMessage(translations.renamingFromTo), minimize: formatMessage(translations.minimize), close: formatMessage(translations.close) } }, maxActiveUploads, externalMessages: { maxFiles: formatMessage(translations.maxFiles, { maxFiles: maxActiveUploads }), projectPoliciesChangeRequired: (fileName) => formatMessage(translations.projectPoliciesChangeRequired, { fileName }), projectPoliciesNoComply: (fileName) => formatMessage(translations.projectPoliciesNoComply, { fileName }) }, onMaxActiveUploadsReached: () => { dispatch( showSystemNotification({ message: formatMessage(translations.maxActiveUploadsReached, { maxFiles: maxActiveUploads }) }) ); } }) ); const onUploadSuccess = (file) => { onItemsUploaded$.next(file.id); targetsRef.current.push(file.id); }; uppy.on('upload-success', onUploadSuccess); return () => { subscription.unsubscribe(); const plugin = uppy.getPlugin('craftercms:Dashboard'); if (plugin) { uppy.removePlugin(plugin); uppy.off('upload-success', onUploadSuccess); } }; // options is removed from dependencies to avoid re-render a new dashboard /* eslint-disable react-hooks/exhaustive-deps */ }, [dispatch, formatMessage, maxActiveUploads, path, site, title, uppy]); return React.createElement('section', { ref: ref, className: classes.dashboard }); } export default UppyDashboard;