Base changes to job upload screen.
This commit is contained in:
@@ -34,6 +34,7 @@
|
|||||||
"markerjs2": "^2.21.1",
|
"markerjs2": "^2.21.1",
|
||||||
"moment-business-days": "^1.2.0",
|
"moment-business-days": "^1.2.0",
|
||||||
"moment-timezone": "^0.5.34",
|
"moment-timezone": "^0.5.34",
|
||||||
|
"normalize-url": "^7.0.3",
|
||||||
"phone": "^3.1.15",
|
"phone": "^3.1.15",
|
||||||
"preval.macro": "^5.0.0",
|
"preval.macro": "^5.0.0",
|
||||||
"prop-types": "^15.8.1",
|
"prop-types": "^15.8.1",
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
<Upload.Dragger
|
||||||
|
multiple={true}
|
||||||
|
fileList={fileList}
|
||||||
|
onChange={(f) => {
|
||||||
|
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 || (
|
||||||
|
<>
|
||||||
|
<p className="ant-upload-drag-icon">
|
||||||
|
<UploadOutlined />
|
||||||
|
</p>
|
||||||
|
<p className="ant-upload-text">
|
||||||
|
Click or drag files to this area to upload.
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Upload.Dragger>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
export default connect(mapStateToProps, null)(DocumentsLocalUploadComponent);
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -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 (
|
||||||
|
<div>
|
||||||
|
<Space wrap>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
getJobMedia(job.id);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SyncOutlined />
|
||||||
|
</Button>
|
||||||
|
<a
|
||||||
|
href={`imexmedia://${bodyshop.localmediaservernetwork}/Jobs/${job.id}`}
|
||||||
|
>
|
||||||
|
<Button>{t("documents.labels.openinexplorer")}</Button>
|
||||||
|
</a>
|
||||||
|
</Space>
|
||||||
|
<Card>
|
||||||
|
<DocumentsLocalUploadComponent job={job} />
|
||||||
|
</Card>
|
||||||
|
<Card title={t("jobs.labels.documents-images")}>
|
||||||
|
<Gallery
|
||||||
|
images={(allMedia && allMedia[job.id]) || []}
|
||||||
|
enableImageSelection={false}
|
||||||
|
backdropClosesModal={true}
|
||||||
|
onClickImage={(props) => {
|
||||||
|
window.open(
|
||||||
|
props.target.src,
|
||||||
|
"_blank",
|
||||||
|
"toolbar=0,location=0,menubar=0"
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -50,6 +50,7 @@ import { selectBodyshop } from "../../redux/user/user.selectors";
|
|||||||
import JobAuditTrail from "../../components/job-audit-trail/job-audit-trail.component";
|
import JobAuditTrail from "../../components/job-audit-trail/job-audit-trail.component";
|
||||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||||
import { insertAuditTrail } from "../../redux/application/application.actions";
|
import { insertAuditTrail } from "../../redux/application/application.actions";
|
||||||
|
import JobsDocumentsLocalGallery from "../../components/jobs-documents-local-gallery/jobs-documents-local-gallery.container";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
@@ -62,6 +63,7 @@ const mapDispatchToProps = (dispatch) => ({
|
|||||||
dispatch(insertAuditTrail({ jobid, operation })),
|
dispatch(insertAuditTrail({ jobid, operation })),
|
||||||
});
|
});
|
||||||
export function JobsDetailPage({
|
export function JobsDetailPage({
|
||||||
|
bodyshop,
|
||||||
setPrintCenterContext,
|
setPrintCenterContext,
|
||||||
jobRO,
|
jobRO,
|
||||||
job,
|
job,
|
||||||
@@ -344,7 +346,11 @@ export function JobsDetailPage({
|
|||||||
}
|
}
|
||||||
key="documents"
|
key="documents"
|
||||||
>
|
>
|
||||||
<JobsDocumentsGalleryContainer jobId={job.id} />
|
{bodyshop.uselocalmediaserver ? (
|
||||||
|
<JobsDocumentsLocalGallery job={job} />
|
||||||
|
) : (
|
||||||
|
<JobsDocumentsGalleryContainer jobId={job.id} />
|
||||||
|
)}
|
||||||
</Tabs.TabPane>
|
</Tabs.TabPane>
|
||||||
<Tabs.TabPane
|
<Tabs.TabPane
|
||||||
tab={
|
tab={
|
||||||
|
|||||||
18
client/src/redux/media/media.actions.js
Normal file
18
client/src/redux/media/media.actions.js
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import MediaActionTypes from "./media.types";
|
||||||
|
|
||||||
|
export const getJobMedia = (jobid) => ({
|
||||||
|
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 },
|
||||||
|
});
|
||||||
24
client/src/redux/media/media.reducer.js
Normal file
24
client/src/redux/media/media.reducer.js
Normal file
@@ -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;
|
||||||
66
client/src/redux/media/media.sagas.js
Normal file
66
client/src/redux/media/media.sagas.js
Normal file
@@ -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)]);
|
||||||
|
}
|
||||||
5
client/src/redux/media/media.selectors.js
Normal file
5
client/src/redux/media/media.selectors.js
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { createSelector } from "reselect";
|
||||||
|
|
||||||
|
const selectMedia = (state) => state.media;
|
||||||
|
|
||||||
|
export const selectAllMedia = createSelector([selectMedia], (media) => media);
|
||||||
10
client/src/redux/media/media.types.js
Normal file
10
client/src/redux/media/media.types.js
Normal file
@@ -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;
|
||||||
@@ -4,6 +4,7 @@ import storage from "redux-persist/lib/storage";
|
|||||||
import { withReduxStateSync } from "redux-state-sync";
|
import { withReduxStateSync } from "redux-state-sync";
|
||||||
import applicationReducer from "./application/application.reducer";
|
import applicationReducer from "./application/application.reducer";
|
||||||
import emailReducer from "./email/email.reducer";
|
import emailReducer from "./email/email.reducer";
|
||||||
|
import mediaReducer from "./media/media.reducer";
|
||||||
import messagingReducer from "./messaging/messaging.reducer";
|
import messagingReducer from "./messaging/messaging.reducer";
|
||||||
import modalsReducer from "./modals/modals.reducer";
|
import modalsReducer from "./modals/modals.reducer";
|
||||||
import techReducer from "./tech/tech.reducer";
|
import techReducer from "./tech/tech.reducer";
|
||||||
@@ -29,6 +30,7 @@ const rootReducer = combineReducers({
|
|||||||
modals: modalsReducer,
|
modals: modalsReducer,
|
||||||
application: persistReducer(applicationPersistConfig, applicationReducer),
|
application: persistReducer(applicationPersistConfig, applicationReducer),
|
||||||
tech: techReducer,
|
tech: techReducer,
|
||||||
|
media: mediaReducer,
|
||||||
});
|
});
|
||||||
|
|
||||||
export default withReduxStateSync(
|
export default withReduxStateSync(
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { emailSagas } from "./email/email.sagas";
|
|||||||
import { modalsSagas } from "./modals/modals.sagas";
|
import { modalsSagas } from "./modals/modals.sagas";
|
||||||
import { applicationSagas } from "./application/application.sagas";
|
import { applicationSagas } from "./application/application.sagas";
|
||||||
import { techSagas } from "./tech/tech.sagas";
|
import { techSagas } from "./tech/tech.sagas";
|
||||||
|
import { mediaSagas } from "./media/media.sagas";
|
||||||
|
|
||||||
export default function* rootSaga() {
|
export default function* rootSaga() {
|
||||||
yield all([
|
yield all([
|
||||||
@@ -15,5 +16,6 @@ export default function* rootSaga() {
|
|||||||
call(modalsSagas),
|
call(modalsSagas),
|
||||||
call(applicationSagas),
|
call(applicationSagas),
|
||||||
call(techSagas),
|
call(techSagas),
|
||||||
|
call(mediaSagas),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9702,6 +9702,11 @@ normalize-url@^3.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-3.3.0.tgz#b2e1c4dc4f7c6d57743df733a4f5978d18650559"
|
resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-3.3.0.tgz#b2e1c4dc4f7c6d57743df733a4f5978d18650559"
|
||||||
integrity sha512-U+JJi7duF1o+u2pynbp2zXDW2/PADgC30f0GsHZtRh+HOcXHnw137TrNlyxxRvWW5fjKd3bcLHPxofWuCjaeZg==
|
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:
|
npm-run-path@^2.0.0:
|
||||||
version "2.0.2"
|
version "2.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f"
|
resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f"
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"license": "UNLICENSED",
|
"license": "UNLICENSED",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "12.22.6",
|
"node": "16.15.0",
|
||||||
"npm": "7.17.0"
|
"npm": "7.17.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
Reference in New Issue
Block a user