diff --git a/client/package.json b/client/package.json index 46275f81a..64baff2a2 100644 --- a/client/package.json +++ b/client/package.json @@ -7,6 +7,7 @@ "antd": "^3.26.0", "apollo-boost": "^0.4.4", "apollo-link-context": "^1.0.19", + "apollo-link-error": "^1.1.12", "apollo-link-logger": "^1.2.3", "apollo-link-ws": "^1.0.19", "dotenv": "^8.2.0", diff --git a/client/src/App/App.container.jsx b/client/src/App/App.container.jsx index c91f582e0..0c56c2748 100644 --- a/client/src/App/App.container.jsx +++ b/client/src/App/App.container.jsx @@ -17,6 +17,7 @@ import { ApolloProvider } from "react-apollo"; import { persistCache } from "apollo-cache-persist"; import initialState from "../graphql/initial-state"; import { shouldRefreshToken, refreshToken } from "../graphql/middleware"; +import errorLink from "../graphql/apollo-error-handling"; class AppContainer extends Component { state = { @@ -72,9 +73,9 @@ class AppContainer extends Component { const token = localStorage.getItem("token"); // return the headers to the context so httpLink can read them if (token) { - if (shouldRefreshToken) { - refreshToken(); - } + // if (shouldRefreshToken) { + // refreshToken(); + // } return { headers: { @@ -91,7 +92,7 @@ class AppContainer extends Component { if (process.env.NODE_ENV === "development") { middlewares.push(apolloLogger); } - middlewares.push(authLink.concat(link)); + middlewares.push(errorLink.concat(authLink.concat(link))); const cache = new InMemoryCache(); diff --git a/client/src/App/App.js b/client/src/App/App.js index 36c1a3956..b9eeb58cc 100644 --- a/client/src/App/App.js +++ b/client/src/App/App.js @@ -52,6 +52,7 @@ export default () => { localStorage.setItem("token", token); const now = new Date(); window.sessionStorage.setItem(`lastTokenRefreshTime`, now); + // window.sessionStorage.setItem("user", user); apolloClient .mutate({ diff --git a/client/src/components/job-tombstone/job-tombstone.component.jsx b/client/src/components/job-tombstone/job-tombstone.component.jsx index 64732411f..8a4d34afb 100644 --- a/client/src/components/job-tombstone/job-tombstone.component.jsx +++ b/client/src/components/job-tombstone/job-tombstone.component.jsx @@ -12,7 +12,8 @@ import { Descriptions, Tag, notification, - Avatar + Avatar, + Layout } from "antd"; import { UPDATE_JOB } from "../../graphql/jobs.queries"; import { useMutation } from "@apollo/react-hooks"; @@ -20,15 +21,16 @@ import FormItemPhone from "../form-items-formatted/phone-form-item.component"; import { useTranslation } from "react-i18next"; import CarImage from "../../assets/car.svg"; +const { Content } = Layout; const formItemLayout = { - labelCol: { - xs: { span: 24 }, - sm: { span: 5 } - }, - wrapperCol: { - xs: { span: 24 }, - sm: { span: 12 } - } + // labelCol: { + // xs: { span: 12 }, + // sm: { span: 5 } + // }, + // wrapperCol: { + // xs: { span: 24 }, + // sm: { span: 12 } + // } }; function JobTombstone({ job, ...otherProps }) { @@ -77,108 +79,110 @@ function JobTombstone({ job, ...otherProps }) { ); return ( -
- {jobContext?.job_status?.name}} - extra={[ - - - - ]}> - - - - {jobContext.vehicle?.v_model_yr ?? t("general.labels.na")}{" "} - {jobContext.vehicle?.v_make_desc ?? t("general.labels.na")}{" "} - {jobContext.vehicle?.v_model_desc ?? t("general.labels.na")} |{" "} - {jobContext.vehicle?.plate_no ?? t("general.labels.na")} - - + + + {jobContext?.job_status?.name}} + extra={[ + + + + ]}> + + + + {jobContext.vehicle?.v_model_yr ?? t("general.labels.na")}{" "} + {jobContext.vehicle?.v_make_desc ?? t("general.labels.na")}{" "} + {jobContext.vehicle?.v_model_desc ?? t("general.labels.na")} |{" "} + {jobContext.vehicle?.plate_no ?? t("general.labels.na")} + + - - {jobContext.est_number} - + + {jobContext.est_number} + - - $ {jobContext.claim_total?.toFixed(2)} - + + $ {jobContext.claim_total?.toFixed(2)} + - - $ {jobContext.deductible?.toFixed(2)} - - - + + $ {jobContext.deductible?.toFixed(2)} + + + - - Information -{ - // - // {getFieldDecorator("est_number", { - // initialValue: jobContext.est_number - // })()} - // - } - + + Information + { + // + // {getFieldDecorator("est_number", { + // initialValue: jobContext.est_number + // })()} + // + } + - - Insurance Information - - {getFieldDecorator("est_co_nm", { - initialValue: jobContext.est_co_nm - })()} - - - - {getFieldDecorator("est_ct_ln", { - initialValue: jobContext.est_ct_ln - })()} + + Insurance Information + + {getFieldDecorator("est_co_nm", { + initialValue: jobContext.est_co_nm + })()} - - {getFieldDecorator("est_ct_fn", { - initialValue: jobContext.est_ct_fn - })()} - - - - - {getFieldDecorator("est_ph1", { - initialValue: jobContext.est_ph1 - })( - - )} - - - {getFieldDecorator("est_ea", { - initialValue: jobContext.est_ea, - rules: [ - { - type: "email", - message: "This is not a valid email address." - } - ] - })()} - - - - + + + {getFieldDecorator("est_ct_ln", { + initialValue: jobContext.est_ct_ln + })()} + + + {getFieldDecorator("est_ct_fn", { + initialValue: jobContext.est_ct_fn + })()} + + + + + {getFieldDecorator("est_ph1", { + initialValue: jobContext.est_ph1 + })( + + )} + + + {getFieldDecorator("est_ea", { + initialValue: jobContext.est_ea, + rules: [ + { + type: "email", + message: "This is not a valid email address." + } + ] + })()} + + +
+ + ); } -export default Form.create({ name: "JobTombstone" })(JobTombstone); +export default Form.create({ name: "JobTombstone" })(JobTombstone); \ No newline at end of file diff --git a/client/src/graphql/apollo-error-handling.js b/client/src/graphql/apollo-error-handling.js new file mode 100644 index 000000000..7ebd8754e --- /dev/null +++ b/client/src/graphql/apollo-error-handling.js @@ -0,0 +1,58 @@ +import { onError } from "apollo-link-error"; +import { Observable } from "apollo-link"; +import { auth } from "../firebase/firebase.utils"; +//https://stackoverflow.com/questions/57163454/refreshing-a-token-with-apollo-client-firebase-auth +const errorLink = onError( + ({ graphQLErrors, networkError, operation, forward }) => { + let access_token = window.localStorage.getItem("token"); + if (graphQLErrors) { + // User access token has expired + if (graphQLErrors[0].message.includes("JWTExpired")) { + if (access_token && access_token !== "undefined") { + // Let's refresh token through async request + return new Observable(observer => { + auth.currentUser + .getIdToken(true) + .then(function(idToken) { + if (!idToken) { + window.localStorage.removeItem("token"); + return console.log("Refresh token has expired"); + } + + window.localStorage.setItem("token", idToken); + + // reset the headers + operation.setContext(({ headers = {} }) => ({ + headers: { + // Re-add old headers + ...headers, + // Switch out old access token for new one + authorization: idToken ? `Bearer ${idToken}` : "" + } + })); + const subscriber = { + next: observer.next.bind(observer), + error: observer.error.bind(observer), + complete: observer.complete.bind(observer) + }; + + // Retry last failed request + forward(operation).subscribe(subscriber); + }) + .catch(error => { + // No refresh or client token available, we force user to login + observer.error(error); + }); + }); + } + } + } + + if (networkError) { + console.log(`[Network error]: ${networkError}`); + //props.history.push("/network-error"); + } + } +); + +export default errorLink; diff --git a/client/src/graphql/middleware.js b/client/src/graphql/middleware.js index 3afdbf5fc..bad7a938a 100644 --- a/client/src/graphql/middleware.js +++ b/client/src/graphql/middleware.js @@ -20,6 +20,7 @@ export function shouldRefreshToken(minutesBeforeShouldRefresh = 45) { return aboutToExpire; } + export async function refreshToken() { try { if (auth.user) { diff --git a/client/src/pages/jobs-detail/jobs-detail.page.jsx b/client/src/pages/jobs-detail/jobs-detail.page.jsx index abe320abe..831581343 100644 --- a/client/src/pages/jobs-detail/jobs-detail.page.jsx +++ b/client/src/pages/jobs-detail/jobs-detail.page.jsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useEffect } from "react"; import { useSubscription } from "@apollo/react-hooks"; import SpinComponent from "../../components/loading-spinner/loading-spinner.component"; import AlertComponent from "../../components/alert/alert.component"; @@ -6,15 +6,26 @@ import JobTombstone from "../../components/job-tombstone/job-tombstone.component import { GET_JOB_BY_PK } from "../../graphql/jobs.queries"; import { Tabs, Icon, Row } from "antd"; import JobLinesContainer from "../../components/job-lines/job-lines.container.component"; +import { useTranslation } from "react-i18next"; function JobsDetailPage({ match }) { const { jobId } = match.params; + const { t } = useTranslation(); const { loading, error, data } = useSubscription(GET_JOB_BY_PK, { variables: { id: jobId }, fetchPolicy: "network-only" }); + + useEffect(() => { + document.title = loading + ? "..." + : t("titles.jobsdetail", { + ro_number: data.jobs_by_pk.ro_number + }); + }, [loading]); + if (loading) return ; - if (error) return ; + if (error) return ; return (
@@ -22,38 +33,35 @@ function JobsDetailPage({ match }) { - + - + Lines } - key="lines" - > + key='lines'> - + Rates } - key="rates" - > + key='rates'> Estimate Rates - + Parts } - key="parts" - > + key='parts'> Estimate Parts diff --git a/client/src/pages/jobs/jobs.page.jsx b/client/src/pages/jobs/jobs.page.jsx index 4a30fc3f5..615e34d38 100644 --- a/client/src/pages/jobs/jobs.page.jsx +++ b/client/src/pages/jobs/jobs.page.jsx @@ -3,16 +3,17 @@ import { useSubscription } from "@apollo/react-hooks"; import AlertComponent from "../../components/alert/alert.component"; import { Col } from "antd"; import { SUBSCRIPTION_ALL_OPEN_JOBS } from "../../graphql/jobs.queries"; - +import { useTranslation } from "react-i18next"; import JobsList from "../../components/jobs-list/jobs-list.component"; export default function JobsPage() { const { loading, error, data } = useSubscription(SUBSCRIPTION_ALL_OPEN_JOBS, { fetchPolicy: "network-only" }); + const { t } = useTranslation(); useEffect(() => { - document.title = "new title"; + document.title = t("titles.jobs"); }, []); if (error) return ; diff --git a/client/src/pages/manage/manage.page.jsx b/client/src/pages/manage/manage.page.jsx index 2ea7d207d..56ad79bc7 100644 --- a/client/src/pages/manage/manage.page.jsx +++ b/client/src/pages/manage/manage.page.jsx @@ -1,6 +1,7 @@ -import React, { lazy, Suspense } from "react"; +import React, { lazy, Suspense, useEffect } from "react"; import { Route } from "react-router"; import { Layout, BackTop } from "antd"; +import { useTranslation } from "react-i18next"; //Component Imports import HeaderContainer from "../../components/header/header.container"; @@ -14,6 +15,12 @@ const JobsDetailPage = lazy(() => import("../jobs-detail/jobs-detail.page")); const { Header, Content, Footer } = Layout; //This page will handle all routing for the entire application. export default function Manage({ match }) { + const { t } = useTranslation(); + + useEffect(() => { + document.title = t("titles.app"); + }, []); + return (
@@ -23,8 +30,7 @@ export default function Manage({ match }) { TODO: Suspended Loading in Manage Page...
} - > + fallback={
TODO: Suspended Loading in Manage Page...
}> diff --git a/client/src/translations/en_us/common.json b/client/src/translations/en_us/common.json index 5b3049124..0b627c30d 100644 --- a/client/src/translations/en_us/common.json +++ b/client/src/translations/en_us/common.json @@ -13,6 +13,11 @@ "save": "Save" } }, + "titles": { + "app": "Bodyshop by ImEX Systems", + "jobs": "All Jobs | $t(titles.app)", + "jobsdetail": "Job {{ro_number}} | $t(titles.app)" + }, "jobs": { "labels": { diff --git a/client/yarn.lock b/client/yarn.lock index 277d11d56..21bf558a7 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -2313,7 +2313,7 @@ apollo-link-context@^1.0.19: apollo-link "^1.2.13" tslib "^1.9.3" -apollo-link-error@^1.0.3: +apollo-link-error@^1.0.3, apollo-link-error@^1.1.12: version "1.1.12" resolved "https://registry.yarnpkg.com/apollo-link-error/-/apollo-link-error-1.1.12.tgz#e24487bb3c30af0654047611cda87038afbacbf9" integrity sha512-psNmHyuy3valGikt/XHJfe0pKJnRX19tLLs6P6EHRxg+6q6JMXNVLYPaQBkL0FkwdTCB0cbFJAGRYCBviG8TDA==