Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update a porting in the Porting Embed #2

Merged
merged 11 commits into from
Feb 26, 2024
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
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
30 changes: 25 additions & 5 deletions lib/PortingEmbed/PortingEmbed.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Porting } from '../types'
import { Porting, UpdatePortingBody } from '../types'
import { PortingForm } from './PortingForm'

export type CustomizableEmbedProps = {
// TODO: add styling options
Expand All @@ -7,13 +8,32 @@ export type CustomizableEmbedProps = {
}
}

export type ValidationChangeEvent = {
isValid: boolean
}

type CoreEmbedProps = {
token: string
initialPorting: Porting
porting: Porting
onValidationChange?: (event: ValidationChangeEvent) => unknown
onPortingUpdate?: (updatedFields: UpdatePortingBody) => unknown
}

type PortingEmbedProps = CoreEmbedProps & CustomizableEmbedProps

export function PortingEmbed({ token: _, initialPorting }: PortingEmbedProps) {
return <div className="__gigsPortingEmbed">Hello {initialPorting.id}!</div>
export function PortingEmbed({
porting,
onPortingUpdate,
onValidationChange,
}: PortingEmbedProps) {
return (
<div className="__gigsPortingEmbed">
<PortingForm
porting={porting}
onValidationChange={onValidationChange}
onSubmit={async (updatedFields) => {
await onPortingUpdate?.(updatedFields)
}}
/>
</div>
)
}
112 changes: 112 additions & 0 deletions lib/PortingEmbed/PortingForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { required, useForm } from '@modular-forms/preact'
import { useSignalEffect } from '@preact/signals'

import { Porting } from '../types'
import { sanitizeSubmitData } from './sanitizeSubmitData'

type Props = {
porting: Porting
onValidationChange?: (event: { isValid: boolean }) => unknown
onSubmit: (data: Partial<PortingForm>) => unknown
}

type PortingForm = {
accountPin: string
accountNumber: string
birthday: string
firstName: string
lastName: string
}

export function PortingForm({ porting, onValidationChange, onSubmit }: Props) {
const [portingForm, { Form, Field }] = useForm<PortingForm>({
initialValues: {
accountNumber: porting.accountNumber || '',
accountPin: '',
birthday: porting.birthday || '',
firstName: porting.firstName || '',
lastName: porting.lastName || '',
},
validateOn: 'blur',
})

useSignalEffect(() => {
const isValid = !portingForm.invalid.value
onValidationChange?.({ isValid })
})

return (
<Form
id="gigsPortingEmbedForm" // TODO: make customizable
role="form"
shouldDirty // only include changed fields in the onSubmit handler
onSubmit={(data) => {
const sanitizedData = sanitizeSubmitData(data)
return onSubmit(sanitizedData)
}}
>
<Field name="accountNumber" validate={[required('Please enter')]}>
{(field, props) => (
<div>
<label for="accountNumber">Account Number</label>
<input
id="accountNumber"
type="text"
value={field.value}
{...props}
/>
{field.error && <div>{field.error}</div>}
</div>
)}
</Field>

<Field
name="accountPin"
validate={porting.accountPinExists ? [] : [required('Please enter')]}
>
{(field, props) => (
<div>
<label for="accountPin">Account PIN</label>
<input
id="accountPin"
type="text"
placeholder={porting.accountPinExists ? '••••' : undefined}
{...props}
/>
{field.error && <div>{field.error}</div>}
</div>
)}
</Field>

<Field name="birthday" validate={[required('Please enter')]}>
{(field, props) => (
<div>
<label for="birthday">Birthday</label>
<input id="birthday" type="text" value={field.value} {...props} />
{field.error && <div>{field.error}</div>}
</div>
)}
</Field>

<Field name="firstName" validate={[required('Please enter')]}>
{(field, props) => (
<div>
<label for="firstName">First Name</label>
<input id="firstName" type="text" value={field.value} {...props} />
{field.error && <div>{field.error}</div>}
</div>
)}
</Field>

<Field name="lastName" validate={[required('Please enter')]}>
{(field, props) => (
<div>
<label for="lastName">Last Name</label>
<input id="lastName" type="text" value={field.value} {...props} />
{field.error && <div>{field.error}</div>}
</div>
)}
</Field>
</Form>
)
}
39 changes: 34 additions & 5 deletions lib/PortingEmbed/__stories__/PortingEmbed.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,49 @@
import { Meta } from '@storybook/preact'

import { portingFactory } from '@/testing/factories/porting'

import { PortingEmbed } from '../PortingEmbed'

export default {
title: 'Porting Embed/Base',
const meta: Meta<typeof PortingEmbed> = {
title: 'Porting/Embed',
component: PortingEmbed,
tags: ['autodocs'],
argTypes: {
token: { control: 'text' },
initialPorting: { control: 'object' },
onPortingUpdate: { action: 'onPortingUpdate' },
onValidationChange: { action: 'onValidationChange' },
},
decorators: [
(Story) => (
<div>
{Story()}
<button form="gigsPortingEmbedForm" type="submit">
Submit!
</button>
</div>
),
],
}

export default meta

export const EmptyPorting = {
args: {
porting: portingFactory.build(),
},
}

export const Primary = {
export const PrefilledPorting = {
args: {
token: 'abc:123',
initialPorting: portingFactory.build(),
porting: portingFactory
.params({
accountNumber: '1234',
accountPinExists: true,
birthday: '01.01.1990',
firstName: 'Jane',
lastName: 'Doe',
})
.build(),
},
}
10 changes: 6 additions & 4 deletions lib/PortingEmbed/__tests__/PortingEmbed.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ import { portingFactory } from '@/testing/factories/porting'

import { PortingEmbed } from '../PortingEmbed'

it('gets the porting', () => {
it('renders a form', () => {
const porting = portingFactory.params({ id: 'prt_123' }).build()
render(<PortingEmbed initialPorting={porting} token="abc:123" />)
const greeting = screen.getByText(/Hello prt_123/i)
expect(greeting).toBeInTheDocument()
render(<PortingEmbed porting={porting} />)
const form = screen.getByRole('form')
expect(form).toBeInTheDocument()
})

// TODO: different forms based on required fields
85 changes: 85 additions & 0 deletions lib/PortingEmbed/__tests__/PortingForm.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { render, screen } from '@testing-library/preact'
import userEvent from '@testing-library/user-event'

import { portingFactory } from '@/testing/factories/porting'

import { PortingForm } from '../PortingForm'

const wrapper = ({ children }: { children: React.ReactNode }) => {
return (
<div>
{children}
<button form="gigsPortingEmbedForm">Submit</button>
</div>
)
}

it('can enter and submit', async () => {
const user = userEvent.setup()
const porting = portingFactory.build()
const submit = vi.fn()
render(<PortingForm porting={porting} onSubmit={submit} />, { wrapper })

await user.type(screen.getByLabelText('Account Number'), '1234')
await user.type(screen.getByLabelText('Account PIN'), '0000')
await user.type(screen.getByLabelText('Birthday'), '01.01.1990')
await user.type(screen.getByLabelText('First Name'), 'Jerry')
await user.type(screen.getByLabelText('Last Name'), 'Seinfeld')
await user.click(screen.getByRole('button', { name: 'Submit' }))

expect(submit).toHaveBeenCalledWith({
accountNumber: '1234',
accountPin: '0000',
birthday: '01.01.1990',
firstName: 'Jerry',
lastName: 'Seinfeld',
})
})

describe('with existing porting fields', () => {
it('prefills the inputs', async () => {
const porting = portingFactory.build({
accountNumber: '1234',
accountPinExists: true,
birthday: '01.01.1990',
firstName: 'Jerry',
lastName: 'Seinfeld',
})
const submit = vi.fn()
render(<PortingForm porting={porting} onSubmit={submit} />, { wrapper })

expect(screen.getByLabelText('Account Number')).toHaveValue('1234')
expect(screen.getByLabelText('Birthday')).toHaveValue('01.01.1990')
expect(screen.getByLabelText('First Name')).toHaveValue('Jerry')
expect(screen.getByLabelText('Last Name')).toHaveValue('Seinfeld')

// the account pin cannot be prefilled, and instead indicates it exists with
// a placeholder
timomeh marked this conversation as resolved.
Show resolved Hide resolved
expect(screen.getByLabelText('Account PIN')).toHaveValue('')
expect(screen.getByLabelText('Account PIN')).toHaveAttribute(
'placeholder',
'••••',
)
})

it('only submits changed fields', async () => {
const user = userEvent.setup()
const porting = portingFactory.build({
accountNumber: '1234',
accountPinExists: true,
birthday: '01.01.1990',
firstName: 'Jerry',
lastName: 'Seinfeld',
})
const submit = vi.fn()
render(<PortingForm porting={porting} onSubmit={submit} />, { wrapper })

await user.clear(screen.getByLabelText('Account Number'))
await user.type(screen.getByLabelText('Account Number'), '5678')
await user.click(screen.getByRole('button', { name: 'Submit' }))

expect(submit).toHaveBeenCalledWith({
accountNumber: '5678',
})
})
})
Loading
Loading