@carbon/ibm-products
Version:
Carbon for IBM Products
298 lines (296 loc) • 10.4 kB
JavaScript
/**
* Copyright IBM Corp. 2020, 2026
*
* 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 { __toESM } from "../../_virtual/_rolldown/runtime.js";
import { require_classnames } from "../../node_modules/classnames/index.js";
import { pkg } from "../../settings.js";
import { getDevtoolsProps } from "../../global/js/utils/devtools.js";
import { usePortalTarget } from "../../global/js/hooks/usePortalTarget.js";
import uuidv4 from "../../global/js/utils/uuidv4.js";
import React, { forwardRef, useState } from "react";
import PropTypes from "prop-types";
import { Button, ComposedModal, FileUploaderDropContainer, FileUploaderItem, ModalBody, ModalFooter, ModalHeader, TextInput, usePrefix } from "@carbon/react";
import { Add } from "@carbon/react/icons";
//#region src/components/ImportModal/ImportModal.tsx
/**
* Copyright IBM Corp. 2021, 2024
*
* This source code is licensed under the Apache-2.0 license found in the
* LICENSE file in the root directory of this source tree.
*/
var import_classnames = /* @__PURE__ */ __toESM(require_classnames());
const componentName = "ImportModal";
const ImportModal = forwardRef(({ accept = [], className, defaultErrorBody, defaultErrorHeader, description, fetchErrorBody, fetchErrorHeader, fileDropHeader, fileDropLabel, fileUploadLabel, hideInputLabel, inputButtonIcon, inputButtonText, inputId, inputLabel, inputPlaceholder, invalidFileTypeErrorBody, invalidFileTypeErrorHeader, invalidIconDescription, maxFileSize, maxFileSizeErrorBody, maxFileSizeErrorHeader, onClose, onRequestSubmit, open, portalTarget: portalTargetIn, primaryButtonText, secondaryButtonText, title, ...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) => {
setFiles([...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 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) {
updateFiles([{
...pendingFile,
fetchError: true
}]);
}
};
const onAddFile = (evt, { addedFiles }) => {
evt.stopPropagation();
updateFiles(addedFiles);
};
const onRemoveFile = (uuid) => {
setFiles(files.filter((f) => f.uuid !== uuid));
};
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.createElement(ComposedModal, {
...rest,
open,
ref,
...getDevtoolsProps(componentName),
"aria-label": title,
className: (0, import_classnames.default)(blockClass, className),
size: "sm",
preventCloseOnClickOutside: true,
onClose: onCloseHandler
}, /* @__PURE__ */ React.createElement(ModalHeader, {
className: `${blockClass}__header`,
title
}), /* @__PURE__ */ React.createElement(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(FileUploaderDropContainer, {
accept,
labelText: fileDropLabel,
onAddFiles: onAddFile,
disabled: hasFiles,
"data-modal-primary-focus": true
}), /* @__PURE__ */ React.createElement("div", { className: `${blockClass}__input-group` }, /* @__PURE__ */ React.createElement(TextInput, {
labelText: /* @__PURE__ */ React.createElement("p", { className: `${blockClass}__label` }, inputLabel),
id: inputId || "",
onChange: inputHandler,
placeholder: inputPlaceholder,
value: importUrl,
disabled: hasFiles,
"aria-label": inputLabel,
hideLabel: hideInputLabel
}), /* @__PURE__ */ React.createElement(Button, {
onClick: fetchFile,
className: `${blockClass}__import-button`,
size: "md",
disabled: importButtonDisabled,
renderIcon: inputButtonIcon ? (props) => /* @__PURE__ */ React.createElement(Add, {
size: 20,
...props
}) : void 0
}, 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(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
})))), /* @__PURE__ */ React.createElement(ModalFooter, { className: `${blockClass}__footer` }, /* @__PURE__ */ React.createElement(Button, {
type: "button",
kind: "secondary",
onClick: onCloseHandler
}, secondaryButtonText), /* @__PURE__ */ React.createElement(Button, {
type: "submit",
kind: "primary",
onClick: onSubmitHandler,
disabled: primaryButtonDisabled
}, primaryButtonText))));
});
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,
/**
* Hide input label
*/
hideInputLabel: PropTypes.bool,
/**
* 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.isRequired,
/**
* 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;
//#endregion
export { ImportModal };