refactor(site): demui create template gallery and form (#27788)

Migrate the create template gallery page and create template form off
MUI and Emotion onto Tailwind and shared form/link primitives
(`FormField`, `Textarea`, `Link`).
This commit is contained in:
Jake Howell
2026-08-05 02:11:44 +10:00
committed by GitHub
parent 8a510314df
commit df1700916e
3 changed files with 79 additions and 112 deletions
@@ -1,8 +1,3 @@
import type { Interpolation, Theme } from "@emotion/react";
import Card from "@mui/material/Card";
import CardActionArea from "@mui/material/CardActionArea";
import CardContent from "@mui/material/CardContent";
import Stack from "@mui/material/Stack";
import { ExternalLinkIcon } from "lucide-react";
import type { FC } from "react";
import { Link as RouterLink } from "react-router";
@@ -58,83 +53,42 @@ export const CreateTemplateGalleryPageView: FC<
>
<PageHeaderTitle>Create a Template</PageHeaderTitle>
</PageHeader>
<Stack spacing={8}>
<Stack direction="row" spacing={4}>
<div className="flex flex-col gap-16">
<div className="flex flex-row gap-8">
<div className="w-[202px]">
<h2 css={styles.sectionTitle}>
<h2 className="m-0 text-base font-normal text-content-primary">
Choose a starting point for your new template
</h2>
</div>
<div className="flex flex-wrap gap-8 h-max">
<Card variant="outlined" className="w-[320px] rounded-[6px]">
<CardActionArea
component={RouterLink}
to="/templates/new"
sx={{ height: 115, padding: 1 }}
>
<CardContent>
<Stack direction="row" spacing={3} className="items-center">
<div css={styles.icon}>
<ExternalImage
src="/emojis/1f4e1.png"
className="w-full h-full"
/>
</div>
<div>
<h4 css={styles.cardTitle}>Upload Template</h4>
<span css={styles.cardDescription}>
Get started by uploading an existing template
</span>
</div>
</Stack>
</CardContent>
</CardActionArea>
</Card>
<div className="flex h-max flex-wrap gap-8">
<RouterLink
to="/templates/new"
className="flex h-[115px] w-[320px] items-center gap-6 rounded-md border border-solid border-border p-4 text-inherit no-underline hover:bg-surface-secondary"
>
<div className="size-8 shrink-0">
<ExternalImage
src="/emojis/1f4e1.png"
className="h-full w-full"
/>
</div>
<div>
<h4 className="m-0 mb-1 text-sm font-semibold text-content-secondary">
Upload Template
</h4>
<span className="block text-[13px] leading-[1.6] text-content-secondary">
Get started by uploading an existing template
</span>
</div>
</RouterLink>
</div>
</Stack>
</div>
{Boolean(error) && <ErrorAlert error={error} />}
{Boolean(!starterTemplatesByTag) && <Loader />}
<StarterTemplates starterTemplatesByTag={starterTemplatesByTag} />
</Stack>
</div>
</Margins>
);
};
const styles = {
sectionTitle: (theme) => ({
color: theme.palette.text.primary,
fontSize: 16,
fontWeight: 400,
margin: 0,
}),
cardTitle: (theme) => ({
color: theme.palette.text.secondary,
fontSize: 14,
fontWeight: 600,
margin: 0,
marginBottom: 4,
}),
cardDescription: (theme) => ({
fontSize: 13,
color: theme.palette.text.secondary,
lineHeight: "1.6",
display: "block",
}),
icon: {
flexShrink: 0,
width: 32,
height: 32,
},
menuItemIcon: (theme) => ({
color: theme.palette.text.secondary,
width: 20,
height: 20,
}),
} satisfies Record<string, Interpolation<Theme>>;
@@ -63,9 +63,9 @@ export const StarterTemplates: FC<StarterTemplatesProps> = ({
: undefined;
return (
<div className="flex flex-row gap-8 items-start">
<div className="flex flex-row items-start gap-8">
{starterTemplatesByTag && tags && (
<div className="flex flex-col gap-4 w-[202px] shrink-0 sticky top-[88px]">
<div className="sticky top-[88px] flex w-[202px] shrink-0 flex-col gap-4">
<h2 className="m-0 text-base font-normal text-content-primary">
Choose a starter template
</h2>
@@ -89,15 +89,13 @@ export const StarterTemplates: FC<StarterTemplatesProps> = ({
</div>
)}
<div className="flex flex-wrap gap-8 h-max">
<div className="flex h-max flex-wrap gap-8">
{visibleTemplates?.map((example) => (
<TemplateExampleCard
css={(theme) => ({
backgroundColor: theme.palette.background.paper,
})}
example={example}
key={example.id}
activeTag={activeTag}
className="bg-surface-primary"
/>
))}
</div>
@@ -1,5 +1,3 @@
import Link from "@mui/material/Link";
import TextField from "@mui/material/TextField";
import { useFormik } from "formik";
import camelCase from "lodash/camelCase";
import capitalize from "lodash/capitalize";
@@ -29,12 +27,16 @@ import {
FormSection,
HorizontalForm,
} from "#/components/Form/Form";
import { FormField } from "#/components/FormField/FormField";
import { IconField } from "#/components/IconField/IconField";
import { Label } from "#/components/Label/Label";
import { Link } from "#/components/Link/Link";
import { OrganizationAutocomplete } from "#/components/OrganizationAutocomplete/OrganizationAutocomplete";
import { Spinner } from "#/components/Spinner/Spinner";
import { Textarea } from "#/components/Textarea/Textarea";
import { ProvisionerTagsField } from "#/modules/provisioners/ProvisionerTagsField";
import { SelectedTemplate } from "#/pages/CreateWorkspacePage/SelectedTemplate";
import { cn } from "#/utils/cn";
import { docs } from "#/utils/docs";
import {
displayNameValidator,
@@ -228,6 +230,10 @@ export const CreateTemplateForm: FC<CreateTemplateFormProps> = (props) => {
onSubmit,
});
const getFieldHelpers = getFormHelpers<CreateTemplateFormData>(form, error);
const descriptionField = getFieldHelpers("description", {
maxLength: MAX_DESCRIPTION_CHAR_LIMIT,
});
const descriptionHelperId = `${descriptionField.id}-helper`;
const permittedOrgsQuery = useQuery({
...permittedOrganizations({
@@ -318,13 +324,13 @@ export const CreateTemplateForm: FC<CreateTemplateFormProps> = (props) => {
<SelectedTemplate template={props.copiedTemplate} />
)}
<TextField
{...getFieldHelpers("name")}
<FormField
field={getFieldHelpers("name")}
label="Name"
disabled={isSubmitting}
onChange={onChangeTrimmed(form)}
fullWidth
required
label="Name"
className="w-full"
/>
</FormFields>
</FormSection>
@@ -335,23 +341,45 @@ export const CreateTemplateForm: FC<CreateTemplateFormProps> = (props) => {
description="A friendly name, description, and icon to help developers identify your template."
>
<FormFields>
<TextField
{...getFieldHelpers("display_name")}
disabled={isSubmitting}
fullWidth
<FormField
field={getFieldHelpers("display_name")}
label="Display name"
disabled={isSubmitting}
className="w-full"
/>
<TextField
{...getFieldHelpers("description", {
maxLength: MAX_DESCRIPTION_CHAR_LIMIT,
})}
disabled={isSubmitting}
rows={5}
multiline
fullWidth
label="Description"
/>
<div className="flex flex-col gap-2">
<Label htmlFor={descriptionField.id}>Description</Label>
<Textarea
id={descriptionField.id}
name={descriptionField.name}
value={descriptionField.value ?? ""}
onChange={descriptionField.onChange}
onBlur={descriptionField.onBlur}
disabled={isSubmitting}
rows={5}
aria-invalid={descriptionField.error}
aria-describedby={
descriptionField.helperText ? descriptionHelperId : undefined
}
className={cn(
descriptionField.error && "border-border-destructive",
)}
/>
{descriptionField.helperText && (
<span
id={descriptionHelperId}
className={cn(
"text-xs",
descriptionField.error
? "text-content-destructive"
: "text-content-secondary",
)}
>
{descriptionField.helperText}
</span>
)}
</div>
<IconField
{...getFieldHelpers("icon")}
@@ -369,7 +397,7 @@ export const CreateTemplateForm: FC<CreateTemplateFormProps> = (props) => {
description={
<>
Tags are a way to control which provisioner daemons complete which
build jobs.&nbsp;
build jobs.{" "}
<Link
href={docs("/admin/provisioners")}
target="_blank"
@@ -427,20 +455,7 @@ export const CreateTemplateForm: FC<CreateTemplateFormProps> = (props) => {
<button
type="button"
onClick={onOpenBuildLogsDrawer}
css={(theme) => ({
backgroundColor: "transparent",
border: 0,
fontWeight: 500,
fontSize: 14,
cursor: "pointer",
color: theme.palette.text.secondary,
"&:hover": {
textDecoration: "underline",
textUnderlineOffset: 4,
color: theme.palette.text.primary,
},
})}
className="cursor-pointer border-0 bg-transparent text-sm font-medium text-content-secondary hover:text-content-primary hover:underline hover:underline-offset-4"
>
Show build logs
</button>