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

users: use generic dests for user CM form, create, and edit dialogs #3661

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions web/src/app/forms/FormField.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,7 @@ FormField.propTypes = {

multiple: p.bool,

destType: p.string,
options: p.arrayOf(
p.shape({
label: p.string,
Expand Down
2 changes: 1 addition & 1 deletion web/src/app/selection/DestinationInputDirect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ export default function DestinationInputDirect(
}

// add live validation icon to the right of the textfield as an endAdornment
if (adorn && props.value === debouncedValue) {
if (adorn && props.value === debouncedValue && !props.disabled) {
iprops = {
endAdornment: <InputAdornment position='end'>{adorn}</InputAdornment>,
...iprops,
Expand Down
56 changes: 56 additions & 0 deletions web/src/app/storybook/defaultDestTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,4 +109,60 @@ export const destTypes: DestinationTypeInfo[] = [
},
],
},
{
type: 'supports-status',
name: 'Single Field Destination Type',
enabled: true,
disabledMessage: 'Single field destination type must be configured.',
userDisclaimer: '',
isContactMethod: true,
isEPTarget: false,
isSchedOnCallNotify: false,
iconURL: '',
iconAltText: '',
supportsStatusUpdates: true,
statusUpdatesRequired: false,
requiredFields: [
{
fieldID: 'phone-number',
labelSingular: 'Phone Number',
labelPlural: 'Phone Numbers',
hint: 'Include country code e.g. +1 (USA), +91 (India), +44 (UK)',
hintURL: '',
placeholderText: '11235550123',
prefix: '+',
inputType: 'tel',
isSearchSelectable: false,
supportsValidation: true,
},
],
},
{
type: 'required-status',
name: 'Single Field Destination Type',
enabled: true,
disabledMessage: 'Single field destination type must be configured.',
userDisclaimer: '',
isContactMethod: true,
isEPTarget: false,
isSchedOnCallNotify: false,
iconURL: '',
iconAltText: '',
supportsStatusUpdates: false,
statusUpdatesRequired: true,
requiredFields: [
{
fieldID: 'phone-number',
labelSingular: 'Phone Number',
labelPlural: 'Phone Numbers',
hint: 'Include country code e.g. +1 (USA), +91 (India), +44 (UK)',
hintURL: '',
placeholderText: '11235550123',
prefix: '+',
inputType: 'tel',
isSearchSelectable: false,
supportsValidation: true,
},
],
},
]
22 changes: 20 additions & 2 deletions web/src/app/users/UserContactMethodCreateDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import { useConfigValue } from '../util/RequireConfig'
import { Dialog, DialogTitle, DialogActions, Button } from '@mui/material'
import DialogContentError from '../dialogs/components/DialogContentError'
import { ContactMethodType } from '../../schema'
import { useExpFlag } from '../util/useExpFlag'
import UserContactMethodCreateDialogDest from './UserContactMethodCreateDialogDest'

type Value = {
name: string
Expand Down Expand Up @@ -36,12 +38,16 @@ const userConflictQuery = gql`

const noSuspense = { suspense: false }

export default function UserContactMethodCreateDialog(props: {
type UserContactMethodCreateDialogProps = {
userID: string
onClose: (contactMethodID?: string) => void
title?: string
subtitle?: string
}): React.ReactNode {
}

function UserContactMethodCreateDialog(
props: UserContactMethodCreateDialogProps,
): React.ReactNode {
const [allowSV, allowE, allowW, allowS] = useConfigValue(
'Twilio.Enable',
'SMTP.Enable',
Expand Down Expand Up @@ -154,3 +160,15 @@ export default function UserContactMethodCreateDialog(props: {
/>
)
}

export default function UserContactMethodCreateDialogSwitch(
props: UserContactMethodCreateDialogProps,
): React.ReactNode {
const isDestTypesSet = useExpFlag('dest-types')

if (isDestTypesSet) {
return <UserContactMethodCreateDialogDest {...props} />
}

return <UserContactMethodCreateDialog {...props} />
}
166 changes: 166 additions & 0 deletions web/src/app/users/UserContactMethodCreateDialogDest.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import React from 'react'
import type { Meta, StoryObj } from '@storybook/react'
import UserContactMethodCreateDialogDest from './UserContactMethodCreateDialogDest'
import { expect } from '@storybook/jest'
import { screen, userEvent, waitFor } from '@storybook/testing-library'
import { handleDefaultConfig, defaultConfig } from '../storybook/graphql'
import { useArgs } from '@storybook/preview-api'
import { HttpResponse, graphql } from 'msw'

const meta = {
title: 'users/UserContactMethodCreateDialogDest',
component: UserContactMethodCreateDialogDest,
tags: ['autodocs'],
parameters: {
msw: {
handlers: [
handleDefaultConfig,
graphql.query('UserConflictCheck', () => {
return HttpResponse.json({
data: {
users: {
nodes: [
{ name: defaultConfig.user.name, id: defaultConfig.user.id },
],
},
},
})
}),
graphql.query('useExpFlag', () => {
return HttpResponse.json({
data: {
users: {
nodes: [
{ name: defaultConfig.user.name, id: defaultConfig.user.id },
],
},
},
})
}),
graphql.mutation('CreateUserContactMethodInput', () => {
return HttpResponse.json({
data: {
createUserContactMethod: {
id: '00000000-0000-0000-0000-000000000000',
},
},
})
}),
graphql.query('ValidateDestination', ({ variables: vars }) => {
return HttpResponse.json({
data: {
destinationFieldValidate:
vars.input.value === '@slack' ||
vars.input.value === '+12225558989' ||
vars.input.value === 'valid@email.com',
},
})
}),
],
},
},
render: function Component(args) {
const [, setArgs] = useArgs()
const onClose = (contactMethodID: string | undefined): void => {
if (args.onClose) args.onClose(contactMethodID)
setArgs({ value: contactMethodID })
}
return <UserContactMethodCreateDialogDest {...args} onClose={onClose} />
},
} satisfies Meta<typeof UserContactMethodCreateDialogDest>

export default meta
type Story = StoryObj<typeof meta>

export const SingleField: Story = {
args: {
userID: defaultConfig.user.id,
title: 'Create New Contact Method',
subtitle: 'Create New Contact Method Subtitle',
},
play: async () => {
await userEvent.clear(screen.getByLabelText('Phone Number'))

await waitFor(async () => {
await userEvent.type(screen.getByLabelText('Phone Number'), '12225558989')
})
await expect(await screen.findByTestId('CheckIcon')).toBeVisible()

const submitButton = await screen.getByRole('button', { name: /SUBMIT/i })
await userEvent.click(submitButton)

await userEvent.clear(screen.getByLabelText('Name'))
await userEvent.type(screen.getByLabelText('Name'), 'TEST')

const retryButton = await screen.getByRole('button', { name: /RETRY/i })
await userEvent.click(retryButton)
},
}

export const MultiField: Story = {
args: {
userID: defaultConfig.user.id,
title: 'Create New Contact Method',
subtitle: 'Create New Contact Method Subtitle',
},
play: async () => {
// Select the next Dest Type
await userEvent.click(await screen.getByLabelText('Dest Type'))
await userEvent.click(
await screen.getByText('Multi Field Destination Type'),
)

// ensure information for phone number renders correctly
await userEvent.clear(screen.getByLabelText('First Item'))
await waitFor(async () => {
await userEvent.type(screen.getByLabelText('First Item'), '12225558989')
})
await expect(await screen.findByTestId('CheckIcon')).toBeVisible()

// ensure information for email renders correctly
await expect(
screen.getByPlaceholderText('foobar@example.com'),
).toBeVisible()
await userEvent.clear(screen.getByLabelText('Second Item'))
await userEvent.type(
screen.getByLabelText('Second Item'),
'valid@email.com',
)

// ensure information for slack renders correctly
await expect(screen.getByPlaceholderText('slack user ID')).toBeVisible()
await expect(screen.getByLabelText('Third Item')).toBeVisible()
await userEvent.clear(screen.getByLabelText('Third Item'))
await userEvent.type(screen.getByLabelText('Third Item'), '@slack')

// Try to submit without all feilds complete
const submitButton = await screen.getByRole('button', { name: /SUBMIT/i })
await userEvent.click(submitButton)

// Name field
await userEvent.clear(screen.getByLabelText('Name'))
await userEvent.type(screen.getByLabelText('Name'), 'TEST')

const retryButton = await screen.getByRole('button', { name: /RETRY/i })
await userEvent.click(retryButton)
},
}

export const DisabledField: Story = {
args: {
userID: defaultConfig.user.id,
title: 'Create New Contact Method',
subtitle: 'Create New Contact Method Subtitle',
},
play: async () => {
// Open option select
await userEvent.click(await screen.getByLabelText('Dest Type'))

// Attempt to click the disabled option
const disabledOption = await screen.getByText('This is disabled')
// Ensure no clicked occurred
userEvent.click(disabledOption, {
pointerEventsCheck: 0,
})
},
}
Loading
Loading