@carbon/ibm-products
Version:
Carbon for IBM Products
300 lines (298 loc) • 11.2 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.
*/
const require_runtime = require("../../_virtual/_rolldown/runtime.js");
const require_index = require("../../node_modules/classnames/index.js");
const require_settings = require("../../settings.js");
const require_devtools = require("../../global/js/utils/devtools.js");
const require_usePortalTarget = require("../../global/js/hooks/usePortalTarget.js");
const require_uuidv4 = require("../../global/js/utils/uuidv4.js");
let react = require("react");
react = require_runtime.__toESM(react);
let prop_types = require("prop-types");
prop_types = require_runtime.__toESM(prop_types);
let _carbon_react = require("@carbon/react");
let _carbon_react_icons = require("@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__ */ require_runtime.__toESM(require_index.default);
const componentName = "ImportModal";
const ImportModal = (0, react.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 = (0, _carbon_react.usePrefix)();
const [files, setFiles] = (0, react.useState)([]);
const [importUrl, setImportUrl] = (0, react.useState)("");
const renderPortalUse = require_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) => {
setFiles([...newFiles.map((file) => {
const newFile = {
uuid: file.uuid || require_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 fetchFile = async (evt) => {
evt.preventDefault();
const fileName = importUrl.substring(importUrl.lastIndexOf("/") + 1).split("?")[0];
const pendingFile = {
name: fileName,
status: "uploading",
uuid: require_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) {
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 = `${require_settings.pkg.prefix}--import-modal`;
return renderPortalUse(/* @__PURE__ */ react.default.createElement(_carbon_react.ComposedModal, {
...rest,
open,
ref,
...require_devtools.getDevtoolsProps(componentName),
"aria-label": title,
className: (0, import_classnames.default)(blockClass, className),
size: "sm",
preventCloseOnClickOutside: true,
onClose: onCloseHandler
}, /* @__PURE__ */ react.default.createElement(_carbon_react.ModalHeader, {
className: `${blockClass}__header`,
title
}), /* @__PURE__ */ react.default.createElement(_carbon_react.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(_carbon_react.FileUploaderDropContainer, {
accept,
labelText: fileDropLabel,
onAddFiles: onAddFile,
disabled: hasFiles,
"data-modal-primary-focus": true
}), /* @__PURE__ */ react.default.createElement("div", { className: `${blockClass}__input-group` }, /* @__PURE__ */ react.default.createElement(_carbon_react.TextInput, {
labelText: /* @__PURE__ */ react.default.createElement("p", { className: `${blockClass}__label` }, inputLabel),
id: inputId || "",
onChange: inputHandler,
placeholder: inputPlaceholder,
value: importUrl,
disabled: hasFiles,
"aria-label": inputLabel,
hideLabel: hideInputLabel
}), /* @__PURE__ */ react.default.createElement(_carbon_react.Button, {
onClick: fetchFile,
className: `${blockClass}__import-button`,
size: "md",
disabled: importButtonDisabled,
renderIcon: inputButtonIcon ? (props) => /* @__PURE__ */ react.default.createElement(_carbon_react_icons.Add, {
size: 20,
...props
}) : void 0
}, 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(_carbon_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
})))), /* @__PURE__ */ react.default.createElement(_carbon_react.ModalFooter, { className: `${blockClass}__footer` }, /* @__PURE__ */ react.default.createElement(_carbon_react.Button, {
type: "button",
kind: "secondary",
onClick: onCloseHandler
}, secondaryButtonText), /* @__PURE__ */ react.default.createElement(_carbon_react.Button, {
type: "submit",
kind: "primary",
onClick: onSubmitHandler,
disabled: primaryButtonDisabled
}, primaryButtonText))));
});
ImportModal.propTypes = {
/**
* Specifies the file types that are valid for importing
*/
accept: prop_types.default.array,
/**
* Optional class name
*/
className: prop_types.default.string,
/**
* The default message shown for an import error
*/
defaultErrorBody: prop_types.default.string.isRequired,
/**
* The default header that is displayed to show an error message
*/
defaultErrorHeader: prop_types.default.string.isRequired,
/**
* Content that is displayed inside the modal
*/
description: prop_types.default.string,
/**
* Optional error body to display specifically for a fetch error
*/
fetchErrorBody: prop_types.default.string,
/**
* Optional error header to display specifically for a fetch error
*/
fetchErrorHeader: prop_types.default.string,
/**
* Header for the drag and drop box
*/
fileDropHeader: prop_types.default.string,
/**
* Label for the drag and drop box
*/
fileDropLabel: prop_types.default.string,
/**
* Label that appears when a file is uploaded to show number of files (1 / 1)
*/
fileUploadLabel: prop_types.default.string,
/**
* Hide input label
*/
hideInputLabel: prop_types.default.bool,
/**
* Button icon for import by url button
*/
inputButtonIcon: prop_types.default.bool,
/**
* Button text for import by url button
*/
inputButtonText: prop_types.default.string.isRequired,
/**
* ID for text input
*/
inputId: prop_types.default.string,
/**
* Header to display above import by url
*/
inputLabel: prop_types.default.string.isRequired,
/**
* Placeholder for text input
*/
inputPlaceholder: prop_types.default.string,
/**
* Optional error message to display specifically for a invalid file type error
*/
invalidFileTypeErrorBody: prop_types.default.string,
/**
* Optional error header to display specifically for a invalid file type error
*/
invalidFileTypeErrorHeader: prop_types.default.string,
/**
* Description for delete file icon
*/
invalidIconDescription: prop_types.default.string,
/**
* File size limit in bytes
*/
maxFileSize: prop_types.default.number,
/**
* Optional error message to display specifically for a max file size error
*/
maxFileSizeErrorBody: prop_types.default.string,
/**
* Optional error header to display specifically for a max file size error
*/
maxFileSizeErrorHeader: prop_types.default.string,
/**
* Specify a handler for closing modal
*/
onClose: prop_types.default.func,
/**
* Specify a handler for "submitting" modal. Access the imported file via `file => {}`
*/
onRequestSubmit: prop_types.default.func.isRequired,
/**
* Specify whether the Modal is currently open
*/
open: prop_types.default.bool.isRequired,
/**
* The DOM node the tearsheet should be rendered within. Defaults to document.body.
*/
portalTarget: prop_types.default.node,
/**
* Specify the text for the primary button
*/
primaryButtonText: prop_types.default.string.isRequired,
/**
* Specify the text for the secondary button
*/
secondaryButtonText: prop_types.default.string.isRequired,
/**
* The text displayed at the top of the modal
*/
title: prop_types.default.string.isRequired
};
ImportModal.displayName = componentName;
//#endregion
exports.ImportModal = ImportModal;