You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

FileTable.js 16KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545
  1. import React, { useState, useEffect } from "react";
  2. import IconButton from "../../components/IconButton/IconButton";
  3. import deleteIcon from "../../assets/images/delete.png";
  4. import editIcon from "../../assets/images/edit.png";
  5. import { useTranslation } from "react-i18next";
  6. import PropTypes from "prop-types";
  7. import { useSelector, useDispatch } from "react-redux";
  8. import {
  9. deleteFileReq,
  10. getFileFiltersReq,
  11. getFilesReq,
  12. setContent,
  13. updateFileReq,
  14. } from "../../store/actions/files/fileActions";
  15. import { Fade, Pagination } from "@mui/material";
  16. import FilterButton from "../../components/Button/FilterButton";
  17. import searchImage from "../../assets/images/search.png";
  18. import DocsFilters from "../../components/Docs/DocsFilters";
  19. import ConfirmDialog from "../../components/MUI/ConfirmDialog";
  20. import FileViewer from "react-file-viewer";
  21. import CustomModal from "../../components/UI/CustomModal";
  22. import xIcon from "../../assets/images/x.png";
  23. import Button from "../../components/Button/Button";
  24. import { selectIsLoadingByActionType } from "../../store/selectors/loadingSelectors";
  25. import { FETCH_FILES_LOADING } from "../../store/actions/files/fileActionConstants";
  26. import { useParams } from "react-router";
  27. import { PAGE_SIZE_FILES } from "../../constants/keyCodeConstants";
  28. import { useTheme } from "@mui/system";
  29. import { useMediaQuery } from "@mui/material";
  30. const FileTable = ({ trigger }) => {
  31. const [file, setFile] = useState(null);
  32. const { data } = useSelector((s) => s.files);
  33. const { filters } = useSelector((s) => s.fileFilters);
  34. const [page, setPage] = useState(1);
  35. const [toggleFiltersDrawer, setToggleFiltersDrawer] = useState(false);
  36. const [isSearchFieldVisible, setIsSearchFieldVisible] = useState(false);
  37. let { id } = useParams();
  38. const [fileForDelete, setFileForDelete] = useState({
  39. open: false,
  40. title: "",
  41. streamId: null,
  42. });
  43. const [openNoteModal, setOpenNoteModal] = useState({
  44. open: false,
  45. note: null,
  46. streamId: "",
  47. });
  48. const { t } = useTranslation();
  49. const dispatch = useDispatch();
  50. const theme = useTheme();
  51. const matches = useMediaQuery(theme.breakpoints.down("370"));
  52. useEffect(() => {
  53. if (data !== null && data.data !== undefined) {
  54. if (data.data.length === 0) {
  55. setPage(getTotalNumberOfPages(data) - 1);
  56. }
  57. }
  58. }, [data]);
  59. const getTotalNumberOfPages = (data) => {
  60. return parseInt(data.total) <= PAGE_SIZE_FILES
  61. ? 1
  62. : Math.ceil(parseInt(data.total) / PAGE_SIZE_FILES);
  63. };
  64. useEffect(() => {
  65. setFile(null);
  66. }, [id]);
  67. useEffect(() => {
  68. if (trigger) {
  69. handleChangeVisibility();
  70. }
  71. }, [trigger]);
  72. const handleToggleFiltersDrawer = () => {
  73. setToggleFiltersDrawer((oldState) => !oldState);
  74. };
  75. useEffect(() => {
  76. handleFilters(page);
  77. }, [page]);
  78. useEffect(() => {
  79. dispatch(getFileFiltersReq());
  80. dispatch(
  81. getFilesReq({
  82. payload: {
  83. pageSize: PAGE_SIZE_FILES,
  84. currentPage: page,
  85. categoryId: id,
  86. extensions: [],
  87. tags: [],
  88. content: "",
  89. },
  90. })
  91. );
  92. setPage(1);
  93. }, [id]);
  94. const handleChange = (_, value) => {
  95. setFile(null);
  96. handleFilters(value);
  97. setPage(value);
  98. };
  99. const handleFilters = (value) => {
  100. var extFilters = [];
  101. filters.extensions
  102. ?.filter((n) => n.isChecked)
  103. .forEach((m) => extFilters.push(m.name));
  104. var tagFilters = [];
  105. filters.tags
  106. ?.filter((n) => n.isChecked)
  107. .forEach((m) => tagFilters.push(m.name));
  108. dispatch(
  109. getFilesReq({
  110. payload: {
  111. pageSize: PAGE_SIZE_FILES,
  112. currentPage: value,
  113. categoryId: id,
  114. extensions: extFilters,
  115. tags: tagFilters,
  116. content: filters.content === undefined ? "" : filters.content,
  117. },
  118. })
  119. );
  120. };
  121. const isLoading = useSelector(
  122. selectIsLoadingByActionType(FETCH_FILES_LOADING)
  123. );
  124. const handleChangeVisibility = () => {
  125. setIsSearchFieldVisible(false);
  126. };
  127. const stopPropagation = (e) => {
  128. e.stopPropagation();
  129. };
  130. const handleChangeContent = (value) => {
  131. dispatch(setContent(value));
  132. };
  133. const deleteFileHandler = (stream_id) => {
  134. dispatch(deleteFileReq({ id: stream_id }));
  135. if (file !== null && stream_id === file.streamId) {
  136. setFile(null);
  137. }
  138. };
  139. const handleKeyDown = (event) => {
  140. if (
  141. event.key === "Enter" &&
  142. filters.content !== "" &&
  143. filters.content !== undefined
  144. ) {
  145. var extFilters = [];
  146. filters.extensions
  147. ?.filter((n) => n.isChecked)
  148. .forEach((m) => extFilters.push(m.name));
  149. var tagFilters = [];
  150. filters.tags
  151. ?.filter((n) => n.isChecked)
  152. .forEach((m) => tagFilters.push(m.name));
  153. dispatch(
  154. getFilesReq({
  155. payload: {
  156. pageSize: PAGE_SIZE_FILES,
  157. currentPage: page,
  158. categoryId: id,
  159. extensions: extFilters,
  160. tags: tagFilters,
  161. content: filters.content,
  162. },
  163. })
  164. );
  165. setPage(1);
  166. setFile(null);
  167. }
  168. };
  169. const input = (
  170. <div>
  171. <input
  172. placeholder="Pretrazi..."
  173. value={filters.content}
  174. onChange={(e) => handleChangeContent(e.target.value)}
  175. onKeyDown={handleKeyDown}
  176. className="candidate-search-field"
  177. style={{ width: matches ? "160px" : "816px" }}
  178. onClick={stopPropagation}
  179. role="input"
  180. />
  181. </div>
  182. );
  183. const onError = (e) => {
  184. console.log(e, "error in file-viewer");
  185. };
  186. const displayFile = (fileStream, streamId, fileType) => {
  187. setFile({ fileStream, streamId, fileType });
  188. };
  189. const getPathForFile = (fileType, fileStream) => {
  190. if (fileType === "docx") {
  191. return `data:application/vnd.openxmlformats-officedocument.wordprocessingml.document;base64,${fileStream}`;
  192. } else if (fileType === "doc") {
  193. return `data:application/msword;base64,${fileStream}`;
  194. } else {
  195. return `data:application/pdf;base64,${fileStream}`;
  196. }
  197. };
  198. const updateFileNoteHandler = () => {
  199. dispatch(
  200. updateFileReq({
  201. id: openNoteModal.streamId,
  202. data: openNoteModal.note,
  203. onSuccessOpenNoteModal,
  204. })
  205. );
  206. };
  207. const onSuccessOpenNoteModal = () => {
  208. setOpenNoteModal({
  209. open: false,
  210. note: null,
  211. streamId: "",
  212. });
  213. };
  214. return isLoading ? (
  215. <div>
  216. <div className="l-t-rectangle"></div>
  217. <div className="r-b-rectangle"></div>
  218. <div className="loader-container h-withHeader">
  219. <span>Loading</span>
  220. </div>
  221. </div>
  222. ) : (
  223. <div>
  224. <ConfirmDialog
  225. open={fileForDelete.open}
  226. title="Brisanje dokumenta"
  227. subtitle={fileForDelete.title}
  228. imgSrc={deleteIcon}
  229. content="Da li ste sigurni za brisanje dokumenta?"
  230. onClose={() => {
  231. setFileForDelete({ open: false, title: "", streamId: null });
  232. }}
  233. onConfirm={() => {
  234. deleteFileHandler(fileForDelete.streamId);
  235. setFileForDelete({ open: false, title: "", streamId: null });
  236. }}
  237. />
  238. <CustomModal
  239. classes="files-custom-modal"
  240. open={openNoteModal.open}
  241. onCloseModal={() =>
  242. setOpenNoteModal({ open: false, note: null, streamId: "" })
  243. }
  244. >
  245. <div className="add-pattern-modal-header">
  246. <div className="add-pattern-modal-header-title">
  247. <div className="add-pattern-modal-header-title-image">
  248. <img src={editIcon} alt="plus" />
  249. </div>
  250. <div className="add-pattern-modal-header-title-title">
  251. <p>Edit text</p>
  252. </div>
  253. <div className="add-pattern-modal-header-title-sub">
  254. <sub> | Note</sub>
  255. </div>
  256. </div>
  257. <div
  258. className="add-pattern-modal-header-close"
  259. onClick={() =>
  260. setOpenNoteModal({ open: false, note: null, streamId: "" })
  261. }
  262. >
  263. <img src={xIcon} alt="close" />
  264. </div>
  265. </div>
  266. <div className="files-edit-note">
  267. <textarea
  268. className="files-note"
  269. value={openNoteModal.note}
  270. onChange={(e) =>
  271. setOpenNoteModal((oldState) => ({
  272. ...oldState,
  273. note: e.target.value,
  274. }))
  275. }
  276. />
  277. </div>
  278. <div className="files-custom-modal-buttons">
  279. <Button
  280. type="button"
  281. variant="contained"
  282. className="c-btn c-btn--primary-outlined"
  283. onClick={() =>
  284. setOpenNoteModal({ open: false, note: null, streamId: "" })
  285. }
  286. >
  287. Close
  288. </Button>
  289. <Button
  290. type="button"
  291. variant="contained"
  292. className="c-btn c-btn--primary"
  293. onClick={updateFileNoteHandler}
  294. >
  295. Save Changes
  296. </Button>
  297. </div>
  298. </CustomModal>
  299. <DocsFilters
  300. open={toggleFiltersDrawer}
  301. handleClose={handleToggleFiltersDrawer}
  302. setPage={setPage}
  303. setFile={setFile}
  304. />
  305. <div
  306. style={{
  307. display: "flex",
  308. justifyContent: "space-between",
  309. position: "relative",
  310. }}
  311. >
  312. <div style={{ width: "fit-content" }}>
  313. <Fade in={isSearchFieldVisible} timeout={500} className="proba">
  314. {input}
  315. </Fade>
  316. <Fade in={isSearchFieldVisible} timeout={500}>
  317. <div
  318. style={{
  319. position: "absolute",
  320. zIndex: 10000,
  321. left: 780,
  322. top: 20,
  323. marginTop: 0,
  324. }}
  325. >
  326. <img src={searchImage} />
  327. </div>
  328. </Fade>
  329. </div>
  330. <div style={{ display: "flex" }}>
  331. {isSearchFieldVisible ? (
  332. ""
  333. ) : (
  334. <IconButton
  335. className="c-btn c-btn--primary-outlined candidate-btn search-field userPageBtn ml-20px no-padding custom-filter-button"
  336. onClick={() => setIsSearchFieldVisible(true)}
  337. >
  338. {!matches ? t("candidates.search") : null}
  339. <img
  340. src={searchImage}
  341. alt="filter"
  342. className="candidates-image"
  343. style={{ marginLeft: matches ? "0px" : "10px" }}
  344. />
  345. </IconButton>
  346. )}
  347. <FilterButton onShowFilters={handleToggleFiltersDrawer} />
  348. </div>
  349. </div>
  350. <div
  351. style={{
  352. display: "flex",
  353. marginTop: "39px",
  354. justifyContent: "space-between",
  355. minHeight: "500px",
  356. }}
  357. >
  358. <div style={{ display: "flex", flexDirection: "column" }}>
  359. <div
  360. className="table-cont"
  361. style={{
  362. display: "flex",
  363. width: matches ? "75vw" : "85vw",
  364. height: "330px",
  365. flexDirection: "row",
  366. }}
  367. >
  368. <table
  369. className={"usersTable-users mini"}
  370. style={{
  371. width: file === null ? "100%" : "360px",
  372. overflowX: "visible",
  373. }}
  374. >
  375. <thead>
  376. <tr
  377. className="headingRow headingRowFiles"
  378. style={{ cursor: "pointer" }}
  379. >
  380. {file === null ? <th>Putanja dokumenta</th> : ""}
  381. <th>Naziv dokumenta</th>
  382. {file === null ? <th>Tip dokumenta</th> : ""}
  383. {file === null ? <th>Veličina dokumenta</th> : ""}
  384. {file === null ? <th>Obrisi dokument</th> : ""}
  385. {file === null ? <th>Note</th> : ""}
  386. <th>Preuzmi dokument</th>
  387. </tr>
  388. </thead>
  389. <tbody>
  390. {data.data?.map((n, index) => (
  391. <tr
  392. key={index}
  393. className="secondaryRow"
  394. onClick={() =>
  395. displayFile(n.file_stream, n.stream_id, n.file_type)
  396. }
  397. >
  398. {file === null ? (
  399. <td className="docs-name">{n.fileName}</td>
  400. ) : (
  401. ""
  402. )}
  403. <td className="docs-name">{n.title}</td>
  404. {file === null ? <td>{n.file_type && n.file_type}</td> : ""}
  405. {file === null ? (
  406. <td className="profession">{n.cached_file_size}kB</td>
  407. ) : (
  408. ""
  409. )}
  410. {file === null ? (
  411. <td className="profession">
  412. <IconButton
  413. className="c-btn c-btn--primary-outlined files-view-page-delete-btn"
  414. onClick={() =>
  415. setFileForDelete({
  416. open: true,
  417. title: n.title,
  418. streamId: n.stream_id,
  419. })
  420. }
  421. >
  422. <img
  423. style={{ width: "12px", height: "12px" }}
  424. src={deleteIcon}
  425. />
  426. </IconButton>
  427. </td>
  428. ) : (
  429. ""
  430. )}
  431. {file === null ? (
  432. <td className="profession">
  433. <IconButton
  434. className="c-btn c-btn--primary-outlined files-view-page-delete-btn"
  435. onClick={() => {
  436. setOpenNoteModal({
  437. open: true,
  438. note: n.note,
  439. streamId: n.stream_id,
  440. });
  441. }}
  442. >
  443. <img
  444. style={{ width: "12px", height: "12px" }}
  445. src={editIcon}
  446. />
  447. </IconButton>
  448. </td>
  449. ) : (
  450. ""
  451. )}
  452. <td>
  453. <div onClick={stopPropagation}>
  454. <a
  455. className="applicant-cv-button"
  456. style={{
  457. width: "100px",
  458. height: "40px",
  459. padding: 8,
  460. }}
  461. download={n.title}
  462. href={getPathForFile(n.file_type, n.file_stream)}
  463. >
  464. {t("common.download")}
  465. </a>
  466. </div>
  467. </td>
  468. </tr>
  469. ))}
  470. </tbody>
  471. </table>
  472. {file !== null ? (
  473. <div
  474. style={{
  475. width: "100%",
  476. marginLeft: "30px",
  477. overflowY: "auto",
  478. }}
  479. >
  480. <FileViewer
  481. fileType={file.fileType}
  482. filePath={getPathForFile(file.fileType, file.fileStream)}
  483. onError={onError}
  484. />
  485. </div>
  486. ) : (
  487. ""
  488. )}
  489. </div>
  490. <Pagination
  491. size={"small"}
  492. count={getTotalNumberOfPages(data)}
  493. color="primary"
  494. className="candidates-pagination"
  495. onChange={handleChange}
  496. shape="rounded"
  497. page={page}
  498. />
  499. </div>
  500. <div
  501. style={{
  502. display: "flex",
  503. justifyContent: "flex-end",
  504. marginBottom: "35px",
  505. }}
  506. ></div>
  507. </div>
  508. </div>
  509. );
  510. };
  511. FileTable.propTypes = {
  512. trigger: PropTypes.number,
  513. };
  514. export default FileTable;