| @@ -116,4 +116,25 @@ | |||
| border-radius:6px; | |||
| outline:none; | |||
| font-size: 18px; | |||
| } | |||
| .files-custom-modal { | |||
| width: 500px !important; | |||
| } | |||
| .files-edit-note { | |||
| margin-bottom: 12px; | |||
| } | |||
| .files-edit-note textarea { | |||
| width: 100%; | |||
| } | |||
| .files-custom-modal-buttons { | |||
| display: flex; | |||
| justify-content: flex-end; | |||
| } | |||
| .files-custom-modal-buttons button { | |||
| margin-left: 8px; | |||
| } | |||
| @@ -1,4 +1,4 @@ | |||
| import { Pagination } from "@mui/material"; | |||
| import { Button, Pagination } from "@mui/material"; | |||
| import React from "react"; | |||
| import { useEffect } from "react"; | |||
| import { useState } from "react"; | |||
| @@ -11,6 +11,7 @@ import { | |||
| deleteFileReq, | |||
| getFileFiltersReq, | |||
| getFilesReq, | |||
| updateFileReq, | |||
| } from "../../store/actions/files/fileActions"; | |||
| import { setContent } from "../../store/actions/files/fileActions"; | |||
| import { selectIsLoadingByActionType } from "../../store/selectors/loadingSelectors"; | |||
| @@ -23,7 +24,10 @@ import PropTypes from "prop-types"; | |||
| import FileViewer from "react-file-viewer"; | |||
| import { useLocation } from "react-router"; | |||
| import deleteIcon from "../../assets/images/delete.png"; | |||
| import editIcon from "../../assets/images/edit.png"; | |||
| import xIcon from "../../assets/images/x.png"; | |||
| import ConfirmDialog from "../../components/MUI/ConfirmDialog"; | |||
| import CustomModal from "../../components/UI/CustomModal"; | |||
| const FilesViewPage = ({ history }) => { | |||
| const [toggleFiltersDrawer, setToggleFiltersDrawer] = useState(false); | |||
| @@ -33,7 +37,16 @@ const FilesViewPage = ({ history }) => { | |||
| const { t } = useTranslation(); | |||
| const [isSearchFieldVisible, setIsSearchFieldVisible] = useState(false); | |||
| const [file, setFile] = useState(null); | |||
| const [fileForDelete, setFileForDelete] = useState({open: false, title: '', streamId: null}); | |||
| const [openNoteModal, setOpenNoteModal] = useState({ | |||
| open: false, | |||
| note: null, | |||
| streamId: "", | |||
| }); | |||
| const [fileForDelete, setFileForDelete] = useState({ | |||
| open: false, | |||
| title: "", | |||
| streamId: null, | |||
| }); | |||
| const { state } = useLocation(); | |||
| const dispatch = useDispatch(); | |||
| @@ -183,6 +196,20 @@ const FilesViewPage = ({ history }) => { | |||
| } | |||
| }; | |||
| const updateFileNoteHandler = () => { | |||
| dispatch( | |||
| updateFileReq({ id: openNoteModal.streamId, data: openNoteModal.note, onSuccessOpenNoteModal }) | |||
| ); | |||
| }; | |||
| const onSuccessOpenNoteModal = () => { | |||
| setOpenNoteModal({ | |||
| open: false, | |||
| note: null, | |||
| streamId: "", | |||
| }); | |||
| }; | |||
| return isLoading ? ( | |||
| <div> | |||
| <div className="l-t-rectangle"></div> | |||
| @@ -200,13 +227,74 @@ const FilesViewPage = ({ history }) => { | |||
| imgSrc={deleteIcon} | |||
| content="Da li ste sigurni za brisanje dokumenta?" | |||
| onClose={() => { | |||
| setFileForDelete({open: false, title: '', streamId: null}); | |||
| setFileForDelete({ open: false, title: "", streamId: null }); | |||
| }} | |||
| onConfirm={() => { | |||
| deleteFileHandler(fileForDelete.streamId) | |||
| setFileForDelete({open: false, title: '', streamId: null}); | |||
| deleteFileHandler(fileForDelete.streamId); | |||
| setFileForDelete({ open: false, title: "", streamId: null }); | |||
| }} | |||
| /> | |||
| <CustomModal | |||
| classes="files-custom-modal" | |||
| open={openNoteModal.open} | |||
| onCloseModal={() => | |||
| setOpenNoteModal({ open: false, note: null, streamId: "" }) | |||
| } | |||
| > | |||
| <div className="add-pattern-modal-header"> | |||
| <div className="add-pattern-modal-header-title"> | |||
| <div className="add-pattern-modal-header-title-image"> | |||
| <img src={editIcon} alt="plus" /> | |||
| </div> | |||
| <div className="add-pattern-modal-header-title-title"> | |||
| <p>Edit text</p> | |||
| </div> | |||
| <div className="add-pattern-modal-header-title-sub"> | |||
| <sub> | Note</sub> | |||
| </div> | |||
| </div> | |||
| <div | |||
| className="add-pattern-modal-header-close" | |||
| onClick={() => | |||
| setOpenNoteModal({ open: false, note: null, streamId: "" }) | |||
| } | |||
| > | |||
| <img src={xIcon} alt="close" /> | |||
| </div> | |||
| </div> | |||
| <div className="files-edit-note"> | |||
| <textarea | |||
| className="files-note" | |||
| value={openNoteModal.note} | |||
| onChange={(e) => | |||
| setOpenNoteModal((oldState) => ({ | |||
| ...oldState, | |||
| note: e.target.value, | |||
| })) | |||
| } | |||
| /> | |||
| </div> | |||
| <div className="files-custom-modal-buttons"> | |||
| <Button | |||
| type="button" | |||
| variant="contained" | |||
| className="c-btn c-btn--primary-outlined" | |||
| onClick={() => | |||
| setOpenNoteModal({ open: false, note: null, streamId: "" }) | |||
| } | |||
| > | |||
| Close | |||
| </Button> | |||
| <Button | |||
| type="button" | |||
| variant="contained" | |||
| className="c-btn c-btn--primary" | |||
| onClick={updateFileNoteHandler} | |||
| > | |||
| Save Changes | |||
| </Button> | |||
| </div> | |||
| </CustomModal> | |||
| <div onClick={() => setIsSearchFieldVisible(false)}> | |||
| <div className="l-t-rectangle"></div> | |||
| <div className="r-b-rectangle"></div> | |||
| @@ -286,6 +374,7 @@ const FilesViewPage = ({ history }) => { | |||
| <th>Tip dokumenta</th> | |||
| {file === null ? <th>Veličina dokumenta</th> : ""} | |||
| <th>Obrisi dokument</th> | |||
| <th>Note</th> | |||
| <th>Preuzmi dokument</th> | |||
| </tr> | |||
| </thead> | |||
| @@ -296,7 +385,9 @@ const FilesViewPage = ({ history }) => { | |||
| <tr | |||
| key={index} | |||
| className="secondaryRow" | |||
| onClick={() => displayFile(n.file_stream, n.stream_id, n.file_type)} | |||
| onClick={() => | |||
| displayFile(n.file_stream, n.stream_id, n.file_type) | |||
| } | |||
| > | |||
| <td className="docs-name">{n.fileName}</td> | |||
| <td className="docs-name">{n.title}</td> | |||
| @@ -309,7 +400,13 @@ const FilesViewPage = ({ history }) => { | |||
| <td className="profession"> | |||
| <IconButton | |||
| className="c-btn c-btn--primary-outlined files-view-page-delete-btn" | |||
| onClick={() => /* deleteFileHandler(n.stream_id) */setFileForDelete({open: true, title: n.title, streamId: n.stream_id})} | |||
| onClick={() => | |||
| setFileForDelete({ | |||
| open: true, | |||
| title: n.title, | |||
| streamId: n.stream_id, | |||
| }) | |||
| } | |||
| > | |||
| <img | |||
| style={{ width: "12px", height: "12px" }} | |||
| @@ -317,25 +414,42 @@ const FilesViewPage = ({ history }) => { | |||
| /> | |||
| </IconButton> | |||
| </td> | |||
| <td> | |||
| <div onClick={stopPropagation}> | |||
| <a | |||
| className="applicant-cv-button" | |||
| style={{ | |||
| width: "100px", | |||
| height: "40px", | |||
| padding: 8, | |||
| <td className="profession"> | |||
| <IconButton | |||
| className="c-btn c-btn--primary-outlined files-view-page-delete-btn" | |||
| onClick={() => { | |||
| setOpenNoteModal({ | |||
| open: true, | |||
| note: n.note, | |||
| streamId: n.stream_id, | |||
| }); | |||
| }} | |||
| download={n.title} | |||
| href={getHrefForDownload( | |||
| n.file_type, | |||
| n.file_stream | |||
| )} | |||
| > | |||
| {t("common.download")} | |||
| </a> | |||
| </div> | |||
| </td> | |||
| <img | |||
| style={{ width: "12px", height: "12px" }} | |||
| src={editIcon} | |||
| /> | |||
| </IconButton> | |||
| </td> | |||
| <td> | |||
| <div onClick={stopPropagation}> | |||
| <a | |||
| className="applicant-cv-button" | |||
| style={{ | |||
| width: "100px", | |||
| height: "40px", | |||
| padding: 8, | |||
| }} | |||
| download={n.title} | |||
| href={getHrefForDownload( | |||
| n.file_type, | |||
| n.file_stream | |||
| )} | |||
| > | |||
| {t("common.download")} | |||
| </a> | |||
| </div> | |||
| </td> | |||
| </tr> | |||
| ))} | |||
| </tbody> | |||
| @@ -83,6 +83,7 @@ export default { | |||
| }, | |||
| files: { | |||
| uploadFile: base + "/files", | |||
| updateFile: base + "/files/update-note", | |||
| all: base + "/files/filtered", | |||
| deleteFile: base + "/files/delete-file", | |||
| }, | |||
| @@ -1,7 +1,9 @@ | |||
| import { deleteRequest, postRequest } from "."; | |||
| import { deleteRequest, postRequest, putRequest } from "."; | |||
| import apiEndpoints from "./apiEndpoints"; | |||
| export const uploadFileRequest = (payload) => | |||
| postRequest(apiEndpoints.files.uploadFile, payload); | |||
| export const updateFileRequest = (payload) => | |||
| putRequest(apiEndpoints.files.uploadFile + "/update-note/" + payload.id, {note: payload.data}); | |||
| export const deleteFileRequest = (id) => | |||
| deleteRequest(apiEndpoints.files.deleteFile + "/" + id); | |||
| @@ -21,6 +21,12 @@ export const FETCH_FILES_ERR = createErrorType(FETCH_FILES_SCOPE); | |||
| export const FETCH_FILES_SUCCESS = createSuccessType(FETCH_FILES_SCOPE); | |||
| export const FETCH_FILES_LOADING = createLoadingType(FETCH_FILES_SCOPE); | |||
| const UPDATE_FILE_SCOPE = "UPDATE_FILE"; | |||
| export const UPDATE_FILE_REQ = createFetchType(UPDATE_FILE_SCOPE); | |||
| export const UPDATE_FILE_ERR = createErrorType(UPDATE_FILE_SCOPE); | |||
| export const UPDATE_FILE_SUCCESS = createSuccessType(UPDATE_FILE_SCOPE); | |||
| export const UPDATE_FILE_LOADING = createLoadingType(UPDATE_FILE_SCOPE); | |||
| const DELETE_FILE_SCOPE = "DELETE_FILE" | |||
| export const DELETE_FILE_REQ = createFetchType(DELETE_FILE_SCOPE) | |||
| export const DELETE_FILE_ERR = createErrorType(DELETE_FILE_SCOPE) | |||
| @@ -11,7 +11,10 @@ import { | |||
| SET_CONTENT, | |||
| DELETE_FILE_REQ, | |||
| DELETE_FILE_ERR, | |||
| DELETE_FILE_SUCCESS | |||
| DELETE_FILE_SUCCESS, | |||
| UPDATE_FILE_REQ, | |||
| UPDATE_FILE_ERR, | |||
| UPDATE_FILE_SUCCESS | |||
| } from "./fileActionConstants"; | |||
| export const getFileFiltersReq = () => ({ | |||
| @@ -61,6 +64,21 @@ export const setContent = (payload) => ({ | |||
| payload | |||
| }) | |||
| export const updateFileReq = (payload) => ({ | |||
| type: UPDATE_FILE_REQ, | |||
| payload | |||
| }); | |||
| export const updateFileError = (payload) => ({ | |||
| type: UPDATE_FILE_ERR, | |||
| payload, | |||
| }); | |||
| export const updateFileAction = (payload) => ({ | |||
| type: UPDATE_FILE_SUCCESS, | |||
| payload | |||
| }); | |||
| export const deleteFileReq = (payload) => ({ | |||
| type: DELETE_FILE_REQ, | |||
| payload | |||
| @@ -3,6 +3,10 @@ import { | |||
| FETCH_FILES_ERR, | |||
| FETCH_FILES_SUCCESS, | |||
| } from "../../actions/files/fileActionConstants"; | |||
| import { | |||
| UPDATE_FILE_SUCCESS, | |||
| UPDATE_FILE_ERR, | |||
| } from "../../actions/files/fileActionConstants"; | |||
| import { | |||
| DELETE_FILE_ERR, | |||
| DELETE_FILE_SUCCESS, | |||
| @@ -17,6 +21,8 @@ export default createReducer( | |||
| { | |||
| [FETCH_FILES_SUCCESS]: setFiles, | |||
| [FETCH_FILES_ERR]: setFilesErrorMessage, | |||
| [UPDATE_FILE_SUCCESS]: updateFileReducer, | |||
| [UPDATE_FILE_ERR]: updateFileError, | |||
| [DELETE_FILE_SUCCESS]: deleteFileReducer, | |||
| [DELETE_FILE_ERR]: deleteFileReducerError, | |||
| }, | |||
| @@ -37,9 +43,29 @@ function setFilesErrorMessage(state, action) { | |||
| }; | |||
| } | |||
| function updateFileReducer(state, action) { | |||
| const newData = state.data.data.map((dat) => | |||
| dat.stream_id === action.payload.id | |||
| ? { ...dat, note: action.payload.data } | |||
| : dat | |||
| ); | |||
| return { | |||
| ...state, | |||
| data: { ...state.data, data: newData }, | |||
| }; | |||
| } | |||
| function updateFileError(state, action) { | |||
| return { | |||
| ...state, | |||
| fetchFilesErrorMessage: action.payload, | |||
| }; | |||
| } | |||
| function deleteFileReducer(state, action) { | |||
| const newArr = state.data.data.filter(x => x.stream_id !== action.payload) | |||
| return { ...state, data: {...state.data, data: newArr} }; | |||
| const newArr = state.data.data.filter((x) => x.stream_id !== action.payload); | |||
| return { ...state, data: { ...state.data, data: newArr } }; | |||
| } | |||
| function deleteFileReducerError(state, action) { | |||
| @@ -8,10 +8,24 @@ import { JWT_TOKEN } from "../../constants/localStorage"; | |||
| import { addHeaderToken } from "../../request"; | |||
| import { rejectErrorCodeHelper } from "../../util/helpers/rejectErrorCodeHelper"; | |||
| import { UPLOAD_FILE_REQ } from "../actions/uploadFile/uploadFileActionConstants"; | |||
| import { deleteFileRequest, uploadFileRequest } from "../../request/filesRequest"; | |||
| import { | |||
| deleteFileRequest, | |||
| updateFileRequest, | |||
| uploadFileRequest, | |||
| } from "../../request/filesRequest"; | |||
| import { getAllFilesReq } from "../../request/fileRequests"; | |||
| import { DELETE_FILE_REQ, FETCH_FILES_REQ } from "../actions/files/fileActionConstants"; | |||
| import { deleteFileAction, deleteFileError, getFileError, getFileSuccess } from "../actions/files/fileActions"; | |||
| import { | |||
| DELETE_FILE_REQ, | |||
| FETCH_FILES_REQ, | |||
| UPDATE_FILE_REQ, | |||
| } from "../actions/files/fileActionConstants"; | |||
| import { | |||
| deleteFileAction, | |||
| deleteFileError, | |||
| getFileError, | |||
| getFileSuccess, | |||
| updateFileAction, | |||
| } from "../actions/files/fileActions"; | |||
| export function* uploadFileSaga({ payload }) { | |||
| try { | |||
| @@ -48,12 +62,27 @@ export function* getAll({ payload }) { | |||
| } | |||
| } | |||
| export function* updateFileSaga({ payload }) { | |||
| try { | |||
| const JwtToken = yield call(authScopeStringGetHelper, JWT_TOKEN); | |||
| yield call(addHeaderToken, JwtToken); | |||
| yield call(updateFileRequest, { id: payload.id, data: payload.data }); | |||
| yield put(updateFileAction(payload)); | |||
| payload.onSuccessOpenNoteModal(); | |||
| } catch (error) { | |||
| if (error.response && error.response.data) { | |||
| const errorMessage = yield call(rejectErrorCodeHelper, error); | |||
| yield put(deleteFileError(errorMessage)); | |||
| } | |||
| } | |||
| } | |||
| export function* deleteFileSaga({ payload }) { | |||
| try { | |||
| const JwtToken = yield call(authScopeStringGetHelper, JWT_TOKEN); | |||
| yield call(addHeaderToken, JwtToken); | |||
| yield call(deleteFileRequest, payload.id); | |||
| yield put(deleteFileAction(payload.id)) | |||
| yield put(deleteFileAction(payload.id)); | |||
| } catch (error) { | |||
| if (error.response && error.response.data) { | |||
| const errorMessage = yield call(rejectErrorCodeHelper, error); | |||
| @@ -65,5 +94,6 @@ export function* deleteFileSaga({ payload }) { | |||
| export default function* filesSaga() { | |||
| yield all([takeLatest(UPLOAD_FILE_REQ, uploadFileSaga)]); | |||
| yield all([takeEvery(FETCH_FILES_REQ, getAll)]); | |||
| yield all([takeEvery(UPDATE_FILE_REQ, updateFileSaga)]); | |||
| yield all([takeLatest(DELETE_FILE_REQ, deleteFileSaga)]); | |||
| } | |||