UNPKG

@carbon/ibm-products

Version:

Carbon for IBM Products

352 lines (346 loc) 11.4 kB
/** * Copyright IBM Corp. 2020, 2025 * * This source code is licensed under the Apache-2.0 license found in the * LICENSE file in the root directory of this source tree. */ 'use strict'; var _rollupPluginBabelHelpers = require('../../_virtual/_rollupPluginBabelHelpers.js'); var react = require('@carbon/react'); var React = require('react'); var icons = require('@carbon/react/icons'); var index = require('../../_virtual/index.js'); var cx = require('classnames'); var devtools = require('../../global/js/utils/devtools.js'); var settings = require('../../settings.js'); var usePortalTarget = require('../../global/js/hooks/usePortalTarget.js'); var uuidv4 = require('../../global/js/utils/uuidv4.js'); const componentName = 'ImportModal'; exports.ImportModal = /*#__PURE__*/React.forwardRef((_ref, ref) => { let { // The component props, in alphabetical order (for consistency). accept = [], className, defaultErrorBody, defaultErrorHeader, description, fetchErrorBody, fetchErrorHeader, fileDropHeader, fileDropLabel, fileUploadLabel, inputButtonIcon, inputButtonText, inputId, inputLabel, inputPlaceholder, invalidFileTypeErrorBody, invalidFileTypeErrorHeader, invalidIconDescription, maxFileSize, maxFileSizeErrorBody, maxFileSizeErrorHeader, onClose, onRequestSubmit, open, portalTarget: portalTargetIn, primaryButtonText, secondaryButtonText, title, // Collect any other property values passed in. ...rest } = _ref; const carbonPrefix = react.usePrefix(); const [files, setFiles] = React.useState([]); const [importUrl, setImportUrl] = React.useState(''); const renderPortalUse = usePortalTarget.usePortalTarget(portalTargetIn); const isInvalidFileType = file => { const acceptSet = new Set(accept); const name = file.name; const mimeType = file.type; const extension = `.${name.split('.').pop()}`; if (acceptSet.has(mimeType) || acceptSet.has(extension) || accept.length === 0) { return false; } return true; }; const updateFiles = newFiles => { const updatedFiles = newFiles.map(file => { const newFile = { uuid: file.uuid || uuidv4.default(), status: 'edit', iconDescription: invalidIconDescription, name: file.name, fileSize: file.size, invalidFileType: file.invalidFileType, fileData: file, fetchError: file.fetchError }; if (newFile.fetchError) { newFile.errorBody = fetchErrorBody || defaultErrorBody; newFile.errorSubject = fetchErrorHeader || defaultErrorHeader; newFile.invalid = true; } else if (newFile.invalidFileType) { newFile.errorBody = invalidFileTypeErrorBody || defaultErrorBody; newFile.errorSubject = invalidFileTypeErrorHeader || defaultErrorHeader; newFile.invalid = true; } else if (maxFileSize && (newFile?.fileSize ?? 0) > maxFileSize) { newFile.errorBody = maxFileSizeErrorBody || defaultErrorBody; newFile.errorSubject = maxFileSizeErrorHeader || defaultErrorHeader; newFile.invalid = true; } return newFile; }); const finalFiles = [...updatedFiles]; setFiles(finalFiles); }; const fetchFile = async evt => { evt.preventDefault(); const fileName = importUrl.substring(importUrl.lastIndexOf('/') + 1).split('?')[0]; const pendingFile = { name: fileName, status: 'uploading', uuid: uuidv4.default() }; setFiles([pendingFile]); try { const response = await fetch(importUrl); if (!response.ok || response.status !== 200) { throw new Error(`${response.status}`); } const blob = await response.blob(); const fetchedFile = new File([blob], fileName, { type: blob.type }); fetchedFile.invalidFileType = isInvalidFileType(fetchedFile); fetchedFile.uuid = pendingFile.uuid; updateFiles([fetchedFile]); } catch (err) { const failedFile = { ...pendingFile, fetchError: true }; updateFiles([failedFile]); } }; const onAddFile = (evt, _ref2) => { let { addedFiles } = _ref2; evt.stopPropagation(); updateFiles(addedFiles); }; const onRemoveFile = uuid => { const updatedFiles = files.filter(f => f.uuid !== uuid); setFiles(updatedFiles); }; const onSubmitHandler = () => { onRequestSubmit(files); }; const inputHandler = evt => { setImportUrl(evt.target.value); }; const onCloseHandler = () => { setFiles([]); setImportUrl(''); if (onClose) { onClose(); } }; const numberOfFiles = files.length; const numberOfValidFiles = files.filter(f => !f.invalid).length; const hasFiles = numberOfFiles > 0; const primaryButtonDisabled = !hasFiles || !(numberOfValidFiles > 0); const importButtonDisabled = !importUrl || hasFiles; const fileStatusString = `${numberOfValidFiles} / ${numberOfFiles} ${fileUploadLabel}`; const blockClass = `${settings.pkg.prefix}--import-modal`; return renderPortalUse(/*#__PURE__*/React.createElement(react.ComposedModal, _rollupPluginBabelHelpers.extends({}, rest, { open, ref }, devtools.getDevtoolsProps(componentName), { "aria-label": title, className: cx(blockClass, className), size: "sm", preventCloseOnClickOutside: true, onClose: onCloseHandler }), /*#__PURE__*/React.createElement(react.ModalHeader, { className: `${blockClass}__header`, title: title }), /*#__PURE__*/React.createElement(react.ModalBody, { className: `${blockClass}__body-container` }, description && /*#__PURE__*/React.createElement("p", { className: `${blockClass}__body` }, description), fileDropHeader && /*#__PURE__*/React.createElement("p", { className: `${blockClass}__file-drop-header` }, fileDropHeader), /*#__PURE__*/React.createElement(react.FileUploaderDropContainer, { accept: accept, labelText: fileDropLabel, onAddFiles: onAddFile, disabled: hasFiles, "data-modal-primary-focus": true }), inputLabel && /*#__PURE__*/React.createElement("p", { className: `${blockClass}__label` }, inputLabel), /*#__PURE__*/React.createElement("div", { className: `${blockClass}__input-group` }, /*#__PURE__*/React.createElement(react.TextInput, { labelText: "", id: inputId || '', onChange: inputHandler, placeholder: inputPlaceholder, value: importUrl, disabled: hasFiles, "aria-label": inputLabel }), /*#__PURE__*/React.createElement(react.Button, { onClick: fetchFile, className: `${blockClass}__import-button`, size: "md", disabled: importButtonDisabled, renderIcon: inputButtonIcon ? props => /*#__PURE__*/React.createElement(icons.Add, _rollupPluginBabelHelpers.extends({ size: 20 }, props)) : undefined }, inputButtonText)), /*#__PURE__*/React.createElement("div", { className: `${carbonPrefix}--file-container ${blockClass}__file-container` }, hasFiles && /*#__PURE__*/React.createElement("p", { className: `${blockClass}__helper-text` }, fileStatusString), files.map(file => /*#__PURE__*/React.createElement(react.FileUploaderItem, { key: file.uuid, onDelete: () => onRemoveFile(file.uuid), name: file.name, status: file.status, size: "lg", uuid: file.uuid, iconDescription: file.iconDescription, invalid: file.invalid, errorBody: file.errorBody, errorSubject: file.errorSubject, filesize: file.fileSize /* cspell:disable-line */ })))), /*#__PURE__*/React.createElement(react.ModalFooter, { className: `${blockClass}__footer` }, /*#__PURE__*/React.createElement(react.Button, { type: "button", kind: "secondary", onClick: onCloseHandler }, secondaryButtonText), /*#__PURE__*/React.createElement(react.Button, { type: "submit", kind: "primary", onClick: onSubmitHandler, disabled: primaryButtonDisabled }, primaryButtonText)))); }); // Return a placeholder if not released and not enabled by feature flag exports.ImportModal = settings.pkg.checkComponentEnabled(exports.ImportModal, componentName); exports.ImportModal.propTypes = { /** * Specifies the file types that are valid for importing */ accept: index.default.array, /** * Optional class name */ className: index.default.string, /** * The default message shown for an import error */ defaultErrorBody: index.default.string.isRequired, /** * The default header that is displayed to show an error message */ defaultErrorHeader: index.default.string.isRequired, /** * Content that is displayed inside the modal */ description: index.default.string, /** * Optional error body to display specifically for a fetch error */ fetchErrorBody: index.default.string, /** * Optional error header to display specifically for a fetch error */ fetchErrorHeader: index.default.string, /** * Header for the drag and drop box */ fileDropHeader: index.default.string, /** * Label for the drag and drop box */ fileDropLabel: index.default.string, /** * Label that appears when a file is uploaded to show number of files (1 / 1) */ fileUploadLabel: index.default.string, /** * Button icon for import by url button */ inputButtonIcon: index.default.bool, /** * Button text for import by url button */ inputButtonText: index.default.string.isRequired, /** * ID for text input */ inputId: index.default.string, /** * Header to display above import by url */ inputLabel: index.default.string, /** * Placeholder for text input */ inputPlaceholder: index.default.string, /** * Optional error message to display specifically for a invalid file type error */ invalidFileTypeErrorBody: index.default.string, /** * Optional error header to display specifically for a invalid file type error */ invalidFileTypeErrorHeader: index.default.string, /** * Description for delete file icon */ invalidIconDescription: index.default.string, /** * File size limit in bytes */ maxFileSize: index.default.number, /** * Optional error message to display specifically for a max file size error */ maxFileSizeErrorBody: index.default.string, /** * Optional error header to display specifically for a max file size error */ maxFileSizeErrorHeader: index.default.string, /** * Specify a handler for closing modal */ onClose: index.default.func, /** * Specify a handler for "submitting" modal. Access the imported file via `file => {}` */ onRequestSubmit: index.default.func.isRequired, /** * Specify whether the Modal is currently open */ open: index.default.bool.isRequired, /** * The DOM node the tearsheet should be rendered within. Defaults to document.body. */ portalTarget: index.default.node, /** * Specify the text for the primary button */ primaryButtonText: index.default.string.isRequired, /** * Specify the text for the secondary button */ secondaryButtonText: index.default.string.isRequired, /** * The text displayed at the top of the modal */ title: index.default.string.isRequired }; exports.ImportModal.displayName = componentName;