diff --git a/package-lock.json b/package-lock.json index d00e4ffd..1fb7ea5f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7849,7 +7849,8 @@ "normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==" + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "optional": true }, "to-regex-range": { "version": "5.0.1", @@ -18631,20 +18632,20 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, "react-joyride": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/react-joyride/-/react-joyride-2.3.0.tgz", - "integrity": "sha512-aY7+dgmBKbgGoMjN828qXnMAqeA6QvwvSWxj/fyvxIIIx0iu3wNVT/A5NZ0wHxiVDav+Df9YZuL412Q6C0l7gw==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/react-joyride/-/react-joyride-2.3.1.tgz", + "integrity": "sha512-MmyhECU3V+4kZAJrcDPPXcXxaoTpwc7g+E7Cq6QZ5IqJZrWYSVvpVCfudQcdcf6BsNbgawRhvCvbQyeWoPtNig==", "requires": { "deep-diff": "^1.0.2", "deepmerge": "^4.2.2", "exenv": "^1.2.2", - "is-lite": "^0.8.0", + "is-lite": "^0.8.1", "nested-property": "^4.0.0", - "react-floater": "^0.7.2", + "react-floater": "^0.7.3", "react-is": "^16.13.1", "scroll": "^3.0.1", "scrollparent": "^2.0.1", - "tree-changes": "^0.6.1" + "tree-changes": "^0.7.1" } }, "react-konva": { @@ -21007,9 +21008,9 @@ "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==" }, "tar": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.0.tgz", - "integrity": "sha512-DUCttfhsnLCjwoDoFcI+B2iJgYa93vBnDUATYEeRx6sntCTdN01VnqsIuTlALXla/LWooNg0yEGeB+Y8WdFxGA==", + "version": "6.1.6", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.6.tgz", + "integrity": "sha512-oaWyu5dQbHaYcyZCTfyPpC+VmI62/OM2RTUYavTk1MDr1cwW5Boi3baeYQKiZbY2uSQJGr+iMOzb/JFxLrft+g==", "requires": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", @@ -21386,13 +21387,12 @@ } }, "tree-changes": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/tree-changes/-/tree-changes-0.6.1.tgz", - "integrity": "sha512-J+eb7K2hHq8BexW71U5jPHr8RGtd7270plLBjz2a5ubsMpEhInH14k2kafHoCL+bncYS3ixGidToVwpzwC8kug==", + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/tree-changes/-/tree-changes-0.7.1.tgz", + "integrity": "sha512-sPIt8PKDi0OQTglr7lsetcB9DU19Ls/ZgFSjFvK6DWJGisAn4sOxtjpmQfuqjexQE4UU9U53LNmataL1kRJ3Uw==", "requires": { "fast-deep-equal": "^3.1.3", - "is-lite": "^0.8.0", - "react": "^16.8.0 || ^17.0.0" + "is-lite": "^0.8.1" } }, "tryer": { @@ -22152,8 +22152,7 @@ }, "ssri": { "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.1.tgz", - "integrity": "sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA==", + "resolved": "", "requires": { "figgy-pudding": "^3.5.1" } diff --git a/src/App.jsx b/src/App.jsx index 3059a631..c0ebb34c 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,4 +1,4 @@ -import React, { useContext } from "react"; +import React from "react"; import { SnackbarProvider } from "notistack"; import { Box } from "@material-ui/core"; import Header from "./components/Header"; @@ -8,13 +8,11 @@ import { BuildProvider, TestRunProvider, SocketProvider, + HelpProvider, } from "./contexts"; import Router from "./Router"; -import { HelpContext } from "./contexts/help.context"; function App() { - const { getHelpSteps } = useContext(HelpContext); - const { populateHelpSteps } = useContext(HelpContext); return ( @@ -22,14 +20,14 @@ function App() { - +
- + - + diff --git a/src/components/GuidedTour.tsx b/src/components/GuidedTour.tsx index 460976d7..b4f106e7 100644 --- a/src/components/GuidedTour.tsx +++ b/src/components/GuidedTour.tsx @@ -1,54 +1,69 @@ -import React, { useContext, FunctionComponent } from "react"; +import React, { FunctionComponent } from "react"; import Joyride, { CallBackProps, STATUS } from "react-joyride"; -import { IconButton, Avatar, } from "@material-ui/core"; +import { IconButton, Avatar } from "@material-ui/core"; import { HelpOutline } from "@material-ui/icons"; -import { HelpContext } from "../contexts/help.context"; +import { useHelpState } from "../contexts"; const GuidedTour: FunctionComponent = () => { - const [run, setRun] = React.useState(false); + const [run, setRun] = React.useState(false); + const { helpSteps } = useHelpState(); - const handleJoyrideCallback = (data: CallBackProps) => { - const { status } = data; - const finishedStatuses: string[] = [STATUS.FINISHED, STATUS.SKIPPED]; + const getHelpSteps = React.useCallback(() => { + const firstStep = helpSteps[0]; + //Below line is to prevent application breaking if element is not present for any reason (e.g. if the user deletes build or if there is no data.) + if ( + firstStep && + document.getElementById(firstStep.target.toString().slice(1)) + ) { + helpSteps.every((e) => { + e.disableBeacon = true; + e.hideCloseButton = true; + }); + return helpSteps; + } + return []; + }, [helpSteps]); - if (finishedStatuses.includes(status)) { - setRun(false); - } - }; + const handleJoyrideCallback = (data: CallBackProps) => { + const { status } = data; + const finishedStatuses: string[] = [STATUS.FINISHED, STATUS.SKIPPED]; - const handleClickStart = (e: React.MouseEvent) => { - e.preventDefault(); - setRun(true); - }; + if (finishedStatuses.includes(status)) { + setRun(false); + } + }; - const { getHelpSteps } = useContext(HelpContext); + const handleClickStart = (e: React.MouseEvent) => { + e.preventDefault(); + setRun(true); + }; - return ( - - - - - - - - - ); + return ( + + + + + + + + + ); }; export default GuidedTour; diff --git a/src/components/LoginForm.tsx b/src/components/LoginForm.tsx index 81f3046c..dd50f4b0 100644 --- a/src/components/LoginForm.tsx +++ b/src/components/LoginForm.tsx @@ -1,4 +1,4 @@ -import React, { useState, FormEvent, useEffect, useContext } from "react"; +import React, { useState, FormEvent, useEffect } from "react"; import { Link } from "react-router-dom"; import { Button, @@ -8,7 +8,12 @@ import { CardActions, Typography, } from "@material-ui/core"; -import { useUserDispatch, login, HelpContext } from "../contexts"; +import { + useUserDispatch, + login, + setHelpSteps, + useHelpDispatch, +} from "../contexts"; import { routes } from "../constants"; import { useSnackbar } from "notistack"; import { TextValidator, ValidatorForm } from "react-material-ui-form-validator"; @@ -18,6 +23,7 @@ const LoginForm = () => { const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const dispatch = useUserDispatch(); + const helpDispatch = useHelpDispatch(); const handleSubmit = (event: FormEvent) => { event.preventDefault(); @@ -31,16 +37,15 @@ const LoginForm = () => { const helpSteps = [ { target: "#loginform-1", - content: "Default admin account: visual-regression-tracker@example.com / 123456. Make sure to change it's password.", + content: + "Default admin account: visual-regression-tracker@example.com / 123456. Make sure to change it's password.", }, ]; - + useEffect(() => { - populateHelpSteps(helpSteps); + setHelpSteps(helpDispatch, helpSteps); }); - const { populateHelpSteps } = useContext(HelpContext); - const errorForTwoChar = "Enter at least two characters."; const locatorLoginForm = "loginform-1"; diff --git a/src/contexts/help.context.tsx b/src/contexts/help.context.tsx index 7d14508b..741d1b54 100644 --- a/src/contexts/help.context.tsx +++ b/src/contexts/help.context.tsx @@ -1,23 +1,69 @@ -import { createContext } from 'react'; -import { Step } from 'react-joyride'; - -let helpSteps: Step[] = []; - -let getHelpSteps = (): Step[] => { - const firstStep = helpSteps[0]; - //Below line is to prevent application breaking if element is not present for any reason (e.g. if the user deletes build or if there is no data.) - if (firstStep && document.getElementById(firstStep.target.toString().slice(1))) { - helpSteps.every((e) => { - e.disableBeacon = true; - e.hideCloseButton = true; - }); - return helpSteps; - } - return []; +import React from "react"; +import { Step } from "react-joyride"; + +interface ISetStepAction { + type: "setSteps"; + payload: Array; +} + +type IAction = ISetStepAction; + +type Dispatch = (action: IAction) => void; +type State = { + helpSteps: Array; }; -const populateHelpSteps = (steps: any) => { - helpSteps = steps; +type HelpProviderProps = { children: React.ReactNode }; + +const StateContext = React.createContext(undefined); +const DispatchContext = React.createContext(undefined); + +const initialState: State = { + helpSteps: [], }; -export const HelpContext = createContext({ getHelpSteps, populateHelpSteps }); +function reducer(state: State, action: IAction): State { + switch (action.type) { + case "setSteps": + return { + ...state, + helpSteps: action.payload, + }; + default: + return state; + } +} + +function HelpProvider({ children }: HelpProviderProps) { + const [state, dispatch] = React.useReducer(reducer, initialState); + + return ( + + + {children} + + + ); +} + +function useHelpState() { + const context = React.useContext(StateContext); + if (context === undefined) { + throw new Error("must be used within a HelpContext"); + } + return context; +} + +function useHelpDispatch() { + const context = React.useContext(DispatchContext); + if (context === undefined) { + throw new Error("must be used within a HelpContext"); + } + return context; +} + +function setHelpSteps(dispatch: Dispatch, data: Array) { + dispatch({ type: "setSteps", payload: data }); +} + +export { HelpProvider, useHelpDispatch, useHelpState, setHelpSteps }; diff --git a/src/pages/ProjectListPage.tsx b/src/pages/ProjectListPage.tsx index 8e35c6ac..538ae435 100644 --- a/src/pages/ProjectListPage.tsx +++ b/src/pages/ProjectListPage.tsx @@ -1,4 +1,4 @@ -import React, { useContext, useEffect } from "react"; +import React, { useEffect } from "react"; import { Grid, Typography, @@ -17,7 +17,8 @@ import { createProject, updateProject, setProjectEditState, - HelpContext, + useHelpDispatch, + setHelpSteps, } from "../contexts"; import { Link } from "react-router-dom"; import { Delete, Add, Edit } from "@material-ui/icons"; @@ -31,6 +32,7 @@ const ProjectsListPage = () => { const { enqueueSnackbar } = useSnackbar(); const projectState = useProjectState(); const projectDispatch = useProjectDispatch(); + const helpDispatch = useHelpDispatch(); const [createDialogOpen, setCreateDialogOpen] = React.useState(false); const [updateDialogOpen, setUpdateDialogOpen] = React.useState(false); @@ -49,10 +51,8 @@ const ProjectsListPage = () => { }, ]; - const { populateHelpSteps } = useContext(HelpContext); - useEffect(() => { - populateHelpSteps(helpSteps); + setHelpSteps(helpDispatch, helpSteps); }); const toggleCreateDialogOpen = () => { diff --git a/src/pages/ProjectPage.tsx b/src/pages/ProjectPage.tsx index 96916832..0eae9055 100644 --- a/src/pages/ProjectPage.tsx +++ b/src/pages/ProjectPage.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useContext } from "react"; +import React, { useEffect } from "react"; import { Grid, Box, makeStyles } from "@material-ui/core"; import { useParams, useHistory } from "react-router-dom"; import BuildList from "../components/BuildList"; @@ -6,7 +6,7 @@ import ProjectSelect from "../components/ProjectSelect"; import TestRunList from "../components/TestRunList"; import BuildDetails from "../components/BuildDetails"; import { TestDetailsDialog } from "../components/TestDetailsDialog"; -import { HelpContext } from "../contexts/help.context"; +import { useHelpDispatch, setHelpSteps } from "../contexts"; const useStyles = makeStyles((theme) => ({ root: { @@ -18,6 +18,7 @@ const ProjectPage = () => { const classes = useStyles(); const { projectId } = useParams<{ projectId: string }>(); const history = useHistory(); + const helpDispatch = useHelpDispatch(); const helpSteps = [ { @@ -28,11 +29,11 @@ const ProjectPage = () => { }, { target: "#build-list", - title:"List of test runs", + title: "List of test runs", content: (
- If you see 'No Builds', please run your image - comparison from any client. + If you see 'No Builds', please run your image comparison from any + client.
), }, @@ -42,14 +43,16 @@ const ProjectPage = () => { }, { target: "#test-run-list", - content:
On selecting a build, shows all comparisons for the selected build.
, + content: ( +
+ On selecting a build, shows all comparisons for the selected build. +
+ ), }, ]; - const { populateHelpSteps } = useContext(HelpContext); - useEffect(() => { - populateHelpSteps(helpSteps); + setHelpSteps(helpDispatch, helpSteps); }); return ( diff --git a/src/pages/TestVariationListPage.tsx b/src/pages/TestVariationListPage.tsx index f2d92716..592564dd 100644 --- a/src/pages/TestVariationListPage.tsx +++ b/src/pages/TestVariationListPage.tsx @@ -1,4 +1,4 @@ -import React, { useContext } from "react"; +import React from "react"; import TestVariationList from "../components/TestVariationList"; import { useHistory, useParams } from "react-router-dom"; import { TestVariation } from "../types"; @@ -8,11 +8,12 @@ import ProjectSelect from "../components/ProjectSelect"; import Filters from "../components/Filters"; import { TestVariationMergeForm } from "../components/TestVariationMergeForm"; import { useSnackbar } from "notistack"; -import { HelpContext } from "../contexts/help.context"; +import { setHelpSteps, useHelpDispatch } from "../contexts/help.context"; const TestVariationListPage: React.FunctionComponent = () => { const history = useHistory(); const { enqueueSnackbar } = useSnackbar(); + const helpDispatch = useHelpDispatch(); const { projectId = "" } = useParams<{ projectId: string }>(); const [testVariations, setTestVariations] = React.useState( [] @@ -41,19 +42,23 @@ const TestVariationListPage: React.FunctionComponent = () => { target: "#select-branch", title: "Merge from one branch to another", content: ( -
Select the branch from/to which you want to merge the variations.
+
+ Select the branch from/to which you want to merge the variations. +
), }, { target: "#reset-filter", - content:
Only filtered items are displayed/merged to the target branch.
, + content: ( +
+ Only filtered items are displayed/merged to the target branch. +
+ ), }, ]; - const { populateHelpSteps } = useContext(HelpContext); - React.useEffect(() => { - populateHelpSteps(helpSteps); + setHelpSteps(helpDispatch, helpSteps); }); React.useEffect(() => { @@ -69,7 +74,7 @@ const TestVariationListPage: React.FunctionComponent = () => { }) ); } - }, [projectId, enqueueSnackbar, populateHelpSteps]); + }, [projectId, enqueueSnackbar]); React.useEffect(() => { setFilteredItems( @@ -84,7 +89,16 @@ const TestVariationListPage: React.FunctionComponent = () => { (browser ? t.browser === browser : true) // by browser ) ); - }, [query, branchName, os, device, browser, viewport, customTags, testVariations]); + }, [ + query, + branchName, + os, + device, + browser, + viewport, + customTags, + testVariations, + ]); const handleDelete = (id: string) => { testVariationService