Switch to using creation mode in XState

still problems in tests
This commit is contained in:
Presley
2022-04-27 18:18:07 +00:00
parent 30b8799bc2
commit cf8442fa4b
5 changed files with 54 additions and 40 deletions

View File

@ -1,6 +1,6 @@
import { useActor } from "@xstate/react" import { useActor } from "@xstate/react"
import React, { useContext } from "react" import React, { useContext } from "react"
import { useNavigate } from "react-router" import { Navigate } from "react-router"
import { CreateUserRequest } from "../../../api/typesGenerated" import { CreateUserRequest } from "../../../api/typesGenerated"
import { CreateUserForm } from "../../../components/CreateUserForm/CreateUserForm" import { CreateUserForm } from "../../../components/CreateUserForm/CreateUserForm"
import { XServiceContext } from "../../../xServices/StateContext" import { XServiceContext } from "../../../xServices/StateContext"
@ -13,17 +13,19 @@ export const CreateUserPage = () => {
const xServices = useContext(XServiceContext) const xServices = useContext(XServiceContext)
const [usersState, usersSend] = useActor(xServices.usersXService) const [usersState, usersSend] = useActor(xServices.usersXService)
const { createUserError, createUserFormErrors } = usersState.context const { createUserError, createUserFormErrors } = usersState.context
const navigate = useNavigate()
// There is no field for organization id in Community Edition, so handle its field error like a generic error // There is no field for organization id in Community Edition, so handle its field error like a generic error
const genericError = (createUserError || createUserFormErrors?.organization_id) ? Language.unknownError : undefined const genericError = createUserError || createUserFormErrors?.organization_id ? Language.unknownError : undefined
return ( if (usersState.matches("creationMode")){
<CreateUserForm return <CreateUserForm
formErrors={createUserFormErrors} formErrors={createUserFormErrors}
onSubmit={(user: CreateUserRequest) => usersSend({ type: "CREATE", user })} onSubmit={(user: CreateUserRequest) => usersSend({ type: "CREATE", user })}
onCancel={() => navigate("/users")} onCancel={() => {usersSend("EXIT_CREATION_MODE")}}
isLoading={usersState.hasTag("loading")} isLoading={usersState.hasTag("loading")}
error={genericError} error={genericError}
/> />
) } else {
// on cancel or success, redirect
return <Navigate to="/users"/>
}
} }

View File

@ -1,6 +1,6 @@
import { useActor } from "@xstate/react" import { useActor } from "@xstate/react"
import React, { useContext, useEffect } from "react" import React, { useContext, useEffect } from "react"
import { useNavigate } from "react-router" import { Navigate } from "react-router"
import { ErrorSummary } from "../../components/ErrorSummary/ErrorSummary" import { ErrorSummary } from "../../components/ErrorSummary/ErrorSummary"
import { XServiceContext } from "../../xServices/StateContext" import { XServiceContext } from "../../xServices/StateContext"
import { UsersPageView } from "./UsersPageView" import { UsersPageView } from "./UsersPageView"
@ -9,7 +9,6 @@ export const UsersPage: React.FC = () => {
const xServices = useContext(XServiceContext) const xServices = useContext(XServiceContext)
const [usersState, usersSend] = useActor(xServices.usersXService) const [usersState, usersSend] = useActor(xServices.usersXService)
const { users, pager, getUsersError } = usersState.context const { users, pager, getUsersError } = usersState.context
const navigate = useNavigate()
/** /**
* Fetch users on component mount * Fetch users on component mount
@ -20,13 +19,16 @@ export const UsersPage: React.FC = () => {
if (usersState.matches("error")) { if (usersState.matches("error")) {
return <ErrorSummary error={getUsersError} /> return <ErrorSummary error={getUsersError} />
} else if (usersState.matches("creationMode")) {
console.log("loop")
return <Navigate to="/users/create" />
} else { } else {
return ( return (
<UsersPageView <UsersPageView
users={users} users={users}
pager={pager} pager={pager}
openUserCreationDialog={() => { openUserCreationDialog={() => {
navigate("/users/create") usersSend("ENTER_CREATION_MODE")
}} }}
/> />
) )

View File

@ -3,6 +3,7 @@ import { render as wrappedRender, RenderResult } from "@testing-library/react"
import { createMemoryHistory } from "history" import { createMemoryHistory } from "history"
import React from "react" import React from "react"
import { MemoryRouter, Route, Routes, unstable_HistoryRouter as HistoryRouter } from "react-router-dom" import { MemoryRouter, Route, Routes, unstable_HistoryRouter as HistoryRouter } from "react-router-dom"
import { AppRouter } from "../AppRouter"
import { RequireAuth } from "../components/RequireAuth/RequireAuth" import { RequireAuth } from "../components/RequireAuth/RequireAuth"
import { dark } from "../theme" import { dark } from "../theme"
import { XServiceProvider } from "../xServices/StateContext" import { XServiceProvider } from "../xServices/StateContext"

View File

@ -1,6 +1,5 @@
import { useInterpret } from "@xstate/react" import { useInterpret } from "@xstate/react"
import React, { createContext } from "react" import React, { createContext } from "react"
import { useNavigate } from "react-router"
import { ActorRefFrom } from "xstate" import { ActorRefFrom } from "xstate"
import { authMachine } from "./auth/authXService" import { authMachine } from "./auth/authXService"
import { buildInfoMachine } from "./buildInfo/buildInfoXService" import { buildInfoMachine } from "./buildInfo/buildInfoXService"
@ -23,13 +22,12 @@ interface XServiceContextType {
export const XServiceContext = createContext({} as XServiceContextType) export const XServiceContext = createContext({} as XServiceContextType)
export const XServiceProvider: React.FC = ({ children }) => { export const XServiceProvider: React.FC = ({ children }) => {
const navigate = useNavigate()
return ( return (
<XServiceContext.Provider <XServiceContext.Provider
value={{ value={{
authXService: useInterpret(authMachine), authXService: useInterpret(authMachine),
buildInfoXService: useInterpret(buildInfoMachine), buildInfoXService: useInterpret(buildInfoMachine),
usersXService: useInterpret(usersMachine.withContext({ users: [], navigate })), usersXService: useInterpret(usersMachine)
}} }}
> >
{children} {children}

View File

@ -1,4 +1,3 @@
import { NavigateFunction } from "react-router"
import { assign, createMachine } from "xstate" import { assign, createMachine } from "xstate"
import * as API from "../../api" import * as API from "../../api"
import { ApiError, FieldErrors, isApiError, mapApiErrorToFieldErrors } from "../../api/errors" import { ApiError, FieldErrors, isApiError, mapApiErrorToFieldErrors } from "../../api/errors"
@ -16,10 +15,13 @@ export interface UsersContext {
getUsersError?: Error | unknown getUsersError?: Error | unknown
createUserError?: Error | unknown createUserError?: Error | unknown
createUserFormErrors?: FieldErrors createUserFormErrors?: FieldErrors
navigate?: NavigateFunction
} }
export type UsersEvent = { type: "GET_USERS" } | { type: "CREATE"; user: TypesGen.CreateUserRequest } export type UsersEvent =
| { type: "GET_USERS" }
| { type: "ENTER_CREATION_MODE" }
| { type: "EXIT_CREATION_MODE" }
| { type: "CREATE"; user: TypesGen.CreateUserRequest }
export const usersMachine = createMachine( export const usersMachine = createMachine(
{ {
@ -45,7 +47,7 @@ export const usersMachine = createMachine(
idle: { idle: {
on: { on: {
GET_USERS: "gettingUsers", GET_USERS: "gettingUsers",
CREATE: "creatingUser", ENTER_CREATION_MODE: "creationMode",
}, },
}, },
gettingUsers: { gettingUsers: {
@ -67,13 +69,22 @@ export const usersMachine = createMachine(
}, },
tags: "loading", tags: "loading",
}, },
creationMode: {
initial: "idle",
states: {
idle: {
on: {
CREATE: "creatingUser",
EXIT_CREATION_MODE: "#usersState.idle"
},
},
creatingUser: { creatingUser: {
invoke: { invoke: {
src: "createUser", src: "createUser",
id: "createUser", id: "createUser",
onDone: { onDone: {
target: "idle", target: "#usersState.idle",
actions: ["displayCreateUserSuccess", "redirectToUsersPage", "clearCreateUserError"], actions: ["displayCreateUserSuccess", "clearCreateUserError"],
}, },
onError: [ onError: [
{ {
@ -89,6 +100,9 @@ export const usersMachine = createMachine(
}, },
tags: "loading", tags: "loading",
}, },
},
},
error: { error: {
on: { on: {
GET_USERS: "gettingUsers", GET_USERS: "gettingUsers",
@ -102,7 +116,7 @@ export const usersMachine = createMachine(
createUser: (_, event) => API.createUser(event.user), createUser: (_, event) => API.createUser(event.user),
}, },
guards: { guards: {
isFormError: (_, event) => isApiError(event.data) isFormError: (_, event) => isApiError(event.data),
}, },
actions: { actions: {
assignUsers: assign({ assignUsers: assign({
@ -121,7 +135,7 @@ export const usersMachine = createMachine(
}), }),
assignCreateUserFormErrors: assign({ assignCreateUserFormErrors: assign({
// the guard ensures it is ApiError // the guard ensures it is ApiError
createUserFormErrors: (_, event) => mapApiErrorToFieldErrors((event.data as ApiError).response.data) createUserFormErrors: (_, event) => mapApiErrorToFieldErrors((event.data as ApiError).response.data),
}), }),
clearCreateUserError: assign((context: UsersContext) => ({ clearCreateUserError: assign((context: UsersContext) => ({
...context, ...context,
@ -130,9 +144,6 @@ export const usersMachine = createMachine(
displayCreateUserSuccess: () => { displayCreateUserSuccess: () => {
displaySuccess(Language.createUserSuccess) displaySuccess(Language.createUserSuccess)
}, },
redirectToUsersPage: (context) => {
context.navigate && context.navigate("/users")
},
}, },
}, },
) )