Skip to content

Commit

Permalink
Improve form submission logic
Browse files Browse the repository at this point in the history
  • Loading branch information
Oksamies committed Nov 20, 2023
1 parent bdcef6c commit 61c7b08
Show file tree
Hide file tree
Showing 7 changed files with 89 additions and 74 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@
animation: rotate 2s linear infinite;
}

.errorMessage {
color: #f1385a;
font-weight: 500;
font-size: 0.75rem;
}

@keyframes rotate {
100% {
transform: rotate(360deg);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,104 +1,114 @@
"use client";
import { faArrowsRotate } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useController, useForm, Form } from "react-hook-form";
import { useController, useForm } from "react-hook-form";
import * as Button from "../../Button";
import { TextInput } from "../../TextInput/TextInput";
import { useToast } from "../../Toast/Provider";
import styles from "./CreateTeamForm.module.css";
import { z } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import { isErrorResponse } from "../../../utils/type_guards";
import { useEffect } from "react";
import {
FormErrorResponse,
isFormErrorResponse,
} from "../../../utils/type_guards";

export function CreateTeamForm() {
const toast = useToast();

// "Required field" error is different from value being too little
// "Required field" error only checks, if the field has been touched. (It's probably broken)
// Check that the value is "> 0"
const schema = z.object({
teamName: z
name: z
.string({ required_error: "Team name is required" })
.min(1, { message: "Team name is required" }),
});

const {
control,
formState: { isSubmitting, errors },
handleSubmit,
setError,
} = useForm<z.infer<typeof schema>>({
mode: "onSubmit",
resolver: zodResolver(schema),
});

const teamName = useController({
const name = useController({
control: control,
name: "teamName",
name: "name",
});

// We have to do this because the RHF Form component is in beta and it
// doesn't have a callback prop like "onValidation" that could take in
// the generalized addToast error informing block.
useEffect(() => {
if (errors.teamName) {
console.log(errors);
const onSubmit = async (data: z.infer<typeof schema>) => {
// SESSION TODO: Add sessionid here
const session = { sessionid: "74hmbkylkgqge1ne22tzuvzuz6u51tdg" };

Check failure

Code scanning / CodeQL

Hard-coded credentials Critical

The hard-coded value "74hmbkylkgqge1ne22tzuvzuz6u51tdg" is used as
authorization header
.

const payload = JSON.stringify(data);
let response = undefined;
try {
// TODO: Change to dev env url
response = await fetch(
`http://thunderstore.temp/api/cyberstorm/teams/create/`,
{
method: "POST",
headers: {
authorization: `Session ${session.sessionid}`,
"Content-Type": "application/json",
},
body: payload,
}
);
} catch (e) {
toast.addToast({
variant: "danger",
message: errors.teamName.message,
message: "There was a problem reaching the server",
duration: 30000,
});
}
}, [errors]);

return (
<Form
control={control}
action="https://thunderstore.io/api/team/create"
method="post"
onSubmit={() => {
toast.addToast({ variant: "info", message: "Creating team" });
}}
onSuccess={(response) => {
console.log(response);
if (response) {
const responseJson = await response.json();
if (response.ok) {
toast.addToast({ variant: "success", message: "Team created" });
}}
onError={(response) => {
if (isErrorResponse(response)) {
toast.addToast({
variant: "danger",
message: response.error.message,
duration: 30000,
});
} else {
// TODO: Add sentry error here
console.log("TODO: Sentry error logging missing!");
toast.addToast({
variant: "danger",
message: "Unhandled form response error",
duration: 30000,
});
}
}}
validateStatus={(status) => status === 200}
className={styles.root}
>
} else if (isFormErrorResponse(responseJson)) {
const errors = responseJson as FormErrorResponse;
Object.keys(errors).forEach((key) => {
setError(key, { type: "manual", message: errors[key] });
});
} else {
// TODO: Add sentry error here
toast.addToast({
variant: "danger",
message: "Skidaddle skidoodle the server gave you a noodle",
duration: 30000,
});
}
}
};

return (
<form onSubmit={handleSubmit(onSubmit)} className={styles.root}>
<div className={styles.dialog}>
<div className={styles.dialogText}>
Enter the name of the team you wish to create. Team names can contain
the characters a-z A-Z 0-9 _ and must not start or end with an _
</div>
<TextInput
{...teamName.field}
ref={teamName.field.ref}
placeholder={"ExampleTeamName"}
color={
teamName.fieldState.isDirty
? teamName.fieldState.invalid
? "red"
: "green"
: undefined
}
disabled={isSubmitting}
/>
<div>
<TextInput
{...name.field}
ref={name.field.ref}
placeholder={"ExampleName"}
color={
name.fieldState.isDirty
? name.fieldState.invalid
? "red"
: "green"
: undefined
}
disabled={isSubmitting}
/>
<span className={styles.errorMessage}>{errors.name?.message}</span>
</div>
</div>
<div className={styles.footer}>
<Button.Root
Expand All @@ -116,7 +126,7 @@ export function CreateTeamForm() {
)}
</Button.Root>
</div>
</Form>
</form>
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,12 @@ export function Profile(props: ProfileProps) {
<SettingItem
title="Profile Summary"
description="A short introduction of a max of 160 characters"
content={<TextInput placeholder={userData.description} />}
content={<TextInput placeholder="This is an description" />}
/>
<SettingItem
title="Abut Me"
description="A more comprehensive introduction of yourself shown in the About-tab on your user page"
content={<TextInput placeholder={userData.about} />}
content={<TextInput placeholder="It's about me" />}
/>
<div className={styles.save}>
<Button.Root>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ export function TeamDetails(props: Props) {
<SettingItem
title="Profile Summary"
description="A short description shown in header and profile cards"
content={<TextInput placeholder={teamData.description} />}
content={<TextInput placeholder="This is an description" />}
/>
<SettingItem
title="Abut Us"
Expand Down
7 changes: 3 additions & 4 deletions packages/cyberstorm/src/components/TextInput/TextInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,10 @@ import { classnames } from "../../utils/utils";

export interface TextInputProps
extends React.ComponentPropsWithoutRef<"input"> {
value?: string;
leftIcon?: JSX.Element;
rightIcon?: JSX.Element;
color?: string;
enterHook?: (value: string) => string | void;
enterHook?: (value: string | number | readonly string[]) => string | void;
}

/**
Expand All @@ -28,7 +27,7 @@ export const TextInput = React.forwardRef<HTMLInputElement, TextInputProps>(
} = props;
const onEnter = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (enterHook && e.key === "Enter") {
enterHook(value.toLowerCase());
enterHook(value);
}
};

Expand All @@ -47,7 +46,7 @@ export const TextInput = React.forwardRef<HTMLInputElement, TextInputProps>(
leftIcon ? styles.hasLeftIcon : null
)}
value={value}
onKeyDown={(e) => onEnter(e)}
onKeyDown={onEnter}
data-color={color}
/>
{rightIcon ? (
Expand Down
2 changes: 1 addition & 1 deletion packages/cyberstorm/src/components/Toast/Provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ interface ContextInterface {
remove: (id: string) => void;
}

export const ToastContext = createContext<ContextInterface | null>(null);
const ToastContext = createContext<ContextInterface | null>(null);

export function Provider(props: { toastDuration: number } & PropsWithChildren) {
const [state, dispatch] = useReducer(toastReducer, initState);
Expand Down
12 changes: 6 additions & 6 deletions packages/cyberstorm/src/utils/type_guards.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ export const isRecord = (obj: unknown): obj is Record<string, unknown> =>
export const isStringArray = (arr: unknown): arr is string[] =>
Array.isArray(arr) && arr.every((s) => typeof s === "string");

interface ErrorResponse {
error: { message: string };
export interface FormErrorResponse {
[key: string]: string[];
}

export function isErrorResponse(response: unknown): response is ErrorResponse {
export function isFormErrorResponse(
response: unknown
): response is FormErrorResponse {
return (
isRecord(response) &&
isRecord(response.error) &&
typeof response.error.message === "string"
isRecord(response) && Object.values(response).every((v) => isStringArray(v))
);
}

0 comments on commit 61c7b08

Please sign in to comment.