From 4d43e92665cb73c46e2f712e219f46809526fde5 Mon Sep 17 00:00:00 2001 From: ethan-haynes Date: Fri, 23 Feb 2024 09:02:56 -0600 Subject: [PATCH] users: use generic destinations for User Notification Rule List component (#3701) * adding destype to user NR list * remove console.log * Update web/src/app/users/UserNotificationRuleListDest.tsx Co-authored-by: Nathaniel Caza * remove unused values in test data --------- Co-authored-by: Ethan-Haynes Co-authored-by: Nathaniel Caza --- web/src/app/users/UserDetails.tsx | 16 +- .../UserNotificationRuleListDest.stories.tsx | 177 ++++++++++++++++++ .../users/UserNotificationRuleListDest.tsx | 142 ++++++++++++++ 3 files changed, 331 insertions(+), 4 deletions(-) create mode 100644 web/src/app/users/UserNotificationRuleListDest.stories.tsx create mode 100644 web/src/app/users/UserNotificationRuleListDest.tsx diff --git a/web/src/app/users/UserDetails.tsx b/web/src/app/users/UserDetails.tsx index 47df34c945..38ad309a6c 100644 --- a/web/src/app/users/UserDetails.tsx +++ b/web/src/app/users/UserDetails.tsx @@ -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' @@ -250,10 +251,17 @@ export default function UserDetails(props: { readOnly={props.readOnly} /> )} - + {hasDestTypesFlag ? ( + + ) : ( + + )} {!mobile && ( diff --git a/web/src/app/users/UserNotificationRuleListDest.stories.tsx b/web/src/app/users/UserNotificationRuleListDest.stories.tsx new file mode 100644 index 0000000000..2026c9cc5d --- /dev/null +++ b/web/src/app/users/UserNotificationRuleListDest.stories.tsx @@ -0,0 +1,177 @@ +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', + displayInfo: { + text: '+1 555-555-5555', + iconAltText: 'Voice Call', + }, + }, + }, + }, + ], + }, + } + : { + 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', + displayInfo: { + text: '+1 763-351-1103', + iconAltText: 'Voice Call', + }, + }, + }, + }, + { + id: 'eea77488-3748-4af8-99ba-18855f9a540d', + delayMinutes: 247, + contactMethod: { + id: '1111', + name: 'Dewayne', + dest: { + type: 'builtin-twilio-sms', + displayInfo: { + text: '+1 763-346-2643', + iconAltText: 'Text Message', + }, + }, + }, + }, + ], + }, + }, + }) + }), + ], + }, + }, + render: function Component(args) { + return + }, +} satisfies Meta + +export default meta +type Story = StoryObj + +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() + }, +} diff --git a/web/src/app/users/UserNotificationRuleListDest.tsx b/web/src/app/users/UserNotificationRuleListDest.tsx new file mode 100644 index 0000000000..c7731bfb28 --- /dev/null +++ b/web/src/app/users/UserNotificationRuleListDest.tsx @@ -0,0 +1,142 @@ +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 + displayInfo { + text + iconAltText + } + } + } + } + } + } +` + +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 + if (error) return + + const { user }: { user: User } = data + + return ( + + + setShowAddDialog(true)} + startIcon={} + disabled={user?.contactMethods.length === 0} + > + Add Rule + + ) : null + } + /> + { + 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 : ( + setDeleteID(nr.id)} + color='secondary' + > + + + ), + } + }, + )} + emptyMessage='No notification rules' + /> + + + {showAddDialog && ( + setShowAddDialog(false)} + /> + )} + {deleteID && ( + setDeleteID(null)} + /> + )} + + + ) +}