diff --git a/bodyshop_translations.babel b/bodyshop_translations.babel index abc4f8118..57326a1e9 100644 --- a/bodyshop_translations.babel +++ b/bodyshop_translations.babel @@ -12377,6 +12377,32 @@ + + errors + + + notfound + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + itemtypes diff --git a/client/package.json b/client/package.json index 57f1b5afd..d7d61881b 100644 --- a/client/package.json +++ b/client/package.json @@ -29,6 +29,7 @@ "jsreport-browser-client-dist": "^1.3.0", "libphonenumber-js": "^1.9.17", "logrocket": "^1.2.0", + "markerjs2": "^2.8.1", "moment-business-days": "^1.2.0", "phone": "^2.4.21", "preval.macro": "^5.0.0", diff --git a/client/src/App/App.jsx b/client/src/App/App.jsx index 89d96e27c..cbf096f8f 100644 --- a/client/src/App/App.jsx +++ b/client/src/App/App.jsx @@ -4,6 +4,7 @@ import { useTranslation } from "react-i18next"; import { connect } from "react-redux"; import { Route, Switch } from "react-router-dom"; import { createStructuredSelector } from "reselect"; +import DocumentEditorContainer from "../components/document-editor/document-editor.container"; import ErrorBoundary from "../components/error-boundary/error-boundary.component"; //Component Imports import LoadingSpinner from "../components/loading-spinner/loading-spinner.component"; @@ -118,6 +119,13 @@ export function App({ checkUserSession, currentUser, online, setOnline }) { component={TechPageContainer} /> + + + ); diff --git a/client/src/components/document-editor/document-editor.component.jsx b/client/src/components/document-editor/document-editor.component.jsx new file mode 100644 index 000000000..ba5748d32 --- /dev/null +++ b/client/src/components/document-editor/document-editor.component.jsx @@ -0,0 +1,93 @@ +//import "tui-image-editor/dist/tui-image-editor.css"; +import { Spin } from "antd"; +import * as markerjs2 from "markerjs2"; +import React, { useEffect, useRef } from "react"; +import { useState } from "react"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { + selectBodyshop, + selectCurrentUser, +} from "../../redux/user/user.selectors"; +import { handleUpload } from "../documents-upload/documents-upload.utility"; +import { GenerateSrcUrl } from "../jobs-documents-gallery/job-documents.utility"; + +const mapStateToProps = createStructuredSelector({ + currentUser: selectCurrentUser, + bodyshop: selectBodyshop, +}); +const mapDispatchToProps = (dispatch) => ({ + //setUserLanguage: language => dispatch(setUserLanguage(language)) +}); + +export function DocumentEditorComponent({ currentUser, bodyshop, document }) { + const imgRef = useRef(null); + const [loading, setLoading] = useState(false); + const markerArea = useRef(null); + const triggerUpload = async (dataUrl) => { + setLoading(true); + handleUpload( + { + filename: `${document.key.split("/").pop()}-${Date.now()}.jpg`, + file: await b64toBlob(dataUrl), + onSuccess: () => setLoading(false), + onError: () => setLoading(false), + }, + { + bodyshop: bodyshop, + uploaded_by: currentUser.email, + jobId: document.jobid, + //billId: billId, + tagsArray: ["edited"], + //callback: callbackAfterUpload, + } + ); + }; + + useEffect(() => { + if (imgRef.current !== null) { + // create a marker.js MarkerArea + markerArea.current = new markerjs2.MarkerArea(imgRef.current); + // attach an event handler to assign annotated image back to our image element + markerArea.current.addCloseEventListener((closeEvent) => { + console.log("Close Event", closeEvent); + }); + + markerArea.current.addRenderEventListener((dataUrl) => { + triggerUpload(dataUrl); + }); + // launch marker.js + + markerArea.current.renderAtNaturalSize = true; + markerArea.current.renderImageType = "image/jpeg"; + markerArea.current.renderImageQuality = 1; + //markerArea.current.settings.displayMode = "inline"; + markerArea.current.show(); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [imgRef.current, triggerUpload]); + + async function b64toBlob(url) { + const res = await fetch(url); + return await res.blob(); + } + + return ( +
+ + sample + +
+ ); +} + +export default connect( + mapStateToProps, + mapDispatchToProps +)(DocumentEditorComponent); diff --git a/client/src/components/document-editor/document-editor.container.jsx b/client/src/components/document-editor/document-editor.container.jsx new file mode 100644 index 000000000..7e19b98d4 --- /dev/null +++ b/client/src/components/document-editor/document-editor.container.jsx @@ -0,0 +1,33 @@ +import { useQuery } from "@apollo/client"; +import { Modal, Result } from "antd"; +import queryString from "query-string"; +import React from "react"; +import { useLocation } from "react-router"; +import { GET_DOCUMENT_BY_PK } from "../../graphql/documents.queries"; +import AlertComponent from "../alert/alert.component"; +import LoadingSpinner from "../loading-spinner/loading-spinner.component"; +import DocumentEditor from "./document-editor.component"; +import { useTranslation } from "react-i18next"; + +export default function DocumentEditorContainer() { + //Get the image details for the image to be saved. + //Get the document id from the search string. + const { documentId } = queryString.parse(useLocation().search); + const { t } = useTranslation(); + + const { loading, error, data } = useQuery(GET_DOCUMENT_BY_PK, { + variables: { documentId }, + skip: !documentId, + }); + + if (loading) return ; + if (error) return ; + + if (!data.documents_by_pk) + return ; + return ( +
+ +
+ ); +} diff --git a/client/src/components/documents-upload/documents-upload.utility.js b/client/src/components/documents-upload/documents-upload.utility.js index 5856d985e..72893ef88 100644 --- a/client/src/components/documents-upload/documents-upload.utility.js +++ b/client/src/components/documents-upload/documents-upload.utility.js @@ -21,8 +21,10 @@ export const handleUpload = (ev, context) => { const { onError, onSuccess, onProgress } = ev; const { bodyshop, jobId } = context; - let key = `${bodyshop.id}/${jobId}/${ev.file.name.replace(/\.[^/.]+$/, "")}`; - let extension = ev.file.name.split(".").pop(); + const fileName = ev.file.name || ev.filename; + + let key = `${bodyshop.id}/${jobId}/${fileName.replace(/\.[^/.]+$/, "")}`; + let extension = fileName.split(".").pop(); uploadToCloudinary( key, extension, diff --git a/client/src/components/jobs-documents-gallery/job-documents.utility.js b/client/src/components/jobs-documents-gallery/job-documents.utility.js new file mode 100644 index 000000000..8dde8cc66 --- /dev/null +++ b/client/src/components/jobs-documents-gallery/job-documents.utility.js @@ -0,0 +1,14 @@ +import { DetermineFileType } from "../documents-upload/documents-upload.utility"; + +export const GenerateSrcUrl = (value) => { + return `${process.env.REACT_APP_CLOUDINARY_ENDPOINT}/${DetermineFileType( + value.type + )}/upload/${value.key}${value.extension ? `.${value.extension}` : ""}`; +}; + +export const GenerateThumbUrl = (value) => + `${process.env.REACT_APP_CLOUDINARY_ENDPOINT}/${DetermineFileType( + value.type + )}/upload/${process.env.REACT_APP_CLOUDINARY_THUMB_TRANSFORMATIONS}/${ + value.key + }`; diff --git a/client/src/components/jobs-documents-gallery/jobs-documents-gallery.component.jsx b/client/src/components/jobs-documents-gallery/jobs-documents-gallery.component.jsx index e4c3d0eba..ed27f08cb 100644 --- a/client/src/components/jobs-documents-gallery/jobs-documents-gallery.component.jsx +++ b/client/src/components/jobs-documents-gallery/jobs-documents-gallery.component.jsx @@ -5,6 +5,7 @@ import Gallery from "react-grid-gallery"; import { useTranslation } from "react-i18next"; import DocumentsUploadComponent from "../documents-upload/documents-upload.component"; import { DetermineFileType } from "../documents-upload/documents-upload.utility"; +import { GenerateSrcUrl, GenerateThumbUrl } from "./job-documents.utility"; import JobsDocumentsDownloadButton from "./jobs-document-gallery.download.component"; import JobsDocumentsGalleryReassign from "./jobs-document-gallery.reassign.component"; import JobsDocumentsDeleteButton from "./jobs-documents-gallery.delete.component"; @@ -29,16 +30,8 @@ function JobsDocumentsComponent({ const fileType = DetermineFileType(value.type); if (value.type.startsWith("image")) { acc.images.push({ - src: `${ - process.env.REACT_APP_CLOUDINARY_ENDPOINT - }/${DetermineFileType(value.type)}/upload/${value.key}${ - value.extension ? `.${value.extension}` : "" - }`, - thumbnail: `${ - process.env.REACT_APP_CLOUDINARY_ENDPOINT - }/${DetermineFileType(value.type)}/upload/${ - process.env.REACT_APP_CLOUDINARY_THUMB_TRANSFORMATIONS - }/${value.key}`, + src: GenerateSrcUrl(value), + thumbnail: GenerateThumbUrl(value), thumbnailHeight: 225, thumbnailWidth: 225, isSelected: false, @@ -52,28 +45,17 @@ function JobsDocumentsComponent({ } else { let thumb; switch (fileType) { - case "video": - thumb = `${process.env.REACT_APP_CLOUDINARY_ENDPOINT}/${fileType}/upload/${process.env.REACT_APP_CLOUDINARY_THUMB_TRANSFORMATIONS}/${value.key}`; - break; case "raw": thumb = `${window.location.origin}/file.png`; break; default: - thumb = `${ - process.env.REACT_APP_CLOUDINARY_ENDPOINT - }/${fileType}/upload/${ - process.env.REACT_APP_CLOUDINARY_THUMB_TRANSFORMATIONS - }/${value.key}${value.extension ? `.${value.extension}` : ""}`; + thumb = GenerateThumbUrl(value); break; } const fileName = value.key.split("/").pop(); acc.other.push({ - src: `${ - process.env.REACT_APP_CLOUDINARY_ENDPOINT - }/${fileType}/upload/${value.key}${ - value.extension ? `.${value.extension}` : "" - }`, + src: GenerateSrcUrl(value), thumbnail: thumb, tags: [ { diff --git a/client/src/graphql/documents.queries.js b/client/src/graphql/documents.queries.js index d0c11f914..f9cc8877b 100644 --- a/client/src/graphql/documents.queries.js +++ b/client/src/graphql/documents.queries.js @@ -1,5 +1,20 @@ import { gql } from "@apollo/client"; +export const GET_DOCUMENT_BY_PK = gql` + query GET_DOCUMENT_BY_PK($documentId: uuid!) { + documents_by_pk(id: $documentId) { + id + name + key + type + size + takenat + extension + jobid + } + } +`; + export const GET_DOCUMENTS_BY_JOB = gql` query GET_DOCUMENTS_BY_JOB($jobId: uuid!) { jobs_by_pk(id: $jobId) { diff --git a/client/src/translations/en_us/common.json b/client/src/translations/en_us/common.json index 164bdd128..e2ffadc03 100644 --- a/client/src/translations/en_us/common.json +++ b/client/src/translations/en_us/common.json @@ -792,6 +792,9 @@ "submitticket": "Submit a Support Ticket", "view": "View" }, + "errors": { + "notfound": "No record was found." + }, "itemtypes": { "contract": "CC Contract", "courtesycar": "Courtesy Car", diff --git a/client/src/translations/es/common.json b/client/src/translations/es/common.json index 4fae2a134..606c03ce3 100644 --- a/client/src/translations/es/common.json +++ b/client/src/translations/es/common.json @@ -792,6 +792,9 @@ "submitticket": "", "view": "" }, + "errors": { + "notfound": "" + }, "itemtypes": { "contract": "", "courtesycar": "", diff --git a/client/src/translations/fr/common.json b/client/src/translations/fr/common.json index 63fde29b6..f7ee47e50 100644 --- a/client/src/translations/fr/common.json +++ b/client/src/translations/fr/common.json @@ -792,6 +792,9 @@ "submitticket": "", "view": "" }, + "errors": { + "notfound": "" + }, "itemtypes": { "contract": "", "courtesycar": "", diff --git a/client/yarn.lock b/client/yarn.lock index e6fdbac30..2283b5afb 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -8302,6 +8302,11 @@ map-visit@^1.0.0: dependencies: object-visit "^1.0.0" +markerjs2@^2.8.1: + version "2.8.1" + resolved "https://registry.yarnpkg.com/markerjs2/-/markerjs2-2.8.1.tgz#33c455cc1edd8fa9a5e5b39ed782dcd1b923c917" + integrity sha512-M9AflvjOD5aIcBM0HZWW6u1h/NRdzfq73B9ILv1YehF88PeF0tYT5HIsi9PaSJ6EUOR/vWysZN08f3EyDCJixw== + material-colors@^1.2.1: version "1.2.6" resolved "https://registry.npmjs.org/material-colors/-/material-colors-1.2.6.tgz"