mirror of https://github.com/coder/coder.git
fix(site): Fix template icon field validation (#7394)
This commit is contained in:
parent
614bdfbf3c
commit
8909110f58
|
@ -1,3 +1,4 @@
|
|||
import { mockApiError } from "testHelpers/entities"
|
||||
import {
|
||||
getValidationErrorMessage,
|
||||
isApiError,
|
||||
|
@ -7,17 +8,14 @@ import {
|
|||
describe("isApiError", () => {
|
||||
it("returns true when the object is an API Error", () => {
|
||||
expect(
|
||||
isApiError({
|
||||
isAxiosError: true,
|
||||
response: {
|
||||
data: {
|
||||
message: "Invalid entry",
|
||||
errors: [
|
||||
{ detail: "Username is already in use", field: "username" },
|
||||
],
|
||||
},
|
||||
},
|
||||
}),
|
||||
isApiError(
|
||||
mockApiError({
|
||||
message: "Invalid entry",
|
||||
validations: [
|
||||
{ detail: "Username is already in use", field: "username" },
|
||||
],
|
||||
}),
|
||||
),
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
|
@ -48,24 +46,21 @@ describe("mapApiErrorToFieldErrors", () => {
|
|||
describe("getValidationErrorMessage", () => {
|
||||
it("returns multiple validation messages", () => {
|
||||
expect(
|
||||
getValidationErrorMessage({
|
||||
response: {
|
||||
data: {
|
||||
message: "Invalid user search query.",
|
||||
validations: [
|
||||
{
|
||||
field: "status",
|
||||
detail: `Query param "status" has invalid value: "inactive" is not a valid user status`,
|
||||
},
|
||||
{
|
||||
field: "q",
|
||||
detail: `Query element "role:a:e" can only contain 1 ':'`,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
isAxiosError: true,
|
||||
}),
|
||||
getValidationErrorMessage(
|
||||
mockApiError({
|
||||
message: "Invalid user search query.",
|
||||
validations: [
|
||||
{
|
||||
field: "status",
|
||||
detail: `Query param "status" has invalid value: "inactive" is not a valid user status`,
|
||||
},
|
||||
{
|
||||
field: "q",
|
||||
detail: `Query element "role:a:e" can only contain 1 ':'`,
|
||||
},
|
||||
],
|
||||
}),
|
||||
),
|
||||
).toEqual(
|
||||
`Query param "status" has invalid value: "inactive" is not a valid user status\nQuery element "role:a:e" can only contain 1 ':'`,
|
||||
)
|
||||
|
@ -73,28 +68,18 @@ describe("getValidationErrorMessage", () => {
|
|||
|
||||
it("non-API error returns empty validation message", () => {
|
||||
expect(
|
||||
getValidationErrorMessage({
|
||||
response: {
|
||||
data: {
|
||||
error: "Invalid user search query.",
|
||||
},
|
||||
},
|
||||
isAxiosError: true,
|
||||
}),
|
||||
getValidationErrorMessage(new Error("Invalid user search query.")),
|
||||
).toEqual("")
|
||||
})
|
||||
|
||||
it("no validations field returns empty validation message", () => {
|
||||
expect(
|
||||
getValidationErrorMessage({
|
||||
response: {
|
||||
data: {
|
||||
message: "Invalid user search query.",
|
||||
detail: `Query element "role:a:e" can only contain 1 ':'`,
|
||||
},
|
||||
},
|
||||
isAxiosError: true,
|
||||
}),
|
||||
getValidationErrorMessage(
|
||||
mockApiError({
|
||||
message: "Invalid user search query.",
|
||||
detail: `Query element "role:a:e" can only contain 1 ':'`,
|
||||
}),
|
||||
),
|
||||
).toEqual("")
|
||||
})
|
||||
})
|
||||
|
|
|
@ -24,30 +24,16 @@ export type ApiError = AxiosError<ApiErrorResponse> & {
|
|||
}
|
||||
|
||||
export const isApiError = (err: unknown): err is ApiError => {
|
||||
if (axios.isAxiosError(err)) {
|
||||
const response = err.response?.data
|
||||
if (!response) {
|
||||
return false
|
||||
}
|
||||
|
||||
return (
|
||||
typeof response.message === "string" &&
|
||||
(typeof response.errors === "undefined" || Array.isArray(response.errors))
|
||||
)
|
||||
}
|
||||
|
||||
return false
|
||||
return axios.isAxiosError(err) && err.response !== undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* ApiErrors contain useful error messages in their response body. They contain an overall message
|
||||
* and may also contain errors for specific form fields.
|
||||
* @param error ApiError
|
||||
* @returns true if the ApiError contains error messages for specific form fields.
|
||||
*/
|
||||
export const hasApiFieldErrors = (error: ApiError): boolean =>
|
||||
Array.isArray(error.response.data.validations)
|
||||
|
||||
export const isApiValidationError = (error: unknown): error is ApiError => {
|
||||
return isApiError(error) && hasApiFieldErrors(error)
|
||||
}
|
||||
|
||||
export const mapApiErrorToFieldErrors = (
|
||||
apiErrorResponse: ApiErrorResponse,
|
||||
): FieldErrors => {
|
||||
|
@ -63,10 +49,6 @@ export const mapApiErrorToFieldErrors = (
|
|||
return result
|
||||
}
|
||||
|
||||
export const isApiValidationError = (error: unknown): error is ApiError => {
|
||||
return isApiError(error) && hasApiFieldErrors(error)
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param error
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { Story } from "@storybook/react"
|
||||
import { AlertBanner } from "./AlertBanner"
|
||||
import Button from "@material-ui/core/Button"
|
||||
import { makeMockApiError } from "testHelpers/entities"
|
||||
import { mockApiError } from "testHelpers/entities"
|
||||
import { AlertBannerProps } from "./alertTypes"
|
||||
import Link from "@material-ui/core/Link"
|
||||
|
||||
|
@ -16,7 +16,7 @@ const ExampleAction = (
|
|||
</Button>
|
||||
)
|
||||
|
||||
const mockError = makeMockApiError({
|
||||
const mockError = mockApiError({
|
||||
message: "Email or password was invalid",
|
||||
detail: "Password is invalid",
|
||||
})
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { action } from "@storybook/addon-actions"
|
||||
import { Story } from "@storybook/react"
|
||||
import { CreateUserForm, CreateUserFormProps } from "./CreateUserForm"
|
||||
import { mockApiError } from "testHelpers/entities"
|
||||
|
||||
export default {
|
||||
title: "components/CreateUserForm",
|
||||
|
@ -18,22 +19,14 @@ Ready.args = {
|
|||
isLoading: false,
|
||||
}
|
||||
|
||||
export const UnknownError = Template.bind({})
|
||||
UnknownError.args = {
|
||||
onCancel: action("cancel"),
|
||||
onSubmit: action("submit"),
|
||||
isLoading: false,
|
||||
error: "Something went wrong",
|
||||
}
|
||||
|
||||
export const FormError = Template.bind({})
|
||||
FormError.args = {
|
||||
onCancel: action("cancel"),
|
||||
onSubmit: action("submit"),
|
||||
isLoading: false,
|
||||
formErrors: {
|
||||
username: "Username taken",
|
||||
},
|
||||
error: mockApiError({
|
||||
validations: [{ field: "username", detail: "Username taken" }],
|
||||
}),
|
||||
}
|
||||
|
||||
export const Loading = Template.bind({})
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import FormHelperText from "@material-ui/core/FormHelperText"
|
||||
import TextField from "@material-ui/core/TextField"
|
||||
import { FormikContextType, FormikErrors, useFormik } from "formik"
|
||||
import { FormikContextType, useFormik } from "formik"
|
||||
import { FC } from "react"
|
||||
import * as Yup from "yup"
|
||||
import * as TypesGen from "../../api/typesGenerated"
|
||||
|
@ -27,9 +26,8 @@ export const Language = {
|
|||
export interface CreateUserFormProps {
|
||||
onSubmit: (user: TypesGen.CreateUserRequest) => void
|
||||
onCancel: () => void
|
||||
formErrors?: FormikErrors<TypesGen.CreateUserRequest>
|
||||
error?: unknown
|
||||
isLoading: boolean
|
||||
error?: string
|
||||
myOrgId: string
|
||||
}
|
||||
|
||||
|
@ -44,7 +42,7 @@ const validationSchema = Yup.object({
|
|||
|
||||
export const CreateUserForm: FC<
|
||||
React.PropsWithChildren<CreateUserFormProps>
|
||||
> = ({ onSubmit, onCancel, formErrors, isLoading, error, myOrgId }) => {
|
||||
> = ({ onSubmit, onCancel, error, isLoading, myOrgId }) => {
|
||||
const form: FormikContextType<TypesGen.CreateUserRequest> =
|
||||
useFormik<TypesGen.CreateUserRequest>({
|
||||
initialValues: {
|
||||
|
@ -58,7 +56,7 @@ export const CreateUserForm: FC<
|
|||
})
|
||||
const getFieldHelpers = getFormHelpers<TypesGen.CreateUserRequest>(
|
||||
form,
|
||||
formErrors,
|
||||
error,
|
||||
)
|
||||
|
||||
return (
|
||||
|
@ -92,7 +90,6 @@ export const CreateUserForm: FC<
|
|||
variant="outlined"
|
||||
/>
|
||||
</Stack>
|
||||
{error && <FormHelperText error>{error}</FormHelperText>}
|
||||
<FormFooter onCancel={onCancel} isLoading={isLoading} />
|
||||
</form>
|
||||
</FullPageForm>
|
||||
|
|
|
@ -4,6 +4,7 @@ import {
|
|||
SearchBarWithFilter,
|
||||
SearchBarWithFilterProps,
|
||||
} from "./SearchBarWithFilter"
|
||||
import { mockApiError } from "testHelpers/entities"
|
||||
|
||||
export default {
|
||||
title: "components/SearchBarWithFilter",
|
||||
|
@ -34,18 +35,13 @@ WithError.args = {
|
|||
{ query: userFilterQuery.active, name: "Active users" },
|
||||
{ query: "random query", name: "Random query" },
|
||||
],
|
||||
error: {
|
||||
response: {
|
||||
data: {
|
||||
message: "Invalid user search query.",
|
||||
validations: [
|
||||
{
|
||||
field: "status",
|
||||
detail: `Query param "status" has invalid value: "inactive" is not a valid user status`,
|
||||
},
|
||||
],
|
||||
error: mockApiError({
|
||||
message: "Invalid user search query.",
|
||||
validations: [
|
||||
{
|
||||
field: "status",
|
||||
detail: `Query param "status" has invalid value: "inactive" is not a valid user status`,
|
||||
},
|
||||
},
|
||||
isAxiosError: true,
|
||||
},
|
||||
],
|
||||
}),
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { Story } from "@storybook/react"
|
||||
import { AccountForm, AccountFormProps } from "./SettingsAccountForm"
|
||||
import { mockApiError } from "testHelpers/entities"
|
||||
|
||||
export default {
|
||||
title: "components/SettingsAccountForm",
|
||||
|
@ -35,20 +36,15 @@ Loading.args = {
|
|||
export const WithError = Template.bind({})
|
||||
WithError.args = {
|
||||
...Example.args,
|
||||
updateProfileError: {
|
||||
response: {
|
||||
data: {
|
||||
message: "Username is invalid",
|
||||
validations: [
|
||||
{
|
||||
field: "username",
|
||||
detail: "Username is too long.",
|
||||
},
|
||||
],
|
||||
updateProfileError: mockApiError({
|
||||
message: "Username is invalid",
|
||||
validations: [
|
||||
{
|
||||
field: "username",
|
||||
detail: "Username is too long.",
|
||||
},
|
||||
},
|
||||
isAxiosError: true,
|
||||
},
|
||||
],
|
||||
}),
|
||||
initialTouched: {
|
||||
username: true,
|
||||
},
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { Story } from "@storybook/react"
|
||||
import { SecurityForm, SecurityFormProps } from "./SettingsSecurityForm"
|
||||
import { mockApiError } from "testHelpers/entities"
|
||||
|
||||
export default {
|
||||
title: "components/SettingsSecurityForm",
|
||||
|
@ -36,20 +37,15 @@ Loading.args = {
|
|||
export const WithError = Template.bind({})
|
||||
WithError.args = {
|
||||
...Example.args,
|
||||
updateSecurityError: {
|
||||
response: {
|
||||
data: {
|
||||
message: "Old password is incorrect",
|
||||
validations: [
|
||||
{
|
||||
field: "old_password",
|
||||
detail: "Old password is incorrect.",
|
||||
},
|
||||
],
|
||||
updateSecurityError: mockApiError({
|
||||
message: "Old password is incorrect",
|
||||
validations: [
|
||||
{
|
||||
field: "old_password",
|
||||
detail: "Old password is incorrect.",
|
||||
},
|
||||
},
|
||||
isAxiosError: true,
|
||||
},
|
||||
],
|
||||
}),
|
||||
initialTouched: {
|
||||
old_password: true,
|
||||
},
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { Story } from "@storybook/react"
|
||||
import { makeMockApiError } from "testHelpers/entities"
|
||||
import { mockApiError } from "testHelpers/entities"
|
||||
import { SignInForm, SignInFormProps } from "./SignInForm"
|
||||
|
||||
export default {
|
||||
|
@ -37,7 +37,7 @@ SigningIn.args = {
|
|||
export const WithError = Template.bind({})
|
||||
WithError.args = {
|
||||
...SignedOut.args,
|
||||
error: makeMockApiError({
|
||||
error: mockApiError({
|
||||
message: "Email or password was invalid",
|
||||
validations: [
|
||||
{
|
||||
|
|
|
@ -95,7 +95,7 @@ Failed.args = {
|
|||
...Running.args,
|
||||
workspace: Mocks.MockFailedWorkspace,
|
||||
workspaceErrors: {
|
||||
[WorkspaceErrors.BUILD_ERROR]: Mocks.makeMockApiError({
|
||||
[WorkspaceErrors.BUILD_ERROR]: Mocks.mockApiError({
|
||||
message: "A workspace build is already active.",
|
||||
}),
|
||||
},
|
||||
|
@ -152,7 +152,7 @@ export const GetBuildsError = Template.bind({})
|
|||
GetBuildsError.args = {
|
||||
...Running.args,
|
||||
workspaceErrors: {
|
||||
[WorkspaceErrors.GET_BUILDS_ERROR]: Mocks.makeMockApiError({
|
||||
[WorkspaceErrors.GET_BUILDS_ERROR]: Mocks.mockApiError({
|
||||
message: "There is a problem fetching builds.",
|
||||
}),
|
||||
},
|
||||
|
@ -162,7 +162,7 @@ export const CancellationError = Template.bind({})
|
|||
CancellationError.args = {
|
||||
...Failed.args,
|
||||
workspaceErrors: {
|
||||
[WorkspaceErrors.CANCELLATION_ERROR]: Mocks.makeMockApiError({
|
||||
[WorkspaceErrors.CANCELLATION_ERROR]: Mocks.mockApiError({
|
||||
message: "Job could not be canceled.",
|
||||
}),
|
||||
},
|
||||
|
|
|
@ -8,7 +8,7 @@ import {
|
|||
emptySchedule,
|
||||
} from "pages/WorkspaceSettingsPage/WorkspaceSchedulePage/schedule"
|
||||
import { emptyTTL } from "pages/WorkspaceSettingsPage/WorkspaceSchedulePage/ttl"
|
||||
import { makeMockApiError } from "testHelpers/entities"
|
||||
import { mockApiError } from "testHelpers/entities"
|
||||
import {
|
||||
WorkspaceScheduleForm,
|
||||
WorkspaceScheduleFormProps,
|
||||
|
@ -81,7 +81,7 @@ export const WithError = Template.bind({})
|
|||
WithError.args = {
|
||||
initialValues: { ...defaultInitialValues, ttl: 100 },
|
||||
initialTouched: { ttl: true },
|
||||
submitScheduleError: makeMockApiError({
|
||||
submitScheduleError: mockApiError({
|
||||
message: "Something went wrong.",
|
||||
validations: [{ field: "ttl_ms", detail: "Invalid time until shutdown." }],
|
||||
}),
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { ComponentMeta, Story } from "@storybook/react"
|
||||
import {
|
||||
makeMockApiError,
|
||||
mockApiError,
|
||||
mockParameterSchema,
|
||||
MockParameterSchemas,
|
||||
MockTemplate,
|
||||
|
@ -85,7 +85,7 @@ export const GetTemplatesError = Template.bind({})
|
|||
GetTemplatesError.args = {
|
||||
...Parameters.args,
|
||||
createWorkspaceErrors: {
|
||||
[CreateWorkspaceErrors.GET_TEMPLATES_ERROR]: makeMockApiError({
|
||||
[CreateWorkspaceErrors.GET_TEMPLATES_ERROR]: mockApiError({
|
||||
message: "Failed to fetch templates.",
|
||||
detail: "You do not have permission to access this resource.",
|
||||
}),
|
||||
|
@ -97,7 +97,7 @@ export const GetTemplateSchemaError = Template.bind({})
|
|||
GetTemplateSchemaError.args = {
|
||||
...Parameters.args,
|
||||
createWorkspaceErrors: {
|
||||
[CreateWorkspaceErrors.GET_TEMPLATE_SCHEMA_ERROR]: makeMockApiError({
|
||||
[CreateWorkspaceErrors.GET_TEMPLATE_SCHEMA_ERROR]: mockApiError({
|
||||
message: 'Failed to fetch template schema for "docker-amd64".',
|
||||
detail: "You do not have permission to access this resource.",
|
||||
}),
|
||||
|
@ -109,7 +109,7 @@ export const CreateWorkspaceError = Template.bind({})
|
|||
CreateWorkspaceError.args = {
|
||||
...Parameters.args,
|
||||
createWorkspaceErrors: {
|
||||
[CreateWorkspaceErrors.CREATE_WORKSPACE_ERROR]: makeMockApiError({
|
||||
[CreateWorkspaceErrors.CREATE_WORKSPACE_ERROR]: mockApiError({
|
||||
message:
|
||||
'Workspace "test" already exists in the "docker-amd64" template.',
|
||||
validations: [
|
||||
|
|
|
@ -1,8 +1,5 @@
|
|||
import { ComponentMeta, Story } from "@storybook/react"
|
||||
import {
|
||||
makeMockApiError,
|
||||
MockDeploymentDAUResponse,
|
||||
} from "testHelpers/entities"
|
||||
import { mockApiError, MockDeploymentDAUResponse } from "testHelpers/entities"
|
||||
import {
|
||||
GeneralSettingsPageView,
|
||||
GeneralSettingsPageViewProps,
|
||||
|
@ -43,5 +40,7 @@ NoDAUs.args = {
|
|||
export const DAUError = Template.bind({})
|
||||
DAUError.args = {
|
||||
deploymentDAUs: undefined,
|
||||
getDeploymentDAUsError: makeMockApiError({ message: "Error fetching DAUs." }),
|
||||
getDeploymentDAUsError: mockApiError({
|
||||
message: "Error fetching DAUs.",
|
||||
}),
|
||||
}
|
||||
|
|
|
@ -20,7 +20,7 @@ export const CreateGroupPage: FC = () => {
|
|||
},
|
||||
},
|
||||
})
|
||||
const { createGroupFormErrors } = createState.context
|
||||
const { error } = createState.context
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -34,7 +34,7 @@ export const CreateGroupPage: FC = () => {
|
|||
data,
|
||||
})
|
||||
}}
|
||||
formErrors={createGroupFormErrors}
|
||||
formErrors={error}
|
||||
isLoading={createState.matches("creatingGroup")}
|
||||
/>
|
||||
</>
|
||||
|
|
|
@ -26,7 +26,7 @@ export const SettingsGroupPage: FC = () => {
|
|||
onUpdate: navigateToGroup,
|
||||
},
|
||||
})
|
||||
const { updateGroupFormErrors, group } = editState.context
|
||||
const { error, group } = editState.context
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -40,7 +40,7 @@ export const SettingsGroupPage: FC = () => {
|
|||
sendEditEvent({ type: "UPDATE", data })
|
||||
}}
|
||||
group={group}
|
||||
formErrors={updateGroupFormErrors}
|
||||
formErrors={error}
|
||||
isLoading={editState.matches("loading")}
|
||||
isUpdating={editState.matches("updating")}
|
||||
/>
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
import { fireEvent, screen, waitFor } from "@testing-library/react"
|
||||
import userEvent from "@testing-library/user-event"
|
||||
import * as API from "api/api"
|
||||
import { rest } from "msw"
|
||||
import { history, MockUser, render } from "testHelpers/renderHelpers"
|
||||
import { history, render } from "testHelpers/renderHelpers"
|
||||
import { server } from "testHelpers/server"
|
||||
import { Language as SetupLanguage } from "xServices/setup/setupXService"
|
||||
import { SetupPage } from "./SetupPage"
|
||||
import { Language as PageViewLanguage } from "./SetupPageView"
|
||||
import { MockUser } from "testHelpers/entities"
|
||||
|
||||
const fillForm = async ({
|
||||
username = "someuser",
|
||||
|
@ -47,18 +46,6 @@ describe("Setup Page", () => {
|
|||
expect(errorMessage).toBeDefined()
|
||||
})
|
||||
|
||||
it("shows generic error message", async () => {
|
||||
jest.spyOn(API, "createFirstUser").mockRejectedValueOnce({
|
||||
data: "unknown error",
|
||||
})
|
||||
render(<SetupPage />)
|
||||
await fillForm()
|
||||
const errorMessage = await screen.findByText(
|
||||
SetupLanguage.createFirstUserError,
|
||||
)
|
||||
expect(errorMessage).toBeDefined()
|
||||
})
|
||||
|
||||
it("shows API error message", async () => {
|
||||
const fieldErrorMessage = "invalid username"
|
||||
server.use(
|
||||
|
|
|
@ -22,8 +22,7 @@ export const SetupPage: FC = () => {
|
|||
},
|
||||
},
|
||||
})
|
||||
const { createFirstUserFormErrors, createFirstUserErrorMessage } =
|
||||
setupState.context
|
||||
const { error } = setupState.context
|
||||
|
||||
useEffect(() => {
|
||||
if (authState.matches("signedIn")) {
|
||||
|
@ -38,8 +37,7 @@ export const SetupPage: FC = () => {
|
|||
</Helmet>
|
||||
<SetupPageView
|
||||
isLoading={setupState.hasTag("loading")}
|
||||
formErrors={createFirstUserFormErrors}
|
||||
genericError={createFirstUserErrorMessage}
|
||||
error={error}
|
||||
onSubmit={(firstUser) => {
|
||||
setupSend({ type: "CREATE_FIRST_USER", firstUser })
|
||||
}}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { action } from "@storybook/addon-actions"
|
||||
import { Story } from "@storybook/react"
|
||||
import { SetupPageView, SetupPageViewProps } from "./SetupPageView"
|
||||
import { mockApiError } from "testHelpers/entities"
|
||||
|
||||
export default {
|
||||
title: "pages/SetupPageView",
|
||||
|
@ -14,27 +15,18 @@ const Template: Story<SetupPageViewProps> = (args: SetupPageViewProps) => (
|
|||
export const Ready = Template.bind({})
|
||||
Ready.args = {
|
||||
onSubmit: action("submit"),
|
||||
isCreating: false,
|
||||
}
|
||||
|
||||
export const UnknownError = Template.bind({})
|
||||
UnknownError.args = {
|
||||
onSubmit: action("submit"),
|
||||
isCreating: false,
|
||||
genericError: "Something went wrong",
|
||||
}
|
||||
|
||||
export const FormError = Template.bind({})
|
||||
FormError.args = {
|
||||
onSubmit: action("submit"),
|
||||
isCreating: false,
|
||||
formErrors: {
|
||||
username: "Username taken",
|
||||
},
|
||||
error: mockApiError({
|
||||
validations: [{ field: "username", detail: "Username taken" }],
|
||||
}),
|
||||
}
|
||||
|
||||
export const Loading = Template.bind({})
|
||||
Loading.args = {
|
||||
onSubmit: action("submit"),
|
||||
isCreating: true,
|
||||
isLoading: true,
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import Box from "@material-ui/core/Box"
|
||||
import Checkbox from "@material-ui/core/Checkbox"
|
||||
import FormHelperText from "@material-ui/core/FormHelperText"
|
||||
import { makeStyles } from "@material-ui/core/styles"
|
||||
import TextField from "@material-ui/core/TextField"
|
||||
import Typography from "@material-ui/core/Typography"
|
||||
|
@ -8,7 +7,7 @@ import { LoadingButton } from "components/LoadingButton/LoadingButton"
|
|||
import { SignInLayout } from "components/SignInLayout/SignInLayout"
|
||||
import { Stack } from "components/Stack/Stack"
|
||||
import { Welcome } from "components/Welcome/Welcome"
|
||||
import { FormikContextType, FormikErrors, useFormik } from "formik"
|
||||
import { FormikContextType, useFormik } from "formik"
|
||||
import { getFormHelpers, nameValidator, onChangeTrimmed } from "utils/formUtils"
|
||||
import * as Yup from "yup"
|
||||
import * as TypesGen from "../../api/typesGenerated"
|
||||
|
@ -35,15 +34,13 @@ const validationSchema = Yup.object({
|
|||
|
||||
export interface SetupPageViewProps {
|
||||
onSubmit: (firstUser: TypesGen.CreateFirstUserRequest) => void
|
||||
formErrors?: FormikErrors<TypesGen.CreateFirstUserRequest>
|
||||
genericError?: string
|
||||
error?: unknown
|
||||
isLoading?: boolean
|
||||
}
|
||||
|
||||
export const SetupPageView: React.FC<SetupPageViewProps> = ({
|
||||
onSubmit,
|
||||
formErrors,
|
||||
genericError,
|
||||
error,
|
||||
isLoading,
|
||||
}) => {
|
||||
const form: FormikContextType<TypesGen.CreateFirstUserRequest> =
|
||||
|
@ -59,7 +56,7 @@ export const SetupPageView: React.FC<SetupPageViewProps> = ({
|
|||
})
|
||||
const getFieldHelpers = getFormHelpers<TypesGen.CreateFirstUserRequest>(
|
||||
form,
|
||||
formErrors,
|
||||
error,
|
||||
)
|
||||
const styles = useStyles()
|
||||
|
||||
|
@ -93,9 +90,6 @@ export const SetupPageView: React.FC<SetupPageViewProps> = ({
|
|||
type="password"
|
||||
variant="outlined"
|
||||
/>
|
||||
{genericError && (
|
||||
<FormHelperText error>{genericError}</FormHelperText>
|
||||
)}
|
||||
<div className={styles.callout}>
|
||||
<Box display="flex">
|
||||
<div>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Story } from "@storybook/react"
|
||||
import {
|
||||
makeMockApiError,
|
||||
mockApiError,
|
||||
MockOrganization,
|
||||
MockTemplateExample,
|
||||
} from "testHelpers/entities"
|
||||
|
@ -33,7 +33,7 @@ Error.args = {
|
|||
context: {
|
||||
exampleId: MockTemplateExample.id,
|
||||
organizationId: MockOrganization.id,
|
||||
error: makeMockApiError({
|
||||
error: mockApiError({
|
||||
message: `Example ${MockTemplateExample.id} not found.`,
|
||||
}),
|
||||
starterTemplate: undefined,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Story } from "@storybook/react"
|
||||
import {
|
||||
makeMockApiError,
|
||||
mockApiError,
|
||||
MockOrganization,
|
||||
MockTemplateExample,
|
||||
MockTemplateExample2,
|
||||
|
@ -36,7 +36,7 @@ export const Error = Template.bind({})
|
|||
Error.args = {
|
||||
context: {
|
||||
organizationId: MockOrganization.id,
|
||||
error: makeMockApiError({
|
||||
error: mockApiError({
|
||||
message: "Error on loading the template examples",
|
||||
}),
|
||||
starterTemplatesByTag: undefined,
|
||||
|
|
|
@ -7,6 +7,7 @@ import {
|
|||
nameValidator,
|
||||
templateDisplayNameValidator,
|
||||
onChangeTrimmed,
|
||||
iconValidator,
|
||||
} from "utils/formUtils"
|
||||
import * as Yup from "yup"
|
||||
import i18next from "i18next"
|
||||
|
@ -37,8 +38,8 @@ export const getValidationSchema = (): Yup.AnyObjectSchema =>
|
|||
MAX_DESCRIPTION_CHAR_LIMIT,
|
||||
i18next.t("descriptionMaxError", { ns: "templateSettingsPage" }),
|
||||
),
|
||||
|
||||
allow_user_cancel_workspace_jobs: Yup.boolean(),
|
||||
icon: iconValidator,
|
||||
})
|
||||
|
||||
export interface TemplateSettingsForm {
|
||||
|
@ -74,7 +75,7 @@ export const TemplateSettingsForm: FC<TemplateSettingsForm> = ({
|
|||
onSubmit,
|
||||
initialTouched,
|
||||
})
|
||||
const getFieldHelpers = getFormHelpers<UpdateTemplateMeta>(form, error)
|
||||
const getFieldHelpers = getFormHelpers(form, error)
|
||||
const { t } = useTranslation("templateSettingsPage")
|
||||
const styles = useStyles()
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { action } from "@storybook/addon-actions"
|
||||
import { Story } from "@storybook/react"
|
||||
import { makeMockApiError, MockTemplate } from "testHelpers/entities"
|
||||
import { mockApiError, MockTemplate } from "testHelpers/entities"
|
||||
import {
|
||||
TemplateSettingsPageView,
|
||||
TemplateSettingsPageViewProps,
|
||||
|
@ -25,7 +25,7 @@ Example.args = {}
|
|||
|
||||
export const SaveTemplateSettingsError = Template.bind({})
|
||||
SaveTemplateSettingsError.args = {
|
||||
submitError: makeMockApiError({
|
||||
submitError: mockApiError({
|
||||
message: 'Template "test" already exists.',
|
||||
validations: [
|
||||
{
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { action } from "@storybook/addon-actions"
|
||||
import { Story } from "@storybook/react"
|
||||
import {
|
||||
makeMockApiError,
|
||||
mockApiError,
|
||||
MockTemplateVersion,
|
||||
MockTemplateVersionVariable1,
|
||||
MockTemplateVersionVariable2,
|
||||
|
@ -69,7 +69,7 @@ WithUpdateTemplateError.args = {
|
|||
MockTemplateVersionVariable4,
|
||||
],
|
||||
errors: {
|
||||
updateTemplateError: makeMockApiError({
|
||||
updateTemplateError: mockApiError({
|
||||
message: "Something went wrong.",
|
||||
}),
|
||||
},
|
||||
|
|
|
@ -2,7 +2,7 @@ import { action } from "@storybook/addon-actions"
|
|||
import { Story } from "@storybook/react"
|
||||
import { UseTabResult } from "hooks/useTab"
|
||||
import {
|
||||
makeMockApiError,
|
||||
mockApiError,
|
||||
MockOrganization,
|
||||
MockTemplate,
|
||||
MockTemplateVersion,
|
||||
|
@ -64,7 +64,7 @@ Error.args = {
|
|||
...defaultArgs.context,
|
||||
currentVersion: undefined,
|
||||
currentFiles: undefined,
|
||||
error: makeMockApiError({
|
||||
error: mockApiError({
|
||||
message: "Error on loading the template version",
|
||||
}),
|
||||
},
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { ComponentMeta, Story } from "@storybook/react"
|
||||
import {
|
||||
makeMockApiError,
|
||||
mockApiError,
|
||||
MockOrganization,
|
||||
MockPermissions,
|
||||
MockTemplate,
|
||||
|
@ -89,7 +89,7 @@ Error.args = {
|
|||
...MockPermissions,
|
||||
createTemplates: false,
|
||||
},
|
||||
error: makeMockApiError({
|
||||
error: mockApiError({
|
||||
message: "Something went wrong fetching templates.",
|
||||
}),
|
||||
templates: undefined,
|
||||
|
|
|
@ -5,6 +5,7 @@ import { renderWithAuth } from "../../../testHelpers/renderHelpers"
|
|||
import * as AuthXService from "../../../xServices/auth/authXService"
|
||||
import { AccountPage } from "./AccountPage"
|
||||
import i18next from "i18next"
|
||||
import { mockApiError } from "testHelpers/entities"
|
||||
|
||||
const { t } = i18next
|
||||
|
||||
|
@ -54,17 +55,14 @@ describe("AccountPage", () => {
|
|||
|
||||
describe("when the username is already taken", () => {
|
||||
it("shows an error", async () => {
|
||||
jest.spyOn(API, "updateProfile").mockRejectedValueOnce({
|
||||
isAxiosError: true,
|
||||
response: {
|
||||
data: {
|
||||
message: "Invalid profile",
|
||||
validations: [
|
||||
{ detail: "Username is already in use", field: "username" },
|
||||
],
|
||||
},
|
||||
},
|
||||
})
|
||||
jest.spyOn(API, "updateProfile").mockRejectedValueOnce(
|
||||
mockApiError({
|
||||
message: "Invalid profile",
|
||||
validations: [
|
||||
{ detail: "Username is already in use", field: "username" },
|
||||
],
|
||||
}),
|
||||
)
|
||||
|
||||
const { user } = renderPage()
|
||||
await fillAndSubmitForm()
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { Story } from "@storybook/react"
|
||||
import { makeMockApiError } from "testHelpers/entities"
|
||||
import { mockApiError } from "testHelpers/entities"
|
||||
import { SSHKeysPageView, SSHKeysPageViewProps } from "./SSHKeysPageView"
|
||||
|
||||
export default {
|
||||
|
@ -39,7 +39,7 @@ export const WithGetSSHKeyError = Template.bind({})
|
|||
WithGetSSHKeyError.args = {
|
||||
...Example.args,
|
||||
hasLoaded: false,
|
||||
getSSHKeyError: makeMockApiError({
|
||||
getSSHKeyError: mockApiError({
|
||||
message: "Failed to get SSH key",
|
||||
}),
|
||||
}
|
||||
|
@ -47,7 +47,7 @@ WithGetSSHKeyError.args = {
|
|||
export const WithRegenerateSSHKeyError = Template.bind({})
|
||||
WithRegenerateSSHKeyError.args = {
|
||||
...Example.args,
|
||||
regenerateSSHKeyError: makeMockApiError({
|
||||
regenerateSSHKeyError: mockApiError({
|
||||
message: "Failed to regenerate SSH key",
|
||||
}),
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import * as SecurityForm from "../../../components/SettingsSecurityForm/Settings
|
|||
import { renderWithAuth } from "../../../testHelpers/renderHelpers"
|
||||
import { SecurityPage } from "./SecurityPage"
|
||||
import i18next from "i18next"
|
||||
import { mockApiError } from "testHelpers/entities"
|
||||
|
||||
const { t } = i18next
|
||||
|
||||
|
@ -54,17 +55,14 @@ describe("SecurityPage", () => {
|
|||
|
||||
describe("when the old_password is incorrect", () => {
|
||||
it("shows an error", async () => {
|
||||
jest.spyOn(API, "updateUserPassword").mockRejectedValueOnce({
|
||||
isAxiosError: true,
|
||||
response: {
|
||||
data: {
|
||||
message: "Incorrect password.",
|
||||
validations: [
|
||||
{ detail: "Incorrect password.", field: "old_password" },
|
||||
],
|
||||
},
|
||||
},
|
||||
})
|
||||
jest.spyOn(API, "updateUserPassword").mockRejectedValueOnce(
|
||||
mockApiError({
|
||||
message: "Incorrect password.",
|
||||
validations: [
|
||||
{ detail: "Incorrect password.", field: "old_password" },
|
||||
],
|
||||
}),
|
||||
)
|
||||
|
||||
const { user } = renderPage()
|
||||
await fillAndSubmitForm()
|
||||
|
@ -79,15 +77,12 @@ describe("SecurityPage", () => {
|
|||
|
||||
describe("when the password is invalid", () => {
|
||||
it("shows an error", async () => {
|
||||
jest.spyOn(API, "updateUserPassword").mockRejectedValueOnce({
|
||||
isAxiosError: true,
|
||||
response: {
|
||||
data: {
|
||||
message: "Invalid password.",
|
||||
validations: [{ detail: "Invalid password.", field: "password" }],
|
||||
},
|
||||
},
|
||||
})
|
||||
jest.spyOn(API, "updateUserPassword").mockRejectedValueOnce(
|
||||
mockApiError({
|
||||
message: "Invalid password.",
|
||||
validations: [{ detail: "Invalid password.", field: "password" }],
|
||||
}),
|
||||
)
|
||||
|
||||
const { user } = renderPage()
|
||||
await fillAndSubmitForm()
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { Story } from "@storybook/react"
|
||||
import { makeMockApiError, MockTokens } from "testHelpers/entities"
|
||||
import { mockApiError, MockTokens } from "testHelpers/entities"
|
||||
import { TokensPageView, TokensPageViewProps } from "./TokensPageView"
|
||||
|
||||
export default {
|
||||
|
@ -41,7 +41,7 @@ export const WithGetTokensError = Template.bind({})
|
|||
WithGetTokensError.args = {
|
||||
...Example.args,
|
||||
hasLoaded: false,
|
||||
getTokensError: makeMockApiError({
|
||||
getTokensError: mockApiError({
|
||||
message: "Failed to get tokens.",
|
||||
}),
|
||||
}
|
||||
|
@ -50,7 +50,7 @@ export const WithDeleteTokenError = Template.bind({})
|
|||
WithDeleteTokenError.args = {
|
||||
...Example.args,
|
||||
hasLoaded: false,
|
||||
deleteTokenError: makeMockApiError({
|
||||
deleteTokenError: mockApiError({
|
||||
message: "Failed to delete token.",
|
||||
}),
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Story } from "@storybook/react"
|
||||
import {
|
||||
makeMockApiError,
|
||||
mockApiError,
|
||||
MockWorkspaceProxies,
|
||||
MockPrimaryWorkspaceProxy,
|
||||
MockHealthyWildWorkspaceProxy,
|
||||
|
@ -61,7 +61,7 @@ export const WithProxiesError = Template.bind({})
|
|||
WithProxiesError.args = {
|
||||
...Example.args,
|
||||
hasLoaded: false,
|
||||
getWorkspaceProxiesError: makeMockApiError({
|
||||
getWorkspaceProxiesError: mockApiError({
|
||||
message: "Failed to get proxies.",
|
||||
}),
|
||||
}
|
||||
|
@ -70,7 +70,7 @@ export const WithSelectProxyError = Template.bind({})
|
|||
WithSelectProxyError.args = {
|
||||
...Example.args,
|
||||
hasLoaded: false,
|
||||
selectProxyError: makeMockApiError({
|
||||
selectProxyError: mockApiError({
|
||||
message: "Failed to select proxy.",
|
||||
}),
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { fireEvent, screen } from "@testing-library/react"
|
||||
import userEvent from "@testing-library/user-event"
|
||||
import { rest } from "msw"
|
||||
import * as API from "../../../api/api"
|
||||
import { Language as FormLanguage } from "../../../components/CreateUserForm/CreateUserForm"
|
||||
import { Language as FooterLanguage } from "../../../components/FormFooter/FormFooter"
|
||||
import {
|
||||
|
@ -14,7 +13,9 @@ import { Language as CreateUserLanguage } from "../../../xServices/users/createU
|
|||
import { CreateUserPage } from "./CreateUserPage"
|
||||
|
||||
const renderCreateUserPage = async () => {
|
||||
renderWithAuth(<CreateUserPage />)
|
||||
renderWithAuth(<CreateUserPage />, {
|
||||
extraRoutes: [{ path: "/users", element: <div>Users Page</div> }],
|
||||
})
|
||||
await waitForLoaderToBeRemoved()
|
||||
}
|
||||
|
||||
|
@ -51,18 +52,6 @@ describe("Create User Page", () => {
|
|||
expect(errorMessage).toBeDefined()
|
||||
})
|
||||
|
||||
it("shows generic error message", async () => {
|
||||
jest.spyOn(API, "createUser").mockRejectedValueOnce({
|
||||
data: "unknown error",
|
||||
})
|
||||
await renderCreateUserPage()
|
||||
await fillForm({})
|
||||
const errorMessage = await screen.findByText(
|
||||
CreateUserLanguage.createUserError,
|
||||
)
|
||||
expect(errorMessage).toBeDefined()
|
||||
})
|
||||
|
||||
it("shows API error message", async () => {
|
||||
const fieldErrorMessage = "username already in use"
|
||||
server.use(
|
||||
|
|
|
@ -23,13 +23,7 @@ export const CreateUserPage: FC = () => {
|
|||
},
|
||||
},
|
||||
})
|
||||
const { createUserErrorMessage, createUserFormErrors } =
|
||||
createUserState.context
|
||||
// There is no field for organization id in Community Edition, so handle its field error like a generic error
|
||||
const genericError =
|
||||
createUserErrorMessage ||
|
||||
createUserFormErrors?.organization_id ||
|
||||
(!myOrgId ? Language.unknownError : undefined)
|
||||
const { error } = createUserState.context
|
||||
|
||||
return (
|
||||
<Margins>
|
||||
|
@ -37,7 +31,7 @@ export const CreateUserPage: FC = () => {
|
|||
<title>{pageTitle("Create User")}</title>
|
||||
</Helmet>
|
||||
<CreateUserForm
|
||||
formErrors={createUserFormErrors}
|
||||
error={error}
|
||||
onSubmit={(user: TypesGen.CreateUserRequest) =>
|
||||
createUserSend({ type: "CREATE", user })
|
||||
}
|
||||
|
@ -46,7 +40,6 @@ export const CreateUserPage: FC = () => {
|
|||
navigate("/users")
|
||||
}}
|
||||
isLoading={createUserState.hasTag("loading")}
|
||||
error={genericError}
|
||||
myOrgId={myOrgId}
|
||||
/>
|
||||
</Margins>
|
||||
|
|
|
@ -4,6 +4,7 @@ import {
|
|||
MockUser,
|
||||
MockUser2,
|
||||
MockAssignableSiteRoles,
|
||||
mockApiError,
|
||||
} from "testHelpers/entities"
|
||||
import { UsersPageView, UsersPageViewProps } from "./UsersPageView"
|
||||
|
||||
|
@ -42,18 +43,13 @@ EmptyPage.args = { users: [], isNonInitialPage: true }
|
|||
export const Error = Template.bind({})
|
||||
Error.args = {
|
||||
users: undefined,
|
||||
error: {
|
||||
response: {
|
||||
data: {
|
||||
message: "Invalid user search query.",
|
||||
validations: [
|
||||
{
|
||||
field: "status",
|
||||
detail: `Query param "status" has invalid value: "inactive" is not a valid user status`,
|
||||
},
|
||||
],
|
||||
error: mockApiError({
|
||||
message: "Invalid user search query.",
|
||||
validations: [
|
||||
{
|
||||
field: "status",
|
||||
detail: `Query param "status" has invalid value: "inactive" is not a valid user status`,
|
||||
},
|
||||
},
|
||||
isAxiosError: true,
|
||||
},
|
||||
],
|
||||
}),
|
||||
}
|
||||
|
|
|
@ -341,6 +341,8 @@ export const MockTemplate: TypesGen.Template = {
|
|||
created_by_name: "test_creator",
|
||||
icon: "/icon/code.svg",
|
||||
allow_user_cancel_workspace_jobs: true,
|
||||
allow_user_autostart: false,
|
||||
allow_user_autostop: false,
|
||||
}
|
||||
|
||||
export const MockTemplateVersionFiles: TemplateVersionFiles = {
|
||||
|
@ -1251,6 +1253,7 @@ type MockAPIInput = {
|
|||
}
|
||||
|
||||
type MockAPIOutput = {
|
||||
isAxiosError: true
|
||||
response: {
|
||||
data: {
|
||||
message: string
|
||||
|
@ -1258,16 +1261,15 @@ type MockAPIOutput = {
|
|||
validations: FieldError[] | undefined
|
||||
}
|
||||
}
|
||||
isAxiosError: boolean
|
||||
}
|
||||
|
||||
type MakeMockApiErrorFunction = (input: MockAPIInput) => MockAPIOutput
|
||||
|
||||
export const makeMockApiError: MakeMockApiErrorFunction = ({
|
||||
export const mockApiError = ({
|
||||
message,
|
||||
detail,
|
||||
validations,
|
||||
}) => ({
|
||||
}: MockAPIInput): MockAPIOutput => ({
|
||||
// This is how axios can check if it is an axios error when calling isAxiosError
|
||||
isAxiosError: true,
|
||||
response: {
|
||||
data: {
|
||||
message: message ?? "Something went wrong.",
|
||||
|
@ -1275,7 +1277,6 @@ export const makeMockApiError: MakeMockApiErrorFunction = ({
|
|||
validations: validations ?? undefined,
|
||||
},
|
||||
},
|
||||
isAxiosError: true,
|
||||
})
|
||||
|
||||
export const MockEntitlements: TypesGen.Entitlements = {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { FormikContextType } from "formik/dist/types"
|
||||
import { getFormHelpers, nameValidator, onChangeTrimmed } from "./formUtils"
|
||||
import { mockApiError } from "testHelpers/entities"
|
||||
|
||||
interface TestType {
|
||||
untouchedGoodField: string
|
||||
|
@ -69,9 +70,17 @@ describe("form util functions", () => {
|
|||
})
|
||||
describe("with API errors", () => {
|
||||
it("shows an error if there is only an API error", () => {
|
||||
const getFieldHelpers = getFormHelpers<TestType>(form, {
|
||||
touchedGoodField: "API error!",
|
||||
})
|
||||
const getFieldHelpers = getFormHelpers<TestType>(
|
||||
form,
|
||||
mockApiError({
|
||||
validations: [
|
||||
{
|
||||
field: "touchedGoodField",
|
||||
detail: "API error!",
|
||||
},
|
||||
],
|
||||
}),
|
||||
)
|
||||
const result = getFieldHelpers("touchedGoodField")
|
||||
expect(result.error).toBeTruthy()
|
||||
expect(result.helperText).toEqual("API error!")
|
||||
|
@ -83,9 +92,17 @@ describe("form util functions", () => {
|
|||
expect(result.helperText).toEqual("oops!")
|
||||
})
|
||||
it("shows the API error if both are present", () => {
|
||||
const getFieldHelpers = getFormHelpers<TestType>(form, {
|
||||
touchedBadField: "API error!",
|
||||
})
|
||||
const getFieldHelpers = getFormHelpers<TestType>(
|
||||
form,
|
||||
mockApiError({
|
||||
validations: [
|
||||
{
|
||||
field: "touchedBadField",
|
||||
detail: "API error!",
|
||||
},
|
||||
],
|
||||
}),
|
||||
)
|
||||
const result = getFieldHelpers("touchedBadField")
|
||||
expect(result.error).toBeTruthy()
|
||||
expect(result.helperText).toEqual("API error!")
|
||||
|
|
|
@ -34,37 +34,32 @@ interface FormHelpers {
|
|||
}
|
||||
|
||||
export const getFormHelpers =
|
||||
<T>(form: FormikContextType<T>, error?: Error | unknown) =>
|
||||
<TFormValues>(form: FormikContextType<TFormValues>, error?: unknown) =>
|
||||
(
|
||||
name: string,
|
||||
HelperText: ReactNode = "",
|
||||
backendErrorName?: string,
|
||||
fieldName: keyof TFormValues | string,
|
||||
helperText?: ReactNode,
|
||||
// backendFieldName is used when the value in the form is named different from the backend
|
||||
backendFieldName?: string,
|
||||
): FormHelpers => {
|
||||
const apiValidationErrors = isApiValidationError(error)
|
||||
? (mapApiErrorToFieldErrors(error.response.data) as FormikErrors<T>)
|
||||
: // This should not return the error since it is not and api validation error but I didn't have time to fix this and tests
|
||||
error
|
||||
|
||||
if (typeof name !== "string") {
|
||||
throw new Error(
|
||||
`name must be type of string, instead received '${typeof name}'`,
|
||||
)
|
||||
}
|
||||
|
||||
const apiErrorName = backendErrorName ?? name
|
||||
|
||||
// getIn is a util function from Formik that gets at any depth of nesting
|
||||
// and is necessary for the types to work
|
||||
const touched = getIn(form.touched, name)
|
||||
const apiError = getIn(apiValidationErrors, apiErrorName)
|
||||
const frontendError = getIn(form.errors, name)
|
||||
const returnError = apiError ?? frontendError
|
||||
? (mapApiErrorToFieldErrors(
|
||||
error.response.data,
|
||||
) as FormikErrors<TFormValues> & { [key: string]: string })
|
||||
: undefined
|
||||
// Since the fieldName can be a path string like parameters[0].value we need to use getIn
|
||||
const touched = Boolean(getIn(form.touched, fieldName.toString()))
|
||||
const formError = getIn(form.errors, fieldName.toString())
|
||||
// Since the field in the form can be diff from the backend, we need to
|
||||
// check for both when getting the error
|
||||
const apiField = backendFieldName ?? fieldName
|
||||
const apiError = apiValidationErrors?.[apiField.toString()]
|
||||
const errorToDisplay = apiError ?? formError
|
||||
|
||||
return {
|
||||
...form.getFieldProps(name),
|
||||
id: name,
|
||||
error: touched && Boolean(returnError),
|
||||
helperText: touched ? returnError || HelperText : HelperText,
|
||||
...form.getFieldProps(fieldName),
|
||||
id: fieldName.toString(),
|
||||
error: touched && Boolean(errorToDisplay),
|
||||
helperText: touched ? errorToDisplay ?? helperText : helperText,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -102,3 +97,5 @@ export const templateDisplayNameValidator = (
|
|||
Language.nameTooLong(displayName, templateDisplayNameMaxLength),
|
||||
)
|
||||
.optional()
|
||||
|
||||
export const iconValidator = Yup.string().label("Icon").max(256)
|
||||
|
|
|
@ -1,14 +1,6 @@
|
|||
import { createGroup } from "api/api"
|
||||
import {
|
||||
ApiError,
|
||||
getErrorMessage,
|
||||
hasApiFieldErrors,
|
||||
isApiError,
|
||||
mapApiErrorToFieldErrors,
|
||||
} from "api/errors"
|
||||
import { CreateGroupRequest, Group } from "api/typesGenerated"
|
||||
import { displayError } from "components/GlobalSnackbar/utils"
|
||||
import { createMachine } from "xstate"
|
||||
import { createMachine, assign } from "xstate"
|
||||
|
||||
export const createGroupMachine = createMachine(
|
||||
{
|
||||
|
@ -16,7 +8,7 @@ export const createGroupMachine = createMachine(
|
|||
schema: {
|
||||
context: {} as {
|
||||
organizationId: string
|
||||
createGroupFormErrors?: unknown
|
||||
error?: unknown
|
||||
},
|
||||
services: {} as {
|
||||
createGroup: {
|
||||
|
@ -45,37 +37,23 @@ export const createGroupMachine = createMachine(
|
|||
target: "idle",
|
||||
actions: ["onCreate"],
|
||||
},
|
||||
onError: [
|
||||
{
|
||||
target: "idle",
|
||||
cond: "hasFieldErrors",
|
||||
actions: ["assignCreateGroupFormErrors"],
|
||||
},
|
||||
{
|
||||
target: "idle",
|
||||
actions: ["displayCreateGroupError"],
|
||||
},
|
||||
],
|
||||
onError: {
|
||||
target: "idle",
|
||||
actions: ["assignError"],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
guards: {
|
||||
hasFieldErrors: (_, event) =>
|
||||
isApiError(event.data) && hasApiFieldErrors(event.data),
|
||||
},
|
||||
services: {
|
||||
createGroup: ({ organizationId }, { data }) =>
|
||||
createGroup(organizationId, data),
|
||||
},
|
||||
actions: {
|
||||
displayCreateGroupError: (_, { data }) => {
|
||||
const message = getErrorMessage(data, "Error on creating the group.")
|
||||
displayError(message)
|
||||
},
|
||||
assignCreateGroupFormErrors: (_, event) =>
|
||||
mapApiErrorToFieldErrors((event.data as ApiError).response.data),
|
||||
assignError: assign({
|
||||
error: (_, event) => event.data,
|
||||
}),
|
||||
},
|
||||
},
|
||||
)
|
||||
|
|
|
@ -1,11 +1,5 @@
|
|||
import { getGroup, patchGroup } from "api/api"
|
||||
import {
|
||||
ApiError,
|
||||
getErrorMessage,
|
||||
hasApiFieldErrors,
|
||||
isApiError,
|
||||
mapApiErrorToFieldErrors,
|
||||
} from "api/errors"
|
||||
import { getErrorMessage } from "api/errors"
|
||||
import { Group } from "api/typesGenerated"
|
||||
import { displayError } from "components/GlobalSnackbar/utils"
|
||||
import { assign, createMachine } from "xstate"
|
||||
|
@ -17,7 +11,7 @@ export const editGroupMachine = createMachine(
|
|||
context: {} as {
|
||||
groupId: string
|
||||
group?: Group
|
||||
updateGroupFormErrors?: unknown
|
||||
error?: unknown
|
||||
},
|
||||
services: {} as {
|
||||
loadGroup: {
|
||||
|
@ -61,26 +55,15 @@ export const editGroupMachine = createMachine(
|
|||
onDone: {
|
||||
actions: ["onUpdate"],
|
||||
},
|
||||
onError: [
|
||||
{
|
||||
target: "idle",
|
||||
cond: "hasFieldErrors",
|
||||
actions: ["assignUpdateGroupFormErrors"],
|
||||
},
|
||||
{
|
||||
target: "idle",
|
||||
actions: ["displayUpdateGroupError"],
|
||||
},
|
||||
],
|
||||
onError: {
|
||||
target: "idle",
|
||||
actions: ["assignError"],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
guards: {
|
||||
hasFieldErrors: (_, event) =>
|
||||
isApiError(event.data) && hasApiFieldErrors(event.data),
|
||||
},
|
||||
services: {
|
||||
loadGroup: ({ groupId }) => getGroup(groupId),
|
||||
|
||||
|
@ -104,12 +87,9 @@ export const editGroupMachine = createMachine(
|
|||
const message = getErrorMessage(data, "Failed to the group.")
|
||||
displayError(message)
|
||||
},
|
||||
displayUpdateGroupError: (_, { data }) => {
|
||||
const message = getErrorMessage(data, "Failed to update the group.")
|
||||
displayError(message)
|
||||
},
|
||||
assignUpdateGroupFormErrors: (_, event) =>
|
||||
mapApiErrorToFieldErrors((event.data as ApiError).response.data),
|
||||
assignError: assign({
|
||||
error: (_, event) => event.data,
|
||||
}),
|
||||
},
|
||||
},
|
||||
)
|
||||
|
|
|
@ -1,12 +1,4 @@
|
|||
import * as API from "api/api"
|
||||
import {
|
||||
ApiError,
|
||||
FieldErrors,
|
||||
getErrorMessage,
|
||||
hasApiFieldErrors,
|
||||
isApiError,
|
||||
mapApiErrorToFieldErrors,
|
||||
} from "api/errors"
|
||||
import * as TypesGen from "api/typesGenerated"
|
||||
import { assign, createMachine } from "xstate"
|
||||
|
||||
|
@ -15,8 +7,7 @@ export const Language = {
|
|||
}
|
||||
|
||||
export interface SetupContext {
|
||||
createFirstUserErrorMessage?: string
|
||||
createFirstUserFormErrors?: FieldErrors
|
||||
error?: unknown
|
||||
firstUser?: TypesGen.CreateFirstUserRequest
|
||||
}
|
||||
|
||||
|
@ -52,7 +43,7 @@ export const setupMachine =
|
|||
},
|
||||
},
|
||||
creatingFirstUser: {
|
||||
entry: "clearCreateFirstUserError",
|
||||
entry: "clearError",
|
||||
invoke: {
|
||||
src: "createFirstUser",
|
||||
id: "createFirstUser",
|
||||
|
@ -62,17 +53,10 @@ export const setupMachine =
|
|||
target: "firstUserCreated",
|
||||
},
|
||||
],
|
||||
onError: [
|
||||
{
|
||||
actions: "assignCreateFirstUserFormErrors",
|
||||
cond: "hasFieldErrors",
|
||||
target: "idle",
|
||||
},
|
||||
{
|
||||
actions: "assignCreateFirstUserError",
|
||||
target: "idle",
|
||||
},
|
||||
],
|
||||
onError: {
|
||||
actions: "assignError",
|
||||
target: "idle",
|
||||
},
|
||||
},
|
||||
tags: "loading",
|
||||
},
|
||||
|
@ -86,28 +70,16 @@ export const setupMachine =
|
|||
services: {
|
||||
createFirstUser: (_, event) => API.createFirstUser(event.firstUser),
|
||||
},
|
||||
guards: {
|
||||
hasFieldErrors: (_, event) =>
|
||||
isApiError(event.data) && hasApiFieldErrors(event.data),
|
||||
},
|
||||
actions: {
|
||||
assignFirstUserData: assign({
|
||||
firstUser: (_, event) => event.firstUser,
|
||||
}),
|
||||
assignCreateFirstUserError: assign({
|
||||
createFirstUserErrorMessage: (_, event) =>
|
||||
getErrorMessage(event.data, Language.createFirstUserError),
|
||||
assignError: assign({
|
||||
error: (_, event) => event.data,
|
||||
}),
|
||||
assignCreateFirstUserFormErrors: assign({
|
||||
// the guard ensures it is ApiError
|
||||
createFirstUserFormErrors: (_, event) =>
|
||||
mapApiErrorToFieldErrors((event.data as ApiError).response.data),
|
||||
clearError: assign({
|
||||
error: (_) => undefined,
|
||||
}),
|
||||
clearCreateFirstUserError: assign((context: SetupContext) => ({
|
||||
...context,
|
||||
createFirstUserErrorMessage: undefined,
|
||||
createFirstUserFormErrors: undefined,
|
||||
})),
|
||||
},
|
||||
},
|
||||
)
|
||||
|
|
|
@ -1,24 +1,14 @@
|
|||
import { assign, createMachine } from "xstate"
|
||||
import * as API from "../../api/api"
|
||||
import {
|
||||
ApiError,
|
||||
FieldErrors,
|
||||
getErrorMessage,
|
||||
hasApiFieldErrors,
|
||||
isApiError,
|
||||
mapApiErrorToFieldErrors,
|
||||
} from "../../api/errors"
|
||||
import * as TypesGen from "../../api/typesGenerated"
|
||||
import { displaySuccess } from "../../components/GlobalSnackbar/utils"
|
||||
|
||||
export const Language = {
|
||||
createUserSuccess: "Successfully created user.",
|
||||
createUserError: "Error on creating the user.",
|
||||
}
|
||||
|
||||
export interface CreateUserContext {
|
||||
createUserErrorMessage?: string
|
||||
createUserFormErrors?: FieldErrors
|
||||
error?: unknown
|
||||
}
|
||||
|
||||
export type CreateUserEvent =
|
||||
|
@ -44,11 +34,11 @@ export const createUserMachine = createMachine(
|
|||
idle: {
|
||||
on: {
|
||||
CREATE: "creatingUser",
|
||||
CANCEL_CREATE_USER: { actions: ["clearCreateUserError"] },
|
||||
CANCEL_CREATE_USER: { actions: ["clearError"] },
|
||||
},
|
||||
},
|
||||
creatingUser: {
|
||||
entry: "clearCreateUserError",
|
||||
entry: "clearError",
|
||||
invoke: {
|
||||
src: "createUser",
|
||||
id: "createUser",
|
||||
|
@ -56,17 +46,10 @@ export const createUserMachine = createMachine(
|
|||
target: "idle",
|
||||
actions: ["displayCreateUserSuccess", "redirectToUsersPage"],
|
||||
},
|
||||
onError: [
|
||||
{
|
||||
target: "idle",
|
||||
cond: "hasFieldErrors",
|
||||
actions: ["assignCreateUserFormErrors"],
|
||||
},
|
||||
{
|
||||
target: "idle",
|
||||
actions: ["assignCreateUserError"],
|
||||
},
|
||||
],
|
||||
onError: {
|
||||
target: "idle",
|
||||
actions: ["assignError"],
|
||||
},
|
||||
},
|
||||
tags: "loading",
|
||||
},
|
||||
|
@ -76,25 +59,11 @@ export const createUserMachine = createMachine(
|
|||
services: {
|
||||
createUser: (_, event) => API.createUser(event.user),
|
||||
},
|
||||
guards: {
|
||||
hasFieldErrors: (_, event) =>
|
||||
isApiError(event.data) && hasApiFieldErrors(event.data),
|
||||
},
|
||||
actions: {
|
||||
assignCreateUserError: assign({
|
||||
createUserErrorMessage: (_, event) =>
|
||||
getErrorMessage(event.data, Language.createUserError),
|
||||
assignError: assign({
|
||||
error: (_, event) => event.data,
|
||||
}),
|
||||
assignCreateUserFormErrors: assign({
|
||||
// the guard ensures it is ApiError
|
||||
createUserFormErrors: (_, event) =>
|
||||
mapApiErrorToFieldErrors((event.data as ApiError).response.data),
|
||||
}),
|
||||
clearCreateUserError: assign((context: CreateUserContext) => ({
|
||||
...context,
|
||||
createUserErrorMessage: undefined,
|
||||
createUserFormErrors: undefined,
|
||||
})),
|
||||
clearError: assign({ error: (_) => undefined }),
|
||||
displayCreateUserSuccess: () => {
|
||||
displaySuccess(Language.createUserSuccess)
|
||||
},
|
||||
|
|
Loading…
Reference in New Issue