Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
4768404
feat(forms): migrate Input to TypeScript
talissoncosta Jun 11, 2026
9506e60
refactor(integrations): render Checkbox directly
talissoncosta Jun 11, 2026
8098a18
fix(forms): update Input call sites for the typed component
talissoncosta Jun 11, 2026
3f8c7e6
refactor: import KeyboardEvent from react in BreadcrumbSeparator
talissoncosta Jun 12, 2026
a6adb9e
feat(forms): add FieldLabel and FieldError primitives
talissoncosta Jun 12, 2026
5bfc3f6
refactor(forms): tokenise input colours in _input.scss
talissoncosta Jun 12, 2026
418973a
refactor(forms): tokenise input colours in _forms.scss
talissoncosta Jun 12, 2026
37c4d6a
refactor(forms): fold info-icon tooltip into FieldLabel, drop LabelWi…
talissoncosta Jun 15, 2026
1993d97
refactor(forms): migrate InputGroup to TypeScript
talissoncosta Jun 15, 2026
92455d8
refactor(forms): compose FieldLabel and FieldError in InputGroup
talissoncosta Jun 15, 2026
b6ae7f9
refactor(forms): drop dead top-level props from InputGroup + consumers
talissoncosta Jun 15, 2026
484f4cf
fix(forms): vertically centre FieldLabel's tooltip icon
talissoncosta Jun 15, 2026
73b3787
docs(forms): add InputGroup Storybook story
talissoncosta Jun 15, 2026
b6e5271
fix(forms): match prod label layout for tooltip icon and unsaved badge
talissoncosta Jun 15, 2026
7581761
docs(forms): use the real forgot-password example in InputGroup story
talissoncosta Jun 15, 2026
1838b31
refactor(forms): move login Forgot-password below the field, drop rig…
talissoncosta Jun 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions frontend/documentation/components/FieldError.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import React from 'react'
import type { Meta, StoryObj } from 'storybook'

import FieldError from 'components/base/forms/FieldError'

const meta: Meta<typeof FieldError> = {
component: FieldError,
parameters: { layout: 'padded' },
title: 'Components/Forms/FieldError',
}
export default meta

type Story = StoryObj<typeof FieldError>

export const Default: Story = {
render: () => <FieldError error='This field is required.' />,
}

export const RichMessage: Story = {
render: () => (
<FieldError
error={
<>
Must be a valid <strong>email address</strong>.
</>
}
/>
),
}
33 changes: 33 additions & 0 deletions frontend/documentation/components/FieldLabel.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import React from 'react'
import type { Meta, StoryObj } from 'storybook'

import FieldLabel from 'components/base/forms/FieldLabel'

const meta: Meta<typeof FieldLabel> = {
component: FieldLabel,
parameters: { layout: 'padded' },
title: 'Components/Forms/FieldLabel',
}
export default meta

type Story = StoryObj<typeof FieldLabel>

export const Default: Story = {
render: () => <FieldLabel htmlFor='email'>Email</FieldLabel>,
}

export const Required: Story = {
render: () => (
<FieldLabel htmlFor='email' required>
Email
</FieldLabel>
),
}

export const WithTooltip: Story = {
render: () => (
<FieldLabel htmlFor='email' tooltip='We never share your email.'>
Email
</FieldLabel>
),
}
103 changes: 103 additions & 0 deletions frontend/documentation/components/InputGroup.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import React, { ComponentProps, useState } from 'react'
import type { Meta, StoryObj } from 'storybook'

import InputGroup from 'components/base/forms/InputGroup'

// Stateful wrapper so the field is controlled in the story (InputGroup forwards
// value/onChange to the underlying control).
type FieldProps = ComponentProps<typeof InputGroup> & { initialValue?: string }

const Field = ({ initialValue, ...props }: FieldProps) => {
const [value, setValue] = useState(initialValue ?? '')
return (
<InputGroup
{...props}
value={value}
onChange={(
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
) => setValue(e.target.value)}
/>
)
}

const meta: Meta = {
decorators: [
(Story: React.FC) => (
<div style={{ width: 320 }}>
<Story />
</div>
),
],
parameters: { layout: 'centered' },
title: 'Components/Forms/InputGroup',
}
export default meta

type Story = StoryObj

export const Default: Story = {
render: () => <Field title='Email' placeholder='you@example.com' />,
}

// Info-icon tooltip after the label — the icon sits centred against the text.
export const WithTooltip: Story = {
render: () => (
<Field
title='Email'
tooltip='We never share your email.'
placeholder='you@example.com'
/>
),
}

export const WithError: Story = {
render: () => (
<Field
title='Email'
initialValue='not-an-email'
isValid={false}
inputProps={{ error: 'Enter a valid email address.', name: 'email' }}
/>
),
}

export const MultipleErrors: Story = {
render: () => (
<Field
title='Password'
type='password'
initialValue='abc'
isValid={false}
inputProps={{
error: ['At least 8 characters', 'Must include a number'],
name: 'password',
}}
/>
),
}

export const Disabled: Story = {
render: () => <Field title='Email' initialValue='you@example.com' disabled />,
}

export const Textarea: Story = {
render: () => (
<Field textarea title='Description' initialValue='A short description…' />
),
}

// "Unsaved" badge beside the label, used when a field has pending changes.
export const Unsaved: Story = {
render: () => <Field title='Display name' unsaved initialValue='Jane' />,
}

export const Sizes: Story = {
render: () => (
<div className='d-flex flex-column gap-3'>
<Field size='large' title='Large' placeholder='Large' />
<Field title='Default' placeholder='Default' />
<Field size='small' title='Small' placeholder='Small' />
<Field size='xSmall' title='xSmall' placeholder='xSmall' />
</div>
),
}
57 changes: 0 additions & 57 deletions frontend/documentation/components/LabelWithTooltip.stories.tsx

This file was deleted.

19 changes: 12 additions & 7 deletions frontend/web/components/BreadcrumbSeparator.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import React, { FC, ReactNode, useEffect, useRef, useState } from 'react'
import React, {
FC,
KeyboardEvent,
ReactNode,
useEffect,
useRef,
useState,
} from 'react'
import { IonIcon } from '@ionic/react'
import { checkmarkCircle, chevronDown, chevronUp } from 'ionicons/icons'
import InlineModal from './InlineModal'
Expand Down Expand Up @@ -334,10 +341,8 @@ const BreadcrumbSeparator: FC<BreadcrumbSeparatorType> = ({
>
<Input
autoFocus={focus === 'organisation'}
onKeyDown={(e: KeyboardEvent) =>
navigateOrganisations(e, organisations)
}
onChange={(e: KeyboardEvent) => {
onKeyDown={(e) => navigateOrganisations(e, organisations)}
onChange={(e) => {
setOrganisationSearch(Utils.safeParseEventValue(e))
}}
search
Expand Down Expand Up @@ -390,11 +395,11 @@ const BreadcrumbSeparator: FC<BreadcrumbSeparatorType> = ({
)}
>
<Input
onChange={(e: InputEvent) => {
onChange={(e) => {
setProjectSearch(Utils.safeParseEventValue(e))
}}
autoFocus={focus === 'project'}
onKeyDown={(e: KeyboardEvent) => navigateProjects(e)}
onKeyDown={(e) => navigateProjects(e)}
search
className='full-width'
inputClassName='border-0 bg-transparent border-bottom-1'
Expand Down
2 changes: 1 addition & 1 deletion frontend/web/components/ChangeRequestsSetting.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ const ChangeRequestsSetting: FC<ChangeRequestsSettingType> = ({
name='env-name'
disabled={isLoading}
min={0}
onChange={(e: InputEvent) => {
onChange={(e) => {
if (!Utils.safeParseEventValue(e)) return
onChange(parseInt(Utils.safeParseEventValue(e)))
}}
Expand Down
2 changes: 1 addition & 1 deletion frontend/web/components/GroupSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ const GroupSelect: FC<GroupSelectType> = ({
<Input
disabled={disabled}
value={filter}
onChange={(e: InputEvent) => setFilter(Utils.safeParseEventValue(e))}
onChange={(e) => setFilter(Utils.safeParseEventValue(e))}
className='full-width mb-2'
placeholder='Type or choose a Group'
search
Expand Down
8 changes: 2 additions & 6 deletions frontend/web/components/PermissionsTabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -110,9 +110,7 @@ const PermissionsTabs: FC<PermissionsTabsType> = ({
type='text'
className='ml-3'
value={searchProject}
onChange={(e: InputEvent) =>
setSearchProject(Utils.safeParseEventValue(e))
}
onChange={(e) => setSearchProject(Utils.safeParseEventValue(e))}
size='small'
placeholder='Search Projects'
search
Expand Down Expand Up @@ -142,9 +140,7 @@ const PermissionsTabs: FC<PermissionsTabsType> = ({
type='text'
className='ml-3'
value={searchEnv}
onChange={(e: InputEvent) =>
setSearchEnv(Utils.safeParseEventValue(e))
}
onChange={(e) => setSearchEnv(Utils.safeParseEventValue(e))}
size='small'
placeholder='Search Environments'
search
Expand Down
2 changes: 0 additions & 2 deletions frontend/web/components/RegexTester.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ const RegexTester: FC<RegexTesterType> = ({
<InputGroup
title='Regular Expression'
value={regex}
autoValidate
inputProps={{
className: 'full-width',
onClick: forceSelectionRange,
Expand All @@ -65,7 +64,6 @@ const RegexTester: FC<RegexTesterType> = ({
id='regex'
title='Test Input'
value={exampleText}
autoValidate
inputProps={{
autoValidate: true,
className: 'full-width',
Expand Down
2 changes: 1 addition & 1 deletion frontend/web/components/RolesSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ const RoleSelect: FC<RoleSelectType> = ({
<Input
disabled={disabled}
value={filter}
onChange={(e: InputEvent) => setFilter(Utils.safeParseEventValue(e))}
onChange={(e) => setFilter(Utils.safeParseEventValue(e))}
className='full-width mb-2'
placeholder='Type or choose a Role'
search
Expand Down
26 changes: 0 additions & 26 deletions frontend/web/components/base/LabelWithTooltip.tsx

This file was deleted.

30 changes: 30 additions & 0 deletions frontend/web/components/base/forms/FieldError.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import React, { FC, ReactNode } from 'react'
import cn from 'classnames'

interface FieldErrorProps {
// The field-level error to show; renders nothing when falsy.
error?: ReactNode
// Set so the control can reference it via aria-describedby.
id?: string
className?: string
}

// Inline, per-field validation message shown beneath a control. The small-text
// counterpart to ErrorMessage (which is a banner alert for API/form-level
// errors) — use FieldError for a single field's validation.
const FieldError: FC<FieldErrorProps> = ({ className, error, id }) => {
if (!error) {
return null
}
return (
<span
id={id}
className={cn('text-danger text-small d-block mt-1', className)}
role='alert'
>
{error}
</span>
)
}

export default FieldError
Loading
Loading