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 (
+
+
+
+
+
+ );
+}
+
+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"