@carbon/ibm-products
Version:
Carbon for IBM Products
352 lines (346 loc) • 11.4 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.
*/
'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;