Skip to content
This repository has been archived by the owner on Jun 7, 2023. It is now read-only.

Commit

Permalink
send file size and type to API
Browse files Browse the repository at this point in the history
  • Loading branch information
nramkissoon committed Apr 4, 2023
1 parent 12e0576 commit bf737f8
Show file tree
Hide file tree
Showing 8 changed files with 387 additions and 377 deletions.
5 changes: 5 additions & 0 deletions .changeset/wise-carrots-exercise.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@uploadjoy/uploader-component-playground": minor
---

send file name, size, and type to API
1 change: 0 additions & 1 deletion .npmignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
.changeset/
src/
.github/

tsconfig.json
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"format:check": "prettier -c src/*.ts --ignore-path ./.gitignore"
},
"peerDependencies": {
"react": "^18.0.0"
"react": ">= 18.0.0 < 19"
},
"devDependencies": {
"@changesets/cli": "^2.26.0",
Expand Down
373 changes: 1 addition & 372 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,372 +1 @@
"use client";

import {
HTMLProps,
Reducer,
useCallback,
useEffect,
useMemo,
useReducer,
useRef,
} from "react";
import { fromEvent } from "file-selector";
import {
acceptPropAsAcceptAttr,
composeEventHandlers,
canUseFileSystemAccessAPI,
isAbort,
isPropagationStopped,
isSecurityError,
onDocumentDragOver,
pickerOptionsFromAccept,
isEventWithFiles,
isEdge,
AcceptProp,
FileValidator,
noop,
UploaderError,
fileTypeIsAcceptable,
fileSizeIsAcceptable,
validateFile,
} from "./utils";
import { PresignedUrlFetchResponse, getPresignedUrls } from "./presignedUrls";
import { submit } from "./submit";

type UseInputProps = {
accept?: AcceptProp;
acceptAll?: boolean;
multiple?: boolean;
disabled?: boolean;
validator?: FileValidator;
minSize?: number;
maxSize?: number;
maxFiles?: number;
fileAccess: "public" | "private";
folder?: string;
useFsAccessApi: boolean;
getFilesFromEvent: typeof fromEvent;
onFileDialogCancel?: () => void;
onFileDialogOpen?: () => void;
onError?: (error: Error) => void;
onUploadProgress?: (event: ProgressEvent, file: File) => void;
};

type UseInputPropsState = {
isFileDialogActive: boolean;
isFocused: boolean;
acceptedFiles: File[];
fileRejections: {
file: File;
errors: (UploaderError | { code: string; message: string })[];
}[];
presignedUrls?: PresignedUrlFetchResponse;
};

const initialState: UseInputPropsState = {
isFileDialogActive: false,
isFocused: false,
acceptedFiles: [],
fileRejections: [],
};

type UseInputPropsAction = {
type: "focus" | "blur" | "openDialog" | "closeDialog" | "setFiles" | "reset";
} & Partial<UseInputPropsState>;

const reducer: Reducer<UseInputPropsState, UseInputPropsAction> = (
state,
action,
) => {
switch (action.type) {
case "focus":
return { ...state, isFocused: true };
case "blur":
return { ...state, isFocused: false };
case "openDialog":
return { ...state, isFileDialogActive: true };
case "closeDialog":
return { ...state, isFileDialogActive: false };
case "setFiles":
return {
...state,
acceptedFiles: action.acceptedFiles ?? [],
fileRejections: action.fileRejections ?? [],
presignedUrls: action.presignedUrls ?? undefined,
};
case "reset":
return {
...initialState,
};
}
};

const useInput = ({
acceptAll = false,
disabled = false,
getFilesFromEvent = fromEvent,
maxSize = Infinity,
minSize = 0,
maxFiles = 0,
multiple = true,
validator,
useFsAccessApi = true,
accept,
onFileDialogCancel,
onFileDialogOpen,
onError,
fileAccess = "private",
folder,
onUploadProgress,
}: UseInputProps) => {
const acceptAttr = useMemo(() => acceptPropAsAcceptAttr(accept), [accept]);
const pickerTypes = useMemo(() => pickerOptionsFromAccept(accept), [accept]);

const onFileDialogOpenCb = useMemo(
() => (typeof onFileDialogOpen === "function" ? onFileDialogOpen : noop),
[onFileDialogOpen],
);
const onFileDialogCancelCb = useMemo(
() =>
typeof onFileDialogCancel === "function" ? onFileDialogCancel : noop,
[onFileDialogCancel],
);

const inputRef = useRef<HTMLInputElement>(null);

const [state, dispatch] = useReducer(reducer, initialState);
const { isFocused, isFileDialogActive, acceptedFiles, presignedUrls } = state;

const fsAccessApiWorksRef = useRef(
typeof window !== "undefined" &&
window.isSecureContext &&
useFsAccessApi &&
canUseFileSystemAccessAPI(),
);

// Update file dialog active state when the window is focused on
const onWindowFocus = () => {
// Execute the timeout only if the file dialog is opened in the browser
if (!fsAccessApiWorksRef.current && isFileDialogActive) {
setTimeout(() => {
if (inputRef.current) {
const { files } = inputRef.current;

if (!files || files.length === 0) {
dispatch({ type: "closeDialog" });
onFileDialogCancelCb();
}
}
}, 300);
}
};

useEffect(() => {
window.addEventListener("focus", onWindowFocus, false);
return () => {
window.removeEventListener("focus", onWindowFocus, false);
};
}, [inputRef, isFileDialogActive, onFileDialogCancelCb, fsAccessApiWorksRef]);

const onErrCb = useCallback(
(e: Error) => {
if (onError) {
onError(e);
} else {
// Let the user know something's gone wrong if they haven't provided the onError cb.
console.error(e);
}
},
[onError],
);

const setFiles = useCallback(
async (files: File[]) => {
const acceptedFiles: File[] = [];
const fileRejections: {
file: File;
errors: (UploaderError | { code: string; message: string })[];
}[] = [];

files.forEach((file) => {
const { file: maybeValidatedFile, errors } = validateFile(
file,
acceptAttr,
acceptAll,
minSize,
maxSize,
validator,
);

if (errors.length === 0) {
acceptedFiles.push(maybeValidatedFile);
} else {
fileRejections.push({
file: maybeValidatedFile,
errors,
});
}
});

if (
(!multiple && acceptedFiles.length > 1) ||
(multiple && maxFiles >= 1 && acceptedFiles.length > maxFiles)
) {
// Reject everything and empty accepted files
acceptedFiles.forEach((file) => {
fileRejections.push({
file,
errors: [
{
code: "too-many-files",
message: `Too many files. Maximum allowed is ${maxFiles}.`,
},
],
});
});
acceptedFiles.splice(0);
}

if (acceptedFiles.length > 0) {
const presignedUrls = await getPresignedUrls({
fileNames: acceptedFiles.map((file) => file.name),
fileAccess,
folder: folder ?? "",
apiUrl: "/api/uploadjoy/presigned-urls",
});

dispatch({
acceptedFiles,
fileRejections,
presignedUrls,
type: "setFiles",
});
}

dispatch({
acceptedFiles,
fileRejections,
type: "setFiles",
});
},
[dispatch, multiple, acceptAttr, minSize, maxSize, maxFiles, validator],
);

// Fn for opening the file dialog programmatically
const openFileDialog = useCallback(() => {
// No point to use FS access APIs if context is not secure
// https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts#feature_detection
if (fsAccessApiWorksRef.current) {
dispatch({ type: "openDialog" });
onFileDialogOpenCb();
// https://developer.mozilla.org/en-US/docs/Web/API/window/showOpenFilePicker
const opts = {
multiple,
types: pickerTypes ?? undefined,
};
window
.showOpenFilePicker(opts)
.then((handles) => getFilesFromEvent(handles))
.then(async (files) => {
await setFiles(files as File[]);
dispatch({ type: "closeDialog" });
})
.catch((e: Error) => {
// AbortError means the user canceled
if (isAbort(e)) {
onFileDialogCancelCb();
dispatch({ type: "closeDialog" });
} else if (isSecurityError(e)) {
fsAccessApiWorksRef.current = false;
// CORS, so cannot use this API
// Try using the input
if (inputRef.current) {
inputRef.current.value = "";
inputRef.current.click();
} else {
onErrCb(
new Error(
"Cannot open the file picker because the https://developer.mozilla.org/en-US/docs/Web/API/File_System_Access_API is not supported and no <input> was provided.",
),
);
}
} else {
onErrCb(e);
}
});
return;
}
if (inputRef.current) {
dispatch({ type: "openDialog" });
onFileDialogOpenCb();
inputRef.current.value = "";
inputRef.current.click();
}
}, [
dispatch,
onFileDialogOpenCb,
onFileDialogCancelCb,
useFsAccessApi,
setFiles,
onErrCb,
pickerTypes,
multiple,
]);

const onInputElementClick = useCallback((event: MouseEvent) => {
event.stopPropagation();
}, []);

// eslint-disable-next-line @typescript-eslint/ban-types
const composeHandler = (fn: Function) => {
return disabled ? null : fn;
};

const getInputProps = useMemo(
() =>
({ onChange, onClick, ...rest }: HTMLProps<HTMLInputElement> = {}) => {
const inputProps = {
accept: acceptAttr,
multiple,
type: "file",
style: { display: "none" },
onChange: composeHandler(composeEventHandlers(onChange ?? noop)),
onClick: composeHandler(
composeEventHandlers(onClick ?? noop, onInputElementClick),
),
tabIndex: -1,
ref: inputRef,
};

return {
...inputProps,
...rest,
};
},
[inputRef, accept, multiple, disabled],
);

const uploadFiles = useCallback(async () => {
if (!presignedUrls) return;
await submit({
acceptedFiles,
presignedUrls,
onProgress: onUploadProgress ?? noop,
});
}, [acceptedFiles, presignedUrls, submit]);

const reset = useCallback(() => {
dispatch({ type: "reset" });
}, [dispatch]);

return {
...state,
isFocused: isFocused && !disabled,
getInputProps,
inputRef,
open: composeHandler(openFileDialog),
uploadFiles: composeHandler(uploadFiles),
reset,
};
};

export { useInput };
export * from "./react";
Loading

0 comments on commit bf737f8

Please sign in to comment.