From 5461aae6f6cf5020ec96def6d86fdab4724f7275 Mon Sep 17 00:00:00 2001 From: Patrick Fic <> Date: Wed, 4 May 2022 18:13:58 -0700 Subject: [PATCH 1/4] Base changes to job upload screen. --- client/package.json | 1 + .../documents-local-upload.component.jsx | 67 +++++++++++++++++ .../documents-local-upload.utility.js | 56 ++++++++++++++ ...jobs-documents-local-gallery.container.jsx | 73 +++++++++++++++++++ .../jobs-detail.page.component.jsx | 8 +- client/src/redux/media/media.actions.js | 18 +++++ client/src/redux/media/media.reducer.js | 24 ++++++ client/src/redux/media/media.sagas.js | 66 +++++++++++++++++ client/src/redux/media/media.selectors.js | 5 ++ client/src/redux/media/media.types.js | 10 +++ client/src/redux/root.reducer.js | 2 + client/src/redux/root.saga.js | 2 + client/yarn.lock | 5 ++ package.json | 2 +- 14 files changed, 337 insertions(+), 2 deletions(-) create mode 100644 client/src/components/documents-local-upload/documents-local-upload.component.jsx create mode 100644 client/src/components/documents-local-upload/documents-local-upload.utility.js create mode 100644 client/src/components/jobs-documents-local-gallery/jobs-documents-local-gallery.container.jsx create mode 100644 client/src/redux/media/media.actions.js create mode 100644 client/src/redux/media/media.reducer.js create mode 100644 client/src/redux/media/media.sagas.js create mode 100644 client/src/redux/media/media.selectors.js create mode 100644 client/src/redux/media/media.types.js diff --git a/client/package.json b/client/package.json index 5c05e9556..216bf52b7 100644 --- a/client/package.json +++ b/client/package.json @@ -34,6 +34,7 @@ "markerjs2": "^2.21.1", "moment-business-days": "^1.2.0", "moment-timezone": "^0.5.34", + "normalize-url": "^7.0.3", "phone": "^3.1.15", "preval.macro": "^5.0.0", "prop-types": "^15.8.1", diff --git a/client/src/components/documents-local-upload/documents-local-upload.component.jsx b/client/src/components/documents-local-upload/documents-local-upload.component.jsx new file mode 100644 index 000000000..cd42f6667 --- /dev/null +++ b/client/src/components/documents-local-upload/documents-local-upload.component.jsx @@ -0,0 +1,67 @@ +import { UploadOutlined } from "@ant-design/icons"; +import { Result, Upload } from "antd"; +import React, { useEffect, useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { + selectBodyshop, + selectCurrentUser, +} from "../../redux/user/user.selectors"; +import { handleUpload } from "./documents-local-upload.utility"; + +const mapStateToProps = createStructuredSelector({ + currentUser: selectCurrentUser, + bodyshop: selectBodyshop, +}); + +export function DocumentsLocalUploadComponent({ + children, + currentUser, + bodyshop, + job, + callbackAfterUpload, +}) { + const { t } = useTranslation(); + const [fileList, setFileList] = useState([]); + + const handleDone = (uid) => { + setTimeout(() => { + setFileList((fileList) => fileList.filter((x) => x.uid !== uid)); + }, 2000); + }; + + return ( + { + if (f.event && f.event.percent === 100) handleDone(f.file.uid); + + setFileList(f.fileList); + }} + customRequest={(ev) => + handleUpload({ + ev, + context: { + jobid: job.id, + callback: callbackAfterUpload, + }, + }) + } + accept="audio/*, video/*, image/*, .pdf, .doc, .docx, .xls, .xlsx" + > + {children || ( + <> +

+ +

+

+ Click or drag files to this area to upload. +

+ + )} +
+ ); +} +export default connect(mapStateToProps, null)(DocumentsLocalUploadComponent); diff --git a/client/src/components/documents-local-upload/documents-local-upload.utility.js b/client/src/components/documents-local-upload/documents-local-upload.utility.js new file mode 100644 index 000000000..020425515 --- /dev/null +++ b/client/src/components/documents-local-upload/documents-local-upload.utility.js @@ -0,0 +1,56 @@ +import cleanAxios from "../../utils/CleanAxios"; +import { store } from "../../redux/store"; +import { addMediaForJob } from "../../redux/media/media.actions"; +import normalizeUrl from "normalize-url"; + +export const handleUpload = async ({ ev, context }) => { + const { onError, onSuccess, onProgress, file } = ev; + const { jobid, callbackAfterUpload } = context; + + var options = { + headers: { "X-Requested-With": "XMLHttpRequest" }, + onUploadProgress: (e) => { + if (!!onProgress) onProgress({ percent: (e.loaded / e.total) * 100 }); + }, + }; + + const formData = new FormData(); + + formData.append("jobid", jobid); + formData.append("file", file); + const bodyshop = store.getState().user.bodyshop; + + const imexMediaServerResponse = await cleanAxios.post( + normalizeUrl(`${bodyshop.localmediaserverhttp}/jobs/upload`), + formData, + { + ...options, + } + ); + + if (imexMediaServerResponse.status !== 200) { + if (!!onError) { + onError(imexMediaServerResponse.statusText); + } + } else { + onSuccess(file); + store.dispatch( + addMediaForJob({ + jobid, + media: imexMediaServerResponse.data.map((d) => { + return { + ...d, + src: normalizeUrl(`${bodyshop.localmediaserverhttp}/${d.src}`), + thumbnail: normalizeUrl( + `${bodyshop.localmediaserverhttp}/${d.thumbnail}` + ), + }; + }), + }) + ); + } + + if (callbackAfterUpload) { + callbackAfterUpload(); + } +}; diff --git a/client/src/components/jobs-documents-local-gallery/jobs-documents-local-gallery.container.jsx b/client/src/components/jobs-documents-local-gallery/jobs-documents-local-gallery.container.jsx new file mode 100644 index 000000000..e67b53e77 --- /dev/null +++ b/client/src/components/jobs-documents-local-gallery/jobs-documents-local-gallery.container.jsx @@ -0,0 +1,73 @@ +import React, { useEffect } from "react"; +import { SyncOutlined } from "@ant-design/icons"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { selectBodyshop } from "../../redux/user/user.selectors"; +import { selectAllMedia } from "../../redux/media/media.selectors"; +import { getJobMedia } from "../../redux/media/media.actions"; +import { Button, Card, Space } from "antd"; +import { useTranslation } from "react-i18next"; +import Gallery from "react-grid-gallery"; +import DocumentsLocalUploadComponent from "../documents-local-upload/documents-local-upload.component"; +import { Link } from "react-router-dom"; +const mapStateToProps = createStructuredSelector({ + bodyshop: selectBodyshop, + allMedia: selectAllMedia, +}); +const mapDispatchToProps = (dispatch) => ({ + getJobMedia: (id) => dispatch(getJobMedia(id)), +}); +export default connect( + mapStateToProps, + mapDispatchToProps +)(JobsDocumentsLocalGallery); + +export function JobsDocumentsLocalGallery({ + bodyshop, + getJobMedia, + allMedia, + job, +}) { + const { t } = useTranslation(); + useEffect(() => { + if (job) { + getJobMedia(job.id); + } + }, [job, getJobMedia]); + + return ( +
+ + + + + + + + + + + { + window.open( + props.target.src, + "_blank", + "toolbar=0,location=0,menubar=0" + ); + }} + /> + +
+ ); +} diff --git a/client/src/pages/jobs-detail/jobs-detail.page.component.jsx b/client/src/pages/jobs-detail/jobs-detail.page.component.jsx index 63a34b554..f2614e9c9 100644 --- a/client/src/pages/jobs-detail/jobs-detail.page.component.jsx +++ b/client/src/pages/jobs-detail/jobs-detail.page.component.jsx @@ -50,6 +50,7 @@ import { selectBodyshop } from "../../redux/user/user.selectors"; import JobAuditTrail from "../../components/job-audit-trail/job-audit-trail.component"; import AuditTrailMapping from "../../utils/AuditTrailMappings"; import { insertAuditTrail } from "../../redux/application/application.actions"; +import JobsDocumentsLocalGallery from "../../components/jobs-documents-local-gallery/jobs-documents-local-gallery.container"; const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop, @@ -62,6 +63,7 @@ const mapDispatchToProps = (dispatch) => ({ dispatch(insertAuditTrail({ jobid, operation })), }); export function JobsDetailPage({ + bodyshop, setPrintCenterContext, jobRO, job, @@ -344,7 +346,11 @@ export function JobsDetailPage({ } key="documents" > - + {bodyshop.uselocalmediaserver ? ( + + ) : ( + + )} ({ + type: MediaActionTypes.GET_MEDIA_FOR_JOB, + payload: jobid, +}); +export const setJobMedia = ({ jobid, media }) => ({ + type: MediaActionTypes.SET_MEDIA_FOR_JOB, + payload: { jobid, media }, +}); +export const addMediaForJob = ({ jobid, media }) => ({ + type: MediaActionTypes.ADD_MEDIA_FOR_JOB, + payload: { jobid, media }, +}); +export const getJobMediaError = ({ error, message }) => ({ + type: MediaActionTypes.GET_MEDIA_FOR_JOB_ERROR, + payload: { error, message }, +}); diff --git a/client/src/redux/media/media.reducer.js b/client/src/redux/media/media.reducer.js new file mode 100644 index 000000000..8e7761f88 --- /dev/null +++ b/client/src/redux/media/media.reducer.js @@ -0,0 +1,24 @@ +import MediaActionTypes from "./media.types"; + +const INITIAL_STATE = { error: null }; + +const mediaReducer = (state = INITIAL_STATE, action) => { + switch (action.type) { + case MediaActionTypes.SET_MEDIA_FOR_JOB: + return { ...state, [action.payload.jobid]: action.payload.media }; + case MediaActionTypes.GET_MEDIA_FOR_JOB_ERROR: + return { ...state, error: action.payload }; + case MediaActionTypes.ADD_MEDIA_FOR_JOB: + return { + ...state, + [action.payload.jobid]: [ + ...state[action.payload.jobid], + ...(action.payload.media || []), + ], + }; + default: + return state; + } +}; + +export default mediaReducer; diff --git a/client/src/redux/media/media.sagas.js b/client/src/redux/media/media.sagas.js new file mode 100644 index 000000000..dcef73625 --- /dev/null +++ b/client/src/redux/media/media.sagas.js @@ -0,0 +1,66 @@ +import { all, call, takeLatest, put, select } from "redux-saga/effects"; +import { getJobMediaError, setJobMedia } from "./media.actions"; +import MediaActionTypes from "./media.types"; +import cleanAxios from "../../utils/CleanAxios"; +import normalizeUrl from "normalize-url"; +export function* onSetJobMedia() { + yield takeLatest(MediaActionTypes.GET_MEDIA_FOR_JOB, getJobMedia); +} +export function* getJobMedia({ payload: jobid }) { + try { + const localmediaserverhttp = (yield select( + (state) => state.user.bodyshop.localmediaserverhttp + )).trim(); + + if (localmediaserverhttp && localmediaserverhttp !== "") { + const imagesFetch = yield cleanAxios.post( + `${localmediaserverhttp}/jobs/list`, + { + jobid, + } + ); + const documentsFetch = yield cleanAxios.post( + `${localmediaserverhttp}/bills/list`, + { + jobid, + } + ); + + yield put( + setJobMedia({ + jobid, + media: [ + ...imagesFetch.data.map((d, idx) => { + return { + ...d, + src: normalizeUrl(`${localmediaserverhttp}/${d.src}`), + thumbnail: normalizeUrl( + `${localmediaserverhttp}/${d.thumbnail}` + ), + + key: idx, + }; + }), + ...documentsFetch.data.map((d, idx) => { + return { + ...d, + src: normalizeUrl(`${localmediaserverhttp}/${d.src}`), + thumbnail: normalizeUrl( + `${localmediaserverhttp}/${d.thumbnail}` + ), + + key: idx, + }; + }), + ], + }) + ); + } + } catch (error) { + yield put(getJobMediaError(error)); + } +} + +export function* mediaSagas() { + yield all([call(onSetJobMedia)]); +} diff --git a/client/src/redux/media/media.selectors.js b/client/src/redux/media/media.selectors.js new file mode 100644 index 000000000..e5c930e5c --- /dev/null +++ b/client/src/redux/media/media.selectors.js @@ -0,0 +1,5 @@ +import { createSelector } from "reselect"; + +const selectMedia = (state) => state.media; + +export const selectAllMedia = createSelector([selectMedia], (media) => media); diff --git a/client/src/redux/media/media.types.js b/client/src/redux/media/media.types.js new file mode 100644 index 000000000..aa3d7ba11 --- /dev/null +++ b/client/src/redux/media/media.types.js @@ -0,0 +1,10 @@ +const MediaActionTypes = { + SET_MEDIA_FOR_JOB: "SET_MEDIA_FOR_JOB", + GET_MEDIA_FOR_JOB: "GET_MEDIA_FOR_JOB", + GET_MEDIA_FOR_JOB_ERROR: "GET_MEDIA_FOR_JOB_ERROR", + ADD_MEDIA_FOR_JOB: "ADD_MEDIA_FOR_JOB", + POST_MEDIA_FOR_JOB: "POST_MEDIA_FOR_JOB", + POST_MEDIA_FOR_JOB_SUCCESS: "POST_MEDIA_FOR_JOB_SUCCESS", + POST_MEDIA_FOR_JOB_ERROR: "POST_MEDIA_FOR_JOB_ERROR", +}; +export default MediaActionTypes; diff --git a/client/src/redux/root.reducer.js b/client/src/redux/root.reducer.js index f29081378..a61ac43eb 100644 --- a/client/src/redux/root.reducer.js +++ b/client/src/redux/root.reducer.js @@ -4,6 +4,7 @@ import storage from "redux-persist/lib/storage"; import { withReduxStateSync } from "redux-state-sync"; import applicationReducer from "./application/application.reducer"; import emailReducer from "./email/email.reducer"; +import mediaReducer from "./media/media.reducer"; import messagingReducer from "./messaging/messaging.reducer"; import modalsReducer from "./modals/modals.reducer"; import techReducer from "./tech/tech.reducer"; @@ -29,6 +30,7 @@ const rootReducer = combineReducers({ modals: modalsReducer, application: persistReducer(applicationPersistConfig, applicationReducer), tech: techReducer, + media: mediaReducer, }); export default withReduxStateSync( diff --git a/client/src/redux/root.saga.js b/client/src/redux/root.saga.js index d18dd9d9c..d2fe2965c 100644 --- a/client/src/redux/root.saga.js +++ b/client/src/redux/root.saga.js @@ -6,6 +6,7 @@ import { emailSagas } from "./email/email.sagas"; import { modalsSagas } from "./modals/modals.sagas"; import { applicationSagas } from "./application/application.sagas"; import { techSagas } from "./tech/tech.sagas"; +import { mediaSagas } from "./media/media.sagas"; export default function* rootSaga() { yield all([ @@ -15,5 +16,6 @@ export default function* rootSaga() { call(modalsSagas), call(applicationSagas), call(techSagas), + call(mediaSagas), ]); } diff --git a/client/yarn.lock b/client/yarn.lock index 7e6e0248d..94e47d3b9 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -9702,6 +9702,11 @@ normalize-url@^3.0.0: resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-3.3.0.tgz#b2e1c4dc4f7c6d57743df733a4f5978d18650559" integrity sha512-U+JJi7duF1o+u2pynbp2zXDW2/PADgC30f0GsHZtRh+HOcXHnw137TrNlyxxRvWW5fjKd3bcLHPxofWuCjaeZg== +normalize-url@^7.0.3: + version "7.0.3" + resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-7.0.3.tgz#12e56889f7a54b2d5b09616f36c442a9063f61af" + integrity sha512-RiCOdwdPnzvwcBFJE4iI1ss3dMVRIrEzFpn8ftje6iBfzBInqlnRrNhxcLwBEKjPPXQKzm1Ptlxtaiv9wdcj5w== + npm-run-path@^2.0.0: version "2.0.2" resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f" diff --git a/package.json b/package.json index 36054c692..9fbecaa1f 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "0.0.1", "license": "UNLICENSED", "engines": { - "node": "12.22.6", + "node": "16.15.0", "npm": "7.17.0" }, "scripts": { From a1e4f3827d6bc25602c9a3dc1ab915677666effe Mon Sep 17 00:00:00 2001 From: Patrick Fic <> Date: Thu, 5 May 2022 15:46:58 -0700 Subject: [PATCH 2/4] Uploads and viewing from bills. --- bodyshop_translations.babel | 23 ++++- .../bill-detail-edit.container.jsx | 44 +++++---- .../bill-enter-modal.container.jsx | 41 ++++++--- .../documents-local-upload.component.jsx | 4 + .../documents-local-upload.utility.js | 15 ++- .../jobs-documents-gallery.component.jsx | 4 +- ...jobs-documents-local-gallery.container.jsx | 60 +++++++++--- ...ments-local-gallery.reassign.component.jsx | 92 +++++++++++++++++++ .../temporary-docs.component.jsx | 21 ++++- client/src/redux/media/media.actions.js | 16 ++++ client/src/redux/media/media.reducer.js | 12 ++- client/src/redux/media/media.sagas.js | 46 +++++++++- client/src/redux/media/media.types.js | 2 + client/src/redux/user/user.reducer.js | 10 +- client/src/translations/en_us/common.json | 1 + client/src/translations/es/common.json | 1 + client/src/translations/fr/common.json | 1 + 17 files changed, 341 insertions(+), 52 deletions(-) create mode 100644 client/src/components/jobs-documents-local-gallery/jobs-documents-local-gallery.reassign.component.jsx diff --git a/bodyshop_translations.babel b/bodyshop_translations.babel index a7de63bcb..53a04fe72 100644 --- a/bodyshop_translations.babel +++ b/bodyshop_translations.babel @@ -1,4 +1,4 @@ - +