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 && }
);
}