From 7d82fb8f0433baa61c203efbf4e31841fcb5528a Mon Sep 17 00:00:00 2001 From: Patrick Fic <> Date: Thu, 27 May 2021 20:16:50 -0700 Subject: [PATCH 01/24] Added Crisp scripts. --- client/src/App/App.container.jsx | 21 ++++++++++++++++++++- client/src/redux/user/user.sagas.js | 8 ++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/client/src/App/App.container.jsx b/client/src/App/App.container.jsx index 1f5298da2..590e31678 100644 --- a/client/src/App/App.container.jsx +++ b/client/src/App/App.container.jsx @@ -3,11 +3,12 @@ import { ConfigProvider } from "antd"; import enLocale from "antd/es/locale/en_US"; import LogRocket from "logrocket"; import moment from "moment"; -import React from "react"; +import React, { useEffect } from "react"; import GlobalLoadingBar from "../components/global-loading-bar/global-loading-bar.component"; import client from "../utils/GraphQLClient"; import App from "./App"; import { useTranslation } from "react-i18next"; +import JiraSupportComponent from "../components/jira-support-widget/jira-support-widget.component"; moment.locale("en-US"); if (process.env.NODE_ENV === "production") LogRocket.init("gvfvfw/bodyshopapp"); @@ -15,6 +16,23 @@ if (process.env.NODE_ENV === "production") LogRocket.init("gvfvfw/bodyshopapp"); export default function AppContainer() { const { t } = useTranslation(); + useEffect(() => { + // Include the Crisp code here, without the tags + window.$crisp = []; + window.CRISP_WEBSITE_ID = "36724f62-2eb0-4b29-9cdd-9905fb99913e"; + + var d = document; + var s = d.createElement("script"); + + s.src = "https://client.crisp.chat/l.js"; + s.async = 1; + d.getElementsByTagName("head")[0].appendChild(s); + + return () => { + d.getElementsByTagName("head")[0].removeChild(s); + }; + }, []); + return ( + ); diff --git a/client/src/redux/user/user.sagas.js b/client/src/redux/user/user.sagas.js index cbeba03af..c467aceed 100644 --- a/client/src/redux/user/user.sagas.js +++ b/client/src/redux/user/user.sagas.js @@ -164,6 +164,12 @@ export function* onSignInSuccess() { export function* signInSuccessSaga({ payload }) { LogRocket.identify(payload.email); + try { + window.$crisp.push(["set", "user:email", [payload.email]]); + window.$crisp.push(["set", "user:nickname", [payload.displayName]]); + } catch (error) { + console.log("Error updating Crisp settings.", error); + } // if (!payload.email.includes("@imex.")) yield put(setInstanceId(payload.uid)); analytics.setUserId(payload.email); @@ -220,6 +226,8 @@ export function* SetAuthLevelFromShopDetails({ payload }) { try { const userEmail = yield select((state) => state.user.currentUser.email); + window.$crisp.push(["set", "user:company", [payload.shopname]]); + const authRecord = payload.associations.filter( (a) => a.useremail === userEmail ); From 5da8c77b3a73acda41376e3da4d6f2b0ec30469e Mon Sep 17 00:00:00 2001 From: Patrick Fic <> Date: Thu, 27 May 2021 21:13:36 -0700 Subject: [PATCH 02/24] Added error handling to Crisp. --- .../error-boundary/error-boundary.component.jsx | 17 +++++++++++++++++ client/src/redux/user/user.sagas.js | 6 +++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/client/src/components/error-boundary/error-boundary.component.jsx b/client/src/components/error-boundary/error-boundary.component.jsx index cef01e299..0eca73dea 100644 --- a/client/src/components/error-boundary/error-boundary.component.jsx +++ b/client/src/components/error-boundary/error-boundary.component.jsx @@ -57,6 +57,23 @@ ${this.state.error.stack} if (this.state.hasErrored === true) { logImEXEvent("error_boundary_rendered", { error, info }); + window.$crisp.push([ + "set", + "session:event", + [ + [ + [ + "error_boundary", + { + error: this.state.error.message, + stack: this.state.error.stack, + }, + "red", + ], + ], + ], + ]); + return (
Date: Fri, 28 May 2021 18:18:09 -0700 Subject: [PATCH 03/24] WIP Photo Editor --- bodyshop_translations.babel | 26 ++++++ client/package.json | 1 + client/src/App/App.jsx | 8 ++ .../document-editor.component.jsx | 93 +++++++++++++++++++ .../document-editor.container.jsx | 33 +++++++ .../documents-upload.utility.js | 6 +- .../job-documents.utility.js | 14 +++ .../jobs-documents-gallery.component.jsx | 28 +----- client/src/graphql/documents.queries.js | 15 +++ client/src/translations/en_us/common.json | 3 + client/src/translations/es/common.json | 3 + client/src/translations/fr/common.json | 3 + client/yarn.lock | 5 + 13 files changed, 213 insertions(+), 25 deletions(-) create mode 100644 client/src/components/document-editor/document-editor.component.jsx create mode 100644 client/src/components/document-editor/document-editor.container.jsx create mode 100644 client/src/components/jobs-documents-gallery/job-documents.utility.js 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" From c28d4c15a09d144617bce1d076f78aba5ed0e242 Mon Sep 17 00:00:00 2001 From: Patrick Fic <> Date: Mon, 31 May 2021 10:45:06 -0700 Subject: [PATCH 04/24] IO-1144 Image Editor Changes. --- bodyshop_translations.babel | 42 +++++++++++ .../document-editor.component.jsx | 70 ++++++++++++------- .../document-editor.container.jsx | 40 +++++++++-- .../jobs-documents-gallery.component.jsx | 47 ++++++++++++- client/src/translations/en_us/common.json | 4 +- client/src/translations/es/common.json | 2 + client/src/translations/fr/common.json | 2 + 7 files changed, 172 insertions(+), 35 deletions(-) diff --git a/bodyshop_translations.babel b/bodyshop_translations.babel index 57326a1e9..b4512fbe2 100644 --- a/bodyshop_translations.babel +++ b/bodyshop_translations.babel @@ -11040,6 +11040,27 @@ + + uploading + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + usage false @@ -11087,6 +11108,27 @@ + + edituploaded + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + insert false diff --git a/client/src/components/document-editor/document-editor.component.jsx b/client/src/components/document-editor/document-editor.component.jsx index ba5748d32..cb12c1aec 100644 --- a/client/src/components/document-editor/document-editor.component.jsx +++ b/client/src/components/document-editor/document-editor.component.jsx @@ -1,8 +1,8 @@ //import "tui-image-editor/dist/tui-image-editor.css"; -import { Spin } from "antd"; +import { Result } from "antd"; import * as markerjs2 from "markerjs2"; -import React, { useEffect, useRef } from "react"; -import { useState } from "react"; +import React, { useCallback, useEffect, useRef, useState } from "react"; +import { useTranslation } from "react-i18next"; import { connect } from "react-redux"; import { createStructuredSelector } from "reselect"; import { @@ -11,6 +11,7 @@ import { } from "../../redux/user/user.selectors"; import { handleUpload } from "../documents-upload/documents-upload.utility"; import { GenerateSrcUrl } from "../jobs-documents-gallery/job-documents.utility"; +import LoadingSpinner from "../loading-spinner/loading-spinner.component"; const mapStateToProps = createStructuredSelector({ currentUser: selectCurrentUser, @@ -23,37 +24,49 @@ const mapDispatchToProps = (dispatch) => ({ export function DocumentEditorComponent({ currentUser, bodyshop, document }) { const imgRef = useRef(null); const [loading, setLoading] = useState(false); + const [uploaded, setuploaded] = 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, - } - ); - }; + const { t } = useTranslation(); + + const triggerUpload = useCallback( + async (dataUrl) => { + setLoading(true); + handleUpload( + { + filename: `${document.key.split("/").pop()}-${Date.now()}.jpg`, + file: await b64toBlob(dataUrl), + onSuccess: () => { + setLoading(false); + setuploaded(true); + }, + onError: () => setLoading(false), + }, + { + bodyshop: bodyshop, + uploaded_by: currentUser.email, + jobId: document.jobid, + //billId: billId, + tagsArray: ["edited"], + //callback: callbackAfterUpload, + } + ); + }, + [bodyshop, currentUser, document] + ); useEffect(() => { if (imgRef.current !== null) { // create a marker.js MarkerArea markerArea.current = new markerjs2.MarkerArea(imgRef.current); + console.log(`markerArea.current`, markerArea.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) => { + imgRef.current.src = dataUrl; + markerArea.current.close(); triggerUpload(dataUrl); }); // launch marker.js @@ -65,7 +78,7 @@ export function DocumentEditorComponent({ currentUser, bodyshop, document }) { markerArea.current.show(); } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [imgRef.current, triggerUpload]); + }, [triggerUpload]); async function b64toBlob(url) { const res = await fetch(url); @@ -74,7 +87,7 @@ export function DocumentEditorComponent({ currentUser, bodyshop, document }) { return (
- + {!loading && !uploaded && ( - + )} + {loading && } + {uploaded && ( + + )}
); } diff --git a/client/src/components/document-editor/document-editor.container.jsx b/client/src/components/document-editor/document-editor.container.jsx index 7e19b98d4..3819f27f8 100644 --- a/client/src/components/document-editor/document-editor.container.jsx +++ b/client/src/components/document-editor/document-editor.container.jsx @@ -1,29 +1,55 @@ import { useQuery } from "@apollo/client"; -import { Modal, Result } from "antd"; +import { Result } from "antd"; import queryString from "query-string"; -import React from "react"; +import React, { useEffect } from "react"; +import { useTranslation } from "react-i18next"; +import { connect } from "react-redux"; import { useLocation } from "react-router"; +import { QUERY_BODYSHOP } from "../../graphql/bodyshop.queries"; import { GET_DOCUMENT_BY_PK } from "../../graphql/documents.queries"; +import { setBodyshop } from "../../redux/user/user.actions"; 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() { +const mapDispatchToProps = (dispatch) => ({ + setBodyshop: (bs) => dispatch(setBodyshop(bs)), +}); + +export default connect(null, mapDispatchToProps)(DocumentEditorContainer); + +export function DocumentEditorContainer({ setBodyshop }) { //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: loadingShop, + error: errorShop, + data: dataShop, + } = useQuery(QUERY_BODYSHOP, { + fetchPolicy: "network-only", + }); + + useEffect(() => { + if (dataShop) setBodyshop(dataShop.bodyshops[0]); + }, [dataShop, setBodyshop]); const { loading, error, data } = useQuery(GET_DOCUMENT_BY_PK, { variables: { documentId }, skip: !documentId, }); - if (loading) return ; - if (error) return ; + if (loading || loadingShop) return ; + if (error || errorShop) + return ( + + ); - if (!data.documents_by_pk) + if (!data || !data.documents_by_pk) return ; return (
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 ed27f08cb..c975388f2 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 @@ -1,5 +1,5 @@ -import { FileExcelFilled } from "@ant-design/icons"; -import { Card, Col, Row, Space } from "antd"; +import { FileExcelFilled, EditFilled, SyncOutlined } from "@ant-design/icons"; +import { Card, Col, Row, Space, Button } from "antd"; import React, { useEffect, useState } from "react"; import Gallery from "react-grid-gallery"; import { useTranslation } from "react-i18next"; @@ -23,6 +23,25 @@ function JobsDocumentsComponent({ }) { const [galleryImages, setgalleryImages] = useState({ images: [], other: [] }); const { t } = useTranslation(); + const [index, setIndex] = useState(0); + + const onCurrentImageChange = (index) => { + setIndex(index); + }; + + useEffect(() => { + console.log("Added event listening for reteching."); + window.addEventListener("storage", (ev) => { + if (ev.key === "refetch" && ev.newValue === true) { + refetch && refetch(); + localStorage.setItem("refetch", false); + } + }); + + return () => { + window.removeEventListener("storage"); + }; + }, [refetch]); useEffect(() => { let documents = data.reduce( @@ -101,6 +120,9 @@ function JobsDocumentsComponent({ + { + console.log(`Clicked`); + const newWindow = window.open( + `${window.location.protocol}//${window.location.host}/edit?documentId=${galleryImages.images[index].id}`, + "_blank", + "noopener,noreferrer" + ); + if (newWindow) newWindow.opener = null; + }} + > + + , + ]} onClickImage={(props) => { window.open( props.target.src, diff --git a/client/src/translations/en_us/common.json b/client/src/translations/en_us/common.json index e2ffadc03..c2f2d2791 100644 --- a/client/src/translations/en_us/common.json +++ b/client/src/translations/en_us/common.json @@ -6,7 +6,7 @@ }, "errors": { "deleting": "Error encountered while deleting allocation. {{message}}", - "saving": "Error while allocating. {{message}}", + "saving": "Error while allocating. {{message}}", "validation": "Please ensure all fields are entered correctly. " }, "fields": { @@ -701,10 +701,12 @@ "upload": "Upload", "upload_limitexceeded": "Uploading all selected documents will exceed the job storage limit for your shop. ", "upload_limitexceeded_title": "Unable to upload document(s)", + "uploading": "Uploading...", "usage": "of job storage used. ({{used}} / {{total}})" }, "successes": { "delete": "Document(s) deleted successfully.", + "edituploaded": "Edited document uploaded successfully. Please close this window and refresh the documents list.", "insert": "Uploaded document successfully. ", "updated": "Document updated successfully. " } diff --git a/client/src/translations/es/common.json b/client/src/translations/es/common.json index 606c03ce3..5cba0da9a 100644 --- a/client/src/translations/es/common.json +++ b/client/src/translations/es/common.json @@ -701,10 +701,12 @@ "upload": "Subir", "upload_limitexceeded": "", "upload_limitexceeded_title": "", + "uploading": "", "usage": "" }, "successes": { "delete": "Documento eliminado con éxito.", + "edituploaded": "", "insert": "Documento cargado con éxito.", "updated": "" } diff --git a/client/src/translations/fr/common.json b/client/src/translations/fr/common.json index f7ee47e50..c480fc6cb 100644 --- a/client/src/translations/fr/common.json +++ b/client/src/translations/fr/common.json @@ -701,10 +701,12 @@ "upload": "Télécharger", "upload_limitexceeded": "", "upload_limitexceeded_title": "", + "uploading": "", "usage": "" }, "successes": { "delete": "Le document a bien été supprimé.", + "edituploaded": "", "insert": "Document téléchargé avec succès.", "updated": "" } From af6bb18db225a7b6934d22fd3e843e445990726c Mon Sep 17 00:00:00 2001 From: Patrick Fic <> Date: Mon, 31 May 2021 10:50:28 -0700 Subject: [PATCH 05/24] IO-1138 Additional offline check. --- client/src/App/App.jsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/client/src/App/App.jsx b/client/src/App/App.jsx index cbf096f8f..fbaedfd0a 100644 --- a/client/src/App/App.jsx +++ b/client/src/App/App.jsx @@ -40,8 +40,12 @@ const mapDispatchToProps = (dispatch) => ({ export function App({ checkUserSession, currentUser, online, setOnline }) { useEffect(() => { + if (!navigator.onLine) { + setOnline(false); + } + checkUserSession(); - }, [checkUserSession]); + }, [checkUserSession, setOnline]); //const b = Grid.useBreakpoint(); // console.log("Breakpoints:", b); From afb0c85e9f8437c715aa679e7aa1ea843ad00c9a Mon Sep 17 00:00:00 2001 From: Patrick Fic <> Date: Mon, 31 May 2021 10:56:13 -0700 Subject: [PATCH 06/24] IO-1167 IO-775 Minor bug fixes. --- .../accounting-payments-table.component.jsx | 13 +++------- .../job-detail-lines/job-lines.component.jsx | 8 +++++-- .../jobs-documents-gallery.component.jsx | 24 +++++++++---------- 3 files changed, 21 insertions(+), 24 deletions(-) diff --git a/client/src/components/accounting-payments-table/accounting-payments-table.component.jsx b/client/src/components/accounting-payments-table/accounting-payments-table.component.jsx index 35308356e..f2a72dd92 100644 --- a/client/src/components/accounting-payments-table/accounting-payments-table.component.jsx +++ b/client/src/components/accounting-payments-table/accounting-payments-table.component.jsx @@ -5,7 +5,7 @@ import { Link } from "react-router-dom"; import { logImEXEvent } from "../../firebase/firebase.utils"; import CurrencyFormatter from "../../utils/CurrencyFormatter"; import { DateFormatter, DateTimeFormatter } from "../../utils/DateFormatter"; -import { alphaSort } from "../../utils/sorters"; +import { alphaSort, dateSort } from "../../utils/sorters"; import PaymentExportButton from "../payment-export-button/payment-export-button.component"; import PaymentsExportAllButton from "../payments-export-all-button/payments-export-all-button.component"; @@ -41,19 +41,12 @@ export default function AccountingPayablesTableComponent({ title: t("payments.fields.date"), dataIndex: "date", key: "date", - sorter: (a, b) => alphaSort(a.date, b.date), + sorter: (a, b) => dateSort(a.date, b.date), sortOrder: state.sortedInfo.columnKey === "date" && state.sortedInfo.order, render: (text, record) => {record.date}, }, - { - title: t("payments.fields.date"), - dataIndex: "date", - key: "date", - sorter: (a, b) => alphaSort(a.date, b.date), - sortOrder: - state.sortedInfo.columnKey === "date" && state.sortedInfo.order, - }, + { title: t("jobs.fields.owner"), dataIndex: "owner", diff --git a/client/src/components/job-detail-lines/job-lines.component.jsx b/client/src/components/job-detail-lines/job-lines.component.jsx index cd88da672..67e8381c9 100644 --- a/client/src/components/job-detail-lines/job-lines.component.jsx +++ b/client/src/components/job-detail-lines/job-lines.component.jsx @@ -22,6 +22,7 @@ import { createStructuredSelector } from "reselect"; import { DELETE_JOB_LINE_BY_PK } from "../../graphql/jobs-lines.queries"; import { selectJobReadOnly } from "../../redux/application/application.selectors"; import { setModalContext } from "../../redux/modals/modals.actions"; +import { selectTechnician } from "../../redux/tech/tech.selectors"; import { onlyUnique } from "../../utils/arrayHelper"; import CurrencyFormatter from "../../utils/CurrencyFormatter"; import { alphaSort } from "../../utils/sorters"; @@ -37,6 +38,7 @@ import PartsOrderModalContainer from "../parts-order-modal/parts-order-modal.con const mapStateToProps = createStructuredSelector({ //currentUser: selectCurrentUser jobRO: selectJobReadOnly, + technician: selectTechnician, }); const mapDispatchToProps = (dispatch) => ({ @@ -48,6 +50,7 @@ const mapDispatchToProps = (dispatch) => ({ export function JobLinesComponent({ jobRO, + technician, setPartsOrderContext, loading, refetch, @@ -364,7 +367,8 @@ export function JobLinesComponent({ disabled={ (job && !job.converted) || (selectedLines.length > 0 ? false : true) || - jobRO + jobRO || + technician } onClick={() => { setPartsOrderContext({ @@ -399,7 +403,7 @@ export function JobLinesComponent({