feat: form updates

This commit is contained in:
Derock 2023-11-28 21:51:59 -05:00
parent 0cc8515bbd
commit b326241ced
No known key found for this signature in database
5 changed files with 137 additions and 17 deletions

View file

@ -14,10 +14,11 @@ import { Input } from "~/components/ui/input";
import { Label } from "~/components/ui/label";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/components/ui/tabs";
import { useForm } from "@mantine/form";
import { Textarea } from "~/components/ui/textarea";
import React, { Suspense } from "react";
import { useQuery } from "@tanstack/react-query";
import { toast } from "sonner";
import { api } from "~/trpc/react";
import { FormInputGroup } from "~/components/FormInput";
// kinda large dependency, but syntax highlighting is nice
const Editor = React.lazy(() => import("./YAMLEditor"));
@ -29,9 +30,12 @@ enum ProjectType {
}
export function CreateProjectButton() {
const create = api.projects.create.useMutation();
const form = useForm({
initialValues: {
name: "",
internalName: "",
type: ProjectType.Template,
composeURL: "",
composeFile:
@ -44,6 +48,18 @@ export function CreateProjectButton() {
return "A Docker Compose file is required";
}
},
name: (value) => !value && "A project name is required",
internalName: (value) => {
if (!value) {
return "An internal project name is required";
}
if (!/^[a-z0-9\-]+$/g.test(value)) {
return "Internal project names can only contain letters, numbers, and dashes";
}
},
},
});
@ -56,20 +72,30 @@ export function CreateProjectButton() {
{
enabled: false,
cacheTime: 0,
retry: false,
onSuccess(data) {
form.setFieldValue("composeFile", data);
toast.success("Fetched Docker Compose file");
},
onError(error) {
toast.error("Failed to fetch Docker Compose file");
console.error(error);
},
},
);
// reset fetchComposeFile when the URL changes
React.useEffect(() => {
fetchComposeFile.remove();
}, [form.values.composeURL]);
return (
<Dialog>
<DialogTrigger asChild>
<Button variant="outline">Create Project</Button>
</DialogTrigger>
<DialogContent className="m-4 max-w-4xl overflow-scroll [&>*]:max-w-full">
<DialogContent className="overflow-auto sm:max-w-4xl [&>*]:max-w-full">
<DialogHeader>
<DialogTitle>Create a Project</DialogTitle>
<DialogDescription>
@ -80,24 +106,62 @@ export function CreateProjectButton() {
</DialogHeader>
<form
onSubmit={form.onSubmit(() => {})}
onSubmit={form.onSubmit((data) => {
create.mutate({
friendlyName: data.name,
internalName: data.internalName,
});
})}
className="max-w-full space-y-4"
id="create-project-form"
>
<FormInputGroup
label="Project Name"
description="The name of your project"
required
form={form}
formKey="name"
>
<div className="space-y-2">
<Label htmlFor="project-name">Project Name</Label>
<Input
id="project-name"
placeholder="My Project"
className="w-full"
{...form.getInputProps("name")}
onChange={(e) => {
form.setFieldValue("name", e.target.value);
if (!form.isDirty("internalName")) {
form.setFieldValue(
"internalName",
e.target.value.replace(/[^a-zA-Z\d:]/g, "-").toLowerCase(),
);
form.setDirty({ internalName: false });
}
}}
/>
</div>
</FormInputGroup>
<FormInputGroup
label="Internal Project Name"
description="The name of your project, used internally for networking and DNS"
required
form={form}
formKey="internalName"
>
<Input
id="project-internal-name"
placeholder="my-project"
className="w-full"
{...form.getInputProps("internalName")}
/>
</FormInputGroup>
<Tabs defaultValue="templates" className="w-full max-w-full">
<TabsList className="mx-auto">
<TabsTrigger value="templates">From a Template</TabsTrigger>
<TabsTrigger value="compose">
From a Docker Compose file
</TabsTrigger>
<TabsTrigger value="blank">Create Blank Project</TabsTrigger>
<TabsList className="mx-auto flex w-fit flex-wrap items-center text-center">
<TabsTrigger value="templates">Template</TabsTrigger>
<TabsTrigger value="compose">Docker Compose</TabsTrigger>
<TabsTrigger value="blank">Blank Project</TabsTrigger>
</TabsList>
<TabsContent value="templates">
@ -109,18 +173,27 @@ export function CreateProjectButton() {
<TabsContent value="compose" className="space-y-2">
<Label htmlFor="compose-url">Docker Compose URL</Label>
<div className="flex flex-row gap-2">
{/* Maybe we should add the ability to fetch URLs protected by CORS by routing through the server
Currently sites like pastebin.com will not work due to strict CORS policies
If we do this, make sure the user cannot access private URLs and proper input validation is done */}
<Input
id="compose-url"
placeholder="https://example.com/docker-compose.yml"
className="flex-grow"
type="url"
{...form.getInputProps("composeURL")}
/>
<Button
variant="outline"
type="button"
variant={fetchComposeFile.isSuccess ? "success" : "outline"}
onClick={() => fetchComposeFile.refetch()}
isLoading={fetchComposeFile.isFetching}
disabled={
fetchComposeFile.isFetching || !form.values.composeURL
}
>
Fetch
{fetchComposeFile.isSuccess ? "Fetched!" : "Fetch"}
</Button>
</div>
@ -140,7 +213,13 @@ export function CreateProjectButton() {
</form>
<DialogFooter>
<Button type="submit">Create Project</Button>
<Button
type="submit"
form="create-project-form"
isLoading={create.isLoading}
>
Create Project
</Button>
</DialogFooter>
</DialogContent>
</Dialog>

View file

@ -1,6 +1,6 @@
"use client";
import CodeMirror from "@uiw/react-codemirror";
import CodeMirror, { EditorView } from "@uiw/react-codemirror";
// import { yaml } from "@nicktomlin/codemirror-lang-yaml-lite";
import { langs } from "@uiw/codemirror-extensions-langs";
import { useTheme } from "next-themes";
@ -20,7 +20,7 @@ export default function YAMLEditor({
value={value}
onChange={onChange}
theme={theme.theme === "dark" ? "dark" : "light"}
extensions={[langs.yaml()]}
extensions={[langs.yaml(), EditorView.lineWrapping]}
data-enable-grammarly="false"
spellCheck={false}
width="100%"

View file

View file

@ -0,0 +1,40 @@
import { GetInputProps, UseFormReturnType } from "@mantine/form/lib/types";
import { Label } from "./ui/label";
import { Required } from "./ui/required";
export function FormInputGroup<Values>({
children,
label,
description,
required,
form,
formKey,
}: {
children: React.ReactNode;
label: string;
description?: string;
required?: boolean;
formKey: keyof Values;
form: UseFormReturnType<Values, any>;
}) {
const inputProps = form.getInputProps(formKey);
return (
<div className="flex flex-col space-y-1">
<Label>
{label}
{required && <Required />}
{inputProps.error && (
<span className="text-red-500"> - {inputProps.error}</span>
)}
</Label>
{/* screen readers should probably read the description first */}
{description && <div className="sr-only">{description}</div>}
{children}
{description && (
<div className="text-sm text-gray-500">{description}</div>
)}
</div>
);
}

View file

@ -19,6 +19,7 @@ const buttonVariants = cva(
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
success: "bg-green-500 text-white shadow-sm hover:bg-green-600",
},
size: {
default: "h-9 px-4 py-2",