diff --git a/app/components/form/fields/ComboboxField.tsx b/app/components/form/fields/ComboboxField.tsx index 6190133be..efa055829 100644 --- a/app/components/form/fields/ComboboxField.tsx +++ b/app/components/form/fields/ComboboxField.tsx @@ -21,7 +21,7 @@ import { getSelectedLabelFromValue, type ComboboxBaseProps, } from '~/ui/lib/Combobox' -import { capitalize, normalizeName } from '~/util/str' +import { capitalize } from '~/util/str' import { ErrorMessage } from './ErrorMessage' @@ -60,7 +60,7 @@ export function ComboboxField< ? 'Select an option or enter a custom value' : 'Select an option', items, - transform = (value) => normalizeName(value, true), + transform, validate, ...props }: ComboboxFieldProps) { diff --git a/app/components/form/fields/NameField.tsx b/app/components/form/fields/NameField.tsx index 75a2de693..2457c2282 100644 --- a/app/components/form/fields/NameField.tsx +++ b/app/components/form/fields/NameField.tsx @@ -7,7 +7,7 @@ */ import type { FieldPath, FieldValues } from 'react-hook-form' -import { capitalize, normalizeName } from '~/util/str' +import { capitalize } from '~/util/str' import { TextField, type TextFieldProps } from './TextField' @@ -30,7 +30,6 @@ export function NameField< required={required} label={label} name={name} - transform={(value) => normalizeName(value)} // https://www.stefanjudis.com/snippets/turn-off-password-managers/ data-1p-ignore data-bwignore diff --git a/app/ui/lib/Combobox.tsx b/app/ui/lib/Combobox.tsx index 25868875a..436992169 100644 --- a/app/ui/lib/Combobox.tsx +++ b/app/ui/lib/Combobox.tsx @@ -66,7 +66,6 @@ export type ComboboxBaseProps = { onEnter?: (event: React.KeyboardEvent) => void /** * Optional function to transform the value entered into the input as the user types. - * Defaults in ComboboxField to running the `normalizeName` function on the input value. */ transform?: (value: string) => string } diff --git a/app/util/str.spec.tsx b/app/util/str.spec.tsx index 52ee240e2..272fabda6 100644 --- a/app/util/str.spec.tsx +++ b/app/util/str.spec.tsx @@ -14,7 +14,6 @@ import { commaSeries, extractText, kebabCase, - normalizeName, pluralize, titleCase, } from './str' @@ -123,43 +122,6 @@ describe('extractText', () => { }) }) -describe('normalizeName', () => { - it('converts to lowercase', () => { - expect(normalizeName('Hello')).toBe('hello') - }) - - it('replaces spaces with dashes', () => { - expect(normalizeName('Hello World')).toBe('hello-world') - }) - - it('removes non-alphanumeric characters', () => { - expect(normalizeName('Hello, World!')).toBe('hello-world') - }) - - it('caps at 63 characters', () => { - expect(normalizeName('aaa')).toBe('aaa') - expect(normalizeName('aaaaaaaaa')).toBe('aaaaaaaaa') - expect(normalizeName('a'.repeat(63))).toBe('a'.repeat(63)) - expect(normalizeName('a'.repeat(64))).toBe('a'.repeat(63)) - }) - - it('can optionally start with numbers', () => { - expect(normalizeName('123abc')).toBe('abc') - expect(normalizeName('123abc', false)).toBe('abc') - expect(normalizeName('123abc', true)).toBe('123abc') - }) - - it('can optionally start with a dash', () => { - expect(normalizeName('-abc')).toBe('abc') - expect(normalizeName('-abc', false)).toBe('abc') - expect(normalizeName('-abc', true)).toBe('-abc') - }) - - it('does not complain when multiple dashes are present', () => { - expect(normalizeName('a--b')).toBe('a--b') - }) -}) - test('addDashes', () => { expect(addDashes([], 'abcdefgh')).toEqual('abcdefgh') expect(addDashes([3], 'abcdefgh')).toEqual('abcd-efgh') diff --git a/app/util/str.ts b/app/util/str.ts index 5e6b4ac1a..a6c323a42 100644 --- a/app/util/str.ts +++ b/app/util/str.ts @@ -58,26 +58,6 @@ export const titleCase = (text: string): string => { */ export const isAllZeros = (base64Data: string) => /^A*=*$/.test(base64Data) -/** Clean up text so that it conforms to Name field syntax rules: - * - lowercase only - * - no spaces - * - only letters/numbers/dashes allowed - * - capped at 63 characters - * By default, it must start with a letter; this can be overriden with the second argument, - * for contexts where we want to allow numbers at the start, like searching in comboboxes. - */ -export const normalizeName = (text: string, allowNonLetterStart = false): string => { - const normalizedName = text - .toLowerCase() - .replace(/[\s_]+/g, '-') // Replace spaces and underscores with dashes - .replace(/[^a-z0-9-]/g, '') // Remove non-alphanumeric (or dash) characters - .slice(0, 63) // Limit string to 63 characters - if (allowNonLetterStart) { - return normalizedName - } - return normalizedName.replace(/^[^a-z]+/, '') // Remove any non-letter characters from the start -} - /** * Extract the string contents of a ReactNode, so <>This highlighted text becomes "This highlighted text" */ diff --git a/test/e2e/firewall-rules.e2e.ts b/test/e2e/firewall-rules.e2e.ts index 55c40870d..547f8ed77 100644 --- a/test/e2e/firewall-rules.e2e.ts +++ b/test/e2e/firewall-rules.e2e.ts @@ -199,8 +199,7 @@ test('firewall rule form targets table', async ({ page }) => { // now add a subnet by entering text await selectOption(page, 'Target type', 'VPC subnet') // test that the name typed in is normalized - await subnetNameField.fill('ABC 123') - await expect(subnetNameField).toHaveValue('abc-123') + await subnetNameField.fill('abc-123') // hit enter to submit the subform await subnetNameField.press('Enter') await subnetNameField.press('Enter') diff --git a/test/e2e/project-create.e2e.ts b/test/e2e/project-create.e2e.ts index 944c57a60..4e356439b 100644 --- a/test/e2e/project-create.e2e.ts +++ b/test/e2e/project-create.e2e.ts @@ -30,16 +30,17 @@ test.describe('Project create', () => { }) test('shows field-level validation error and does not POST', async ({ page }) => { - const input = page.getByRole('textbox', { name: 'Name' }) - await input.pressSequentially('no sPoNgEbOb_CaSe or spaces') - await expect(input).toHaveValue('no-spongebob-case-or-spaces') - await input.fill('no-ending-dash-') - // submit to trigger validation - await page.getByRole('button', { name: 'Create project' }).click() - - await expect( - page.getByText('Must end with a letter or number', { exact: true }).nth(0) - ).toBeVisible() + const expectInputError = async (text: string, error: string) => { + await page.getByRole('textbox', { name: 'Name' }).fill(text) + await page.getByRole('button', { name: 'Create project' }).click() + await expect(page.getByText(error).first()).toBeVisible() + } + await expectInputError('', 'Name is required') + await expectInputError('no spaces', 'Can only contain lower-case') + await expectInputError('no-UPPERCASE', 'Can only contain lower-case') + await expectInputError('no-ending-dash-', 'Must end with a letter or number') + await expectInputError('123-oops', 'Must start with a lower-case letter') + await expectInputError('HULK-SMASH', 'Can only contain lower-case') }) test('shows form-level error for known server error', async ({ page }) => {