added upload progress and upload errors to entity input

This commit is contained in:
Victor committed 2024-08-16 15:00:29 -07:00
1 parent 349ca44af5
commit 178d59dc13
9 files changed
+203 -33

No files matched your search

+1
View File
@@ -35,6 +35,7 @@
"@react-spring/types": "^9.7.3",
"@typeform/embed": "^4.8.1",
"@types/dom-mediacapture-record": "^1.0.19",
"axios": "^1.7.4",
"framer-motion": "6.5.1",
"local-storage": "^2.0.0",
"moment": "^2.29.4",
@@ -1,33 +1,39 @@
import GetApiHost from "./GetApiHost";
import axios, { AxiosProgressEvent } from "axios";
const { formatUrl, host = "" } = GetApiHost();
const { formatUrl } = GetApiHost();
const MakeMultipartRequest = (endpoint = "", body: any) => {
export type OnUploadProgress = (progressEvent: AxiosProgressEvent) => void;
const MakeMultipartRequest = (
endpoint = "",
body: any,
onUploadProgress?: OnUploadProgress
) => {
const formData = new FormData();
Object.keys(body).forEach((key) => formData.append(key, body[key]));
Object.keys(body).forEach(key => formData.append(key, body[key]));
formData.append('source', "file");
formData.append("source", "file");
return fetch(formatUrl(endpoint), {
method: 'POST',
credentials: 'include',
return axios
.post(formatUrl(endpoint), formData, {
withCredentials: true,
headers: {
'Accept': 'application/json',
Accept: "application/json",
},
body: formData,
})
.then(res => res.json())
.then(res => {
if (res && res.success) {
return res;
} else {
return { success : false };
}
})
.catch(e => {
return { success : false };
});
}
...(onUploadProgress ? { onUploadProgress } : {}),
})
.then(({ data }) => {
if (data && data.success) {
return data;
} else {
return { success: false };
}
})
.catch(e => {
return { success: false };
});
};
export default MakeMultipartRequest;
export default MakeMultipartRequest;
@@ -1,4 +1,6 @@
import MakeMultipartRequest from "../MakeMultipartRequest";
import MakeMultipartRequest, {
OnUploadProgress,
} from "../MakeMultipartRequest";
export interface UploadVideoMediaRequest {
file: any;
@@ -15,6 +17,13 @@ export interface UploadVideoMediaResponse {
success: boolean;
}
export const UploadVideoMedia = (request: UploadVideoMediaRequest) => {
return MakeMultipartRequest("/v1/media_files/upload/new_video", request);
export const UploadVideoMedia = (
request: UploadVideoMediaRequest,
onUploadProgress?: OnUploadProgress
) => {
return MakeMultipartRequest(
"/v1/media_files/upload/new_video",
request,
onUploadProgress
);
};
@@ -81,6 +81,26 @@
color: rgba(#fff, 0.5);
pointer-events: none;
}
.fy-entity-input-progress-indicator {
align-items: center;
display: flex;
flex-direction: column;
position: relative;
.fy-entity-input-progress-number {
position: absolute;
text-align: center;
font-size: 0.625rem;
line-height: 0.625rem;
height: 0.625rem;
top: 50%;
transform: translateY(-50%);
left: 0;
right: 0;
bottom: 0;
}
}
}
.fy-entity-input-landscape {
@@ -135,6 +155,17 @@
padding: 0.5rem;
}
.fy-entity-input-error {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
button {
margin-top: 1rem;
}
}
.fy-entity-input-preview-controls {
// background-color: green;
display: grid;
@@ -5,7 +5,12 @@ import useMeasure from "react-use-measure";
import { MediaFile } from "@storyteller/components/src/api/media_files/GetMedia";
import Iframe from "react-iframe";
import { Area, Point } from "react-easy-crop";
import { Label, Spinner, ZoomSliderOnChangeEvent } from "components/common";
import {
Button,
Label,
Spinner,
ZoomSliderOnChangeEvent,
} from "components/common";
import {
AcceptTypes,
EntityModeProp,
@@ -13,6 +18,7 @@ import {
MediaFilters,
UploaderResponse,
} from "components/entities/EntityTypes";
import { WorkIndicator } from "components/svg";
import { useMedia, useMediaUploader } from "hooks";
import { BucketConfig } from "@storyteller/components/src/api/BucketConfig";
import { Prompt } from "@storyteller/components/src/api/prompts/GetPrompts";
@@ -52,6 +58,9 @@ export interface SlideProps {
cropProps?: CropProps;
isNarrow: boolean;
media?: MediaFile;
resetUploader: () => void;
uploaderBusy: boolean;
uploadProgress: number;
}
interface AniProps {
@@ -63,8 +72,47 @@ interface AniProps {
style: any;
}
const MediaBusy = () => {
return <Spinner />;
const MediaError = ({ resetUploader }: SlideProps) => {
return (
<>
<h4>There was a problem with your upload</h4>
<Button
{...{
label: "Try another upload",
onClick: resetUploader,
variant: "secondary",
}}
/>
</>
);
};
const MediaBusy = ({ uploaderBusy, uploadProgress }: SlideProps) => {
return (
<div {...{ className: "fy-entity-input-loader" }}>
{uploaderBusy ? (
<>
Uploading
<div {...{ className: "fy-entity-input-progress-indicator" }}>
<div {...{ className: "fy-entity-input-progress-number" }}>
{uploadProgress}%
</div>
<WorkIndicator
{...{
failure: false,
max: 100,
progressPercentage: uploadProgress,
stage: 1,
success: false,
}}
/>
</div>
</>
) : (
<Spinner />
)}
</div>
);
};
const EntityInputFull = ({ clear, cropProps, isNarrow, media }: SlideProps) => {
@@ -216,7 +264,13 @@ export default function EntityInput({
onChange({ target: { name, value: token } });
};
const { busy: uploaderBusy, inputProps } = useMediaUploader({
const {
busy: uploaderBusy,
error: uploaderError,
inputProps,
uploadProgress,
reset: resetUploader,
} = useMediaUploader({
autoUpload: true,
onSuccess: (res: UploaderResponse) => {
reload();
@@ -230,7 +284,7 @@ export default function EntityInput({
};
const busy = mediaBusy || uploaderBusy;
const index = busy ? 0 : media ? 1 : 2;
const index = uploaderError ? 3 : busy ? 0 : media ? 1 : 2;
const [animating, animatingSet] = useState(false);
if (debug)
@@ -284,6 +338,7 @@ export default function EntityInput({
isLeaving,
style,
showFilters: showMediaBrowserFilters,
uploaderBusy,
};
return [
@@ -292,6 +347,7 @@ export default function EntityInput({
className:
"fy-entity-input-busy d-flex justify-content-center align-items-center",
render: MediaBusy,
uploadProgress,
...sharedProps,
}}
/>,
@@ -320,6 +376,14 @@ export default function EntityInput({
...rest,
}}
/>,
<AnimatedSlide
{...{
className: "fy-entity-input-error",
render: MediaError,
resetUploader,
...sharedProps,
}}
/>,
][i];
})}
</div>
@@ -4,16 +4,17 @@ import { circle } from "../sharedSVGProps";
interface Props {
className?: string;
max?: number;
progressPercentage: number;
stage: number;
}
export default function DashedCircle({
className = "",
max = 87,
progressPercentage,
stage = 0,
}: Props) {
const max = 87;
const progressBar = (max - 16) * (progressPercentage / 100);
const progressSegment = progressBar / 3;
@@ -5,6 +5,7 @@ import "./WorkIndicator.scss";
interface Props {
failure: boolean;
max?: number;
progressPercentage: number;
stage: number;
success: boolean;
@@ -12,6 +13,7 @@ interface Props {
export default function WorkIndicator({
failure = false,
max,
progressPercentage,
stage = 0,
success = false,
@@ -23,6 +25,7 @@ export default function WorkIndicator({
<DashedCircle
{...{
className: "work-indicator-circle-marker",
max,
progressPercentage,
stage,
}}
@@ -30,6 +30,7 @@ export default function useMediaUploader({ autoUpload, onSuccess = n }: Props) {
const [engineSubtype, engineSubtypeSet] = useState<
MediaFileSubtype | undefined
>();
const [uploadProgress, uploadProgressSet] = useState(0);
const { file, clear, inputProps } = useFile({
...(autoUpload
@@ -63,7 +64,12 @@ export default function useMediaUploader({ autoUpload, onSuccess = n }: Props) {
if (isAudio) return UploadAudioMedia(mediaConfig);
if (isEngineAsset) return UploadEngineAsset(engineConfig);
if (isImage) return UploadImageMedia(mediaConfig);
else return UploadVideoMedia(mediaConfig);
else
return UploadVideoMedia(mediaConfig, uploadEvent =>
uploadProgressSet(
Math.round((uploadEvent.loaded * 100) / (uploadEvent?.total || 0))
)
);
};
if (inputFile) {
@@ -72,6 +78,8 @@ export default function useMediaUploader({ autoUpload, onSuccess = n }: Props) {
statusSet(FetchStatus.success);
onSuccess(res);
todo();
} else {
statusSet(FetchStatus.error);
}
});
}
@@ -92,6 +100,7 @@ export default function useMediaUploader({ autoUpload, onSuccess = n }: Props) {
clear,
engineSubtype,
engineSubtypeChange,
error: status === FetchStatus.error,
mediaClass,
mediaClassChange,
file,
@@ -103,6 +112,12 @@ export default function useMediaUploader({ autoUpload, onSuccess = n }: Props) {
),
isImage: isSelectedType(MediaFilters.image, extension(file?.name || "")),
isVideo: isSelectedType(MediaFilters.video, extension(file?.name || "")),
reset: () => {
clear();
statusSet(FetchStatus.ready);
},
status,
upload,
uploadProgress,
};
}
+40
View File
@@ -4488,6 +4488,7 @@ __metadata:
"@react-spring/types": ^9.7.3
"@typeform/embed": ^4.8.1
"@types/dom-mediacapture-record": ^1.0.19
axios: ^1.7.4
craco: ^0.0.3
cross-env: ^7.0.3
framer-motion: 6.5.1
@@ -6972,6 +6973,17 @@ __metadata:
languageName: node
linkType: hard
"axios@npm:^1.7.4":
version: 1.7.4
resolution: "axios@npm:1.7.4"
dependencies:
follow-redirects: ^1.15.6
form-data: ^4.0.0
proxy-from-env: ^1.1.0
checksum: 0c17039a9acfe6a566fca8431ba5c1b455c83d30ea6157fec68a6722878fcd30f3bd32d172f6bee0c51fe75ca98e6414ddcd968a87b5606b573731629440bfaf
languageName: node
linkType: hard
"axobject-query@npm:^2.2.0":
version: 2.2.0
resolution: "axobject-query@npm:2.2.0"
@@ -11022,6 +11034,16 @@ __metadata:
languageName: node
linkType: hard
"follow-redirects@npm:^1.15.6":
version: 1.15.6
resolution: "follow-redirects@npm:1.15.6"
peerDependenciesMeta:
debug:
optional: true
checksum: a62c378dfc8c00f60b9c80cab158ba54e99ba0239a5dd7c81245e5a5b39d10f0c35e249c3379eae719ff0285fff88c365dd446fab19dee771f1d76252df1bbf5
languageName: node
linkType: hard
"for-each@npm:^0.3.3":
version: 0.3.3
resolution: "for-each@npm:0.3.3"
@@ -11087,6 +11109,17 @@ __metadata:
languageName: node
linkType: hard
"form-data@npm:^4.0.0":
version: 4.0.0
resolution: "form-data@npm:4.0.0"
dependencies:
asynckit: ^0.4.0
combined-stream: ^1.0.8
mime-types: ^2.1.12
checksum: 01135bf8675f9d5c61ff18e2e2932f719ca4de964e3be90ef4c36aacfc7b9cb2fceb5eca0b7e0190e3383fe51c5b37f4cb80b62ca06a99aaabfcfd6ac7c9328c
languageName: node
linkType: hard
"form-data@npm:~2.3.2":
version: 2.3.3
resolution: "form-data@npm:2.3.3"
@@ -17489,6 +17522,13 @@ __metadata:
languageName: node
linkType: hard
"proxy-from-env@npm:^1.1.0":
version: 1.1.0
resolution: "proxy-from-env@npm:1.1.0"
checksum: ed7fcc2ba0a33404958e34d95d18638249a68c430e30fcb6c478497d72739ba64ce9810a24f53a7d921d0c065e5b78e3822759800698167256b04659366ca4d4
languageName: node
linkType: hard
"prr@npm:~1.0.1":
version: 1.0.1
resolution: "prr@npm:1.0.1"