Skip to content

fix(site): Fix template icon field validation #7394

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
May 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 31 additions & 46 deletions site/src/api/errors.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { mockApiError } from "testHelpers/entities"
import {
getValidationErrorMessage,
isApiError,
Expand All @@ -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)
})

Expand Down Expand Up @@ -48,53 +46,40 @@ 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 ':'`,
)
})

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("")
})
})
28 changes: 5 additions & 23 deletions site/src/api/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 => {
Expand All @@ -63,10 +49,6 @@ export const mapApiErrorToFieldErrors = (
return result
}

export const isApiValidationError = (error: unknown): error is ApiError => {
return isApiError(error) && hasApiFieldErrors(error)
}

/**
*
* @param error
Expand Down
4 changes: 2 additions & 2 deletions site/src/components/AlertBanner/AlertBanner.stories.tsx
Original file line number Diff line number Diff line change
@@ -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"

Expand All @@ -16,7 +16,7 @@ const ExampleAction = (
</Button>
)

const mockError = makeMockApiError({
const mockError = mockApiError({
message: "Email or password was invalid",
detail: "Password is invalid",
})
Expand Down
15 changes: 4 additions & 11 deletions site/src/components/CreateUserForm/CreateUserForm.stories.tsx
Original file line number Diff line number Diff line change
@@ -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",
Expand All @@ -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({})
Expand Down
11 changes: 4 additions & 7 deletions site/src/components/CreateUserForm/CreateUserForm.tsx
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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
}

Expand All @@ -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: {
Expand All @@ -58,7 +56,7 @@ export const CreateUserForm: FC<
})
const getFieldHelpers = getFormHelpers<TypesGen.CreateUserRequest>(
form,
formErrors,
error,
)

return (
Expand Down Expand Up @@ -92,7 +90,6 @@ export const CreateUserForm: FC<
variant="outlined"
/>
</Stack>
{error && <FormHelperText error>{error}</FormHelperText>}
<FormFooter onCancel={onCancel} isLoading={isLoading} />
</form>
</FullPageForm>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
SearchBarWithFilter,
SearchBarWithFilterProps,
} from "./SearchBarWithFilter"
import { mockApiError } from "testHelpers/entities"

export default {
title: "components/SearchBarWithFilter",
Expand Down Expand Up @@ -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,
},
],
}),
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Story } from "@storybook/react"
import { AccountForm, AccountFormProps } from "./SettingsAccountForm"
import { mockApiError } from "testHelpers/entities"

export default {
title: "components/SettingsAccountForm",
Expand Down Expand Up @@ -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,
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Story } from "@storybook/react"
import { SecurityForm, SecurityFormProps } from "./SettingsSecurityForm"
import { mockApiError } from "testHelpers/entities"

export default {
title: "components/SettingsSecurityForm",
Expand Down Expand Up @@ -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,
},
Expand Down
4 changes: 2 additions & 2 deletions site/src/components/SignInForm/SignInForm.stories.tsx
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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: [
{
Expand Down
Loading