Skip to content

Commit 05eac64

Browse files
authored
feat: add a character counter for fields with length limits (coder#11558)
- refactors`getFormHelpers` to accept an options object - adds a `maxLength` option which will display a message and character counter for fields with length limits - set `maxLength` option for template description fields
1 parent f9f94b5 commit 05eac64

File tree

16 files changed

+277
-132
lines changed

16 files changed

+277
-132
lines changed

site/src/pages/CreateTemplatePage/CreateTemplateForm.tsx

+26-22
Original file line numberDiff line numberDiff line change
@@ -337,7 +337,9 @@ export const CreateTemplateForm: FC<CreateTemplateFormProps> = (props) => {
337337
/>
338338

339339
<TextField
340-
{...getFieldHelpers("description")}
340+
{...getFieldHelpers("description", {
341+
maxLength: MAX_DESCRIPTION_CHAR_LIMIT,
342+
})}
341343
disabled={isSubmitting}
342344
rows={5}
343345
multiline
@@ -363,10 +365,11 @@ export const CreateTemplateForm: FC<CreateTemplateFormProps> = (props) => {
363365
<FormFields>
364366
<Stack direction="row" css={styles.ttlFields}>
365367
<TextField
366-
{...getFieldHelpers(
367-
"default_ttl_hours",
368-
<DefaultTTLHelperText ttl={form.values.default_ttl_hours} />,
369-
)}
368+
{...getFieldHelpers("default_ttl_hours", {
369+
helperText: (
370+
<DefaultTTLHelperText ttl={form.values.default_ttl_hours} />
371+
),
372+
})}
370373
disabled={isSubmitting}
371374
onChange={onChangeTrimmed(form)}
372375
fullWidth
@@ -377,12 +380,13 @@ export const CreateTemplateForm: FC<CreateTemplateFormProps> = (props) => {
377380

378381
<Stack direction="row" css={styles.ttlFields}>
379382
<TextField
380-
{...getFieldHelpers(
381-
"autostop_requirement_days_of_week",
382-
<AutostopRequirementDaysHelperText
383-
days={form.values.autostop_requirement_days_of_week}
384-
/>,
385-
)}
383+
{...getFieldHelpers("autostop_requirement_days_of_week", {
384+
helperText: (
385+
<AutostopRequirementDaysHelperText
386+
days={form.values.autostop_requirement_days_of_week}
387+
/>
388+
),
389+
})}
386390
disabled={
387391
isSubmitting ||
388392
form.values.use_max_ttl ||
@@ -408,13 +412,14 @@ export const CreateTemplateForm: FC<CreateTemplateFormProps> = (props) => {
408412
</TextField>
409413

410414
<TextField
411-
{...getFieldHelpers(
412-
"autostop_requirement_weeks",
413-
<AutostopRequirementWeeksHelperText
414-
days={form.values.autostop_requirement_days_of_week}
415-
weeks={form.values.autostop_requirement_weeks}
416-
/>,
417-
)}
415+
{...getFieldHelpers("autostop_requirement_weeks", {
416+
helperText: (
417+
<AutostopRequirementWeeksHelperText
418+
days={form.values.autostop_requirement_days_of_week}
419+
weeks={form.values.autostop_requirement_weeks}
420+
/>
421+
),
422+
})}
418423
disabled={
419424
isSubmitting ||
420425
form.values.use_max_ttl ||
@@ -453,17 +458,16 @@ export const CreateTemplateForm: FC<CreateTemplateFormProps> = (props) => {
453458
</Stack>
454459

455460
<TextField
456-
{...getFieldHelpers(
457-
"max_ttl_hours",
458-
allowAdvancedScheduling ? (
461+
{...getFieldHelpers("max_ttl_hours", {
462+
helperText: allowAdvancedScheduling ? (
459463
<MaxTTLHelperText ttl={form.values.max_ttl_hours} />
460464
) : (
461465
<>
462466
You need an enterprise license to use it.{" "}
463467
<Link href={docs("/enterprise")}>Learn more</Link>.
464468
</>
465469
),
466-
)}
470+
})}
467471
disabled={
468472
isSubmitting ||
469473
!form.values.use_max_ttl ||

site/src/pages/CreateUserPage/CreateUserForm.tsx

+8-10
Original file line numberDiff line numberDiff line change
@@ -132,10 +132,9 @@ export const CreateUserForm: FC<
132132
label={Language.emailLabel}
133133
/>
134134
<TextField
135-
{...getFieldHelpers(
136-
"login_type",
137-
"Authentication method for this user",
138-
)}
135+
{...getFieldHelpers("login_type", {
136+
helperText: "Authentication method for this user",
137+
})}
139138
select
140139
id="login_type"
141140
data-testid="login-type-input"
@@ -180,12 +179,11 @@ export const CreateUserForm: FC<
180179
})}
181180
</TextField>
182181
<TextField
183-
{...getFieldHelpers(
184-
"password",
185-
form.values.login_type === "password"
186-
? ""
187-
: "No password required for this login type",
188-
)}
182+
{...getFieldHelpers("password", {
183+
helperText:
184+
form.values.login_type !== "password" &&
185+
"No password required for this login type",
186+
})}
189187
autoComplete="current-password"
190188
fullWidth
191189
id="password"

site/src/pages/DeploySettingsPage/AppearanceSettingsPage/AppearanceSettingsPageView.tsx

+4-4
Original file line numberDiff line numberDiff line change
@@ -214,10 +214,10 @@ export const AppearanceSettingsPageView: FC<
214214
/>
215215
<Stack spacing={0}>
216216
<TextField
217-
{...serviceBannerFieldHelpers(
218-
"message",
219-
"Markdown bold, italics, and links are supported.",
220-
)}
217+
{...serviceBannerFieldHelpers("message", {
218+
helperText:
219+
"Markdown bold, italics, and links are supported.",
220+
})}
221221
fullWidth
222222
label="Message"
223223
multiline

site/src/pages/GroupsPage/CreateGroupPageView.tsx

+3-4
Original file line numberDiff line numberDiff line change
@@ -52,10 +52,9 @@ export const CreateGroupPageView: FC<CreateGroupPageViewProps> = ({
5252
label="Name"
5353
/>
5454
<TextField
55-
{...getFieldHelpers(
56-
"display_name",
57-
"Optional: keep empty to default to the name.",
58-
)}
55+
{...getFieldHelpers("display_name", {
56+
helperText: "Optional: keep empty to default to the name.",
57+
})}
5958
fullWidth
6059
label="Display Name"
6160
/>

site/src/pages/GroupsPage/SettingsGroupPageView.tsx

+6-8
Original file line numberDiff line numberDiff line change
@@ -73,10 +73,9 @@ const UpdateGroupForm: FC<UpdateGroupFormProps> = ({
7373
) : (
7474
<>
7575
<TextField
76-
{...getFieldHelpers(
77-
"display_name",
78-
"Optional: keep empty to default to the name.",
79-
)}
76+
{...getFieldHelpers("display_name", {
77+
helperText: "Optional: keep empty to default to the name.",
78+
})}
8079
onChange={onChangeTrimmed(form)}
8180
autoComplete="display_name"
8281
autoFocus
@@ -94,11 +93,10 @@ const UpdateGroupForm: FC<UpdateGroupFormProps> = ({
9493
</>
9594
)}
9695
<TextField
97-
{...getFieldHelpers(
98-
"quota_allowance",
99-
`This group gives ${form.values.quota_allowance} quota credits to each
96+
{...getFieldHelpers("quota_allowance", {
97+
helperText: `This group gives ${form.values.quota_allowance} quota credits to each
10098
of its members.`,
101-
)}
99+
})}
102100
onChange={onChangeTrimmed(form)}
103101
autoFocus
104102
fullWidth

site/src/pages/TemplateSettingsPage/Sidebar.tsx

+2-5
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,12 @@ import GeneralIcon from "@mui/icons-material/SettingsOutlined";
44
import SecurityIcon from "@mui/icons-material/LockOutlined";
55
import { type FC } from "react";
66
import type { Template } from "api/typesGenerated";
7-
import { Avatar } from "components/Avatar/Avatar";
7+
import { ExternalAvatar } from "components/Avatar/Avatar";
88
import {
99
Sidebar as BaseSidebar,
1010
SidebarHeader,
1111
SidebarNavItem,
1212
} from "components/Sidebar/Sidebar";
13-
import { ExternalImage } from "components/ExternalImage/ExternalImage";
1413

1514
interface SidebarProps {
1615
template: Template;
@@ -21,9 +20,7 @@ export const Sidebar: FC<SidebarProps> = ({ template }) => {
2120
<BaseSidebar>
2221
<SidebarHeader
2322
avatar={
24-
<Avatar variant="square" fitImage>
25-
<ExternalImage src={template.icon} css={{ width: "100%" }} />
26-
</Avatar>
23+
<ExternalAvatar src={template.icon} variant="square" fitImage />
2724
}
2825
title={template.display_name || template.name}
2926
linkTo={`/templates/${template.name}`}

site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsForm.tsx

+6-2
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,16 @@ import {
2929
import { EnterpriseBadge } from "components/Badges/Badges";
3030

3131
const MAX_DESCRIPTION_CHAR_LIMIT = 128;
32+
const MAX_DESCRIPTION_MESSAGE =
33+
"Please enter a description that is no longer than 128 characters.";
3234

3335
export const getValidationSchema = (): Yup.AnyObjectSchema =>
3436
Yup.object({
3537
name: nameValidator("Name"),
3638
display_name: templateDisplayNameValidator("Display name"),
3739
description: Yup.string().max(
3840
MAX_DESCRIPTION_CHAR_LIMIT,
39-
"Please enter a description that is less than or equal to 128 characters.",
41+
MAX_DESCRIPTION_MESSAGE,
4042
),
4143
allow_user_cancel_workspace_jobs: Yup.boolean(),
4244
icon: iconValidator,
@@ -119,7 +121,9 @@ export const TemplateSettingsForm: FC<TemplateSettingsForm> = ({
119121
/>
120122

121123
<TextField
122-
{...getFieldHelpers("description")}
124+
{...getFieldHelpers("description", {
125+
maxLength: MAX_DESCRIPTION_CHAR_LIMIT,
126+
})}
123127
multiline
124128
disabled={isSubmitting}
125129
fullWidth

site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.test.tsx

+2-4
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ const fillAndSubmitForm = async ({
8282
await userEvent.type(iconField, icon);
8383

8484
const allowCancelJobsField = screen.getByRole("checkbox", {
85-
name: "Allow users to cancel in-progress workspace jobs. Depending on your template, canceling builds may leave workspaces in an unhealthy state. This option isn't recommended for most use cases.",
85+
name: /allow users to cancel in-progress workspace jobs/i,
8686
});
8787
// checkbox is checked by default, so it must be clicked to get unchecked
8888
if (!allow_user_cancel_workspace_jobs) {
@@ -123,8 +123,6 @@ describe("TemplateSettingsPage", () => {
123123
"Nam quis nulla. Integer malesuada. In in enim a arcu imperdiet malesuada. Sed vel lectus. Donec odio urna, tempus molestie, port a",
124124
};
125125
const validate = () => getValidationSchema().validateSync(values);
126-
expect(validate).toThrowError(
127-
"Please enter a description that is less than or equal to 128 characters.",
128-
);
126+
expect(validate).toThrowError();
129127
});
130128
});

site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPageView.stories.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1+
import type { Meta, StoryObj } from "@storybook/react";
12
import { mockApiError, MockTemplate } from "testHelpers/entities";
23
import { TemplateSettingsPageView } from "./TemplateSettingsPageView";
3-
import type { Meta, StoryObj } from "@storybook/react";
44

55
const meta: Meta<typeof TemplateSettingsPageView> = {
66
title: "pages/TemplateSettingsPage",

site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm.tsx

+42-37
Original file line numberDiff line numberDiff line change
@@ -354,10 +354,11 @@ export const TemplateScheduleForm: FC<TemplateScheduleForm> = ({
354354
>
355355
<Stack direction="row" css={styles.ttlFields}>
356356
<TextField
357-
{...getFieldHelpers(
358-
"default_ttl_ms",
359-
<DefaultTTLHelperText ttl={form.values.default_ttl_ms} />,
360-
)}
357+
{...getFieldHelpers("default_ttl_ms", {
358+
helperText: (
359+
<DefaultTTLHelperText ttl={form.values.default_ttl_ms} />
360+
),
361+
})}
361362
disabled={isSubmitting}
362363
fullWidth
363364
inputProps={{ min: 0, step: 1 }}
@@ -373,12 +374,13 @@ export const TemplateScheduleForm: FC<TemplateScheduleForm> = ({
373374
>
374375
<Stack direction="row" css={styles.ttlFields}>
375376
<TextField
376-
{...getFieldHelpers(
377-
"autostop_requirement_days_of_week",
378-
<AutostopRequirementDaysHelperText
379-
days={form.values.autostop_requirement_days_of_week}
380-
/>,
381-
)}
377+
{...getFieldHelpers("autostop_requirement_days_of_week", {
378+
helperText: (
379+
<AutostopRequirementDaysHelperText
380+
days={form.values.autostop_requirement_days_of_week}
381+
/>
382+
),
383+
})}
382384
disabled={isSubmitting || form.values.use_max_ttl}
383385
fullWidth
384386
select
@@ -400,13 +402,14 @@ export const TemplateScheduleForm: FC<TemplateScheduleForm> = ({
400402
</TextField>
401403

402404
<TextField
403-
{...getFieldHelpers(
404-
"autostop_requirement_weeks",
405-
<AutostopRequirementWeeksHelperText
406-
days={form.values.autostop_requirement_days_of_week}
407-
weeks={form.values.autostop_requirement_weeks}
408-
/>,
409-
)}
405+
{...getFieldHelpers("autostop_requirement_weeks", {
406+
helperText: (
407+
<AutostopRequirementWeeksHelperText
408+
days={form.values.autostop_requirement_days_of_week}
409+
weeks={form.values.autostop_requirement_weeks}
410+
/>
411+
),
412+
})}
410413
disabled={
411414
isSubmitting ||
412415
form.values.use_max_ttl ||
@@ -461,17 +464,16 @@ export const TemplateScheduleForm: FC<TemplateScheduleForm> = ({
461464
</Stack>
462465

463466
<TextField
464-
{...getFieldHelpers(
465-
"max_ttl_ms",
466-
allowAdvancedScheduling ? (
467+
{...getFieldHelpers("max_ttl_ms", {
468+
helperText: allowAdvancedScheduling ? (
467469
<MaxTTLHelperText ttl={form.values.max_ttl_ms} />
468470
) : (
469471
<>
470472
You need an enterprise license to use it{" "}
471473
<Link href={docs("/enterprise")}>Learn more</Link>.
472474
</>
473475
),
474-
)}
476+
})}
475477
disabled={
476478
isSubmitting ||
477479
!form.values.use_max_ttl ||
@@ -579,10 +581,11 @@ export const TemplateScheduleForm: FC<TemplateScheduleForm> = ({
579581
label="Enable Failure Cleanup"
580582
/>
581583
<TextField
582-
{...getFieldHelpers(
583-
"failure_ttl_ms",
584-
<FailureTTLHelperText ttl={form.values.failure_ttl_ms} />,
585-
)}
584+
{...getFieldHelpers("failure_ttl_ms", {
585+
helperText: (
586+
<FailureTTLHelperText ttl={form.values.failure_ttl_ms} />
587+
),
588+
})}
586589
disabled={isSubmitting || !form.values.failure_cleanup_enabled}
587590
fullWidth
588591
inputProps={{ min: 0, step: "any" }}
@@ -608,12 +611,13 @@ export const TemplateScheduleForm: FC<TemplateScheduleForm> = ({
608611
label="Enable Dormancy Threshold"
609612
/>
610613
<TextField
611-
{...getFieldHelpers(
612-
"time_til_dormant_ms",
613-
<DormancyTTLHelperText
614-
ttl={form.values.time_til_dormant_ms}
615-
/>,
616-
)}
614+
{...getFieldHelpers("time_til_dormant_ms", {
615+
helperText: (
616+
<DormancyTTLHelperText
617+
ttl={form.values.time_til_dormant_ms}
618+
/>
619+
),
620+
})}
617621
disabled={
618622
isSubmitting || !form.values.inactivity_cleanup_enabled
619623
}
@@ -641,12 +645,13 @@ export const TemplateScheduleForm: FC<TemplateScheduleForm> = ({
641645
label="Enable Dormancy Auto-Deletion"
642646
/>
643647
<TextField
644-
{...getFieldHelpers(
645-
"time_til_dormant_autodelete_ms",
646-
<DormancyAutoDeletionTTLHelperText
647-
ttl={form.values.time_til_dormant_autodelete_ms}
648-
/>,
649-
)}
648+
{...getFieldHelpers("time_til_dormant_autodelete_ms", {
649+
helperText: (
650+
<DormancyAutoDeletionTTLHelperText
651+
ttl={form.values.time_til_dormant_autodelete_ms}
652+
/>
653+
),
654+
})}
650655
disabled={
651656
isSubmitting ||
652657
!form.values.dormant_autodeletion_cleanup_enabled

0 commit comments

Comments
 (0)