mirror of
https://github.com/coder/coder.git
synced 2026-09-22 13:10:21 +08:00
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:
@@ -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.
|
||||
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>
|
||||
|
||||
Reference in New Issue
Block a user