@carbon/ibm-products
Version:
Carbon for IBM Products
352 lines (346 loc) • 11.3 kB
JavaScript
/**
* 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.
*/
import { extends as _extends } from '../../_virtual/_rollupPluginBabelHelpers.js';
import { usePrefix, ComposedModal, ModalHeader, ModalBody, FileUploaderDropContainer, TextInput, Button, FileUploaderItem, ModalFooter } from '@carbon/react';
import React__default, { forwardRef, useState } from 'react';
import { Add } from '@carbon/react/icons';
import PropTypes from '../../_virtual/index.js';
import cx from 'classnames';
import { getDevtoolsProps } from '../../global/js/utils/devtools.js';
import { pkg } from '../../settings.js';
import { usePortalTarget } from '../../global/js/hooks/usePortalTarget.js';
import uuidv4 from '../../global/js/utils/uuidv4.js';
const componentName = 'ImportModal';
let ImportModal = /*#__PURE__*/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 = usePrefix();
const [files, setFiles] = useState([]);
const [importUrl, setImportUrl] = useState('');
const renderPortalUse = 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(),
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()
};
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 = `${pkg.prefix}--import-modal`;
return renderPortalUse(/*#__PURE__*/React__default.createElement(ComposedModal, _extends({}, rest, {
open,
ref
}, getDevtoolsProps(componentName), {
"aria-label": title,
className: cx(blockClass, className),
size: "sm",
preventCloseOnClickOutside: true,
onClose: onCloseHandler
}), /*#__PURE__*/React__default.createElement(ModalHeader, {
className: `${blockClass}__header`,
title: title
}), /*#__PURE__*/React__default.createElement(ModalBody, {
className: `${blockClass}__body-container`
}, description && /*#__PURE__*/React__default.createElement("p", {
className: `${blockClass}__body`
}, description), fileDropHeader && /*#__PURE__*/React__default.createElement("p", {
className: `${blockClass}__file-drop-header`
}, fileDropHeader), /*#__PURE__*/React__default.createElement(FileUploaderDropContainer, {
accept: accept,
labelText: fileDropLabel,
onAddFiles: onAddFile,
disabled: hasFiles,
"data-modal-primary-focus": true
}), inputLabel && /*#__PURE__*/React__default.createElement("p", {
className: `${blockClass}__label`
}, inputLabel), /*#__PURE__*/React__default.createElement("div", {
className: `${blockClass}__input-group`
}, /*#__PURE__*/React__default.createElement(TextInput, {
labelText: "",
id: inputId || '',
onChange: inputHandler,
placeholder: inputPlaceholder,
value: importUrl,
disabled: hasFiles,
"aria-label": inputLabel
}), /*#__PURE__*/React__default.createElement(Button, {
onClick: fetchFile,
className: `${blockClass}__import-button`,
size: "md",
disabled: importButtonDisabled,
renderIcon: inputButtonIcon ? props => /*#__PURE__*/React__default.createElement(Add, _extends({
size: 20
}, props)) : undefined
}, inputButtonText)), /*#__PURE__*/React__default.createElement("div", {
className: `${carbonPrefix}--file-container ${blockClass}__file-container`
}, hasFiles && /*#__PURE__*/React__default.createElement("p", {
className: `${blockClass}__helper-text`
}, fileStatusString), files.map(file => /*#__PURE__*/React__default.createElement(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__default.createElement(ModalFooter, {
className: `${blockClass}__footer`
}, /*#__PURE__*/React__default.createElement(Button, {
type: "button",
kind: "secondary",
onClick: onCloseHandler
}, secondaryButtonText), /*#__PURE__*/React__default.createElement(Button, {
type: "submit",
kind: "primary",
onClick: onSubmitHandler,
disabled: primaryButtonDisabled
}, primaryButtonText))));
});
// Return a placeholder if not released and not enabled by feature flag
ImportModal = pkg.checkComponentEnabled(ImportModal, componentName);
ImportModal.propTypes = {
/**
* Specifies the file types that are valid for importing
*/
accept: PropTypes.array,
/**
* Optional class name
*/
className: PropTypes.string,
/**
* The default message shown for an import error
*/
defaultErrorBody: PropTypes.string.isRequired,
/**
* The default header that is displayed to show an error message
*/
defaultErrorHeader: PropTypes.string.isRequired,
/**
* Content that is displayed inside the modal
*/
description: PropTypes.string,
/**
* Optional error body to display specifically for a fetch error
*/
fetchErrorBody: PropTypes.string,
/**
* Optional error header to display specifically for a fetch error
*/
fetchErrorHeader: PropTypes.string,
/**
* Header for the drag and drop box
*/
fileDropHeader: PropTypes.string,
/**
* Label for the drag and drop box
*/
fileDropLabel: PropTypes.string,
/**
* Label that appears when a file is uploaded to show number of files (1 / 1)
*/
fileUploadLabel: PropTypes.string,
/**
* Button icon for import by url button
*/
inputButtonIcon: PropTypes.bool,
/**
* Button text for import by url button
*/
inputButtonText: PropTypes.string.isRequired,
/**
* ID for text input
*/
inputId: PropTypes.string,
/**
* Header to display above import by url
*/
inputLabel: PropTypes.string,
/**
* Placeholder for text input
*/
inputPlaceholder: PropTypes.string,
/**
* Optional error message to display specifically for a invalid file type error
*/
invalidFileTypeErrorBody: PropTypes.string,
/**
* Optional error header to display specifically for a invalid file type error
*/
invalidFileTypeErrorHeader: PropTypes.string,
/**
* Description for delete file icon
*/
invalidIconDescription: PropTypes.string,
/**
* File size limit in bytes
*/
maxFileSize: PropTypes.number,
/**
* Optional error message to display specifically for a max file size error
*/
maxFileSizeErrorBody: PropTypes.string,
/**
* Optional error header to display specifically for a max file size error
*/
maxFileSizeErrorHeader: PropTypes.string,
/**
* Specify a handler for closing modal
*/
onClose: PropTypes.func,
/**
* Specify a handler for "submitting" modal. Access the imported file via `file => {}`
*/
onRequestSubmit: PropTypes.func.isRequired,
/**
* Specify whether the Modal is currently open
*/
open: PropTypes.bool.isRequired,
/**
* The DOM node the tearsheet should be rendered within. Defaults to document.body.
*/
portalTarget: PropTypes.node,
/**
* Specify the text for the primary button
*/
primaryButtonText: PropTypes.string.isRequired,
/**
* Specify the text for the secondary button
*/
secondaryButtonText: PropTypes.string.isRequired,
/**
* The text displayed at the top of the modal
*/
title: PropTypes.string.isRequired
};
ImportModal.displayName = componentName;
export { ImportModal };