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 destinations for User Notification Rule List component #3701

Merged
merged 5 commits into from
Feb 23, 2024
Merged
Show file tree
Hide file tree
Changes from 3 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
16 changes: 12 additions & 4 deletions web/src/app/users/UserDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import UserContactMethodListDest from './UserContactMethodListDest'
import { AddAlarm, SettingsPhone } from '@mui/icons-material'
import SpeedDial from '../util/SpeedDial'
import UserNotificationRuleList from './UserNotificationRuleList'
import UserNotificationRuleListDest from './UserNotificationRuleListDest'
import { Grid } from '@mui/material'
import UserContactMethodCreateDialog from './UserContactMethodCreateDialog'
import UserNotificationRuleCreateDialog from './UserNotificationRuleCreateDialog'
Expand Down Expand Up @@ -250,10 +251,17 @@ export default function UserDetails(props: {
readOnly={props.readOnly}
/>
)}
<UserNotificationRuleList
userID={userID}
readOnly={props.readOnly}
/>
{hasDestTypesFlag ? (
<UserNotificationRuleListDest
userID={userID}
readOnly={props.readOnly}
/>
) : (
<UserNotificationRuleList
userID={userID}
readOnly={props.readOnly}
/>
)}
{!mobile && (
<Suspense>
<Grid item xs={12}>
Expand Down
205 changes: 205 additions & 0 deletions web/src/app/users/UserNotificationRuleListDest.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
import React from 'react'
import type { Meta, StoryObj } from '@storybook/react'
import UserNotificationRuleListDest from './UserNotificationRuleListDest'
import { expect, within, userEvent, screen } from '@storybook/test'
import { handleDefaultConfig, handleExpFlags } from '../storybook/graphql'
import { HttpResponse, graphql } from 'msw'

const meta = {
title: 'users/UserNotificationRuleListDest',
component: UserNotificationRuleListDest,
tags: ['autodocs'],
parameters: {
msw: {
handlers: [
handleDefaultConfig,
handleExpFlags('dest-types'),
graphql.query('nrList', ({ variables: vars }) => {
return HttpResponse.json({
data:
vars.id === '00000000-0000-0000-0000-000000000000'
? {
user: {
id: '00000000-0000-0000-0000-000000000000',
contactMethods: [
{
id: '12345',
},
],
notificationRules: [
{
id: '123',
delayMinutes: 33,
contactMethod: {
id: '12345',
name: 'Josiah',
dest: {
type: 'single-field',
values: [
{
fieldID: 'phone-number',
value: '+15555555555',
label: '+1 555-555-5555',
},
],
displayInfo: {
text: '+1 555-555-5555',
iconURL: 'test',
iconAltText: 'Voice Call',
},
},
disabled: false,
pending: false,
},
},
],
},
}
: {
user: {
id: '00000000-0000-0000-0000-000000000001',
contactMethods: [
{
id: '67890',
},
{
id: '1111',
},
],
notificationRules: [
{
id: '96814869-1199-4477-832d-e714e7d94aea',
delayMinutes: 71,
contactMethod: {
id: '67890',
name: 'Bridget',
dest: {
type: 'builtin-twilio-voice',
values: [
{
fieldID: 'phone-number',
value: '+17633511103',
label: '+1 763-351-1103',
},
],
displayInfo: {
text: '+1 763-351-1103',
iconURL: 'builtin://phone-voice',
iconAltText: 'Voice Call',
linkURL: '',
},
},
},
},
{
id: 'eea77488-3748-4af8-99ba-18855f9a540d',
delayMinutes: 247,
contactMethod: {
id: '1111',
name: 'Dewayne',
dest: {
type: 'builtin-twilio-sms',
values: [
{
fieldID: 'phone-number',
value: '+17633462643',
label: '+1 763-346-2643',
},
],
displayInfo: {
text: '+1 763-346-2643',
iconURL: 'builtin://phone-text',
iconAltText: 'Text Message',
linkURL: '',
},
},
},
},
],
},
},
})
}),
],
},
},
render: function Component(args) {
return <UserNotificationRuleListDest {...args} />
},
} satisfies Meta<typeof UserNotificationRuleListDest>

export default meta
type Story = StoryObj<typeof meta>

export const SingleContactMethod: Story = {
args: {
userID: '00000000-0000-0000-0000-000000000000',
readOnly: false,
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement)

// ensure correct info is displayed for single-field NR
await expect(
await canvas.findByText(
'After 33 minutes notify me via Voice Call at +1 555-555-5555 (Josiah)',
),
).toBeVisible()

await expect(await screen.findByText('Add Rule')).toHaveAttribute(
'type',
'button',
)
await userEvent.click(
await screen.findByLabelText('Delete notification rule'),
)
await userEvent.click(await screen.findByText('Cancel'))
},
}

export const MultiContactMethods: Story = {
args: {
userID: '00000000-0000-0000-0000-000000000001',
readOnly: false,
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement)

await expect(
await canvas.findByText(
'After 71 minutes notify me via Voice Call at +1 763-351-1103 (Bridget)',
),
).toBeVisible()
await expect(
await canvas.findByText(
'After 247 minutes notify me via Text Message at +1 763-346-2643 (Dewayne)',
),
).toBeVisible()

await expect(await screen.findByText('Add Rule')).toHaveAttribute(
'type',
'button',
)
const deleteButtons = await screen.findAllByLabelText(
'Delete notification rule',
)
expect(deleteButtons).toHaveLength(2)
await userEvent.click(deleteButtons[0])
await userEvent.click(await screen.findByText('Cancel'))
},
}

export const SingleReadOnlyContactMethods: Story = {
args: {
userID: '00000000-0000-0000-0000-000000000000',
readOnly: true,
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement)

// ensure no edit icons exist for read-only NR
await expect(
await canvas.queryByLabelText('Delete notification rule'),
).not.toBeInTheDocument()
},
}
149 changes: 149 additions & 0 deletions web/src/app/users/UserNotificationRuleListDest.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import React, { useState, ReactNode, Suspense } from 'react'
import { gql, useQuery } from 'urql'
import {
Button,
Card,
CardHeader,
Grid,
IconButton,
Theme,
} from '@mui/material'
import makeStyles from '@mui/styles/makeStyles'
import { Add, Delete } from '@mui/icons-material'
import FlatList from '../lists/FlatList'
import { formatNotificationRule, sortNotificationRules } from './util'
import UserNotificationRuleDeleteDialog from './UserNotificationRuleDeleteDialog'
import { styles as globalStyles } from '../styles/materialStyles'
import UserNotificationRuleCreateDialog from './UserNotificationRuleCreateDialog'
import { useIsWidthDown } from '../util/useWidth'
import { ObjectNotFound, GenericError } from '../error-pages'
import { User } from '../../schema'

const query = gql`
query nrList($id: ID!) {
user(id: $id) {
id
contactMethods {
id
}
notificationRules {
id
delayMinutes
contactMethod {
id
name
dest {
type
values {
fieldID
value
label
}
displayInfo {
text
iconURL
iconAltText
linkURL
}
}
}
}
}
}
`

const useStyles = makeStyles((theme: Theme) => {
const { cardHeader } = globalStyles(theme)
return {
cardHeader,
}
})

export default function UserNotificationRuleListDest(props: {
userID: string
readOnly: boolean
}): ReactNode {
const classes = useStyles()
const mobile = useIsWidthDown('md')
const [showAddDialog, setShowAddDialog] = useState(false)
const [deleteID, setDeleteID] = useState(null)

const [{ data, error }] = useQuery({
query,
variables: { id: props.userID },
})

if (data && !data.user)
return <ObjectNotFound type='notifcation rules list' />
if (error) return <GenericError error={error.message} />

const { user }: { user: User } = data

return (
<Grid item xs={12}>
<Card>
<CardHeader
className={classes.cardHeader}
titleTypographyProps={{ component: 'h2', variant: 'h5' }}
title='Notification Rules'
action={
!mobile ? (
<Button
title='Add Notification Rule'
variant='contained'
onClick={() => setShowAddDialog(true)}
startIcon={<Add />}
disabled={user?.contactMethods.length === 0}
>
Add Rule
</Button>
) : null
}
/>
<FlatList
data-cy='notification-rules'
items={sortNotificationRules(user?.notificationRules ?? []).map(
(nr) => {
const formattedValue =
nr.contactMethod.dest.displayInfo.text || 'Unknown Label'
const name = nr.contactMethod.name || 'Unknown User'
const type =
nr.contactMethod.dest.displayInfo.iconAltText || 'Unknown Type'
return {
title: formatNotificationRule(nr.delayMinutes, {
type,
name,
formattedValue,
}),
secondaryAction: props.readOnly ? null : (
<IconButton
aria-label='Delete notification rule'
onClick={() => setDeleteID(nr.id)}
color='secondary'
>
<Delete />
</IconButton>
),
}
},
)}
emptyMessage='No notification rules'
/>
</Card>
<Suspense>
{showAddDialog && (
<UserNotificationRuleCreateDialog
userID={props.userID}
onClose={() => setShowAddDialog(false)}
/>
)}
{deleteID && (
<UserNotificationRuleDeleteDialog
ruleID={deleteID}
onClose={() => setDeleteID(null)}
/>
)}
</Suspense>
</Grid>
)
}
Loading