From 2c678dc70fed856d7313ca41cd6a31567494392c Mon Sep 17 00:00:00 2001 From: arausly Date: Tue, 17 Jun 2025 15:56:36 +0100 Subject: [PATCH] added fileupload changes --- libs/gui-elements | 2 +- .../shared/FileUploader/FileSelectionMenu.tsx | 370 +++++++----------- .../cases/UploadNewFile/UploadNewFile.tsx | 150 ++++--- .../app/views/shared/FileUploader/index.tsx | 2 +- .../ArtefactForms/InputMapper.tsx | 4 +- .../FileUploadModal/FileUploadModal.tsx | 14 +- .../shared/modals/ProjectImportModal.tsx | 78 ++-- 7 files changed, 304 insertions(+), 316 deletions(-) diff --git a/libs/gui-elements b/libs/gui-elements index d667c944c0..92c469cf01 160000 --- a/libs/gui-elements +++ b/libs/gui-elements @@ -1 +1 @@ -Subproject commit d667c944c08bd18b9709803e0da58740d32f10f5 +Subproject commit 92c469cf01991a653d40591d4a80ee176790c172 diff --git a/workspace/src/app/views/shared/FileUploader/FileSelectionMenu.tsx b/workspace/src/app/views/shared/FileUploader/FileSelectionMenu.tsx index 0883cc97aa..c9e968094f 100644 --- a/workspace/src/app/views/shared/FileUploader/FileSelectionMenu.tsx +++ b/workspace/src/app/views/shared/FileUploader/FileSelectionMenu.tsx @@ -1,10 +1,6 @@ import React from "react"; -import Uppy, { UppyFile } from "@uppy/core"; -import "@uppy/core/dist/style.css"; -import "@uppy/drag-drop/dist/style.css"; -import "@uppy/progress-bar/dist/style.css"; -import { Button, Divider, FieldItem, Icon, TextField } from "@eccenca/gui-elements"; +import { Button, Divider, FieldItem, Icon, TextField, Uppy, UppyFile } from "@eccenca/gui-elements"; import { IAutoCompleteFieldProps } from "@eccenca/gui-elements/src/components/AutocompleteField/AutoCompleteField"; import { UploadNewFile } from "./cases/UploadNewFile/UploadNewFile"; import { FileSelectionOptions, FileMenuItems } from "./FileSelectionOptions"; @@ -13,8 +9,6 @@ import { CreateNewFile } from "./cases/CreateNewFile"; import i18next from "../../../../language"; import { requestIfResourceExists } from "@ducks/workspace/requests"; import { legacyApiEndpoint } from "../../../utils/getApiEndpoint"; -import { withTranslation } from "react-i18next"; -import XHR from "@uppy/xhr-upload"; interface IUploaderInstance { /** @@ -41,7 +35,7 @@ export interface IUploaderOptions { defaultValue?: string; /** Indicator that there needs to be a value set/selected, else the file selection (from existing files) can e.g. be reset. */ - required: boolean; + required?: boolean; /** * return uploader API @@ -96,33 +90,15 @@ export interface IUploaderOptions { t(key: string, options?: object | string): string; /** When used inside a modal, the behavior of some components will be optimized. */ - insideModal: boolean; + insideModal?: boolean; /** Callback that is called when the state of all uploads being successfully done has changed. * Reasons for non-success are: uploads are in progress, user interaction is needed, errors have occurred.*/ allFilesSuccessfullyUploadedHandler?: (allSuccessful: boolean) => any; - listenToUploadedFiles: (files: UppyFile[]) => void -} - -interface IState { - // Selected File menu item - selectedFileMenu: FileMenuItems; - - //Show upload process - isUploading: boolean; - - //Update default value in case that file is already given - showActionsMenu: boolean; - - //Filename which shows in input for update action - inputFilename: string; + listenToUploadedFiles?: (files: UppyFile[]) => void; - //Toggle File delete dialog, contains filename or empty string - visibleFileDelete: string; - - // The ID of the file selection menu - id: string; + id?: string; } const noop = () => { @@ -134,212 +110,164 @@ const noop = () => { * with advanced = true, provides full FileUploader with 2 extra options * otherwise provides simple drag and drop uploader */ -class FileSelectionMenu extends React.Component { - private uppy = Uppy({ - // @ts-ignore - logger: Uppy.debugLogger, - }); - - /** - * @see Uppy.upload - */ - public upload = this.uppy.upload; - - /** - * @see Uppy.reset - */ - public reset = this.uppy.reset; - - /** - * @see Uppy.cancelAll - */ - public cancelAll = this.uppy.cancelAll; - - constructor(props) { - super(props); - - this.state = { - selectedFileMenu: props.advanced ? "SELECT" : "NEW", - isUploading: false, - showActionsMenu: false, - inputFilename: props.defaultValue || "", - visibleFileDelete: "", - id: props.id, - }; - - if (props.maxFileUploadSizeBytes) { - this.uppy.setOptions({ - restrictions: { - maxFileSize: props.maxFileUploadSizeBytes, - // Restrict to 1 file if allowMultiple == false - maxNumberOfFiles: props.allowMultiple ? undefined : 1, - }, +export const FileSelectionMenu: React.FC = (props) => { + const [inputFileName, setInputFileName] = React.useState(""); + const [selectedFileMenu, setSelectedFileMenu] = React.useState(props.advanced ? "SELECT" : "NEW"); + const [showActionsMenu, setShowActionMenu] = React.useState(false); + const [uppy, setUppy] = React.useState(); + + const upload = React.useCallback(() => uppy?.upload, [uppy]); + const reset = React.useCallback(() => uppy?.reset, [uppy]); + const cancelAll = React.useCallback(() => uppy?.cancelAll, [uppy]); + + React.useEffect(() => { + props.getInstance && + props.getInstance({ + reset: reset, + upload: upload, + cancelAll: cancelAll, }); - } - this.uppy.use(XHR, { - method: "PUT", - fieldName: "file", - allowMultipleUploads: props.allowMultiple, - // Only upload one file at the same time - limit: 1, - }); - } - - componentDidMount(): void { - if (this.props.getInstance) { - this.props.getInstance({ - reset: this.reset, - upload: this.upload, - cancelAll: this.cancelAll, - }); - } - } - - handleUploadSuccess = (file: any) => { - if (this.props.onUploadSuccess) { - this.props.onUploadSuccess(file); - } - this.setState({ - inputFilename: file.name, - }); - this.toggleFileResourceChange(); - }; - - handleFileMenuChange = (value: FileMenuItems) => { - this.setState({ - selectedFileMenu: value, - }); - this.reset(); - }; + }, []); + + const handleUploadSuccess = React.useCallback( + (file: any) => { + props.onUploadSuccess && props.onUploadSuccess(file); + setInputFileName(file.name); + toggleFileResourceChange(); + }, + [props.onUploadSuccess], + ); + + const handleFileMenuChange = React.useCallback((value: FileMenuItems) => { + setSelectedFileMenu(value); + reset(); + }, []); /** * "Abort and Keep File" Handler * revert value back */ - handleDiscardChanges = () => { - const isVisible = !this.state.showActionsMenu; - if (!isVisible) { - this.handleFileNameChange(this.state.inputFilename); - } else { - // just open - this.toggleFileResourceChange(); - } - }; + const handleDiscardChanges = React.useCallback(() => { + !showActionsMenu ? handleFileNameChange(inputFileName) : toggleFileResourceChange(); + }, [showActionsMenu, inputFileName]); /** * Open/close file uploader options */ - toggleFileResourceChange = () => { - this.setState({ - showActionsMenu: !this.state.showActionsMenu, - }); - }; + const toggleFileResourceChange = React.useCallback(() => setShowActionMenu((s) => !s), []); /** * Change readonly input value * @param value */ - handleFileNameChange = (value: string) => { - this.setState({ - inputFilename: value, - }); - this.props.onChange(value); - this.toggleFileResourceChange(); - }; - - validateBeforeFileAdded = async (fileName: string): Promise => { - return await requestIfResourceExists(this.props.projectId, fileName); - }; - - render() { - const { selectedFileMenu, showActionsMenu, inputFilename } = this.state; - const { allowMultiple, advanced, defaultValue, onProgress, projectId, onChange } = this.props; - - return ( -
- {defaultValue && !showActionsMenu && ( - - } - onClick={this.toggleFileResourceChange} - /> - } - /> - - )} - {defaultValue && showActionsMenu && ( - <> -
+ + )} + + ); +}; diff --git a/workspace/src/app/views/shared/FileUploader/cases/UploadNewFile/UploadNewFile.tsx b/workspace/src/app/views/shared/FileUploader/cases/UploadNewFile/UploadNewFile.tsx index 997bb09fd3..2d630f30e1 100644 --- a/workspace/src/app/views/shared/FileUploader/cases/UploadNewFile/UploadNewFile.tsx +++ b/workspace/src/app/views/shared/FileUploader/cases/UploadNewFile/UploadNewFile.tsx @@ -1,7 +1,16 @@ -import { DragDrop } from "@uppy/react"; import React, { useEffect, useState } from "react"; -import Uppy, { UppyFile } from "@uppy/core"; -import { Button, Icon, Notification, Spacing } from "@eccenca/gui-elements"; +import { + Button, + Icon, + Notification, + Restrictions, + Spacing, + Uppy, + UppyFile, + XHRUploadOptions, + FileUpload, + FileUploadDragDrop, +} from "@eccenca/gui-elements"; import { useTranslation } from "react-i18next"; import { NewFileItem } from "./NewFileItem"; import { ReplacementFileItem } from "./ReplacementFileItem"; @@ -14,7 +23,11 @@ import { CLASSPREFIX as eccgui } from "@eccenca/gui-elements/src/configuration/c interface IProps { // Uppy instance - uppy: Uppy.Uppy; + getUppyInstance?: (uppy?: Uppy) => void; + // xhr upload options + xhrUploadOptions: XHRUploadOptions; + // upload restrictions + restrictions?: Restrictions; // Allow multiple file upload allowMultiple?: boolean; @@ -49,6 +62,8 @@ interface IProps { /** If uploaded files can be deleted in the same dialog. */ allowFileDeletion?: boolean; + // parent uppy? instance + uppy?: Uppy; } /** @@ -56,7 +71,6 @@ interface IProps { */ export function UploadNewFile({ projectId, - uppy, onAdded, onUploadSuccess, validateBeforeAdd, @@ -67,7 +81,12 @@ export function UploadNewFile({ listenToUploadedFiles, uploadInitialFiles, allowFileDeletion, + xhrUploadOptions, + restrictions, + getUppyInstance, + ...restProps }: IProps) { + const [uppy, setUppy] = React.useState(restProps.uppy); // contains files, which need in replacements const [onlyReplacements, setOnlyReplacements] = useState([]); @@ -115,6 +134,7 @@ export function UploadNewFile({ checkFilesSuccessfullyUploaded(); const uploadInitialFilesAsNewFiles = React.useCallback(async () => { + if (!uppy) return; const files = uppy.getFiles(); for (let i = 0; i < files.length; i++) { await uploadAsNewFile(files[i]); @@ -131,28 +151,6 @@ export function UploadNewFile({ listenToUploadedFiles?.(uploadedFiles); }, [uploadedFiles]); - // register/unregister uppy events - useEffect(() => { - const unregisterEvents = () => { - uppy.off("file-added", handleFileAdded); - uppy.off("upload-progress", handleProgress); - uppy.off("upload-success", handleUploadSuccess); - uppy.off("upload-error", handleUploadError); - uppy.off("restriction-failed", onLocalRestrictionFailed); - }; - - // reset events, because of "file-added" store prev state values - unregisterEvents(); - - uppy.on("file-added", handleFileAdded); - uppy.on("upload-progress", handleProgress); - uppy.on("upload-success", handleUploadSuccess); - uppy.on("upload-error", handleUploadError); - uppy.on("restriction-failed", onLocalRestrictionFailed); - - return unregisterEvents; - }, [onlyReplacements, uploadedFiles, filesForRetry]); - /** If a restriction failed, e.g. file size too large, this is fired. */ const onLocalRestrictionFailed = (file: UppyFile & { error?: string }, error: any) => { if (error.isRestriction && error.message) { @@ -219,6 +217,7 @@ export function UploadNewFile({ }; const uploadAsNewFile = async (file: UppyFile) => { + if (!uppy) return; await validateBeforeUploadAsync(file); const notCompletedUploads = uppy.getFiles().filter((f) => !f.progress?.uploadComplete); @@ -236,7 +235,7 @@ export function UploadNewFile({ const upload = async (files): Promise => { try { files.forEach((file) => { - uppy.setFileState(file.id, { + uppy?.setFileState(file.id, { xhrUpload: { endpoint: attachFileNameToEndpoint ? `${uploadEndpoint}?path=${encodeURIComponent(file.name)}` @@ -245,7 +244,7 @@ export function UploadNewFile({ }); }); - await uppy.upload(); + await uppy?.upload(); } catch (e) { throw new Error(e); } @@ -317,6 +316,7 @@ export function UploadNewFile({ }; const handleAbort = (fileId: string) => { + if (!uppy) return; setProgresses((prevState) => { const newState = { ...prevState, @@ -329,13 +329,14 @@ export function UploadNewFile({ }; const handleReplace = (file: UppyFile) => { - uppy.addFile({ + uppy?.addFile({ ...file, source: "REPLACE_ACTION", }); }; const handleRetry = (fileId: string) => { + if (!uppy) return; const files = filesForRetry.filter((f) => f.id === fileId); removeFromRetry(fileId); @@ -349,18 +350,19 @@ export function UploadNewFile({ }; const handleRetryAll = () => { - const files = uppy.getFiles(); + if (!uppy) return; + const files = uppy?.getFiles(); - // reset uppy if all files should retry - uppy.reset(); + // reset uppy? if all files should retry + uppy?.reset(); files.forEach(uppy.addFile); }; const removeFromUppyQueue = (fileId: string) => { - uppy.removeFile(fileId); + uppy?.removeFile(fileId); - // call force on uppy change + // call force on uppy? change forceUpdate(); }; @@ -382,24 +384,68 @@ export function UploadNewFile({ }; const setFileError = (fileId: string, error: string) => { - uppy.setFileState(fileId, { + uppy?.setFileState(fileId, { error, }); // after every file state update forceUpdate required forceUpdate(); }; + // register/unregister uppy events + useEffect(() => { + if (uppy) { + const unregisterEvents = () => { + uppy.off("file-added", handleFileAdded); + uppy.off("upload-progress", handleProgress); + uppy.off("upload-success", handleUploadSuccess); + uppy.off("upload-error", handleUploadError); + uppy.off("restriction-failed", onLocalRestrictionFailed); + }; + + // reset events, because of "file-added" store prev state values + unregisterEvents(); + + uppy.on("file-added", handleFileAdded); + uppy.on("upload-progress", handleProgress); + uppy.on("upload-success", handleUploadSuccess); + uppy.on("upload-error", handleUploadError); + uppy.on("restriction-failed", onLocalRestrictionFailed); + + return unregisterEvents; + } + }, [onlyReplacements, uploadedFiles, filesForRetry, uppy]); + + const handleSetUppyInstance = React.useCallback( + (instance: Uppy) => { + if (!instance) return; + getUppyInstance && getUppyInstance(instance); + setUppy(instance); + }, + [getUppyInstance], + ); + return (
{projectId && showDeleteDialog && ( )} - {(uploadInitialFiles?.alsoAllowOther ?? true) && ( - - )} + + {(uploadInitialFiles?.alsoAllowOther ?? true) && + (!restProps.uppy ? ( + + ) : ( + + ))} {!error ? ( @@ -415,15 +461,17 @@ export function UploadNewFile({ onCancelRetry={removeFromRetry} /> ))} - {uppy.getFiles().map((file) => ( - - ))} + {uppy + ?.getFiles() + .map((file) => ( + + ))} {onlyReplacements.map((file) => ( "", }, }} allowMultiple={false} diff --git a/workspace/src/app/views/shared/modals/FileUploadModal/FileUploadModal.tsx b/workspace/src/app/views/shared/modals/FileUploadModal/FileUploadModal.tsx index 745e532197..0cbbc0f2a3 100644 --- a/workspace/src/app/views/shared/modals/FileUploadModal/FileUploadModal.tsx +++ b/workspace/src/app/views/shared/modals/FileUploadModal/FileUploadModal.tsx @@ -25,7 +25,7 @@ export function FileUploadModal({ isOpen, onDiscard, uploaderOptions = {} }: IFi const [t] = useTranslation(); useDebugValue(!projectId ? "Project ID not provided and upload url is not valid" : ""); - + if (!projectId) { return null; } @@ -49,8 +49,15 @@ export function FileUploadModal({ isOpen, onDiscard, uploaderOptions = {} }: IFi onClose={handleDiscard} preventSimpleClosing={isUploading} actions={ - } > @@ -64,6 +71,7 @@ export function FileUploadModal({ isOpen, onDiscard, uploaderOptions = {} }: IFi }} onProgress={(amount) => setIsUploading(amount > 0 && amount < 1)} // between 0 and 1 maxFileUploadSizeBytes={maxFileUploadSize} + t={t} /> diff --git a/workspace/src/app/views/shared/modals/ProjectImportModal.tsx b/workspace/src/app/views/shared/modals/ProjectImportModal.tsx index 5cff0a7ca6..0600d14f18 100644 --- a/workspace/src/app/views/shared/modals/ProjectImportModal.tsx +++ b/workspace/src/app/views/shared/modals/ProjectImportModal.tsx @@ -14,11 +14,11 @@ import { TitleSubsection, Markdown, StringPreviewContentBlobToggler, + Uppy, + UppyFile, } from "@eccenca/gui-elements"; import { useTranslation } from "react-i18next"; -import Uppy, { UppyFile } from "@uppy/core"; import { workspaceApi } from "../../../utils/getApiEndpoint"; -import XHR from "@uppy/xhr-upload"; import { requestDeleteProjectImport, requestProjectImportDetails, @@ -43,7 +43,7 @@ interface IProps { export function ProjectImportModal({ close, back, maxFileUploadSizeBytes }: IProps) { const [t] = useTranslation(); - const [uppy] = useState(Uppy()); + const [uppy, setUppy] = useState(); const dispatch = useDispatch(); const [loading, setLoading] = useState(false); const [projectImportId, setProjectImportId] = useState(null); @@ -58,25 +58,6 @@ export function ProjectImportModal({ close, back, maxFileUploadSizeBytes }: IPro [string, boolean, boolean] | null >(null); - useEffect(() => { - uppy.use(XHR, { - method: "POST", - fieldName: "file", - metaFields: [], - }); - uppy.getPlugin("XHRUpload").setOptions({ - endpoint: workspaceApi(`/projectImport`), - }); - - if (maxFileUploadSizeBytes) { - uppy.setOptions({ - restrictions: { - maxFileSize: maxFileUploadSizeBytes, - }, - }); - } - }, []); - useEffect(() => { if (projectImportId) { loadProjectImportDetails(projectImportId); @@ -122,7 +103,7 @@ export function ProjectImportModal({ close, back, maxFileUploadSizeBytes }: IPro const handleFileAdded = async () => { setUploadError(null); - await uppy.upload(); + await uppy?.upload(); }; const startProjectImport = async (generateNewProjectId: boolean, overWriteExistingProject: boolean) => { @@ -189,27 +170,48 @@ export function ProjectImportModal({ close, back, maxFileUploadSizeBytes }: IPro t("ProjectImportModal.responseUploadError", "File {{file}} could not be uploaded! {{details}}", { file: fileData.name, details: details, - }) + }), ); - uppy.reset(); + uppy?.reset(); }; + const onUploadSuccess = (file: UppyFile, response) => { - const projectImportId = response?.body?.projectImportId; + const projectImportId = + typeof response?.body === "string" + ? JSON.parse(response.body)?.projectImportId + : response?.body?.projectImportId; + if (projectImportId) { setProjectImportId(projectImportId); } else { setUploadError( t( "ProjectImportModal.responseInvalid", - "Invalid response received from project upload. Project import cannot proceed." - ) + "Invalid response received from project upload. Project import cannot proceed.", + ), ); - uppy.reset(); + uppy?.reset(); } }; + + const restrictions = { + restrictions: maxFileUploadSizeBytes + ? { + maxFileSize: maxFileUploadSizeBytes, + } + : {}, + }; + const uploader = ( {t("ProjectImportModal.importBtn")} - + , ); } else if (projectImportDetails.projectAlreadyExists) { approveReplacement @@ -243,7 +245,7 @@ export function ProjectImportModal({ close, back, maxFileUploadSizeBytes }: IPro disabled={false} > {t("ProjectImportModal.replaceImportBtn")} - + , ) : actions.push( + , ); } } @@ -262,7 +264,7 @@ export function ProjectImportModal({ close, back, maxFileUploadSizeBytes }: IPro actions.push( + , ); // Add 'Back' button actions.push( @@ -272,7 +274,7 @@ export function ProjectImportModal({ close, back, maxFileUploadSizeBytes }: IPro {t("common.words.back", "Back")} )} - + , ); const uploaderElement = ( @@ -342,7 +344,7 @@ export function ProjectImportModal({ close, back, maxFileUploadSizeBytes }: IPro

{t( "ProjectImportModal.warningExistingProject", - "A project with the same ID already exists! Choose to either overwrite the existing project or import the project under a freshly generated ID." + "A project with the same ID already exists! Choose to either overwrite the existing project or import the project under a freshly generated ID.", )}

@@ -393,11 +395,11 @@ export function ProjectImportModal({ close, back, maxFileUploadSizeBytes }: IPro ) : projectDetailsError !== null ? ( errorRetryElement( "Failed to retrieve project import details. " + projectDetailsError, - () => projectImportId && loadProjectImportDetails(projectImportId) + () => projectImportId && loadProjectImportDetails(projectImportId), ) ) : startProjectImportExecutionError ? ( errorRetryElement(`${t("common.messages.anErrorHasOccurred")} ${startProjectImportExecutionError[0]}`, () => - startProjectImport(startProjectImportExecutionError[1], startProjectImportExecutionError[2]) + startProjectImport(startProjectImportExecutionError[1], startProjectImportExecutionError[2]), ) ) : projectImportDetails ? ( projectDetailElement(projectImportDetails)