CEC-758 Add SMS send page and result (#173)

* Add SMS send and result pages

* Update snapshot

Co-authored-by: jwu-fisker <jwu@fiskerinc.com>
This commit is contained in:
arpanetus
2022-08-02 01:11:11 +06:00
committed by GitHub
parent b70afa5312
commit 00af90902e
13 changed files with 1307 additions and 72 deletions

View File

@@ -132,6 +132,10 @@ describe("App", () => {
await check("/tools/certificates/add", "span.MuiButton-label", "Sign In"); await check("/tools/certificates/add", "span.MuiButton-label", "Sign In");
}); });
it("Route /tools/sms/send unauthenticated", async () => {
await check("/tools/sms/send", "span.MuiButton-label", "Sign In");
})
it("Route /page-not-found unauthenticated", async () => { it("Route /page-not-found unauthenticated", async () => {
await check("/page-not-found", "h1", "Page Not Found"); await check("/page-not-found", "h1", "Page Not Found");
}); });
@@ -189,4 +193,9 @@ describe("App", () => {
setToken(TEST_AUTH_OBJECT); setToken(TEST_AUTH_OBJECT);
await check("/tools/certificates/add", "h6", "Create Certificate"); await check("/tools/certificates/add", "h6", "Create Certificate");
}); });
it("Route /tools/sms/send authenticated", async () => {
setToken(TEST_AUTH_OBJECT);
await check("/tools/sms/send", "h6", "Send SMS");
});
}); });

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,63 @@
import React, { useContext, useState } from "react";
import api from "../../services/smsAPI";
const SMSContext = React.createContext();
export class SMS {
constructor(message, ICCID, isAwaited) {
/** @type {string} */
this.messageText = message;
/** @type {string} */
this.ICCID = ICCID;
/** @type {boolean} */
this.await = isAwaited;
}
}
/**
* @param {SMS} data
*/
const validateSend = (data) => {
if (!data.messageText) throw new Error("message is required");
if (!data.ICCID) throw new Error("ICCID is required");
};
export const SMSProvider = ({ children }) => {
const [busy, setBusy] = useState(false);
/**
* @param {SMS} data
* @param {*} token
* @returns
*/
const sendSMS = async (data, token) => {
try {
setBusy(true);
validateSend(data);
const result = await api.send(data, token);
if (result.error) {
throw new Error(`Send message error. ${result.message}`);
}
return result;
} finally {
setBusy(false);
}
};
return (
<SMSContext.Provider
value={{
busy,
sendSMS,
}}
>
{children}
</SMSContext.Provider>
);
};
export const useSMSContext = () => useContext(SMSContext);

View File

@@ -62,8 +62,14 @@ const menuData = [
to: "/tools/certificates/add", to: "/tools/certificates/add",
roles: [Roles.CERTIFICATES], roles: [Roles.CERTIFICATES],
}, },
{
label: "SMS",
to: "/tools/sms/send",
roles: [],
}
], ],
}, },
]; ];
const MenuItem = ({ item, children }) => { const MenuItem = ({ item, children }) => {

View File

@@ -253,6 +253,28 @@ exports[`SideMenu Authenticated 1`] = `
/> />
</a> </a>
</li> </li>
<li>
<a
aria-disabled="false"
class="MuiButtonBase-root MuiListItem-root MuiListItem-gutters MuiListItem-button"
href="/tools/sms/send"
role="button"
tabindex="0"
>
<div
class="MuiListItemText-root"
>
<span
class="MuiTypography-root MuiListItemText-primary MuiTypography-body1 MuiTypography-displayBlock"
>
SMS
</span>
</div>
<span
class="MuiTouchRipple-root"
/>
</a>
</li>
</ul> </ul>
</ul> </ul>
</div> </div>

View File

@@ -34,6 +34,7 @@ const SSOForm = React.lazy(() => import("../SSOForm"));
const VehicleAddForm = React.lazy(() => import("../Cars/Add")); const VehicleAddForm = React.lazy(() => import("../Cars/Add"));
const VehicleUpdateForm = React.lazy(() => import("../Cars/Update")); const VehicleUpdateForm = React.lazy(() => import("../Cars/Update"));
const CertificateCreate = React.lazy(() => import("../Certificates/Add")); const CertificateCreate = React.lazy(() => import("../Certificates/Add"));
const SMSSend = React.lazy(() => import("../SMS/Send"));
const SuppliersList = React.lazy(() => import("../Suppliers/List")); const SuppliersList = React.lazy(() => import("../Suppliers/List"));
const SupplierDetails = React.lazy(() => import("../Suppliers/Details")); const SupplierDetails = React.lazy(() => import("../Suppliers/Details"));
@@ -208,6 +209,14 @@ const SiteRoutes = () => {
groups={groups} groups={groups}
roles={[Roles.CERTIFICATES]} roles={[Roles.CERTIFICATES]}
/> />
<AuthRoute
path="/tools/sms/send"
render={() => <SMSSend />}
type={TYPES.PROTECTED}
token={token}
groups={groups}
// roles={[Roles.CREATE]}
/>
<AuthRoute <AuthRoute
path="/suppliers" path="/suppliers"
render={() => <SuppliersList />} render={() => <SuppliersList />}

View File

@@ -0,0 +1,92 @@
import React, { useRef, useState } from "react";
import {
Button,
FormControlLabel,
Checkbox,
TextField,
} from "@material-ui/core";
import useStyles from "../../useStyles";
import { SMS } from "../../Contexts/SMSContext";
const SendForm = ({ onSend, busy }) => {
const classes = useStyles();
const iccidEl = useRef(null);
const [message, setMessage] = useState(null);
const [isAwaited, setAwait] = useState(false);
const onSubmit = async (event) => {
event.preventDefault();
if (onSend) onSend(new SMS(message, iccidEl.current.value, isAwaited));
};
const onMessageChange = (event) => {
setMessage(event.target.value);
};
const onAwaitChange = (event) => {
setAwait(event.target.checked);
};
return (
<div className={classes.paper}>
<form className={classes.form} noValidate action="{onSubmit}">
<TextField
id="iccid"
name="iccid"
label="ICCID"
variant="outlined"
margin="normal"
inputProps={{
maxLength: "50",
minLength: "15",
}}
required
fullWidth
inputRef={iccidEl}
/>
<TextField
id="message"
name="message"
label="Message"
variant="outlined"
margin="normal"
inputProps={{
maxLength: "320",
}}
required
fullWidth
onChange={onMessageChange}
/>
<FormControlLabel
control={
<Checkbox
checked={isAwaited}
onChange={onAwaitChange}
value="isAwaited"
color="primary"
/>
}
label="Await delivery"
/>
<Button
type="submit"
disabled={busy}
fullWidth
variant="contained"
color="primary"
className={classes.submit}
onClick={onSubmit}
>
{busy ? "Sending..." : "Send"}
</Button>
</form>
</div>
);
};
export default SendForm;

View File

@@ -0,0 +1,39 @@
import { Button } from "@material-ui/core";
import React from "react";
import useStyles from "../../useStyles";
const ViewResult = ({ result, onChangeView }) => {
const classes = useStyles();
const onNewSMS = (event) => {
event.preventDefault();
if (!onChangeView) return;
onChangeView();
};
return (
<div>
<h2>View Result</h2>
<ul>
{Object.keys(result).map((key) => (
<li key={key}>
{key}:{result[key]}
<br />
</li>
))}
</ul>
<Button
type="submit"
fullWidth
variant="contained"
color="primary"
className={classes.submit}
onClick={onNewSMS}
>
Done
</Button>
</div>
);
};
export default ViewResult;

View File

@@ -0,0 +1,21 @@
import React from "react";
import { render, waitFor } from "@testing-library/react";
import ViewResult from "./ViewResult";
describe("ViewResult", () => {
beforeAll(() => {
global.URL.createObjectURL = jest.fn();
global.URL.revokeObjectURL = jest.fn();
});
it("Render", async () => {
const { container } = render(
<ViewResult result={{ result: "success", message: "message" }} />
);
await waitFor(() => {
/* render */
});
expect(container).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,39 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ViewResult Render 1`] = `
<div>
<div>
<h2>
View Result
</h2>
<ul>
<li>
result
:
success
<br />
</li>
<li>
message
:
message
<br />
</li>
</ul>
<button
class="MuiButtonBase-root MuiButton-root MuiButton-contained makeStyles-submit-6 MuiButton-containedPrimary MuiButton-fullWidth"
tabindex="0"
type="submit"
>
<span
class="MuiButton-label"
>
Done
</span>
<span
class="MuiTouchRipple-root"
/>
</button>
</div>
</div>
`;

View File

@@ -0,0 +1,68 @@
import React, { useEffect, useState } from "react";
import { useSMSContext, SMSProvider } from "../../Contexts/SMSContext";
import { useStatusContext } from "../../Contexts/StatusContext";
import { useUserContext } from "../../Contexts/UserContext";
import { logger } from "../../../services/monitoring";
import SendForm from "./SendForm";
import ViewResult from "./ViewResult";
const VIEW_FORM = 0;
const VIEW_RESULT = 1;
const MainForm = () => {
const { busy, sendSMS } = useSMSContext();
const { setMessage, setTitle, setSitePath } = useStatusContext();
const {
token: {
idToken: { jwtToken: token },
},
} = useUserContext();
const [view, setView] = useState(VIEW_FORM);
const [result, setResult] = useState(null);
useEffect(() => {
setTitle("Send SMS");
setSitePath([
{
label: "Tools",
link: "/tools/sms/send",
},
{
label: "Send SMS",
},
]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const onSend = async (data) => {
try {
setResult(await sendSMS(data, token));
setMessage(`Sent ${data.messageText} to ${data.ICCID}`);
setView(VIEW_RESULT);
} catch (e) {
setMessage(e.message);
logger.warn(e.stack);
}
};
const onChangeView = () => {
setResult(null);
setView(VIEW_FORM);
};
if (view === VIEW_RESULT)
return <ViewResult result={result} onChangeView={onChangeView} />;
return <SendForm onSend={onSend} busy={busy} />;
};
const SMSSend = () => (
<SMSProvider>
<MainForm />
</SMSProvider>
);
export default SMSSend;

27
src/services/smsAPI.js Normal file
View File

@@ -0,0 +1,27 @@
import {
errorHandler,
getAuthHeaderOptions,
fetchRespHandler,
} from "../utils/http";
const API_ENDPOINT = process.env.REACT_APP_UPLOAD_SERVICE_URL;
const smsAPI = {
/**
* Sends a SMS to an ICCID
* @param {*} data
* @param {*} token
* @returns
*/
send: async (data, token) =>
fetch(`${API_ENDPOINT}/sms`, {
method: "POST",
headers: Object.assign(
{ "Content-Type": "application/json" },
getAuthHeaderOptions(token)
),
body: JSON.stringify(data),
}).then(fetchRespHandler).catch(errorHandler),
};
export default smsAPI;