Merge development (#40)

* CEC-231 Bulk car selection control (#38)

* Bulk car selection control

* Tweak control alignment

* CEC-231 Fix control css (#39)

* Update test

Co-authored-by: Rafi Greenberg <rgreenberg@fiskerinc.com>
Co-authored-by: Roger Standridge <rstandridge@fiskerinc.com>
This commit is contained in:
John Wu
2021-05-10 11:42:20 -07:00
committed by GitHub
parent c7ffb60542
commit 50e6633d6d
17 changed files with 1966 additions and 1542 deletions

30
.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,30 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "pwa-node",
"request": "launch",
"name": "Launch Program",
"skipFiles": [
"<node_internals>/**"
],
"program": "${file}"
},
{
"name": "Debug CRA Tests",
"type": "node",
"request": "launch",
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/react-scripts",
"args": ["test", "--runInBand", "--no-cache", "--watchAll=false"],
"cwd": "${workspaceRoot}",
"protocol": "inspector",
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"env": { "CI": "true" },
"disableOptimisticBPs": true
}
]
}

2251
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -3,19 +3,19 @@
"version": "0.1.1",
"private": true,
"dependencies": {
"@datadog/browser-rum": "^2.6.2",
"@material-ui/core": "^4.11.3",
"@datadog/browser-rum": "^2.8.1",
"@material-ui/core": "^4.11.4",
"@material-ui/icons": "^4.11.2",
"@testing-library/jest-dom": "^5.11.8",
"@testing-library/react": "^11.2.2",
"@testing-library/user-event": "^12.6.0",
"@testing-library/jest-dom": "^5.12.0",
"@testing-library/react": "^11.2.6",
"@testing-library/user-event": "^12.8.3",
"axios": "^0.21.1",
"clsx": "^1.1.1",
"material-ui-dropzone": "^3.5.0",
"react": "^17.0.1",
"react-dom": "^17.0.1",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-router-dom": "^5.2.0",
"react-scripts": "4.0.1",
"react-scripts": "4.0.3",
"web-vitals": "^0.2.4"
},
"scripts": {
@@ -47,6 +47,6 @@
"node": "12.20.1"
},
"devDependencies": {
"react-test-renderer": "^17.0.1"
"react-test-renderer": "^17.0.2"
}
}

View File

@@ -117,17 +117,12 @@ describe("App", () => {
await check("/update/1", "h1", "Edit Update Package 1");
});
it("Route /carupdate-deploy authenticated", async () => {
setToken(TEST_AUTH_OBJECT);
await check("/carupdate-deploy/1", "h1", "Deploy [1]");
});
it("Route /carupdate-status authenticated", async () => {
setToken(TEST_AUTH_OBJECT);
await check("/carupdate-status/1", "h1", "");
});
it("Route /vehicles authenticated", async () => {
it("Route /vehicles authenticated", async () => {
setToken(TEST_AUTH_OBJECT);
await check("/vehicles", "h1", "Vehicles");
});
@@ -145,4 +140,9 @@ describe("App", () => {
setToken(TEST_AUTH_OBJECT);
await check("/page-not-found", "h1", "Page Not Found");
});
})
it("Route /carupdate-deploy authenticated", async () => {
setToken(TEST_AUTH_OBJECT);
await check("/carupdate-deploy/1", "h1", "Deploy [1]");
});
});

File diff suppressed because it is too large Load Diff

View File

@@ -1,36 +1,20 @@
import React, { useEffect, useState } from "react";
import { useParams, Redirect } from "react-router";
import {
Button,
Chip,
FormControl,
Input,
InputLabel,
MenuItem,
Select,
TextField,
Typography,
useTheme,
} from "@material-ui/core";
import { Button, TextField, Typography } from "@material-ui/core";
import {
UpdatesProvider,
useUpdatesContext,
} from "../../Contexts/UpdatesContext";
import {
useVehicleContext,
VehicleProvider,
} from "../../Contexts/VehicleContext";
import { useUserContext } from "../../Contexts/UserContext";
import { useStatusContext } from "../../Contexts/StatusContext";
import CarSelection from "../../Cars/CarSelection";
import useStyles from "../../useStyles";
import { tsLocalDateTimeString } from "../../../utils/dates";
import menuItemStyle from "../../menuItemStyle";
const MainForm = () => {
const { packageid } = useParams();
const { getPackages, createCarUpdates, packages, busy } = useUpdatesContext();
const { getVehicles, vehicles } = useVehicleContext();
const {
token: {
idToken: { jwtToken: token },
@@ -46,10 +30,7 @@ const MainForm = () => {
const [selectedVehicles, setSelectedVehicles] = useState([]);
const [redirect, setRedirect] = useState("");
const classes = useStyles();
const theme = useTheme();
const handleVehiclesChange = (event) => {
setSelectedVehicles(event.target.value);
};
const onSubmit = async (event) => {
try {
event.preventDefault();
@@ -73,17 +54,12 @@ const MainForm = () => {
setMessage(e.message);
}
};
const handleCarOpen = async () => {
try {
await getVehicles(null, token);
} catch (e) {
setMessage(e.message);
}
};
useEffect(() => {
getData();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [token]);
useEffect(() => {
if (!packages || packages.length === 0) return;
var data = packages[0];
@@ -160,43 +136,7 @@ const MainForm = () => {
fullWidth
placeholder="Release Notes URL"
/>
<FormControl
className={classes.formControl}
variant="outlined"
fullWidth
>
<InputLabel htmlFor="vehicles">Vehicles</InputLabel>
<Select
label="Vehicles"
placeholder="Select vehicles"
id="vehicles"
name="vehicles"
multiple
className={classes.menuProps}
variant="outlined"
onOpen={handleCarOpen}
onChange={handleVehiclesChange}
value={selectedVehicles}
input={<Input id="select-multiple-chip" />}
renderValue={(selected) => (
<div className={classes.chips}>
{selected.map((value) => (
<Chip key={value} label={value} className={classes.chip} />
))}
</div>
)}
>
{vehicles.map((vehicle) => (
<MenuItem
key={vehicle.vin}
value={vehicle.vin}
style={menuItemStyle(vehicle, selectedVehicles, theme)}
>
{vehicle.vin}
</MenuItem>
))}
</Select>
</FormControl>
<CarSelection onSelection={setSelectedVehicles} />
<Button
type="submit"
disabled={busy}
@@ -214,11 +154,9 @@ const MainForm = () => {
};
const UpdatePackageDeployForm = () => (
<VehicleProvider>
<UpdatesProvider>
<MainForm />
</UpdatesProvider>
</VehicleProvider>
<UpdatesProvider>
<MainForm />
</UpdatesProvider>
);
export default UpdatePackageDeployForm;

View File

@@ -0,0 +1,136 @@
import React, { useEffect, useState } from "react";
import PropTypes from "prop-types";
import { FormControl, InputLabel, Select } from "@material-ui/core";
import {
useVehicleContext,
VehicleProvider,
} from "../../Contexts/VehicleContext";
import { useUserContext } from "../../Contexts/UserContext";
import useStyles from "../../useStyles";
const Control = (props) => {
const classes = useStyles();
const {
models,
years,
vehicles,
getModels,
getYears,
getVehicles,
} = useVehicleContext();
const {
token: {
idToken: { jwtToken: token },
},
} = useUserContext();
const [model, setModel] = useState("");
const [year, setYear] = useState(-1);
const handleChangeModel = (event) => {
setModel(event.target.value);
};
const handleChangeYear = (event) => {
setYear(event.target.value);
};
useEffect(() => {
if (!token) return;
(async () => {
try {
await getModels(token);
await getYears(token);
} catch (e) {}
})();
// eslint-disable-next-line
}, [token]);
useEffect(() => {
if (!models || models.length === 0) return;
setModel(models[0]);
}, [models]);
useEffect(() => {
if (!years || years.length === 0) return;
setYear(years[0]);
}, [years]);
useEffect(() => {
if (model === null || year === -1) return;
getVehicles({ model, year }, token);
// eslint-disable-next-line
}, [model, year]);
useEffect(() => {
if (!props.onSelection) return;
const vins = vehicles.map((item) => item.vin);
props.onSelection(vins);
// eslint-disable-next-line
}, [vehicles]);
return (
<div className={classes.form}>
<FormControl className={classes.formControlInline} variant="outlined">
<InputLabel htmlFor="car-model" style={{ backgroundColor: "White" }}>
Model
</InputLabel>
<Select
native
value={model}
onChange={handleChangeModel}
variant="outlined"
inputProps={{
name: "car-model",
id: "car-model",
}}
>
{models.map((item) => (
<option key={item} value={item}>
{item}
</option>
))}
</Select>
</FormControl>
<FormControl className={classes.formControlInline} variant="outlined">
<InputLabel htmlFor="car-year" style={{ backgroundColor: "White" }}>
Year
</InputLabel>
<Select
native
value={year}
onChange={handleChangeYear}
variant="outlined"
inputProps={{
name: "car-year",
id: "car-year",
}}
>
{years.map((item) => (
<option key={item} value={item}>
{item}
</option>
))}
</Select>
</FormControl>
<div className={classes.labelInline}>
{vehicles.length === 0
? "No Cars Selected"
: vehicles.length === 1
? "1 Car Selected"
: `${vehicles.length} Cars Selected`}
</div>
</div>
);
};
const CarSelection = (props) => (
<VehicleProvider>
<Control {...props} />
</VehicleProvider>
);
CarSelection.propTypes = {
onSelection: PropTypes.func,
};
export default CarSelection;

View File

@@ -226,6 +226,6 @@ const validateCreateCarUpdates = (data) => {
}
if (!data.vins || data.vins.length === 0) {
throw new Error("Car ids required");
throw new Error("Cars are required");
}
};

View File

@@ -273,7 +273,7 @@ describe("UpdatesContext", () => {
await waitFor(() =>
expect(screen.getByTestId("busy").innerHTML).toBe("false")
);
checkState("false", "Car ids required", null);
checkState("false", "Cars are required", null);
});
it("with-good-data", async () => {

View File

@@ -29,6 +29,8 @@ export const VehicleProvider = ({ children }) => {
const [busy, setBusy] = useState(false);
const [vehicles, setVehicles] = useState([]);
const [totalVehicles, setTotalVehicles] = useState(0);
const [models, setModels] = useState([]);
const [years, setYears] = useState([]);
const getVehicles = async (search, token) => {
try {
@@ -60,14 +62,40 @@ export const VehicleProvider = ({ children }) => {
}
};
const getModels = async (token) => {
try {
setBusy(true);
const result = await api.getModels(token);
if (result.error) throw new Error(`Get models error. ${result.message}`);
setModels(result.data);
} finally {
setBusy(false);
}
};
const getYears = async (token) => {
try {
setBusy(true);
const result = await api.getYears(token);
if (result.error) throw new Error(`Get years error. ${result.message}`);
setYears(result.data);
} finally {
setBusy(false);
}
};
return (
<VehicleContext.Provider
value={{
busy,
vehicles,
totalVehicles,
models,
years,
getVehicles,
addVehicle,
getModels,
getYears,
}}
>
{children}

View File

@@ -2,6 +2,8 @@ import React from "react";
let busy = false;
let vehicles = [];
let models = ["Ocean", "PEAR"];
let years = [2023, 2024];
let totalVehicles = 0;
let error = null;
@@ -13,8 +15,16 @@ export const useVehicleContext = () => ({
busy,
vehicles,
totalVehicles,
models,
years,
getVehicles: jest.fn(() => vehicles),
addVehicle: jest.fn(),
getModels: jest.fn(() => {
models = ["Ocean", "PEAR"];
}),
getYears: jest.fn(() => {
years = [2023, 2024];
}),
});
export const setBusy = (val) => {

View File

@@ -16,7 +16,7 @@ const menuData = [
roles: [Roles.CREATE, Roles.READ],
},
{
label: "Create Packages",
label: "Create Package",
to: "/package-upload",
roles: [Roles.CREATE],
},
@@ -26,7 +26,7 @@ const menuData = [
roles: [Roles.CREATE, Roles.READ],
},
{
label: "Add Vehicles",
label: "Add Vehicle",
to: "/vehicle-add",
roles: [Roles.CREATE],
},

View File

@@ -66,7 +66,7 @@ exports[`SideMenu Authenticated 1`] = `
<span
class="MuiTypography-root MuiListItemText-primary MuiTypography-body1 MuiTypography-displayBlock"
>
Create Packages
Create Package
</span>
</div>
<span
@@ -110,7 +110,7 @@ exports[`SideMenu Authenticated 1`] = `
<span
class="MuiTypography-root MuiListItemText-primary MuiTypography-body1 MuiTypography-displayBlock"
>
Add Vehicles
Add Vehicle
</span>
</div>
<span

View File

@@ -51,10 +51,10 @@ exports[`File Upload Form Should render 1`] = `
/>
<fieldset
aria-hidden="true"
class="PrivateNotchedOutline-root-24 MuiOutlinedInput-notchedOutline"
class="PrivateNotchedOutline-root-26 MuiOutlinedInput-notchedOutline"
>
<legend
class="PrivateNotchedOutline-legendLabelled-26"
class="PrivateNotchedOutline-legendLabelled-28"
>
<span>
Package name
@@ -97,10 +97,10 @@ exports[`File Upload Form Should render 1`] = `
/>
<fieldset
aria-hidden="true"
class="PrivateNotchedOutline-root-24 MuiOutlinedInput-notchedOutline"
class="PrivateNotchedOutline-root-26 MuiOutlinedInput-notchedOutline"
>
<legend
class="PrivateNotchedOutline-legendLabelled-26"
class="PrivateNotchedOutline-legendLabelled-28"
>
<span>
Version
@@ -143,10 +143,10 @@ exports[`File Upload Form Should render 1`] = `
/>
<fieldset
aria-hidden="true"
class="PrivateNotchedOutline-root-24 MuiOutlinedInput-notchedOutline"
class="PrivateNotchedOutline-root-26 MuiOutlinedInput-notchedOutline"
>
<legend
class="PrivateNotchedOutline-legendLabelled-26"
class="PrivateNotchedOutline-legendLabelled-28"
>
<span>
Description
@@ -190,10 +190,10 @@ exports[`File Upload Form Should render 1`] = `
/>
<fieldset
aria-hidden="true"
class="PrivateNotchedOutline-root-24 MuiOutlinedInput-notchedOutline"
class="PrivateNotchedOutline-root-26 MuiOutlinedInput-notchedOutline"
>
<legend
class="PrivateNotchedOutline-legendLabelled-26"
class="PrivateNotchedOutline-legendLabelled-28"
>
<span>
Release Notes URL

View File

@@ -39,6 +39,17 @@ const useStyles = makeStyles((theme) => ({
width: "100%",
minWidth: 120,
},
formControlInline: {
marginRight: "10px !important",
minWidth: 120,
},
labelInline: {
fontSize: "1.25em",
margin: theme.spacing(2, 0, 1),
display: "inline-flex",
boxSizing: "border-box",
position: "relative",
},
chips: {
display: "flex",
flexWrap: "wrap",

View File

@@ -15,6 +15,16 @@ const vehiclesAPI = {
data.push(vehicle);
return vehicle;
},
getModels: async (token) => {
return {
data: ["Ocean", "Pear"],
};
},
getYears: async (token) => {
return {
data: [2021, 2022],
};
},
};
export default vehiclesAPI;

View File

@@ -10,15 +10,26 @@ const vehiclesAPI = {
})
.then(fetchRespHandler),
getVehicles: async (search, token) => {
const u = addQueryParams(`${API_ENDPOINT}/vehicles`, search);
return fetch(u, {
method: "GET",
headers: Object.assign({ "Content-Type": "application/json" }, getAuthHeaderOptions(token)),
})
.then(fetchRespHandler);
getVehicles: async (search, token) => {
const u = addQueryParams(`${API_ENDPOINT}/vehicles`, search);
return fetch(u, {
method: "GET",
headers: Object.assign({ "Content-Type": "application/json" }, getAuthHeaderOptions(token)),
})
.then(fetchRespHandler)
},
getModels: async (token) => fetch(`${API_ENDPOINT}/vehiclemodels`, {
method: "GET",
headers: Object.assign({ "Content-Type": "application/json" }, getAuthHeaderOptions(token)),
})
.then(fetchRespHandler),
getYears: async (token) => fetch(`${API_ENDPOINT}/vehicleyears`, {
method: "GET",
headers: Object.assign({ "Content-Type": "application/json" }, getAuthHeaderOptions(token)),
})
.then(fetchRespHandler),
};
export default vehiclesAPI;