FileSelector.tsx 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219
  1. /*
  2. * Copyright 2021-2024 Avaiga Private Limited
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
  5. * the License. You may obtain a copy of the License at
  6. *
  7. * http://www.apache.org/licenses/LICENSE-2.0
  8. *
  9. * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
  10. * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
  11. * specific language governing permissions and limitations under the License.
  12. */
  13. import React, {
  14. ChangeEvent,
  15. CSSProperties,
  16. ReactNode,
  17. useCallback,
  18. useContext,
  19. useEffect,
  20. useMemo,
  21. useRef,
  22. useState,
  23. } from "react";
  24. import Button from "@mui/material/Button";
  25. import LinearProgress from "@mui/material/LinearProgress";
  26. import Tooltip from "@mui/material/Tooltip";
  27. import UploadFile from "@mui/icons-material/UploadFile";
  28. import { TaipyContext } from "../../context/taipyContext";
  29. import { createAlertAction, createSendActionNameAction } from "../../context/taipyReducers";
  30. import { useClassNames, useDynamicProperty, useModule } from "../../utils/hooks";
  31. import { expandSx, getCssSize, noDisplayStyle, TaipyActiveProps } from "./utils";
  32. import { uploadFile } from "../../workers/fileupload";
  33. import { SxProps } from "@mui/material";
  34. interface FileSelectorProps extends TaipyActiveProps {
  35. onAction?: string;
  36. defaultLabel?: string;
  37. label?: string;
  38. multiple?: boolean;
  39. extensions?: string;
  40. dropMessage?: string;
  41. notify?: boolean;
  42. width?: string | number;
  43. icon?: ReactNode;
  44. withBorder?: boolean;
  45. onUploadAction?: string;
  46. uploadData?: string;
  47. }
  48. const handleDragOver = (evt: DragEvent) => {
  49. evt.stopPropagation();
  50. evt.preventDefault();
  51. evt.dataTransfer && (evt.dataTransfer.dropEffect = "copy");
  52. };
  53. const defaultSx = { minWidth: "0px" };
  54. const FileSelector = (props: FileSelectorProps) => {
  55. const {
  56. id,
  57. onAction,
  58. defaultLabel = "",
  59. updateVarName = "",
  60. multiple = false,
  61. extensions = ".csv,.xlsx",
  62. dropMessage = "Drop here to Upload",
  63. label,
  64. notify = true,
  65. withBorder = true,
  66. } = props;
  67. const [dropLabel, setDropLabel] = useState("");
  68. const [dropSx, setDropSx] = useState<SxProps>(defaultSx);
  69. const [upload, setUpload] = useState(false);
  70. const [progress, setProgress] = useState(0);
  71. const { state, dispatch } = useContext(TaipyContext);
  72. const butRef = useRef<HTMLElement>(null);
  73. const inputId = useMemo(() => (id || `tp-${Date.now()}-${Math.random()}`) + "-upload-file", [id]);
  74. const module = useModule();
  75. const className = useClassNames(props.libClassName, props.dynamicClassName, props.className);
  76. const active = useDynamicProperty(props.active, props.defaultActive, true);
  77. const hover = useDynamicProperty(props.hoverText, props.defaultHoverText, undefined);
  78. useEffect(
  79. () =>
  80. setDropSx((sx: SxProps) =>
  81. expandSx(
  82. sx,
  83. props.width ? { width: getCssSize(props.width) } : undefined,
  84. withBorder ? undefined : { border: "none" }
  85. )
  86. ),
  87. [props.width, withBorder]
  88. );
  89. const handleFiles = useCallback(
  90. (files: FileList | undefined | null, evt: Event | ChangeEvent) => {
  91. evt.stopPropagation();
  92. evt.preventDefault();
  93. if (files?.length) {
  94. setUpload(true);
  95. uploadFile(
  96. updateVarName,
  97. module,
  98. props.onUploadAction,
  99. props.uploadData,
  100. files,
  101. setProgress,
  102. state.id
  103. ).then(
  104. (value) => {
  105. setUpload(false);
  106. onAction && dispatch(createSendActionNameAction(id, module, onAction));
  107. notify &&
  108. dispatch(
  109. createAlertAction({ atype: "success", message: value, system: false, duration: 3000 })
  110. );
  111. },
  112. (reason) => {
  113. setUpload(false);
  114. notify &&
  115. dispatch(
  116. createAlertAction({ atype: "error", message: reason, system: false, duration: 3000 })
  117. );
  118. }
  119. );
  120. }
  121. },
  122. [state.id, id, onAction, props.onUploadAction, props.uploadData, notify, updateVarName, dispatch, module]
  123. );
  124. const handleChange = useCallback(
  125. (e: ChangeEvent<HTMLInputElement>) => handleFiles(e.target.files, e),
  126. [handleFiles]
  127. );
  128. const handleDrop = useCallback(
  129. (e: DragEvent) => {
  130. setDropLabel("");
  131. setDropSx((sx: SxProps) => ({ ...sx, ...defaultSx }));
  132. handleFiles(e.dataTransfer?.files, e);
  133. },
  134. [handleFiles]
  135. );
  136. const handleDragLeave = useCallback(() => {
  137. setDropLabel("");
  138. setDropSx((sx: SxProps) => ({ ...sx, ...defaultSx }));
  139. }, []);
  140. const handleDragOverWithLabel = useCallback(
  141. (evt: DragEvent) => {
  142. const target = evt.currentTarget as HTMLElement;
  143. setDropSx((sx: SxProps) =>
  144. expandSx(
  145. sx,
  146. (sx as CSSProperties).minWidth === defaultSx.minWidth && target
  147. ? { minWidth: target.clientWidth + "px" }
  148. : undefined
  149. )
  150. );
  151. setDropLabel(dropMessage);
  152. handleDragOver(evt);
  153. },
  154. [dropMessage]
  155. );
  156. useEffect(() => {
  157. const butElt = butRef.current;
  158. const thisHandleDrop = handleDrop;
  159. if (butElt) {
  160. butElt.addEventListener("dragover", handleDragOverWithLabel);
  161. butElt.addEventListener("dragleave", handleDragLeave);
  162. butElt.addEventListener("drop", thisHandleDrop);
  163. }
  164. return () => {
  165. if (butElt) {
  166. butElt.removeEventListener("dragover", handleDragOverWithLabel);
  167. butElt.removeEventListener("dragleave", handleDragLeave);
  168. butElt.removeEventListener("drop", thisHandleDrop);
  169. }
  170. };
  171. }, [handleDrop, handleDragLeave, handleDragOverWithLabel]);
  172. return (
  173. <label htmlFor={inputId} className={className}>
  174. <input
  175. style={noDisplayStyle}
  176. id={inputId}
  177. name="upload-file"
  178. type="file"
  179. accept={extensions}
  180. multiple={multiple}
  181. onChange={handleChange}
  182. disabled={!active || upload}
  183. />
  184. <Tooltip title={hover || ""}>
  185. <span>
  186. <Button
  187. id={id}
  188. component="span"
  189. aria-label="upload"
  190. variant={withBorder ? "outlined" : undefined}
  191. disabled={!active || upload}
  192. sx={dropSx}
  193. ref={butRef}
  194. >
  195. {props.icon || <UploadFile />} {dropLabel || label || defaultLabel}
  196. </Button>
  197. </span>
  198. </Tooltip>
  199. {upload ? <LinearProgress value={progress} /> : null}
  200. </label>
  201. );
  202. };
  203. export default FileSelector;