diff --git a/forms-flow-components/src/components/CustomComponents/ImportModal.tsx b/forms-flow-components/src/components/CustomComponents/ImportModal.tsx new file mode 100644 index 00000000..acbec564 --- /dev/null +++ b/forms-flow-components/src/components/CustomComponents/ImportModal.tsx @@ -0,0 +1,504 @@ +import React, { useEffect, useState } from "react"; +import Modal from "react-bootstrap/Modal"; +import ProgressBar from "react-bootstrap/ProgressBar"; +import Dropdown from "react-bootstrap/Dropdown"; +import { + CloseIcon, + UploadIcon, + SuccessIcon, + FailedIcon, + IButton, + DropdownIcon, +} from "../SvgIcons"; +import { CustomButton } from "../CustomComponents/Button"; +import { useTranslation } from "react-i18next"; + +// Define the types for props +interface FileItem { + form?: { + majorVersion: number; + minorVersion: number; + }; + workflow?: { + majorVersion: number; + minorVersion: number; + }; +} + +interface ProcessVersion { + majorVersion: number; + minorVersion: number; + type: string; +} + +interface ImportModalProps { + showModal: boolean; + onClose: () => void; + uploadActionType: { + IMPORT: string; + VALIDATE: string; + }; + importError: string | null; + importLoader: boolean; + formName: string; + description: string; + handleImport: ( + file: File, + uploadActionType: string, + layoutVersion: string | null, + flowVersion: string | null + ) => void; + fileItems: FileItem | null; + fileType: string; + primaryButtonText: string; + headerText: string; + processVersion: ProcessVersion | null; +} + +export const ImportModal: React.FC = React.memo( + ({ + showModal, + onClose, + uploadActionType, + importError, + importLoader, + formName, + description, + handleImport, + fileItems, + fileType, + primaryButtonText, + headerText, + processVersion, + }) => { + const { t } = useTranslation(); + const computedStyle = getComputedStyle(document.documentElement); + const redColor = computedStyle.getPropertyValue("--ff-red-000"); + const [selectedFile, setSelectedFile] = useState(null); + const [uploadProgress, setUploadProgress] = useState(0); + const [selectedLayoutVersion, setSelectedLayoutVersion] = useState<{ + value: any; + label: string; + } | null>(null); + const [selectedFlowVersion, setSelectedFlowVersion] = useState<{ + value: any; + label: string; + } | null>(null); + const [showFileItems, setShowFileItems] = useState(false); + const [inprogress, setInprogress] = useState(true); + + const layoutOptions = [ + { value: true, label: "Skip, do not import" }, + { + value: "major", + label: `import as version ${ + fileItems?.form?.majorVersion + 1 + }.0 (only impacts new submissions)`, + }, + { + value: "minor", + label: `import as version ${fileItems?.form?.majorVersion}.${fileItems?.form?.minorVersion} (impacts previous and new submissions)`, + }, + ]; + + const flowOptions = [ + { value: true, label: "Skip, do not import" }, + { + value: "major", + label: `import as version ${fileItems?.workflow?.majorVersion ?? 1}.${ + fileItems?.workflow?.minorVersion ?? 0 + } (only impacts new submissions)`, + }, + ]; + + const handleLayoutChange = (option: { value: any; label: string }) => { + setSelectedLayoutVersion(option); + }; + + const handFlowChange = (option: { value: any; label: string }) => { + setSelectedFlowVersion(option); + }; + + const onUpload = (evt: React.ChangeEvent) => { + const file = evt.target.files ? evt.target.files[0] : null; + setSelectedFile(file); + }; + + const resetState = () => { + setSelectedFile(null); + setUploadProgress(0); + }; + + const closeModal = () => { + setSelectedFile(null); + setUploadProgress(0); + setSelectedLayoutVersion(null); + setSelectedFlowVersion(null); + setShowFileItems(false); + onClose(); + }; + + const onImport = () => { + if (selectedFile) { + handleImport( + selectedFile, + uploadActionType.IMPORT, + selectedLayoutVersion?.value, + selectedFlowVersion?.value + ); + } + }; + + useEffect(() => { + if ( + fileItems && + !importError && + Object.values(fileItems).some( + (item) => item?.majorVersion != null || item?.minorVersion != null + ) + ) { + setShowFileItems(true); + } else if ( + processVersion?.majorVersion != null || + processVersion?.minorVersion != null + ) { + setShowFileItems(true); + } else { + setShowFileItems(false); + } + }, [importError, fileItems]); + + useEffect(() => { + if (!showModal) { + closeModal(); + } + }, [showModal]); + + useEffect(() => { + let isMounted = true; + + if (selectedFile) { + handleImport( + selectedFile, + uploadActionType.VALIDATE, + selectedLayoutVersion?.value ?? null, + selectedFlowVersion?.value ?? null + ); + + let start: number | null = null; + const duration = 2000; + + const animateProgress = (timestamp: number) => { + if (!start) start = timestamp; + const progress = Math.min( + ((timestamp - start) / duration) * 100, + 100 + ); + + if (isMounted) { + setUploadProgress(progress); + setInprogress(progress < 100); + } + + if (progress < 100) { + requestAnimationFrame(animateProgress); + } + }; + + const animation = requestAnimationFrame(animateProgress); + + return () => { + isMounted = false; + cancelAnimationFrame(animation); + }; + } + }, [selectedFile]); + + const renderUploadDetails = () => { + return ( +
+
+

{selectedFile?.name}

+ + {renderUploadStatusText()} + + {renderUploadStatusIcon()} +
+
+ {formName} +
+ {!importError && description && ( +
{description}
+ )} + {renderImportError()} +
+ ); + }; + + const getUploadStatusClass = () => { + if (!importLoader && !importError && !inprogress) { + return "upload-status-success"; + } + + if (!importLoader && importError && !inprogress) { + return "upload-status-error"; + } + + if (inprogress) { + return "upload-status-progress"; + } + + return ""; + }; + + + // Function to render the status text based on the upload condition + const renderUploadStatusText = () => { + if (!importLoader && !importError && !inprogress) { + return t("Upload Successful"); + } + if (!importLoader && importError && !inprogress) { + return t("Upload Failed"); + } + if (inprogress) { + return t("Import in progress"); + } + return null; + }; + + // Function to render the correct status icon based on upload progress + const renderUploadStatusIcon = () => { + if (!importLoader && importError) { + return ; + } + if (!importLoader && !inprogress) { + return ; + } + return null; + }; + + // Function to render import errors + const renderImportError = () => { + return ( + importError && ( + {importError} + ) + ); + }; + + // Function to render the import file items and version options + const renderFileItems = () => { + if (showFileItems && !importError) { + return ( +
+ {renderImportNote()} + {renderFileItemDetails()} + {renderLayoutOptions()} + {renderFlowOptions()} +
+ ); + } + return null; + }; + + // Function to render import note when file items are shown + const renderImportNote = () => { + return ( +
+
+ + + {t("Import will create a new version.")} + +
+
+ ); + }; + + // Function to render the file item details (e.g. type and import version) + const renderFileItemDetails = () => { + return ( +
+
{t("Type")}
+
{t("Import")}
+
+ ); + }; + + // Function to render layout version options + const renderLayoutOptions = () => { + return ( + fileItems?.form?.majorVersion && ( +
+
{t("Layout")}
+
+ + +
+
+ {selectedLayoutVersion + ? selectedLayoutVersion.label + : "Skip, do not import"} +
+ +
+
+ + {layoutOptions.map((option) => ( + handleLayoutChange(option)} + > + {option.label} + + ))} + +
+
+
+ ) + ); + }; + + // Function to render flow version options + const renderFlowOptions = () => { + return ( + fileItems?.workflow?.majorVersion && ( +
+
Flow
+
+ + +
+
+ {selectedFlowVersion + ? selectedFlowVersion.label + : "Skip, do not import"} +
+ +
+
+ + {flowOptions.map((option) => ( + handFlowChange(option)} + > + {option.label} + + ))} + +
+
+
+ ) + ); + }; + + // Function to render the file upload area when no file is selected + const renderFileUploadArea = () => { + return ( +
document.getElementById("file-input")?.click()} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + document.getElementById("file-input")?.click(); + } + }} + aria-label="Upload file" + > + +
+ +

+ {t( + `Click or drag a file to this area to import${ + fileType === ".json, .bpmn" ? " (form, layout or bpmn)" : "" + }` + )} +

+

+ {t(`Support for a single ${fileType} file upload. Maximum file + size 20MB.`)} +

+
+
+ ); + }; + + return ( + + + + {t(headerText)} + +
+ { + resetState(); + closeModal(); + }} + /> +
+
+ + {selectedFile ? ( + <> + + {renderUploadDetails()} + {renderFileItems()} + + ) : ( + renderFileUploadArea() + )} + + + { + primaryButtonText === "Try Again" ? closeModal() : onImport(); + }} + buttonLoading={!importError && importLoader} + /> + { + resetState(); + closeModal(); + }} + /> + +
+ ); + } +); diff --git a/forms-flow-components/src/components/SvgIcons/index.tsx b/forms-flow-components/src/components/SvgIcons/index.tsx index c944bb07..50c38907 100644 --- a/forms-flow-components/src/components/SvgIcons/index.tsx +++ b/forms-flow-components/src/components/SvgIcons/index.tsx @@ -1,7 +1,6 @@ const computedStyle = getComputedStyle(document.documentElement); const baseColor = computedStyle.getPropertyValue("--ff-base-600"); const grayColor = computedStyle.getPropertyValue("--ff-gray-800"); - export const ChevronIcon = ({ color = baseColor, width = "10", @@ -635,3 +634,22 @@ export const TickIcon = ({ color = baseColor, ...props }) => ( strokeLinejoin="round"/> ); + +export const DropdownIcon = ({color = baseColor}) => ( + + + +); + diff --git a/forms-flow-components/src/components/index.ts b/forms-flow-components/src/components/index.ts index b90efb87..18f89c1e 100644 --- a/forms-flow-components/src/components/index.ts +++ b/forms-flow-components/src/components/index.ts @@ -17,4 +17,6 @@ export * from "./CustomComponents/TableFooter"; export * from "./CustomComponents/CustomInfo"; export * from "./CustomComponents/BuildModal"; export * from "./CustomComponents/ErrorModal"; +export * from "./CustomComponents/ImportModal"; export * from "./CustomComponents/NoDataFound"; + diff --git a/forms-flow-theme/scss/_forms.scss b/forms-flow-theme/scss/_forms.scss index fa976796..c3490e99 100644 --- a/forms-flow-theme/scss/_forms.scss +++ b/forms-flow-theme/scss/_forms.scss @@ -411,4 +411,44 @@ select option:hover { .form-preview{ pointer-events: none; cursor: not-allowed; -} \ No newline at end of file +} + +.dropdown-main { + display: block !important; + padding: 0 !important; + + .dropdown-menu.show { + width: 100% !important; + overflow: hidden !important; + padding: 0.4375rem !important; + display: block !important; + } + + .dropdown-item { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-size: var(--font-size-xs)!important; + font-weight: var(--font-weight-sm) !important; + } + + .btn.btn-primary, + .btn.btn-success, + .btn.btn-success:hover { + width: 100%; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + background-color: $white !important; + color: $black !important; + display: flex !important; + align-items: left !important; + justify-content: start !important; + font-size: var(--font-size-xs)!important; + font-weight: var(--font-weight-sm) !important; + } + + .dropdown-toggle::after { + display: none !important; + } +} diff --git a/forms-flow-theme/scss/_mixins.scss b/forms-flow-theme/scss/_mixins.scss index 10fdf231..fabe7b7d 100644 --- a/forms-flow-theme/scss/_mixins.scss +++ b/forms-flow-theme/scss/_mixins.scss @@ -56,17 +56,17 @@ } -@mixin text-modal-content { +@mixin text-modal-content { font-size: var(--font-size-sm); font-weight: var(--font-weight-sm); - line-height: var(--text-line-height); + line-height: var(--text-line-height); color: var(--ff-gray-800); } @mixin upload-status-styles($color) { color: $color; text-align: right; - font-size: $font-size-xs; + font-size: var(--font-size-xs); font-weight: var(--font-weight-sm); line-height: var(--text-line-height); } \ No newline at end of file diff --git a/forms-flow-theme/scss/_modal.scss b/forms-flow-theme/scss/_modal.scss index d5dbb512..1a066693 100644 --- a/forms-flow-theme/scss/_modal.scss +++ b/forms-flow-theme/scss/_modal.scss @@ -856,3 +856,4 @@ } } } + diff --git a/forms-flow-theme/scss/fileUpload.scss b/forms-flow-theme/scss/fileUpload.scss index b68d1f5e..a5d436a3 100644 --- a/forms-flow-theme/scss/fileUpload.scss +++ b/forms-flow-theme/scss/fileUpload.scss @@ -35,6 +35,14 @@ $font-size-xs: var(--font-size-xs); margin: 0; } +.upload-size-text { + color: $gray-400; + text-align: center; + font-size: var(--font-size-xs); + font-weight: var(--font-weight-sm); + line-height: var(--text-line-height); +} + .upload-text-description { color: $gray-400; text-align: center; @@ -53,7 +61,7 @@ $font-size-xs: var(--font-size-xs); align-self: stretch; } -.upload-boady { +.upload-body { display: flex; flex-direction: column; align-items: flex-start; @@ -104,9 +112,54 @@ $font-size-xs: var(--font-size-xs); .progress { height: var(--spacer-150) !important; + --ff-progress-border-radius: var(--radius-0150) !important; } -.progress, .progress-stacked { --ff-progress-border-radius: var(--radius-0150) !important; -} \ No newline at end of file +} + +.import-container { + width: 100%; + + .import-details { + display: flex; + padding: var(--spacer-100) 0 var(--spacer-100) 0; + align-items: center; + align-self: stretch; + border-bottom: 1px solid var(--ff-gray-100); + + .file-item-header-text { + display: flex; + justify-content: flex-start; + width: 30%; + color: var(--ff-gray-800); + font-size: 0.875rem; + font-weight: var(--font-weight-lg); + line-height: var(--text-line-height); + } + } + + .file-item-content { + display: flex; + padding: var(--spacer-200) 0 var(--spacer-200) 0; + align-items: center; + align-self: stretch; + border-bottom: 1px solid var(--ff-gray-100); + + .import-layout-text, + .import-workflow-text { + width: 30%; + color: var(--gray-800); + font-size: var(--font-size-xs); + font-weight: var(--font-weight-sm); + line-height: var(--text-line-height); + } + + /* Target the last child */ + &:last-child { + padding-bottom: 0; + border-bottom: none; + } + } +}