diff --git a/site/src/components/FormField/FormField.tsx b/site/src/components/FormField/FormField.tsx index 209bcb5e8e..0f7ba8df16 100644 --- a/site/src/components/FormField/FormField.tsx +++ b/site/src/components/FormField/FormField.tsx @@ -24,6 +24,10 @@ export const FormField: FC = ({
> = ({ - className, - type, - ...props -}) => { +export type InputProps = ComponentPropsWithRef<"input">; + +export const Input: FC = ({ className, type, ...props }) => { return ( = { component: PasswordField, args: { label: "Password", + field: { + id: "password", + name: "password", + error: false, + onBlur: fn(), + onChange: fn(), + }, }, render: function StatefulPasswordField(args) { const [value, setValue] = useState(""); diff --git a/site/src/components/PasswordField/PasswordField.tsx b/site/src/components/PasswordField/PasswordField.tsx index b9d364fbd8..9f80e9c8dc 100644 --- a/site/src/components/PasswordField/PasswordField.tsx +++ b/site/src/components/PasswordField/PasswordField.tsx @@ -1,21 +1,33 @@ -import TextField, { type TextFieldProps } from "@mui/material/TextField"; import type { FC } from "react"; import { keepPreviousData, useQuery } from "react-query"; import { API } from "#/api/api"; +import { Input, type InputProps } from "#/components/Input/Input"; +import { Label } from "#/components/Label/Label"; import { useDebouncedValue } from "#/hooks/debounce"; +import { cn } from "#/utils/cn"; +import type { FormHelpers } from "#/utils/formUtils"; // TODO: @BrunoQuaresma: Unable to integrate Yup + Formik for validation. The // validation was triggering on the onChange event, but the form.errors were not // updating accordingly. Tried various combinations of validateOnBlur and // validateOnChange without success. Further investigation is needed. +type PasswordFieldProps = InputProps & { + label: string; + field: FormHelpers; +}; /** * A password field component that validates the password against the API with * debounced calls. It uses a debounced value to minimize the number of API * calls and displays validation errors. */ -export const PasswordField: FC = (props) => { - const debouncedValue = useDebouncedValue(`${props.value}`, 500); +export const PasswordField: FC = ({ + label, + field, + value, + ...props +}) => { + const debouncedValue = useDebouncedValue(`${value}`, 500); const validatePasswordQuery = useQuery({ queryKey: ["validatePassword", debouncedValue], queryFn: () => API.validateUserPassword(debouncedValue), @@ -24,14 +36,33 @@ export const PasswordField: FC = (props) => { }); const valid = validatePasswordQuery.data?.valid ?? true; + const displayHelper = !valid + ? validatePasswordQuery.data?.details + : field.helperText; + return ( - +
+ + + {displayHelper && ( + + {displayHelper} + + )} +
); }; diff --git a/site/src/pages/SetupPage/SetupPage.test.tsx b/site/src/pages/SetupPage/SetupPage.test.tsx index e6676c4561..374957d723 100644 --- a/site/src/pages/SetupPage/SetupPage.test.tsx +++ b/site/src/pages/SetupPage/SetupPage.test.tsx @@ -24,7 +24,7 @@ const fillForm = async ({ await userEvent.type(emailField, email); await userEvent.type(passwordField, password); const submitButton = screen.getByRole("button", { - name: "Continue with email", + name: "Continue", }); await userEvent.click(submitButton); }; diff --git a/site/src/pages/SetupPage/SetupPageView.stories.tsx b/site/src/pages/SetupPage/SetupPageView.stories.tsx index 8367eaa387..7bd3c2d9d0 100644 --- a/site/src/pages/SetupPage/SetupPageView.stories.tsx +++ b/site/src/pages/SetupPage/SetupPageView.stories.tsx @@ -14,6 +14,16 @@ type Story = StoryObj; export const Ready: Story = {}; +export const WithGitHub: Story = { + args: { + authMethods: { + github: { enabled: true, default_provider_configured: false }, + oidc: { enabled: false, signInText: "", iconUrl: "" }, + password: { enabled: true }, + }, + }, +}; + export const FormError: Story = { args: { error: mockApiError({ diff --git a/site/src/pages/SetupPage/SetupPageView.tsx b/site/src/pages/SetupPage/SetupPageView.tsx index a2e4fbae21..161b3e5397 100644 --- a/site/src/pages/SetupPage/SetupPageView.tsx +++ b/site/src/pages/SetupPage/SetupPageView.tsx @@ -1,24 +1,28 @@ -import Autocomplete from "@mui/material/Autocomplete"; -import Checkbox from "@mui/material/Checkbox"; -import Link from "@mui/material/Link"; -import MenuItem from "@mui/material/MenuItem"; -import TextField from "@mui/material/TextField"; import { isAxiosError } from "axios"; import { type FormikContextType, useFormik } from "formik"; -import type { ChangeEvent, FC } from "react"; +import type { FC, ReactNode } from "react"; import * as Yup from "yup"; import { countries } from "#/api/countriesGenerated"; import type * as TypesGen from "#/api/typesGenerated"; import { Alert, AlertDescription, AlertTitle } from "#/components/Alert/Alert"; import { Button } from "#/components/Button/Button"; +import { Checkbox } from "#/components/Checkbox/Checkbox"; import { ExternalImage } from "#/components/ExternalImage/ExternalImage"; -import { FormFields, VerticalForm } from "#/components/Form/Form"; +import { FormField } from "#/components/FormField/FormField"; import { CoderIcon } from "#/components/Icons/CoderIcon"; +import { Label } from "#/components/Label/Label"; import { PasswordField } from "#/components/PasswordField/PasswordField"; -import { SignInLayout } from "#/components/SignInLayout/SignInLayout"; -import { Spinner } from "#/components/Spinner/Spinner"; -import { Stack } from "#/components/Stack/Stack"; import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "#/components/Select/Select"; +import { Spinner } from "#/components/Spinner/Spinner"; +import { cn } from "#/utils/cn"; +import { + type FormHelpers, getFormHelpers, nameValidator, onChangeTrimmed, @@ -63,6 +67,10 @@ const validationSchema = Yup.object({ ), }), }), + onboarding_info: Yup.object().shape({ + newsletter_marketing: Yup.bool(), + newsletter_releases: Yup.bool(), + }), }); const numberOfDevelopersOptions = [ @@ -73,6 +81,65 @@ const numberOfDevelopersOptions = [ "2500+", ]; +const Field: FC<{ + label: string; + id: string; + error?: boolean; + helperText?: ReactNode; + className?: string; + children: ReactNode; +}> = ({ label, id, error, helperText, className, children }) => ( +
+ + {children} + {helperText && ( + + {helperText} + + )} +
+); + +type SelectFieldProps = FormHelpers & { + label: string; + className?: string; + onValueChange: (value: string) => void; + placeholder?: string; + children: ReactNode; +}; + +const SelectField: FC = ({ + label, + id, + error, + helperText, + className, + value, + onValueChange, + placeholder, + children, +}) => ( + + + +); + interface SetupPageViewProps { onSubmit: (firstUser: TypesGen.CreateFirstUserRequest) => void; error?: unknown; @@ -103,13 +170,15 @@ export const SetupPageView: FC = ({ country: "", developers: "", }, + onboarding_info: { + newsletter_marketing: false, + newsletter_releases: false, + }, }, validationSchema, onSubmit, - // With validate on blur set to true, the form lights up red whenever - // you click out of it. This is a bit jarring. We instead validate - // on submit and change. validateOnBlur: false, + validateOnMount: true, }); const getFieldHelpers = getFormHelpers( form, @@ -117,29 +186,26 @@ export const SetupPageView: FC = ({ ); return ( - -
- -

- Welcome to Coder -

-
({ - marginTop: 12, - color: theme.palette.text.secondary, - })} - > - Let‘s create your first admin user account -
-
- - +
+
+
+ +

+ Welcome to Coder +

+

+ Set up your admin account and start building secure, reproducible + dev environments. +

+
+ +
{authMethods?.github.enabled && ( <>
@@ -151,164 +217,192 @@ export const SetupPageView: FC = ({
)} - { - const email = event.target.value; - const username = usernameFromEmail(email); - form.setFieldValue("username", username); - onChangeTrimmed(form)(event as ChangeEvent); - }} - autoComplete="email" - fullWidth + + {/* Email */} + { + form.setFieldValue("username", usernameFromEmail(email)); + })} /> + + {/* Password */} + + {/* Premium trial toggle */}
); }; diff --git a/site/src/pages/UserSettingsPage/SecurityPage/SecurityForm.tsx b/site/src/pages/UserSettingsPage/SecurityPage/SecurityForm.tsx index 40eebaa0ec..d4f4dbb4f1 100644 --- a/site/src/pages/UserSettingsPage/SecurityPage/SecurityForm.tsx +++ b/site/src/pages/UserSettingsPage/SecurityPage/SecurityForm.tsx @@ -1,4 +1,3 @@ -import TextField from "@mui/material/TextField"; import { type FormikContextType, useFormik } from "formik"; import type { FC } from "react"; import * as Yup from "yup"; @@ -6,6 +5,7 @@ import { Alert } from "#/components/Alert/Alert"; import { ErrorAlert } from "#/components/Alert/ErrorAlert"; import { Button } from "#/components/Button/Button"; import { Form, FormFields } from "#/components/Form/Form"; +import { FormField } from "#/components/FormField/FormField"; import { PasswordField } from "#/components/PasswordField/PasswordField"; import { Spinner } from "#/components/Spinner/Spinner"; import { getFormHelpers } from "#/utils/formUtils"; @@ -67,25 +67,22 @@ export const SecurityForm: FC = ({
{Boolean(error) && } - -