Added additional translations.

This commit is contained in:
Patrick Fic
2020-01-06 08:19:58 -08:00
parent b72665fc81
commit 1e24f8679a
11 changed files with 213 additions and 127 deletions

View File

@@ -7,6 +7,7 @@
"antd": "^3.26.0", "antd": "^3.26.0",
"apollo-boost": "^0.4.4", "apollo-boost": "^0.4.4",
"apollo-link-context": "^1.0.19", "apollo-link-context": "^1.0.19",
"apollo-link-error": "^1.1.12",
"apollo-link-logger": "^1.2.3", "apollo-link-logger": "^1.2.3",
"apollo-link-ws": "^1.0.19", "apollo-link-ws": "^1.0.19",
"dotenv": "^8.2.0", "dotenv": "^8.2.0",

View File

@@ -17,6 +17,7 @@ import { ApolloProvider } from "react-apollo";
import { persistCache } from "apollo-cache-persist"; import { persistCache } from "apollo-cache-persist";
import initialState from "../graphql/initial-state"; import initialState from "../graphql/initial-state";
import { shouldRefreshToken, refreshToken } from "../graphql/middleware"; import { shouldRefreshToken, refreshToken } from "../graphql/middleware";
import errorLink from "../graphql/apollo-error-handling";
class AppContainer extends Component { class AppContainer extends Component {
state = { state = {
@@ -72,9 +73,9 @@ class AppContainer extends Component {
const token = localStorage.getItem("token"); const token = localStorage.getItem("token");
// return the headers to the context so httpLink can read them // return the headers to the context so httpLink can read them
if (token) { if (token) {
if (shouldRefreshToken) { // if (shouldRefreshToken) {
refreshToken(); // refreshToken();
} // }
return { return {
headers: { headers: {
@@ -91,7 +92,7 @@ class AppContainer extends Component {
if (process.env.NODE_ENV === "development") { if (process.env.NODE_ENV === "development") {
middlewares.push(apolloLogger); middlewares.push(apolloLogger);
} }
middlewares.push(authLink.concat(link)); middlewares.push(errorLink.concat(authLink.concat(link)));
const cache = new InMemoryCache(); const cache = new InMemoryCache();

View File

@@ -52,6 +52,7 @@ export default () => {
localStorage.setItem("token", token); localStorage.setItem("token", token);
const now = new Date(); const now = new Date();
window.sessionStorage.setItem(`lastTokenRefreshTime`, now); window.sessionStorage.setItem(`lastTokenRefreshTime`, now);
// window.sessionStorage.setItem("user", user);
apolloClient apolloClient
.mutate({ .mutate({

View File

@@ -12,7 +12,8 @@ import {
Descriptions, Descriptions,
Tag, Tag,
notification, notification,
Avatar Avatar,
Layout
} from "antd"; } from "antd";
import { UPDATE_JOB } from "../../graphql/jobs.queries"; import { UPDATE_JOB } from "../../graphql/jobs.queries";
import { useMutation } from "@apollo/react-hooks"; 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 { useTranslation } from "react-i18next";
import CarImage from "../../assets/car.svg"; import CarImage from "../../assets/car.svg";
const { Content } = Layout;
const formItemLayout = { const formItemLayout = {
labelCol: { // labelCol: {
xs: { span: 24 }, // xs: { span: 12 },
sm: { span: 5 } // sm: { span: 5 }
}, // },
wrapperCol: { // wrapperCol: {
xs: { span: 24 }, // xs: { span: 24 },
sm: { span: 12 } // sm: { span: 12 }
} // }
}; };
function JobTombstone({ job, ...otherProps }) { function JobTombstone({ job, ...otherProps }) {
@@ -77,108 +79,110 @@ function JobTombstone({ job, ...otherProps }) {
); );
return ( return (
<Form onSubmit={handleSubmit} {...formItemLayout}> <Content>
<PageHeader <Form onSubmit={handleSubmit} {...formItemLayout}>
style={{ <PageHeader
border: "1px solid rgb(235, 237, 240)" style={{
}} border: "1px solid rgb(235, 237, 240)"
title={tombstoneTitle} }}
subTitle={ title={tombstoneTitle}
jobContext.owner subTitle={
? (jobContext.owner?.first_name ?? "") + jobContext.owner
" " + ? (jobContext.owner?.first_name ?? "") +
(jobContext.owner?.last_name ?? "") " " +
: t("jobs.labels.no_owner") (jobContext.owner?.last_name ?? "")
} : t("jobs.labels.no_owner")
tags={<Tag color='blue'>{jobContext?.job_status?.name}</Tag>} }
extra={[ tags={<Tag color='blue'>{jobContext?.job_status?.name}</Tag>}
<Form.Item key='1'> extra={[
<Button type='primary' htmlType='submit'> <Form.Item key='1'>
{t("general.labels.save")} <Button type='primary' htmlType='submit'>
</Button> {t("general.labels.save")}
</Form.Item> </Button>
]}> </Form.Item>
<Descriptions size='small' column={5}> ]}>
<Descriptions.Item label={t("jobs.labels.vehicle_info")}> <Descriptions size='small' column={5}>
<Link to={`/manage/vehicles/${jobContext.vehicle?.id}`}> <Descriptions.Item label={t("jobs.labels.vehicle_info")}>
{jobContext.vehicle?.v_model_yr ?? t("general.labels.na")}{" "} <Link to={`/manage/vehicles/${jobContext.vehicle?.id}`}>
{jobContext.vehicle?.v_make_desc ?? t("general.labels.na")}{" "} {jobContext.vehicle?.v_model_yr ?? t("general.labels.na")}{" "}
{jobContext.vehicle?.v_model_desc ?? t("general.labels.na")} |{" "} {jobContext.vehicle?.v_make_desc ?? t("general.labels.na")}{" "}
{jobContext.vehicle?.plate_no ?? t("general.labels.na")} {jobContext.vehicle?.v_model_desc ?? t("general.labels.na")} |{" "}
</Link> {jobContext.vehicle?.plate_no ?? t("general.labels.na")}
</Descriptions.Item> </Link>
</Descriptions.Item>
<Descriptions.Item label={t("jobs.fields.est_number")}> <Descriptions.Item label={t("jobs.fields.est_number")}>
{jobContext.est_number} {jobContext.est_number}
</Descriptions.Item> </Descriptions.Item>
<Descriptions.Item label={t("jobs.fields.claim_total")}> <Descriptions.Item label={t("jobs.fields.claim_total")}>
$ {jobContext.claim_total?.toFixed(2)} $ {jobContext.claim_total?.toFixed(2)}
</Descriptions.Item> </Descriptions.Item>
<Descriptions.Item label={t("jobs.fields.deductible")}> <Descriptions.Item label={t("jobs.fields.deductible")}>
$ {jobContext.deductible?.toFixed(2)} $ {jobContext.deductible?.toFixed(2)}
</Descriptions.Item> </Descriptions.Item>
</Descriptions> </Descriptions>
</PageHeader> </PageHeader>
<Row> <Row>
<Typography.Title level={4}>Information</Typography.Title> <Typography.Title level={4}>Information</Typography.Title>
{ {
// <Form.Item label='Estimate #'> // <Form.Item label='Estimate #'>
// {getFieldDecorator("est_number", { // {getFieldDecorator("est_number", {
// initialValue: jobContext.est_number // initialValue: jobContext.est_number
// })(<Input name='est_number' readOnly onChange={handleChange} />)} // })(<Input name='est_number' readOnly onChange={handleChange} />)}
// </Form.Item> // </Form.Item>
} }
</Row> </Row>
<Row> <Row>
<Typography.Title level={4}>Insurance Information</Typography.Title> <Typography.Title level={4}>Insurance Information</Typography.Title>
<Form.Item label='Insurance Company'> <Form.Item label='Insurance Company'>
{getFieldDecorator("est_co_nm", { {getFieldDecorator("est_co_nm", {
initialValue: jobContext.est_co_nm initialValue: jobContext.est_co_nm
})(<Input name='est_co_nm' onChange={handleChange} />)} })(<Input name='est_co_nm' onChange={handleChange} />)}
</Form.Item>
<Col span={8}>
<Form.Item label='Estimator Last Name'>
{getFieldDecorator("est_ct_ln", {
initialValue: jobContext.est_ct_ln
})(<Input name='est_ct_ln' onChange={handleChange} />)}
</Form.Item> </Form.Item>
<Form.Item label='Estimator First Name'> <Col span={8}>
{getFieldDecorator("est_ct_fn", { <Form.Item label='Estimator Last Name'>
initialValue: jobContext.est_ct_fn {getFieldDecorator("est_ct_ln", {
})(<Input name='est_ct_fn' onChange={handleChange} />)} initialValue: jobContext.est_ct_ln
</Form.Item> })(<Input name='est_ct_ln' onChange={handleChange} />)}
</Col> </Form.Item>
<Col span={8}> <Form.Item label='Estimator First Name'>
<Form.Item label='Estimator Phone #'> {getFieldDecorator("est_ct_fn", {
{getFieldDecorator("est_ph1", { initialValue: jobContext.est_ct_fn
initialValue: jobContext.est_ph1 })(<Input name='est_ct_fn' onChange={handleChange} />)}
})( </Form.Item>
<FormItemPhone </Col>
customInput={Input} <Col span={8}>
name='est_ph1' <Form.Item label='Estimator Phone #'>
onValueChange={handleChange} {getFieldDecorator("est_ph1", {
/> initialValue: jobContext.est_ph1
)} })(
</Form.Item> <FormItemPhone
<Form.Item label='Estimator Email'> customInput={Input}
{getFieldDecorator("est_ea", { name='est_ph1'
initialValue: jobContext.est_ea, onValueChange={handleChange}
rules: [ />
{ )}
type: "email", </Form.Item>
message: "This is not a valid email address." <Form.Item label='Estimator Email'>
} {getFieldDecorator("est_ea", {
] initialValue: jobContext.est_ea,
})(<Input name='est_ea' onChange={handleChange} />)} rules: [
</Form.Item> {
</Col> type: "email",
</Row> message: "This is not a valid email address."
</Form> }
]
})(<Input name='est_ea' onChange={handleChange} />)}
</Form.Item>
</Col>
</Row>
</Form>
</Content>
); );
} }
export default Form.create({ name: "JobTombstone" })(JobTombstone); export default Form.create({ name: "JobTombstone" })(JobTombstone);

View File

@@ -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;

View File

@@ -20,6 +20,7 @@ export function shouldRefreshToken(minutesBeforeShouldRefresh = 45) {
return aboutToExpire; return aboutToExpire;
} }
export async function refreshToken() { export async function refreshToken() {
try { try {
if (auth.user) { if (auth.user) {

View File

@@ -1,4 +1,4 @@
import React from "react"; import React, { useEffect } from "react";
import { useSubscription } from "@apollo/react-hooks"; import { useSubscription } from "@apollo/react-hooks";
import SpinComponent from "../../components/loading-spinner/loading-spinner.component"; import SpinComponent from "../../components/loading-spinner/loading-spinner.component";
import AlertComponent from "../../components/alert/alert.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 { GET_JOB_BY_PK } from "../../graphql/jobs.queries";
import { Tabs, Icon, Row } from "antd"; import { Tabs, Icon, Row } from "antd";
import JobLinesContainer from "../../components/job-lines/job-lines.container.component"; import JobLinesContainer from "../../components/job-lines/job-lines.container.component";
import { useTranslation } from "react-i18next";
function JobsDetailPage({ match }) { function JobsDetailPage({ match }) {
const { jobId } = match.params; const { jobId } = match.params;
const { t } = useTranslation();
const { loading, error, data } = useSubscription(GET_JOB_BY_PK, { const { loading, error, data } = useSubscription(GET_JOB_BY_PK, {
variables: { id: jobId }, variables: { id: jobId },
fetchPolicy: "network-only" fetchPolicy: "network-only"
}); });
useEffect(() => {
document.title = loading
? "..."
: t("titles.jobsdetail", {
ro_number: data.jobs_by_pk.ro_number
});
}, [loading]);
if (loading) return <SpinComponent />; if (loading) return <SpinComponent />;
if (error) return <AlertComponent message={error.message} type="error" />; if (error) return <AlertComponent message={error.message} type='error' />;
return ( return (
<div> <div>
@@ -22,38 +33,35 @@ function JobsDetailPage({ match }) {
<JobTombstone job={data.jobs_by_pk} /> <JobTombstone job={data.jobs_by_pk} />
</Row> </Row>
<Row> <Row>
<Tabs defaultActiveKey="lines"> <Tabs defaultActiveKey='lines'>
<Tabs.TabPane <Tabs.TabPane
tab={ tab={
<span> <span>
<Icon type="bars" /> <Icon type='bars' />
Lines Lines
</span> </span>
} }
key="lines" key='lines'>
>
<JobLinesContainer match={match} /> <JobLinesContainer match={match} />
</Tabs.TabPane> </Tabs.TabPane>
<Tabs.TabPane <Tabs.TabPane
tab={ tab={
<span> <span>
<Icon type="dollar" /> <Icon type='dollar' />
Rates Rates
</span> </span>
} }
key="rates" key='rates'>
>
Estimate Rates Estimate Rates
</Tabs.TabPane> </Tabs.TabPane>
<Tabs.TabPane <Tabs.TabPane
tab={ tab={
<span> <span>
<Icon type="tool1" /> <Icon type='tool1' />
Parts Parts
</span> </span>
} }
key="parts" key='parts'>
>
Estimate Parts Estimate Parts
</Tabs.TabPane> </Tabs.TabPane>
</Tabs> </Tabs>

View File

@@ -3,16 +3,17 @@ import { useSubscription } from "@apollo/react-hooks";
import AlertComponent from "../../components/alert/alert.component"; import AlertComponent from "../../components/alert/alert.component";
import { Col } from "antd"; import { Col } from "antd";
import { SUBSCRIPTION_ALL_OPEN_JOBS } from "../../graphql/jobs.queries"; import { SUBSCRIPTION_ALL_OPEN_JOBS } from "../../graphql/jobs.queries";
import { useTranslation } from "react-i18next";
import JobsList from "../../components/jobs-list/jobs-list.component"; import JobsList from "../../components/jobs-list/jobs-list.component";
export default function JobsPage() { export default function JobsPage() {
const { loading, error, data } = useSubscription(SUBSCRIPTION_ALL_OPEN_JOBS, { const { loading, error, data } = useSubscription(SUBSCRIPTION_ALL_OPEN_JOBS, {
fetchPolicy: "network-only" fetchPolicy: "network-only"
}); });
const { t } = useTranslation();
useEffect(() => { useEffect(() => {
document.title = "new title"; document.title = t("titles.jobs");
}, []); }, []);
if (error) return <AlertComponent message={error.message} />; if (error) return <AlertComponent message={error.message} />;

View File

@@ -1,6 +1,7 @@
import React, { lazy, Suspense } from "react"; import React, { lazy, Suspense, useEffect } from "react";
import { Route } from "react-router"; import { Route } from "react-router";
import { Layout, BackTop } from "antd"; import { Layout, BackTop } from "antd";
import { useTranslation } from "react-i18next";
//Component Imports //Component Imports
import HeaderContainer from "../../components/header/header.container"; 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; const { Header, Content, Footer } = Layout;
//This page will handle all routing for the entire application. //This page will handle all routing for the entire application.
export default function Manage({ match }) { export default function Manage({ match }) {
const { t } = useTranslation();
useEffect(() => {
document.title = t("titles.app");
}, []);
return ( return (
<Layout> <Layout>
<Header> <Header>
@@ -23,8 +30,7 @@ export default function Manage({ match }) {
<Content> <Content>
<ErrorBoundary> <ErrorBoundary>
<Suspense <Suspense
fallback={<div>TODO: Suspended Loading in Manage Page...</div>} fallback={<div>TODO: Suspended Loading in Manage Page...</div>}>
>
<Route exact path={`${match.path}`} component={WhiteBoardPage} /> <Route exact path={`${match.path}`} component={WhiteBoardPage} />
<Route exact path={`${match.path}/jobs`} component={JobsPage} /> <Route exact path={`${match.path}/jobs`} component={JobsPage} />

View File

@@ -13,6 +13,11 @@
"save": "Save" "save": "Save"
} }
}, },
"titles": {
"app": "Bodyshop by ImEX Systems",
"jobs": "All Jobs | $t(titles.app)",
"jobsdetail": "Job {{ro_number}} | $t(titles.app)"
},
"jobs": { "jobs": {
"labels": { "labels": {

View File

@@ -2313,7 +2313,7 @@ apollo-link-context@^1.0.19:
apollo-link "^1.2.13" apollo-link "^1.2.13"
tslib "^1.9.3" 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" version "1.1.12"
resolved "https://registry.yarnpkg.com/apollo-link-error/-/apollo-link-error-1.1.12.tgz#e24487bb3c30af0654047611cda87038afbacbf9" resolved "https://registry.yarnpkg.com/apollo-link-error/-/apollo-link-error-1.1.12.tgz#e24487bb3c30af0654047611cda87038afbacbf9"
integrity sha512-psNmHyuy3valGikt/XHJfe0pKJnRX19tLLs6P6EHRxg+6q6JMXNVLYPaQBkL0FkwdTCB0cbFJAGRYCBviG8TDA== integrity sha512-psNmHyuy3valGikt/XHJfe0pKJnRX19tLLs6P6EHRxg+6q6JMXNVLYPaQBkL0FkwdTCB0cbFJAGRYCBviG8TDA==