diff --git a/enterprise/coderd/groups.go b/enterprise/coderd/groups.go index a681d27859514..b1330993b1add 100644 --- a/enterprise/coderd/groups.go +++ b/enterprise/coderd/groups.go @@ -48,7 +48,8 @@ func (api *API) postGroupByOrganization(rw http.ResponseWriter, r *http.Request) if req.Name == database.EveryoneGroup { httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ - Message: fmt.Sprintf("%q is a reserved keyword and cannot be used for a group name.", database.EveryoneGroup), + Message: "Invalid group name.", + Validations: []codersdk.ValidationError{{Field: "name", Detail: fmt.Sprintf("%q is a reserved group name", req.Name)}}, }) return } @@ -63,7 +64,8 @@ func (api *API) postGroupByOrganization(rw http.ResponseWriter, r *http.Request) }) if database.IsUniqueViolation(err) { httpapi.Write(ctx, rw, http.StatusConflict, codersdk.Response{ - Message: fmt.Sprintf("Group with name %q already exists.", req.Name), + Message: fmt.Sprintf("A group named %q already exists.", req.Name), + Validations: []codersdk.ValidationError{{Field: "name", Detail: "Group names must be unique"}}, }) return } diff --git a/site/src/pages/GroupsPage/CreateGroupPage.tsx b/site/src/pages/GroupsPage/CreateGroupPage.tsx index a41342331e3f6..e76f5fbdb5d21 100644 --- a/site/src/pages/GroupsPage/CreateGroupPage.tsx +++ b/site/src/pages/GroupsPage/CreateGroupPage.tsx @@ -26,7 +26,7 @@ export const CreateGroupPage: FC = () => { }); navigate(`/groups/${newGroup.id}`); }} - formErrors={createGroupMutation.error} + error={createGroupMutation.error} isLoading={createGroupMutation.isLoading} /> diff --git a/site/src/pages/GroupsPage/CreateGroupPageView.stories.tsx b/site/src/pages/GroupsPage/CreateGroupPageView.stories.tsx index 2aeb293ba7f2e..960a510c00ce6 100644 --- a/site/src/pages/GroupsPage/CreateGroupPageView.stories.tsx +++ b/site/src/pages/GroupsPage/CreateGroupPageView.stories.tsx @@ -1,3 +1,4 @@ +import { mockApiError } from "testHelpers/entities"; import { CreateGroupPageView } from "./CreateGroupPageView"; import type { Meta, StoryObj } from "@storybook/react"; @@ -9,6 +10,14 @@ const meta: Meta = { export default meta; type Story = StoryObj; -const Example: Story = {}; +export const Example: Story = {}; -export { Example as CreateGroupPageView }; +export const WithError: Story = { + args: { + error: mockApiError({ + message: "A group named new-group already exists.", + validations: [{ field: "name", detail: "Group names must be unique" }], + }), + initialTouched: { name: true }, + }, +}; diff --git a/site/src/pages/GroupsPage/CreateGroupPageView.tsx b/site/src/pages/GroupsPage/CreateGroupPageView.tsx index 7c6e8b0aef1fa..79a3b81f72007 100644 --- a/site/src/pages/GroupsPage/CreateGroupPageView.tsx +++ b/site/src/pages/GroupsPage/CreateGroupPageView.tsx @@ -1,15 +1,17 @@ import TextField from "@mui/material/TextField"; -import { CreateGroupRequest } from "api/typesGenerated"; +import { type FormikTouched, useFormik } from "formik"; +import { type FC } from "react"; +import { useNavigate } from "react-router-dom"; +import * as Yup from "yup"; +import type { CreateGroupRequest } from "api/typesGenerated"; +import { ErrorAlert } from "components/Alert/ErrorAlert"; import { FormFooter } from "components/FormFooter/FormFooter"; import { FullPageForm } from "components/FullPageForm/FullPageForm"; import { IconField } from "components/IconField/IconField"; import { Margins } from "components/Margins/Margins"; import { Stack } from "components/Stack/Stack"; -import { useFormik } from "formik"; -import { FC } from "react"; -import { useNavigate } from "react-router-dom"; import { getFormHelpers, onChangeTrimmed } from "utils/formUtils"; -import * as Yup from "yup"; +import { isApiValidationError } from "api/errors"; const validationSchema = Yup.object({ name: Yup.string().required().label("Name"), @@ -17,14 +19,17 @@ const validationSchema = Yup.object({ export type CreateGroupPageViewProps = { onSubmit: (data: CreateGroupRequest) => void; - formErrors?: unknown; + error?: unknown; isLoading: boolean; + // Helpful to show field errors on Storybook + initialTouched?: FormikTouched; }; export const CreateGroupPageView: FC = ({ onSubmit, - formErrors, + error, isLoading, + initialTouched, }) => { const navigate = useNavigate(); const form = useFormik({ @@ -36,8 +41,9 @@ export const CreateGroupPageView: FC = ({ }, validationSchema, onSubmit, + initialTouched, }); - const getFieldHelpers = getFormHelpers(form, formErrors); + const getFieldHelpers = getFormHelpers(form, error); const onCancel = () => navigate("/groups"); return ( @@ -45,6 +51,10 @@ export const CreateGroupPageView: FC = ({
+ {Boolean(error) && !isApiValidationError(error) && ( + + )} + ({ @@ -1954,9 +1954,9 @@ export const mockApiError = ({ isAxiosError: true, response: { data: { - message: message ?? "Something went wrong.", - detail: detail ?? undefined, - validations: validations ?? undefined, + message, + detail, + validations, }, }, });