import "filepond-plugin-file-poster/dist/filepond-plugin-file-poster.css"; import "filepond-plugin-image-preview/dist/filepond-plugin-image-preview.css"; import "filepond/dist/filepond.css"; import { getLocalizedString } from "@olenbetong/appframe-core"; import type { DataObject } from "@olenbetong/appframe-data"; import { useData, useFetchData } from "@olenbetong/appframe-react"; import { create, type FilePondErrorDescription, type FilePondFile, type FilePondInitialFile, type FilePond as FilePondInstance, type FilePondOptions, FileStatus, registerPlugin, } from "filepond"; import FilePondPluginImageExifOrientation from "filepond-plugin-image-exif-orientation"; import FilePondPluginImagePreview from "filepond-plugin-image-preview"; import FilePondPluginImageResize from "filepond-plugin-image-resize"; import FilePondPluginImageTransform from "filepond-plugin-image-transform"; import { type RefObject, useEffect, useMemo, useRef } from "react"; export { FileStatus, registerPlugin }; registerPlugin( FilePondPluginImageExifOrientation, FilePondPluginImageResize, FilePondPluginImagePreview, FilePondPluginImageTransform, ); let isMostLikelyMobile = navigator.userAgent.match(/Android/i) || navigator.userAgent.match(/iPhone/i) || navigator.userAgent.match(/iPad/i) || navigator.userAgent.match(/iPod/i); const labels = { labelIdle: isMostLikelyMobile ? getLocalizedString("Take a photo, or upload an existing image/file") : getLocalizedString('Drag & Drop your files or Browse '), labelInvalidField: getLocalizedString("Field contains invalid files"), labelFileWaitingForSize: getLocalizedString("Waiting for size"), labelFileSizeNotAvailable: getLocalizedString("Size not available"), labelFileLoading: getLocalizedString("Loading"), labelFileLoadError: getLocalizedString("Error during load"), labelFileProcessing: getLocalizedString("Uploading"), labelFileProcessingComplete: getLocalizedString("Upload complete"), labelFileProcessingAborted: getLocalizedString("Upload cancelled"), labelFileProcessingError: getLocalizedString("Error during upload"), labelFileProcessingRevertError: getLocalizedString("Error during revert"), labelFileRemoveError: getLocalizedString("Error during remove"), labelTapToCancel: getLocalizedString("tap to cancel"), labelTapToRetry: getLocalizedString("tap to retry"), labelTapToUndo: getLocalizedString("tap to undo"), labelButtonRemoveItem: getLocalizedString("Remove"), labelButtonAbortItemLoad: getLocalizedString("Abort"), labelButtonRetryItemLoad: getLocalizedString("Retry"), labelButtonAbortItemProcessing: getLocalizedString("Cancel"), labelButtonUndoItemProcessing: getLocalizedString("Undo"), labelButtonRetryItemProcessing: getLocalizedString("Retry"), labelButtonProcessItem: getLocalizedString("Upload"), labelMaxFileSizeExceeded: getLocalizedString("File is too large"), labelMaxFileSize: getLocalizedString("Maximum file size is {filesize}"), labelMaxTotalFileSizeExceeded: getLocalizedString("Maximum total size exceeded"), labelMaxTotalFileSize: getLocalizedString("Maximum total file size is {filesize}"), labelFileTypeNotAllowed: getLocalizedString("File of invalid type"), fileValidateTypeLabelExpectedTypes: getLocalizedString("Expects {allButLastType} or {lastType}"), imageValidateSizeLabelFormatError: getLocalizedString("Image type not supported"), imageValidateSizeLabelImageSizeTooSmall: getLocalizedString("Image is too small"), imageValidateSizeLabelImageSizeTooBig: getLocalizedString("Image is too big"), imageValidateSizeLabelExpectedMinSize: getLocalizedString("Minimum size is {minWidth} × {minHeight}"), imageValidateSizeLabelExpectedMaxSize: getLocalizedString("Maximum size is {maxWidth} × {maxHeight}"), imageValidateSizeLabelImageResolutionTooLow: getLocalizedString("Resolution is too low"), imageValidateSizeLabelImageResolutionTooHigh: getLocalizedString("Resolution is too high"), imageValidateSizeLabelExpectedMinResolution: getLocalizedString("Minimum resolution is {minResolution}"), imageValidateSizeLabelExpectedMaxResolution: getLocalizedString("Maximum resolution is {maxResolution}"), }; type FilePondProps> = React.HTMLProps & { dataObject: DataObject; fields: Partial; onUploadStart: () => void; onFileUploaded: (error: FilePondErrorDescription | null, file: FilePondFile) => void; onFinishedUploads: () => void; onError: (error: string) => void; onFileRemoved?: (primKey: string) => void; /** * If the file should be uploaded to a blob field, set the field name here */ fieldName?: string; /** * If the file should be added to an existing record, set the prim key here */ primKey?: string; /** * If a reference to the file input element is needed, pass a ref here. */ inputRef?: RefObject; /** * Options to pass to FilePond */ filePondOptions?: Partial; attachmentPrimKeys?: string[]; whereClause?: string; }; function getFileUrl>( dataObject: DataObject, fieldName?: string, primKey?: string, action = "upload", ) { let url = action === "upload" ? dataObject.dataHandler.getUploadUrl() : dataObject.dataHandler.getFileStoreURL(primKey ?? "", fieldName ?? ""); if (action === "upload") { url += "/"; } return url?.replaceAll("//", "/") ?? ""; } type FileStoreDataObjectRecord = { PrimKey: string; FileName: string | null; FileSize: number | null; FileRef: string | null; ElementRef: string | null; Damage: boolean; DamageID: number | null; }; export function FilePond({ dataObject, fields, onUploadStart, onFileUploaded, onFinishedUploads, onFileRemoved, onError, fieldName, filePondOptions, primKey, attachmentPrimKeys, whereClause, inputRef: inputRefProp, ...props }: FilePondProps) { let inputRef = useRef(null); let pondRef = useRef(null); let filesData = useData(dataObject); let { data: filesFetched } = useFetchData(dataObject, whereClause ?? "1=2"); let files = useMemo(() => { if (whereClause) { return filesFetched; } return filesData; }, [whereClause, filesFetched, filesData]); let clearFilesRef = useRef(false); useEffect(() => { return () => { pondRef.current?.destroy(); pondRef.current = null; }; }, []); useEffect(() => { async function removeUploadedFile(source: string, load: () => void, error: (error: string) => void) { // this method is also called when we clear the files on data object // refresh. Haven't found any elegant way to work around that, so instead // we set this ref to true when we remove the files to have a way // to discover why the files are being removed if (clearFilesRef.current) return; let { dataHandler } = dataObject; try { let result = await dataHandler.destroy({ PrimKey: source, } as Partial); if (result) { onFileRemoved?.(source); load(); } else { error("Failed to delete file on server. Delete request returned false"); } } catch (err: any) { error( err.responseText ?? err.statusText ?? err.message ?? "Failed to delete file on server but no error was returned.", ); } } let options: FilePondOptions = { allowMultiple: Boolean(props.multiple), allowImageResize: true, imageResizeMode: "contain", imageResizeTargetWidth: 2400, imageResizeTargetHeight: 2400, imageResizeUpscale: false, ...labels, onaddfilestart: (file) => { if (file.status !== 5 && file.status !== 6 && file.status !== 10) { onUploadStart(); } }, onprocessfile: (error, file) => { onFileUploaded(error, file); let refFiles = pondRef.current?.getFiles(); let found = refFiles?.find( (file) => file.status !== 2 && file.status !== 5 && file.status !== 6 && file.status !== 10, ); if (found === undefined) { onFinishedUploads(); } }, onprocessfiles: () => onFinishedUploads(), onerror: (error) => onError(error.body), beforeRemoveFile: () => { if (confirm(getLocalizedString("Are you sure you want to remove this file?"))) { return true; } return false; }, server: { load: getFileUrl(dataObject, fieldName, primKey, "view"), restore: getFileUrl(dataObject, fieldName, primKey, "view"), revert: removeUploadedFile, remove: removeUploadedFile, process: { url: getFileUrl(dataObject, fieldName), ondata: (formData) => { let masterChildCriteria = dataObject.getParameter("masterChildCriteria"); let allFields = { ...masterChildCriteria, ...fields }; for (let [field, value] of Object.entries(allFields)) { if (value !== undefined) { formData.append(field, String(value)); } } return formData; }, onload: (response) => JSON.parse(response), }, }, ...filePondOptions, }; if (pondRef.current) { pondRef.current.setOptions(options); } else { pondRef.current = create(inputRefProp?.current ?? inputRef.current ?? undefined, options); } }, [ dataObject, props.multiple, fields, fieldName, primKey, inputRefProp, filePondOptions, onError, onFileUploaded, onUploadStart, onFinishedUploads, onFileRemoved, ]); useEffect(() => { if (pondRef.current) { files.forEach((file, index) => { let options: { index: number } & Partial = { index, type: "local", }; pondRef.current?.addFile(file.PrimKey, options); }); } return () => { // Indicate that we are clearing the files because data has changed, // to prevent the remove callback from deleting the files clearFilesRef.current = true; pondRef.current?.removeFiles(); clearFilesRef.current = false; }; }, [files]); return (
{attachmentPrimKeys && }
); }