From f823b1fd8147c1c4f62b0fd8b2455a20b1b962be Mon Sep 17 00:00:00 2001 From: Daniel Bisgrove Date: Thu, 9 Nov 2023 14:31:31 -0500 Subject: [PATCH 01/20] Moving NavReportsList to shared folder, and then getting it ready for Preferences. --- .../reports/designationAccounts.page.tsx | 14 +- .../donations/[[...contactId]].page.tsx | 18 +- .../reports/expectedMonthlyTotal.page.tsx | 16 +- .../partnerCurrency/[[...contactId]].page.tsx | 14 +- .../reports/responsibilityCenters.page.tsx | 8 +- .../salaryCurrency/[[...contactId]].page.tsx | 8 +- .../Layouts/Primary/NavBar/NavBar.tsx | 8 +- .../Primary/TopBar/Items/NavMenu/NavMenu.tsx | 6 +- .../AccountsListLayout/Header/Header.tsx | 67 -------- .../DesignationAccountsReport.tsx | 16 +- .../DonationsReport/DonationsReport.tsx | 12 +- .../ExpectedMonthlyTotalReport.tsx | 10 +- .../FourteenMonthReport.test.tsx | 2 +- .../Reports/NavReportsList/Item/Item.tsx | 51 ------ .../Reports/NavReportsList/NavReportsList.tsx | 122 ------------- .../Reports/NavReportsList/ReportNavItems.ts | 36 ---- .../PartnerGivingAnalysisReport.test.tsx | 2 +- .../PartnerGivingAnalysisReport.tsx | 17 +- .../ResponsibilityCentersReport.tsx | 20 ++- .../MultiPageLayout/MultiPageHeader.test.tsx} | 13 +- .../MultiPageLayout/MultiPageHeader.tsx | 100 +++++++++++ .../MultiPageMenu}/Item/Item.stories.tsx | 9 +- .../MultiPageMenu}/Item/Item.test.tsx | 3 +- .../MultiPageMenu/Item/Item.tsx | 100 +++++++++++ .../MultiPageMenu/MultiPageMenu.stories.tsx} | 5 +- .../MultiPageMenu/MultiPageMenu.test.tsx} | 15 +- .../MultiPageMenu/MultiPageMenu.tsx | 160 ++++++++++++++++++ .../MultiPageMenu/MultiPageMenuItems.graphql | 6 + .../MultiPageMenu/MultiPageMenuItems.ts | 104 ++++++++++++ 29 files changed, 604 insertions(+), 358 deletions(-) delete mode 100644 src/components/Reports/AccountsListLayout/Header/Header.tsx delete mode 100644 src/components/Reports/NavReportsList/Item/Item.tsx delete mode 100644 src/components/Reports/NavReportsList/NavReportsList.tsx delete mode 100644 src/components/Reports/NavReportsList/ReportNavItems.ts rename src/components/{Reports/AccountsListLayout/Header/Header.test.tsx => Shared/MultiPageLayout/MultiPageHeader.test.tsx} (75%) create mode 100644 src/components/Shared/MultiPageLayout/MultiPageHeader.tsx rename src/components/{Reports/NavReportsList => Shared/MultiPageLayout/MultiPageMenu}/Item/Item.stories.tsx (60%) rename src/components/{Reports/NavReportsList => Shared/MultiPageLayout/MultiPageMenu}/Item/Item.test.tsx (80%) create mode 100644 src/components/Shared/MultiPageLayout/MultiPageMenu/Item/Item.tsx rename src/components/{Reports/NavReportsList/NavReportsList.stories.tsx => Shared/MultiPageLayout/MultiPageMenu/MultiPageMenu.stories.tsx} (75%) rename src/components/{Reports/NavReportsList/NavReportsList.test.tsx => Shared/MultiPageLayout/MultiPageMenu/MultiPageMenu.test.tsx} (90%) create mode 100644 src/components/Shared/MultiPageLayout/MultiPageMenu/MultiPageMenu.tsx create mode 100644 src/components/Shared/MultiPageLayout/MultiPageMenu/MultiPageMenuItems.graphql create mode 100644 src/components/Shared/MultiPageLayout/MultiPageMenu/MultiPageMenuItems.ts diff --git a/pages/accountLists/[accountListId]/reports/designationAccounts.page.tsx b/pages/accountLists/[accountListId]/reports/designationAccounts.page.tsx index 008bffea4..abee35aaa 100644 --- a/pages/accountLists/[accountListId]/reports/designationAccounts.page.tsx +++ b/pages/accountLists/[accountListId]/reports/designationAccounts.page.tsx @@ -3,13 +3,16 @@ import Head from 'next/head'; import { useTranslation } from 'react-i18next'; import Box from '@mui/material/Box'; import { styled } from '@mui/material/styles'; -import { DesignationAccountsReport } from 'src/components/Reports/DesignationAccountsReport/DesignationAccountsReport'; -import Loading from 'src/components/Loading'; -import { SidePanelsLayout } from 'src/components/Layouts/SidePanelsLayout'; import { useAccountListId } from 'src/hooks/useAccountListId'; import useGetAppSettings from 'src/hooks/useGetAppSettings'; -import { NavReportsList } from 'src/components/Reports/NavReportsList/NavReportsList'; import { suggestArticles } from 'src/lib/helpScout'; +import Loading from 'src/components/Loading'; +import { DesignationAccountsReport } from 'src/components/Reports/DesignationAccountsReport/DesignationAccountsReport'; +import { SidePanelsLayout } from 'src/components/Layouts/SidePanelsLayout'; +import { + MultiPageMenu, + NavTypeEnum, +} from 'src/components/Shared/MultiPageLayout/MultiPageMenu/MultiPageMenu'; const DesignationAccountsReportPageWrapper = styled(Box)(({ theme }) => ({ backgroundColor: theme.palette.common.white, @@ -42,12 +45,13 @@ const DesignationAccountsReportPage: React.FC = () => { } leftOpen={isNavListOpen} diff --git a/pages/accountLists/[accountListId]/reports/donations/[[...contactId]].page.tsx b/pages/accountLists/[accountListId]/reports/donations/[[...contactId]].page.tsx index 71e863f2b..43731f1fb 100644 --- a/pages/accountLists/[accountListId]/reports/donations/[[...contactId]].page.tsx +++ b/pages/accountLists/[accountListId]/reports/donations/[[...contactId]].page.tsx @@ -4,16 +4,19 @@ import { useRouter } from 'next/router'; import { useTranslation } from 'react-i18next'; import Box from '@mui/material/Box'; import { styled } from '@mui/material/styles'; -import { DonationsReport } from 'src/components/Reports/DonationsReport/DonationsReport'; -import Loading from 'src/components/Loading'; -import { SidePanelsLayout } from 'src/components/Layouts/SidePanelsLayout'; import { useAccountListId } from 'src/hooks/useAccountListId'; import useGetAppSettings from 'src/hooks/useGetAppSettings'; -import { NavReportsList } from 'src/components/Reports/NavReportsList/NavReportsList'; import { getQueryParam } from 'src/utils/queryParam'; -import { ContactsPage } from '../../contacts/ContactsPage'; -import { ContactsRightPanel } from 'src/components/Contacts/ContactsRightPanel/ContactsRightPanel'; import { suggestArticles } from 'src/lib/helpScout'; +import { DonationsReport } from 'src/components/Reports/DonationsReport/DonationsReport'; +import Loading from 'src/components/Loading'; +import { + MultiPageMenu, + NavTypeEnum, +} from 'src/components/Shared/MultiPageLayout/MultiPageMenu/MultiPageMenu'; +import { SidePanelsLayout } from 'src/components/Layouts/SidePanelsLayout'; +import { ContactsRightPanel } from 'src/components/Contacts/ContactsRightPanel/ContactsRightPanel'; +import { ContactsPage } from '../../contacts/ContactsPage'; const DonationsReportPageWrapper = styled(Box)(({ theme }) => ({ backgroundColor: theme.palette.common.white, @@ -55,12 +58,13 @@ const DonationsReportPage: React.FC = () => { } leftOpen={isNavListOpen} diff --git a/pages/accountLists/[accountListId]/reports/expectedMonthlyTotal.page.tsx b/pages/accountLists/[accountListId]/reports/expectedMonthlyTotal.page.tsx index f55bfc603..d7b561319 100644 --- a/pages/accountLists/[accountListId]/reports/expectedMonthlyTotal.page.tsx +++ b/pages/accountLists/[accountListId]/reports/expectedMonthlyTotal.page.tsx @@ -3,14 +3,17 @@ import Head from 'next/head'; import { useTranslation } from 'react-i18next'; import { Box } from '@mui/material'; import { styled } from '@mui/material/styles'; -import { ExpectedMonthlyTotalReportHeader } from '../../../../src/components/Reports/ExpectedMonthlyTotalReport/Header/ExpectedMonthlyTotalReportHeader'; -import Loading from '../../../../src/components/Loading'; -import { useAccountListId } from '../../../../src/hooks/useAccountListId'; +import { useAccountListId } from 'src/hooks/useAccountListId'; import useGetAppSettings from 'src/hooks/useGetAppSettings'; -import { ExpectedMonthlyTotalReport } from '../../../../src/components/Reports/ExpectedMonthlyTotalReport/ExpectedMonthlyTotalReport'; import { suggestArticles } from 'src/lib/helpScout'; +import Loading from 'src/components/Loading'; +import { ExpectedMonthlyTotalReportHeader } from 'src/components/Reports/ExpectedMonthlyTotalReport/Header/ExpectedMonthlyTotalReportHeader'; +import { ExpectedMonthlyTotalReport } from 'src/components/Reports/ExpectedMonthlyTotalReport/ExpectedMonthlyTotalReport'; import { SidePanelsLayout } from 'src/components/Layouts/SidePanelsLayout'; -import { NavReportsList } from 'src/components/Reports/NavReportsList/NavReportsList'; +import { + MultiPageMenu, + NavTypeEnum, +} from 'src/components/Shared/MultiPageLayout/MultiPageMenu/MultiPageMenu'; const ExpectedMonthlyTotalReportPageWrapper = styled(Box)(({ theme }) => ({ backgroundColor: theme.palette.common.white, @@ -43,12 +46,13 @@ const ExpectedMonthlyTotalReportPage = (): ReactElement => { } leftOpen={isNavListOpen} diff --git a/pages/accountLists/[accountListId]/reports/partnerCurrency/[[...contactId]].page.tsx b/pages/accountLists/[accountListId]/reports/partnerCurrency/[[...contactId]].page.tsx index 63c6206e9..3e3bf26e7 100644 --- a/pages/accountLists/[accountListId]/reports/partnerCurrency/[[...contactId]].page.tsx +++ b/pages/accountLists/[accountListId]/reports/partnerCurrency/[[...contactId]].page.tsx @@ -6,13 +6,16 @@ import Box from '@mui/material/Box'; import { styled } from '@mui/material/styles'; import { FourteenMonthReportCurrencyType } from '../../../../../graphql/types.generated'; import { FourteenMonthReport } from 'src/components/Reports/FourteenMonthReports/FourteenMonthReport'; +import { suggestArticles } from 'src/lib/helpScout'; +import { getQueryParam } from 'src/utils/queryParam'; import Loading from 'src/components/Loading'; -import { SidePanelsLayout } from 'src/components/Layouts/SidePanelsLayout'; import { useAccountListId } from 'src/hooks/useAccountListId'; import useGetAppSettings from 'src/hooks/useGetAppSettings'; -import { NavReportsList } from 'src/components/Reports/NavReportsList/NavReportsList'; -import { suggestArticles } from 'src/lib/helpScout'; -import { getQueryParam } from 'src/utils/queryParam'; +import { + MultiPageMenu, + NavTypeEnum, +} from 'src/components/Shared/MultiPageLayout/MultiPageMenu/MultiPageMenu'; +import { SidePanelsLayout } from 'src/components/Layouts/SidePanelsLayout'; import { ContactsRightPanel } from 'src/components/Contacts/ContactsRightPanel/ContactsRightPanel'; import { ContactsPage } from '../../contacts/ContactsPage'; @@ -55,12 +58,13 @@ const PartnerCurrencyReportPage: React.FC = () => { } leftOpen={isNavListOpen} diff --git a/pages/accountLists/[accountListId]/reports/responsibilityCenters.page.tsx b/pages/accountLists/[accountListId]/reports/responsibilityCenters.page.tsx index cdf15802d..3565702ed 100644 --- a/pages/accountLists/[accountListId]/reports/responsibilityCenters.page.tsx +++ b/pages/accountLists/[accountListId]/reports/responsibilityCenters.page.tsx @@ -8,8 +8,11 @@ import Loading from 'src/components/Loading'; import { SidePanelsLayout } from 'src/components/Layouts/SidePanelsLayout'; import { useAccountListId } from 'src/hooks/useAccountListId'; import useGetAppSettings from 'src/hooks/useGetAppSettings'; -import { NavReportsList } from 'src/components/Reports/NavReportsList/NavReportsList'; import { suggestArticles } from 'src/lib/helpScout'; +import { + MultiPageMenu, + NavTypeEnum, +} from 'src/components/Shared/MultiPageLayout/MultiPageMenu/MultiPageMenu'; const ResponsibilityCentersReportPageWrapper = styled(Box)(({ theme }) => ({ backgroundColor: theme.palette.common.white, @@ -42,12 +45,13 @@ const ResponsibilityCentersReportPage: React.FC = () => { } leftOpen={isNavListOpen} diff --git a/pages/accountLists/[accountListId]/reports/salaryCurrency/[[...contactId]].page.tsx b/pages/accountLists/[accountListId]/reports/salaryCurrency/[[...contactId]].page.tsx index 321ff5d99..0e4c5a907 100644 --- a/pages/accountLists/[accountListId]/reports/salaryCurrency/[[...contactId]].page.tsx +++ b/pages/accountLists/[accountListId]/reports/salaryCurrency/[[...contactId]].page.tsx @@ -10,7 +10,10 @@ import Loading from 'src/components/Loading'; import { SidePanelsLayout } from 'src/components/Layouts/SidePanelsLayout'; import { useAccountListId } from 'src/hooks/useAccountListId'; import useGetAppSettings from 'src/hooks/useGetAppSettings'; -import { NavReportsList } from 'src/components/Reports/NavReportsList/NavReportsList'; +import { + MultiPageMenu, + NavTypeEnum, +} from 'src/components/Shared/MultiPageLayout/MultiPageMenu/MultiPageMenu'; import { suggestArticles } from 'src/lib/helpScout'; import { getQueryParam } from 'src/utils/queryParam'; import { ContactsRightPanel } from 'src/components/Contacts/ContactsRightPanel/ContactsRightPanel'; @@ -55,12 +58,13 @@ const SalaryCurrencyReportPage: React.FC = () => { } leftOpen={isNavListOpen} diff --git a/src/components/Layouts/Primary/NavBar/NavBar.tsx b/src/components/Layouts/Primary/NavBar/NavBar.tsx index 547d740e1..1fab21adf 100644 --- a/src/components/Layouts/Primary/NavBar/NavBar.tsx +++ b/src/components/Layouts/Primary/NavBar/NavBar.tsx @@ -5,10 +5,8 @@ import { makeStyles } from 'tss-react/mui'; import { useRouter } from 'next/router'; import NextLink, { LinkProps } from 'next/link'; import { useTranslation } from 'react-i18next'; -import { - filteredReportNavItems, - toolsRedirectLinks, -} from '../TopBar/Items/NavMenu/NavMenu'; +import { toolsRedirectLinks } from '../TopBar/Items/NavMenu/NavMenu'; +import { reportNavItems } from 'src/components/Shared/MultiPageLayout/MultiPageMenu/MultiPageMenuItems'; import { NavItem } from './NavItem/NavItem'; import { NavTools } from './NavTools/NavTools'; import { ToolsList } from 'src/components/Tool/Home/ToolList'; @@ -133,7 +131,7 @@ export const NavBar: FC = ({ onMobileClose, openMobile }) => { }, { title: t('Reports'), - items: filteredReportNavItems.map((item) => ({ + items: reportNavItems.map((item) => ({ ...item, title: item.title, href: `/accountLists/${accountListId}/reports/${item.id}`, diff --git a/src/components/Layouts/Primary/TopBar/Items/NavMenu/NavMenu.tsx b/src/components/Layouts/Primary/TopBar/Items/NavMenu/NavMenu.tsx index 327536b1d..1ae314d25 100644 --- a/src/components/Layouts/Primary/TopBar/Items/NavMenu/NavMenu.tsx +++ b/src/components/Layouts/Primary/TopBar/Items/NavMenu/NavMenu.tsx @@ -18,7 +18,7 @@ import NextLink from 'next/link'; import { useTranslation } from 'react-i18next'; import Icon from '@mdi/react'; import { useRouter } from 'next/router'; -import { ReportNavItems } from '../../../../../Reports/NavReportsList/ReportNavItems'; +import { reportNavItems } from 'src/components/Shared/MultiPageLayout/MultiPageMenu/MultiPageMenuItems'; import { ToolsList } from '../../../../../Tool/Home/ToolList'; import { useCurrentToolId } from '../../../../../../hooks/useCurrentToolId'; import theme from '../../../../../../theme'; @@ -27,8 +27,6 @@ import { useGetToolNotificationsQuery } from './GetToolNotifcations.generated'; import HandoffLink from 'src/components/HandoffLink'; import { ReportLink } from './ReportLink'; -export const filteredReportNavItems = ReportNavItems; - const useStyles = makeStyles()(() => ({ navListItem: { order: 2, @@ -252,7 +250,7 @@ const NavMenu: React.FC = () => { - {filteredReportNavItems.map(({ id, title }) => ( + {reportNavItems.map(({ id, title }) => ( void; - title: string; - rightExtra?: ReactNode; -} - -const StickyHeader = styled(Box)(({}) => ({ - position: 'sticky', - top: 0, -})); - -const NavListButton = styled(IconButton, { - shouldForwardProp: (prop) => prop !== 'panelOpen', -})(({ panelOpen }: { panelOpen: boolean }) => ({ - display: 'inline-block', - width: 48, - height: 48, - borderradius: 24, - margin: theme.spacing(1), - backgroundColor: panelOpen ? theme.palette.secondary.dark : 'transparent', -})); - -const NavListIcon = styled(FilterList)(({ theme }) => ({ - width: 24, - height: 24, - color: theme.palette.primary.dark, -})); - -export const AccountsListHeader: FC = ({ - title, - rightExtra, - isNavListOpen, - onNavListToggle, - showNavListButton = true, -}) => { - const { t } = useTranslation(); - - return ( - - - {showNavListButton && ( - - - - )} - - {title} - - {rightExtra} - - - ); -}; diff --git a/src/components/Reports/DesignationAccountsReport/DesignationAccountsReport.tsx b/src/components/Reports/DesignationAccountsReport/DesignationAccountsReport.tsx index b0ebaaaae..9a01fe39d 100644 --- a/src/components/Reports/DesignationAccountsReport/DesignationAccountsReport.tsx +++ b/src/components/Reports/DesignationAccountsReport/DesignationAccountsReport.tsx @@ -2,15 +2,18 @@ import React, { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { Box, CircularProgress, Divider, Typography } from '@mui/material'; import { styled } from '@mui/material/styles'; -import { AccountsList as List } from '../AccountsListLayout/List/List'; -import { AccountsListHeader as Header } from '../AccountsListLayout/Header/Header'; -import type { Account } from '../AccountsListLayout/List/ListItem/ListItem'; +import { currencyFormat } from 'src/lib/intlFormat'; +import { useLocale } from 'src/hooks/useLocale'; import { useDesignationAccountsQuery } from './GetDesignationAccounts.generated'; import { useSetActiveDesignationAccountMutation } from './SetActiveDesignationAccount.generated'; +import { + MultiPageHeader, + HeaderTypeEnum, +} from 'src/components/Shared/MultiPageLayout/MultiPageHeader'; import { Notification } from 'src/components/Notification/Notification'; import { EmptyReport } from 'src/components/Reports/EmptyReport/EmptyReport'; -import { currencyFormat } from 'src/lib/intlFormat'; -import { useLocale } from 'src/hooks/useLocale'; +import type { Account } from '../AccountsListLayout/List/ListItem/ListItem'; +import { AccountsList as List } from '../AccountsListLayout/List/List'; interface Props { accountListId: string; @@ -85,11 +88,12 @@ export const DesignationAccountsReport: React.FC = ({ return ( -
{loading ? ( = ({ return ( -
= ({ return ( -
{ expect(queryByText(title)).toBeInTheDocument(); expect(getByTestId('FourteenMonthReport')).toBeInTheDocument(); - expect(queryByTestId('ReportNavList')).toBeNull(); + expect(queryByTestId('MultiPageMenu')).toBeNull(); }); it('filters report by designation account', async () => { diff --git a/src/components/Reports/NavReportsList/Item/Item.tsx b/src/components/Reports/NavReportsList/Item/Item.tsx deleted file mode 100644 index fbe403672..000000000 --- a/src/components/Reports/NavReportsList/Item/Item.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import React from 'react'; -import ListItem from '@mui/material/ListItem'; -import ListItemText from '@mui/material/ListItemText'; -import ArrowForwardIos from '@mui/icons-material/ArrowForwardIos'; -import NextLink from 'next/link'; -import { useTranslation } from 'react-i18next'; -import { useAccountListId } from 'src/hooks/useAccountListId'; -import HandoffLink from 'src/components/HandoffLink'; - -interface ReportOption { - id: string; - title: string; - subTitle?: string; -} - -interface Props { - item: ReportOption; - isSelected: boolean; -} - -export const Item: React.FC = ({ item, isSelected, ...rest }) => { - const accountListId = useAccountListId(); - const { t } = useTranslation(); - - const children = ( - - - - - ); - - if (item.id === 'coaching') { - return {children}; - } else { - return ( - - {children} - - ); - } -}; diff --git a/src/components/Reports/NavReportsList/NavReportsList.tsx b/src/components/Reports/NavReportsList/NavReportsList.tsx deleted file mode 100644 index 63146de85..000000000 --- a/src/components/Reports/NavReportsList/NavReportsList.tsx +++ /dev/null @@ -1,122 +0,0 @@ -import React from 'react'; -import { - Box, - BoxProps, - IconButton, - List, - Slide, - Typography, -} from '@mui/material'; -import { styled } from '@mui/material/styles'; -import { makeStyles } from 'tss-react/mui'; -import Close from '@mui/icons-material/Close'; -import { useTranslation } from 'react-i18next'; -import { Item } from './Item/Item'; -import { ReportNavItems } from './ReportNavItems'; -import { MultiselectFilter } from '../../../../graphql/types.generated'; -import { FilterListItemMultiselect } from 'src/components/Shared/Filters/FilterListItemMultiselect'; -import { useGetDesignationAccountsQuery } from '../DonationsReport/Table/Modal/EditDonation.generated'; -import { useAccountListId } from 'src/hooks/useAccountListId'; - -interface Props { - selectedId: string; - isOpen: boolean; - onClose: () => void; - designationAccounts: string[]; - setDesignationAccounts: (designationAccounts: string[]) => void; -} - -const useStyles = makeStyles()(() => ({ - root: { - overflow: 'hidden', - }, -})); - -const FilterHeader = styled(Box)(({ theme }) => ({ - padding: theme.spacing(2), - borderBottom: '1px solid', - borderBottomColor: theme.palette.grey[200], -})); - -const FilterList = styled(List)(({ theme }) => ({ - '& .MuiListItemIcon-root': { - minWidth: '37px', - }, - '& .FilterListItemMultiselect-root': { - marginBottom: theme.spacing(2), - }, -})); - -export const NavReportsList: React.FC = ({ - selectedId, - isOpen, - onClose, - designationAccounts, - setDesignationAccounts, - ...BoxProps -}) => { - const { classes } = useStyles(); - const { t } = useTranslation(); - const accountListId = useAccountListId(); - - const { data } = useGetDesignationAccountsQuery({ - variables: { - accountListId: accountListId ?? '', - }, - }); - const accounts = - data?.designationAccounts - .flatMap((group) => group.designationAccounts) - .map((account) => ({ - name: account.name, - value: account.id, - placeholder: null, - })) ?? []; - - const filter: MultiselectFilter = { - filterKey: 'designation_account_id', - title: 'Designation Account', - options: accounts, - }; - - return ( - -
- - - - - {t('Reports')} - - - - - - - {accounts.length > 1 && ( - { - setDesignationAccounts(value ?? []); - }} - /> - )} - {ReportNavItems.map((item) => ( - - ))} - - - -
-
- ); -}; diff --git a/src/components/Reports/NavReportsList/ReportNavItems.ts b/src/components/Reports/NavReportsList/ReportNavItems.ts deleted file mode 100644 index 3c21790b4..000000000 --- a/src/components/Reports/NavReportsList/ReportNavItems.ts +++ /dev/null @@ -1,36 +0,0 @@ -export const ReportNavItems = [ - { - id: 'donations', - title: 'Donations', - }, - { - id: 'partnerCurrency', - title: '14 Month Partner Report', - subTitle: 'Partner Currency', - }, - { - id: 'salaryCurrency', - title: '14 Month Salary Report', - subTitle: 'Salary Currency', - }, - { - id: 'designationAccounts', - title: 'Designation Accounts', - }, - { - id: 'responsibilityCenters', - title: 'Responsibility Centers', - }, - { - id: 'expectedMonthlyTotal', - title: 'Expected Monthly Total', - }, - { - id: 'partnerGivingAnalysis', - title: 'Partner Giving Analysis', - }, - { - id: 'coaching', - title: 'Coaching', - }, -]; diff --git a/src/components/Reports/PartnerGivingAnalysisReport/PartnerGivingAnalysisReport.test.tsx b/src/components/Reports/PartnerGivingAnalysisReport/PartnerGivingAnalysisReport.test.tsx index 1f40037ad..15ae8206f 100644 --- a/src/components/Reports/PartnerGivingAnalysisReport/PartnerGivingAnalysisReport.test.tsx +++ b/src/components/Reports/PartnerGivingAnalysisReport/PartnerGivingAnalysisReport.test.tsx @@ -279,7 +279,7 @@ describe('PartnerGivingAnalysisReport', () => { expect(queryByText(title)).toBeInTheDocument(); expect(getByTestId('PartnerGivingAnalysisReport')).toBeInTheDocument(); - expect(queryByTestId('ReportNavList')).toBeNull(); + expect(queryByTestId('MultiPageMenu')).toBeNull(); }); it('shows a placeholder when there are zero contacts', async () => { diff --git a/src/components/Reports/PartnerGivingAnalysisReport/PartnerGivingAnalysisReport.tsx b/src/components/Reports/PartnerGivingAnalysisReport/PartnerGivingAnalysisReport.tsx index e9fa403f5..49241201d 100644 --- a/src/components/Reports/PartnerGivingAnalysisReport/PartnerGivingAnalysisReport.tsx +++ b/src/components/Reports/PartnerGivingAnalysisReport/PartnerGivingAnalysisReport.tsx @@ -5,17 +5,20 @@ import { useDebouncedValue } from 'src/hooks/useDebounce'; import { useMassSelection } from 'src/hooks/useMassSelection'; import { sanitizeFilters } from 'src/lib/sanitizeFilters'; import { useGetPartnerGivingAnalysisIdsForMassSelectionQuery } from 'src/hooks/GetIdsForMassSelection.generated'; +import { useGetPartnerGivingAnalysisReportQuery } from './PartnerGivingAnalysisReport.generated'; import { ReportContactFilterSetInput, PartnerGivingAnalysisReportContact, SortDirection, } from '../../../../graphql/types.generated'; import type { Order } from '../Reports.type'; -import { useGetPartnerGivingAnalysisReportQuery } from './PartnerGivingAnalysisReport.generated'; -import { PartnerGivingAnalysisReportTable as Table } from './Table/Table'; -import { AccountsListHeader as Header } from '../AccountsListLayout/Header/Header'; import { EmptyReport } from 'src/components/Reports/EmptyReport/EmptyReport'; import { ListHeader } from 'src/components/Shared/Header/ListHeader'; +import { + MultiPageHeader, + HeaderTypeEnum, +} from 'src/components/Shared/MultiPageLayout/MultiPageHeader'; +import { PartnerGivingAnalysisReportTable as Table } from './Table/Table'; interface Props { accountListId: string; @@ -130,11 +133,11 @@ export const PartnerGivingAnalysisReport: React.FC = ({ return ( -
= ({ return ( -
{loading ? ( diff --git a/src/components/Reports/AccountsListLayout/Header/Header.test.tsx b/src/components/Shared/MultiPageLayout/MultiPageHeader.test.tsx similarity index 75% rename from src/components/Reports/AccountsListLayout/Header/Header.test.tsx rename to src/components/Shared/MultiPageLayout/MultiPageHeader.test.tsx index 8885184d1..0681c19ff 100644 --- a/src/components/Reports/AccountsListLayout/Header/Header.test.tsx +++ b/src/components/Shared/MultiPageLayout/MultiPageHeader.test.tsx @@ -1,23 +1,24 @@ import React from 'react'; -import { render } from '@testing-library/react'; +import { render, waitFor } from '@testing-library/react'; import { ThemeProvider } from '@mui/material/styles'; import userEvent from '@testing-library/user-event'; -import { AccountsListHeader as Header } from './Header'; +import { MultiPageHeader, HeaderTypeEnum } from './MultiPageHeader'; import theme from 'src/theme'; const totalBalance = 'CA111'; const title = 'test title'; const onNavListToggle = jest.fn(); -describe('AccountsListHeader', () => { +describe('MultiPageHeader', () => { it('default', async () => { const { getByRole, getByText } = render( -
, ); @@ -27,16 +28,18 @@ describe('AccountsListHeader', () => { userEvent.click( getByRole('button', { hidden: true, name: 'Toggle Filter Panel' }), ); + await waitFor(() => expect(onNavListToggle).toHaveBeenCalled()); }); it('should not render rightExtra if undefined', async () => { const { queryByText } = render( -
, ); diff --git a/src/components/Shared/MultiPageLayout/MultiPageHeader.tsx b/src/components/Shared/MultiPageLayout/MultiPageHeader.tsx new file mode 100644 index 000000000..9ce75ed00 --- /dev/null +++ b/src/components/Shared/MultiPageLayout/MultiPageHeader.tsx @@ -0,0 +1,100 @@ +import React, { FC, ReactNode } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Box, IconButton, Typography } from '@mui/material'; +import { styled } from '@mui/material/styles'; +import FilterList from '@mui/icons-material/FilterList'; +import MenuIcon from '@mui/icons-material/Menu'; +import theme from 'src/theme'; + +export enum HeaderTypeEnum { + Report = 'reports', + Settings = 'settings', +} + +interface MultiPageHeaderProps { + isNavListOpen: boolean; + onNavListToggle: () => void; + title: string; + headerType: HeaderTypeEnum; + rightExtra?: ReactNode; +} + +const StickyHeader = styled(Box, { + shouldForwardProp: (prop) => prop !== 'headerType', +})(({ headerType }: { headerType: HeaderTypeEnum }) => ({ + position: 'sticky', + top: 0, + height: 96, + color: + headerType === HeaderTypeEnum.Settings ? theme.palette.common.white : '', + backgroundColor: + headerType === HeaderTypeEnum.Settings ? theme.palette.primary.main : '', + paddingTop: headerType === HeaderTypeEnum.Settings ? theme.spacing(3) : '', + paddingBottom: headerType === HeaderTypeEnum.Settings ? theme.spacing(3) : '', +})); + +const NavListButton = styled(IconButton, { + shouldForwardProp: (prop) => prop !== 'panelOpen', +})(({ panelOpen }: { panelOpen: boolean }) => ({ + display: 'inline-block', + width: 48, + height: 48, + borderradius: 24, + margin: theme.spacing(0), + backgroundColor: panelOpen ? theme.palette.secondary.dark : 'transparent', + marginRight: '8px', + padding: '11px', +})); + +const NavFilterIcon = styled(FilterList)(() => ({ + width: 24, + height: 24, + color: theme.palette.primary.dark, +})); + +const NavMenuIcon = styled(MenuIcon)(() => ({ + width: 24, + height: 24, + color: theme.palette.common.white, +})); + +export const MultiPageHeader: FC = ({ + title, + rightExtra, + isNavListOpen, + onNavListToggle, + headerType, +}) => { + const { t } = useTranslation(); + + let titleAccess; + if (headerType === HeaderTypeEnum.Report) { + titleAccess = t('Toggle Filter Panel'); + } else if (headerType === HeaderTypeEnum.Settings) { + titleAccess = t('Toggle Preferences Menu'); + } + + return ( + + + + {headerType === HeaderTypeEnum.Report && ( + + )} + {headerType === HeaderTypeEnum.Settings && ( + + )} + + + {title} + + {rightExtra} + + + ); +}; diff --git a/src/components/Reports/NavReportsList/Item/Item.stories.tsx b/src/components/Shared/MultiPageLayout/MultiPageMenu/Item/Item.stories.tsx similarity index 60% rename from src/components/Reports/NavReportsList/Item/Item.stories.tsx rename to src/components/Shared/MultiPageLayout/MultiPageMenu/Item/Item.stories.tsx index 4b1a1d97f..88fde8f7c 100644 --- a/src/components/Reports/NavReportsList/Item/Item.stories.tsx +++ b/src/components/Shared/MultiPageLayout/MultiPageMenu/Item/Item.stories.tsx @@ -1,5 +1,6 @@ import React, { ReactElement } from 'react'; import { Item } from './Item'; +import { NavTypeEnum } from '../MultiPageMenu'; export default { title: 'Reports/ReportLayout/NavReportsList/Item', @@ -12,9 +13,13 @@ const item = { }; export const Default = (): ReactElement => ( - + ); export const Selected = (): ReactElement => ( - + ); diff --git a/src/components/Reports/NavReportsList/Item/Item.test.tsx b/src/components/Shared/MultiPageLayout/MultiPageMenu/Item/Item.test.tsx similarity index 80% rename from src/components/Reports/NavReportsList/Item/Item.test.tsx rename to src/components/Shared/MultiPageLayout/MultiPageMenu/Item/Item.test.tsx index 697bc044f..bbd8016fb 100644 --- a/src/components/Reports/NavReportsList/Item/Item.test.tsx +++ b/src/components/Shared/MultiPageLayout/MultiPageMenu/Item/Item.test.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { Item } from './Item'; import { render } from '__tests__/util/testingLibraryReactMock'; import TestWrapper from '__tests__/util/TestWrapper'; +import { NavTypeEnum } from '../MultiPageMenu'; const item = { id: 'testItem', @@ -13,7 +14,7 @@ describe('Item', () => { it('default', () => { const { queryByText } = render( - + , ); expect(queryByText(item.title)).toBeInTheDocument(); diff --git a/src/components/Shared/MultiPageLayout/MultiPageMenu/Item/Item.tsx b/src/components/Shared/MultiPageLayout/MultiPageMenu/Item/Item.tsx new file mode 100644 index 000000000..877ad201c --- /dev/null +++ b/src/components/Shared/MultiPageLayout/MultiPageMenu/Item/Item.tsx @@ -0,0 +1,100 @@ +import React, { useState, useMemo } from 'react'; +import { Collapse, ListItem, ListItemText } from '@mui/material'; +import { ArrowForwardIos } from '@mui/icons-material'; +import NextLink from 'next/link'; +import { useTranslation } from 'react-i18next'; +import { useAccountListId } from 'src/hooks/useAccountListId'; +import HandoffLink from 'src/components/HandoffLink'; +import { NavTypeEnum } from '../MultiPageMenu'; +import { NavItems } from '../MultiPageMenuItems'; +import theme from 'src/theme'; + +interface Props { + item: NavItems; + selectedId: string; + navType: NavTypeEnum; +} + +export const Item: React.FC = ({ + item, + selectedId, + navType, + ...rest +}) => { + const accountListId = useAccountListId(); + const [openSubMenu, setOpenSubMenu] = useState(false); + const { t } = useTranslation(); + + const isSelected = useMemo(() => { + if (item.id === selectedId) return true; + if (!item?.subItems?.length) return false; + return !!item.subItems.find((item) => item.id === selectedId)?.id; + }, [item]); + + const handleClick = () => { + if (isSelected) return; + if (!item?.subItems?.length) return; + setOpenSubMenu(!openSubMenu); + }; + + const children = ( + + + + + ); + + if (item.id === 'coaching') { + return {children}; + } else { + return ( + <> + + {children} + + {item?.subItems?.length && ( + + {item.subItems.map((subItem) => { + return ( + + ); + })} + + )} + + ); + } +}; diff --git a/src/components/Reports/NavReportsList/NavReportsList.stories.tsx b/src/components/Shared/MultiPageLayout/MultiPageMenu/MultiPageMenu.stories.tsx similarity index 75% rename from src/components/Reports/NavReportsList/NavReportsList.stories.tsx rename to src/components/Shared/MultiPageLayout/MultiPageMenu/MultiPageMenu.stories.tsx index 6cd4c0b21..6e7d9dfe4 100644 --- a/src/components/Reports/NavReportsList/NavReportsList.stories.tsx +++ b/src/components/Shared/MultiPageLayout/MultiPageMenu/MultiPageMenu.stories.tsx @@ -1,5 +1,5 @@ import React, { ReactElement } from 'react'; -import { NavReportsList } from './NavReportsList'; +import { MultiPageMenu, NavTypeEnum } from './MultiPageMenu'; const selected = 'salaryCurrency'; @@ -9,12 +9,13 @@ export default { export const Default = (): ReactElement => { return ( - {}} designationAccounts={[]} setDesignationAccounts={() => {}} + navType={NavTypeEnum.Reports} /> ); }; diff --git a/src/components/Reports/NavReportsList/NavReportsList.test.tsx b/src/components/Shared/MultiPageLayout/MultiPageMenu/MultiPageMenu.test.tsx similarity index 90% rename from src/components/Reports/NavReportsList/NavReportsList.test.tsx rename to src/components/Shared/MultiPageLayout/MultiPageMenu/MultiPageMenu.test.tsx index f90c83cfb..98fc06f9b 100644 --- a/src/components/Reports/NavReportsList/NavReportsList.test.tsx +++ b/src/components/Shared/MultiPageLayout/MultiPageMenu/MultiPageMenu.test.tsx @@ -2,11 +2,11 @@ import React from 'react'; import { render, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { ThemeProvider } from '@mui/material/styles'; -import { NavReportsList } from './NavReportsList'; +import { MultiPageMenu, NavTypeEnum } from './MultiPageMenu'; import TestRouter from '__tests__/util/TestRouter'; import { GqlMockedProvider } from '__tests__/util/graphqlMocking'; import theme from 'src/theme'; -import { GetDesignationAccountsQuery } from '../DonationsReport/Table/Modal/EditDonation.generated'; +import { GetDesignationAccountsQuery } from 'src/components/Reports/DonationsReport/Table/Modal/EditDonation.generated'; const accountListId = 'account-list-1'; const selected = 'salaryCurrency'; @@ -16,18 +16,19 @@ const router = { isReady: true, }; -describe('NavReportsList', () => { +describe('MultiPageMenu', () => { it('default', async () => { const { getByText } = render( - {}} designationAccounts={[]} setDesignationAccounts={() => {}} + navType={NavTypeEnum.Reports} /> @@ -72,12 +73,13 @@ describe('NavReportsList', () => { mocks={mocks} onCall={mutationSpy} > - {}} designationAccounts={designationAccounts} setDesignationAccounts={setDesignationAccounts} + navType={NavTypeEnum.Reports} /> @@ -121,12 +123,13 @@ describe('NavReportsList', () => { mocks={mocks} onCall={mutationSpy} > - {}} designationAccounts={[]} setDesignationAccounts={jest.fn()} + navType={NavTypeEnum.Reports} /> diff --git a/src/components/Shared/MultiPageLayout/MultiPageMenu/MultiPageMenu.tsx b/src/components/Shared/MultiPageLayout/MultiPageMenu/MultiPageMenu.tsx new file mode 100644 index 000000000..bc030e15c --- /dev/null +++ b/src/components/Shared/MultiPageLayout/MultiPageMenu/MultiPageMenu.tsx @@ -0,0 +1,160 @@ +import React, { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { + Box, + BoxProps, + IconButton, + List, + Slide, + Typography, +} from '@mui/material'; +import { styled } from '@mui/material/styles'; +import { makeStyles } from 'tss-react/mui'; +import Close from '@mui/icons-material/Close'; +import { useAccountListId } from 'src/hooks/useAccountListId'; +import { Item } from './Item/Item'; +import { MultiselectFilter } from '../../../../../graphql/types.generated'; +import { FilterListItemMultiselect } from 'src/components/Shared/Filters/FilterListItemMultiselect'; +import { useGetDesignationAccountsQuery } from 'src/components/Reports/DonationsReport/Table/Modal/EditDonation.generated'; +import { useGetUserAccessQuery } from './MultiPageMenuItems.generated'; +import { reportNavItems, settingsNavItems } from './MultiPageMenuItems'; + +export enum NavTypeEnum { + Reports = 'reports', + Settings = 'settings', +} + +interface Props { + selectedId: string; + isOpen: boolean; + onClose: () => void; + navType: NavTypeEnum; + designationAccounts?: string[]; + setDesignationAccounts?: (designationAccounts: string[]) => void; +} + +const useStyles = makeStyles()(() => ({ + root: { + overflow: 'hidden', + }, +})); + +const FilterHeader = styled(Box)(({ theme }) => ({ + padding: theme.spacing(2), + borderBottom: '1px solid', + borderBottomColor: theme.palette.grey[200], +})); + +const FilterList = styled(List)(({ theme }) => ({ + '& .MuiListItemIcon-root': { + minWidth: '37px', + }, + '& .FilterListItemMultiselect-root': { + marginBottom: theme.spacing(2), + }, +})); + +export const MultiPageMenu: React.FC = ({ + selectedId, + isOpen, + onClose, + navType, + designationAccounts, + setDesignationAccounts, + ...BoxProps +}) => { + const { classes } = useStyles(); + const { t } = useTranslation(); + const accountListId = useAccountListId(); + const { data: userPrivileges } = useGetUserAccessQuery(); + const navItems = + navType === NavTypeEnum.Reports ? reportNavItems : settingsNavItems; + const navTitle = + navType === NavTypeEnum.Reports ? t('Reports') : t('Settings'); + + const { data } = useGetDesignationAccountsQuery({ + variables: { + accountListId: accountListId ?? '', + }, + skip: !designationAccounts && !setDesignationAccounts, + }); + const accounts = + data?.designationAccounts + .flatMap((group) => group.designationAccounts) + .map((account) => ({ + name: account.name, + value: account.id, + placeholder: null, + })) ?? []; + + const filter: MultiselectFilter = { + filterKey: 'designation_account_id', + title: 'Designation Account', + options: accounts, + }; + + return ( + +
+ + + + + {navTitle} + + + + + + + {designationAccounts && + setDesignationAccounts && + accounts.length > 1 && ( + { + setDesignationAccounts(value ?? []); + }} + /> + )} + {navItems.map((item) => { + const showItem = useMemo(() => { + if (item?.grantedAccess?.length) { + if ( + item.grantedAccess.indexOf('admin') !== -1 && + userPrivileges?.user.admin + ) { + return true; + } + if ( + item.grantedAccess.indexOf('developer') !== -1 && + userPrivileges?.user.developer + ) { + return true; + } + } else return true; + return false; + }, [item]); + + if (!showItem) return null; + return ( + + ); + })} + + + +
+
+ ); +}; diff --git a/src/components/Shared/MultiPageLayout/MultiPageMenu/MultiPageMenuItems.graphql b/src/components/Shared/MultiPageLayout/MultiPageMenu/MultiPageMenuItems.graphql new file mode 100644 index 000000000..e4e6a401c --- /dev/null +++ b/src/components/Shared/MultiPageLayout/MultiPageMenu/MultiPageMenuItems.graphql @@ -0,0 +1,6 @@ +query GetUserAccess { + user { + admin + developer + } +} diff --git a/src/components/Shared/MultiPageLayout/MultiPageMenu/MultiPageMenuItems.ts b/src/components/Shared/MultiPageLayout/MultiPageMenu/MultiPageMenuItems.ts new file mode 100644 index 000000000..5d2691db5 --- /dev/null +++ b/src/components/Shared/MultiPageLayout/MultiPageMenu/MultiPageMenuItems.ts @@ -0,0 +1,104 @@ +export type NavItems = { + id: string; + title: string; + subTitle?: string; + grantedAccess?: string[]; + subItems?: NavItems[]; +}; + +export const reportNavItems: NavItems[] = [ + { + id: 'donations', + title: 'Donations', + }, + { + id: 'partnerCurrency', + title: '14 Month Partner Report', + subTitle: 'Partner Currency', + }, + { + id: 'salaryCurrency', + title: '14 Month Salary Report', + subTitle: 'Salary Currency', + }, + { + id: 'designationAccounts', + title: 'Designation Accounts', + }, + { + id: 'responsibilityCenters', + title: 'Responsibility Centers', + }, + { + id: 'expectedMonthlyTotal', + title: 'Expected Monthly Total', + }, + { + id: 'partnerGivingAnalysis', + title: 'Partner Giving Analysis', + }, + { + id: 'coaching', + title: 'Coaching', + }, +]; + +export const settingsNavItems: NavItems[] = [ + { + id: 'preferences', + title: 'Preferences', + }, + { + id: 'notifications', + title: 'Notifications', + }, + { + id: 'integrations', + title: 'Connect Services', + }, + { + id: 'manageAccounts', + title: 'Manage Accounts', + }, + { + id: 'manageCoaches', + title: 'Manage Coaches', + }, + { + id: 'organizations', + title: 'Manage Organizations', + grantedAccess: ['admin'], + subItems: [ + { + id: 'organizations', + title: 'Impersonate & Share', + grantedAccess: ['admin'], + }, + { + id: 'organizations/accountLists', + title: 'Account Lists', + grantedAccess: ['admin'], + }, + { + id: 'organizations/contacts', + title: 'Contacts', + grantedAccess: ['admin'], + }, + ], + }, + { + id: 'adminConsole', + title: 'Admin Console', + grantedAccess: ['admin', 'developer'], + }, + { + id: 'backendAdmin', + title: 'Backend Admin', + grantedAccess: ['developer'], + }, + { + id: 'sidekiq', + title: 'Sidekiq', + grantedAccess: ['developer'], + }, +]; From 898a1c542d87049b36325ba8200472edb40dfb0d Mon Sep 17 00:00:00 2001 From: Daniel Bisgrove Date: Mon, 13 Nov 2023 09:55:54 -0500 Subject: [PATCH 02/20] Adding wrapper and notification page --- .../settings/notifications.page.tsx | 36 +++++++++ .../[accountListId]/settings/wrapper.tsx | 74 +++++++++++++++++++ 2 files changed, 110 insertions(+) create mode 100644 pages/accountLists/[accountListId]/settings/notifications.page.tsx create mode 100644 pages/accountLists/[accountListId]/settings/wrapper.tsx diff --git a/pages/accountLists/[accountListId]/settings/notifications.page.tsx b/pages/accountLists/[accountListId]/settings/notifications.page.tsx new file mode 100644 index 000000000..a77c08e3d --- /dev/null +++ b/pages/accountLists/[accountListId]/settings/notifications.page.tsx @@ -0,0 +1,36 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { Box } from '@mui/material'; +import { SettingsWrapper } from './wrapper'; +import { NotificationsTable } from 'src/components/Settings/notifications/NotificationsTable'; + +const Notifications: React.FC = () => { + const { t } = useTranslation(); + + return ( + + +

+ Based on an analysis of a partner's giving history, MPDX can + notify you of events that you will probably want to follow up on. The + detection logic is based on a set of rules that are right most of the + time, but you will still want to verify an event manually before + contacting the partner. +

+
+ +

+ For each event MPDX can notify you via email and also create a task + entry reminding you to do something about it. The options below allow + you to control that behavior. +

+
+ +
+ ); +}; + +export default Notifications; diff --git a/pages/accountLists/[accountListId]/settings/wrapper.tsx b/pages/accountLists/[accountListId]/settings/wrapper.tsx new file mode 100644 index 000000000..558503ac6 --- /dev/null +++ b/pages/accountLists/[accountListId]/settings/wrapper.tsx @@ -0,0 +1,74 @@ +import { Box, Container } from '@mui/material'; +import { styled } from '@mui/material/styles'; +import React, { useState } from 'react'; +import Head from 'next/head'; +import useGetAppSettings from 'src/hooks/useGetAppSettings'; +import { SidePanelsLayout } from 'src/components/Layouts/SidePanelsLayout'; +import { + MultiPageMenu, + NavTypeEnum, +} from 'src/components/Shared/MultiPageLayout/MultiPageMenu/MultiPageMenu'; +import { + MultiPageHeader, + HeaderTypeEnum, +} from 'src/components/Shared/MultiPageLayout/MultiPageHeader'; + +const PageContentWrapper = styled(Container)(({ theme }) => ({ + paddingTop: theme.spacing(3), + paddingBottom: theme.spacing(3), +})); + +interface SettingsWrapperProps { + pageTitle: string; + pageHeading: string; + children?: React.ReactNode; +} + +export const SettingsWrapper: React.FC = ({ + pageTitle, + pageHeading, + children, +}) => { + const { appName } = useGetAppSettings(); + const [isNavListOpen, setNavListOpen] = useState(false); + const handleNavListToggle = () => { + setNavListOpen(!isNavListOpen); + }; + + return ( + <> + + + {appName} | {pageTitle} + + + + + } + leftOpen={isNavListOpen} + leftWidth="290px" + mainContent={ + <> + + {children} + + } + /> + + + ); +}; From 3b69efac3d947bdc2f0b2f4cd57658ef34d16409 Mon Sep 17 00:00:00 2001 From: Daniel Bisgrove Date: Mon, 13 Nov 2023 09:56:11 -0500 Subject: [PATCH 03/20] Adding skeleton and graphQL calls --- .../notifications/GetNotifications.graphql | 13 +++ .../NotificationsTableSkeleton.tsx | 91 +++++++++++++++++++ .../notifications/UpdateNotifications.graphql | 11 +++ 3 files changed, 115 insertions(+) create mode 100644 src/components/Settings/notifications/GetNotifications.graphql create mode 100644 src/components/Settings/notifications/NotificationsTableSkeleton.tsx create mode 100644 src/components/Settings/notifications/UpdateNotifications.graphql diff --git a/src/components/Settings/notifications/GetNotifications.graphql b/src/components/Settings/notifications/GetNotifications.graphql new file mode 100644 index 000000000..1b4814436 --- /dev/null +++ b/src/components/Settings/notifications/GetNotifications.graphql @@ -0,0 +1,13 @@ +query getPreferencesNotifications($accountListId: ID!) { + notificationPreferences(accountListId: $accountListId) { + nodes { + app + email + notificationType { + descriptionTemplate + type + } + task + } + } +} diff --git a/src/components/Settings/notifications/NotificationsTableSkeleton.tsx b/src/components/Settings/notifications/NotificationsTableSkeleton.tsx new file mode 100644 index 000000000..15f0635a5 --- /dev/null +++ b/src/components/Settings/notifications/NotificationsTableSkeleton.tsx @@ -0,0 +1,91 @@ +import React from 'react'; +import { + Box, + TableContainer, + Table, + TableHead, + TableRow, + TableBody, + Paper, + Skeleton, +} from '@mui/material'; +import { useTranslation } from 'react-i18next'; +import { + StyledTableHeadCell, + StyledTableHeadSelectCell, + StyledTableCell, + StyledTableRow, + StyledSmartphone, + StyledEmail, + StyledTask, + SelectAllBox, +} from './NotificationsTable'; + +export const NotificationsTableSkeleton: React.FC = () => { + const { t } = useTranslation(); + + return ( + + + + + + {t("Select the types of notifications you'd like to receive")} + + + + {t('In App')} + + + + {t('Email')} + + + + {t('Task')} + + + + + + {t('select all')} + + + {t('select all')} + + + {t('select all')} + + + + + + {new Array(5).fill(0).map((_, idx) => ( + + + + + + + + + + + + + + + ))} + +
+
+ ); +}; diff --git a/src/components/Settings/notifications/UpdateNotifications.graphql b/src/components/Settings/notifications/UpdateNotifications.graphql new file mode 100644 index 000000000..ca7130898 --- /dev/null +++ b/src/components/Settings/notifications/UpdateNotifications.graphql @@ -0,0 +1,11 @@ +mutation UpdateNotificationPreferences( + $input: NotificationPreferencesUpdateMutationInput! +) { + updateNotificationPreferences(input: $input) { + notificationPreferences { + notificationType { + id + } + } + } +} From 7343a900829bf0c3f67d0a2e2119738fca104022 Mon Sep 17 00:00:00 2001 From: Daniel Bisgrove Date: Mon, 13 Nov 2023 09:59:12 -0500 Subject: [PATCH 04/20] Notifications table --- .../notifications/NotificationsTable.test.tsx | 180 ++++++++ .../notifications/NotificationsTable.tsx | 384 ++++++++++++++++++ 2 files changed, 564 insertions(+) create mode 100644 src/components/Settings/notifications/NotificationsTable.test.tsx create mode 100644 src/components/Settings/notifications/NotificationsTable.tsx diff --git a/src/components/Settings/notifications/NotificationsTable.test.tsx b/src/components/Settings/notifications/NotificationsTable.test.tsx new file mode 100644 index 000000000..09a188306 --- /dev/null +++ b/src/components/Settings/notifications/NotificationsTable.test.tsx @@ -0,0 +1,180 @@ +import React from 'react'; +import { render, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { NotificationsTable } from './NotificationsTable'; +import { ThemeProvider } from '@mui/material/styles'; +import { SnackbarProvider } from 'notistack'; +import { GqlMockedProvider } from '../../../../__tests__/util/graphqlMocking'; +import TestRouter from '../../../../__tests__/util/TestRouter'; +import theme from '../../../../src/theme'; +import { NotificationTypeTypeEnum } from '../../../../graphql/types.generated'; + +const mockEnqueue = jest.fn(); + +jest.mock('notistack', () => ({ + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + ...jest.requireActual('notistack'), + useSnackbar: () => { + return { + enqueueSnackbar: mockEnqueue, + }; + }, +})); + +const accountListId = 'test121'; + +const router = { + query: { accountListId }, + isReady: true, +}; +const createNotification = (type) => ({ + app: false, + email: false, + task: false, + notificationType: { + descriptionTemplate: type, + type, + }, +}); +const mocks = { + getPreferencesNotifications: { + notificationPreferences: { + nodes: [ + createNotification(NotificationTypeTypeEnum.CallPartnerOncePerYear), + createNotification(NotificationTypeTypeEnum.LargerGift), + createNotification(NotificationTypeTypeEnum.LongTimeFrameGift), + ], + }, + }, +}; +const mutationSpy = jest.fn(); +const Components = ( + + + + + + + + + +); + +describe('NotificationsTable', () => { + beforeEach(() => { + mutationSpy.mockReset(); + }); + it('Should render the Table and request data', async () => { + const { getByTestId, queryByTestId, getByText } = render(Components); + + expect(getByTestId('skeleton-notifications')).toBeInTheDocument(); + + await waitFor(() => { + expect(queryByTestId('skeleton-notifications')).not.toBeInTheDocument(), + expect( + mutationSpy.mock.calls[0][0].operation.variables.accountListId, + ).toEqual(accountListId); + expect(mutationSpy.mock.calls[0][0].operation.operationName).toEqual( + 'getPreferencesNotifications', + ); + }); + + await waitFor(() => { + expect(getByText('CALL_PARTNER_ONCE_PER_YEAR')).toBeInTheDocument(); + }); + + await waitFor(() => { + expect( + getByTestId('CALL_PARTNER_ONCE_PER_YEAR-app-checkbox'), + ).not.toBeChecked(); + }); + + await waitFor(() => { + expect(getByText('LARGER_GIFT')).toBeInTheDocument(); + }); + }); + + it('Should select all', async () => { + const { queryByTestId, getByTestId, getAllByRole } = render(Components); + + await waitFor(() => + expect(queryByTestId('skeleton-notifications')).not.toBeInTheDocument(), + ); + expect(getAllByRole('checkbox')[0]).not.toBeChecked(); + expect(getAllByRole('checkbox')[1]).not.toBeChecked(); + expect(getAllByRole('checkbox')[2]).not.toBeChecked(); + expect(getAllByRole('checkbox')[3]).not.toBeChecked(); + expect(getAllByRole('checkbox')[4]).not.toBeChecked(); + expect(getAllByRole('checkbox')[5]).not.toBeChecked(); + expect(getAllByRole('checkbox')[6]).not.toBeChecked(); + expect(getAllByRole('checkbox')[7]).not.toBeChecked(); + expect(getAllByRole('checkbox')[8]).not.toBeChecked(); + + // Select all app + userEvent.click(getByTestId('select-all-app')); + expect(getAllByRole('checkbox')[0]).toBeChecked(); + expect(getAllByRole('checkbox')[3]).toBeChecked(); + expect(getAllByRole('checkbox')[6]).toBeChecked(); + + // Select all email + userEvent.click(getByTestId('select-all-email')); + expect(getAllByRole('checkbox')[1]).toBeChecked(); + expect(getAllByRole('checkbox')[4]).toBeChecked(); + expect(getAllByRole('checkbox')[7]).toBeChecked(); + + // Select all tasks + userEvent.click(getByTestId('select-all-task')); + expect(getAllByRole('checkbox')[2]).toBeChecked(); + expect(getAllByRole('checkbox')[5]).toBeChecked(); + expect(getAllByRole('checkbox')[8]).toBeChecked(); + }); + + it('Should send data to server on submit', async () => { + const { queryByTestId, getByTestId, getByRole } = render(Components); + + await waitFor(() => + expect(queryByTestId('skeleton-notifications')).not.toBeInTheDocument(), + ); + // Select all app + userEvent.click(getByTestId('select-all-app')); + + userEvent.click( + getByRole('button', { + name: /save/i, + }), + ); + + await waitFor(() => { + // mutationSpy.mock.calls[1][0].operation.variables.input + expect(mutationSpy.mock.calls[1][0].operation.operationName).toEqual( + 'UpdateNotificationPreferences', + ); + + expect(mutationSpy.mock.calls[1][0].operation.variables.input).toEqual({ + accountListId: accountListId, + attributes: [ + { + app: true, + email: false, + task: false, + notificationType: 'CALL_PARTNER_ONCE_PER_YEAR', + }, + { + app: true, + email: false, + task: false, + notificationType: 'LARGER_GIFT', + }, + { + app: true, + email: false, + task: false, + notificationType: 'LONG_TIME_FRAME_GIFT', + }, + ], + }); + expect(mockEnqueue).toHaveBeenCalled(); + }); + }); +}); diff --git a/src/components/Settings/notifications/NotificationsTable.tsx b/src/components/Settings/notifications/NotificationsTable.tsx new file mode 100644 index 000000000..252a88157 --- /dev/null +++ b/src/components/Settings/notifications/NotificationsTable.tsx @@ -0,0 +1,384 @@ +import * as yup from 'yup'; +import { Formik, FieldArray } from 'formik'; +import { useTranslation } from 'react-i18next'; +import { useSnackbar } from 'notistack'; +import React, { useState, ReactElement } from 'react'; +import { styled } from '@mui/material/styles'; +import { + Box, + Checkbox, + TableContainer, + Table, + TableCell, + TableHead, + TableRow, + TableBody, + Paper, +} from '@mui/material'; +import { Email, Smartphone, Task } from '@mui/icons-material'; +import * as Types from '../../../../graphql/types.generated'; +import { useAccountListId } from 'src/hooks/useAccountListId'; +import { SubmitButton } from 'src/components/common/Modal/ActionButtons/ActionButtons'; +import { useGetPreferencesNotificationsQuery } from './GetNotifications.generated'; +import { NotificationsTableSkeleton } from './NotificationsTableSkeleton'; +import { useUpdateNotificationPreferencesMutation } from './UpdateNotifications.generated'; + +export enum notificationsEnum { + App = 'app', + Email = 'email', + Task = 'task', +} + +export const StyledTableHeadCell = styled(TableCell)(({ theme }) => ({ + backgroundColor: theme.palette.primary.main, + color: theme.palette.common.white, +})); + +export const StyledTableHeadSelectCell = styled(TableCell)(() => ({ + cursor: 'pointer', + fontSize: 14, + paddingTop: 8, + paddingBottom: 8, + top: 88, +})); + +export const StyledTableCell = styled(TableCell)(() => ({ + fontSize: 14, + paddingTop: 8, + paddingBottom: 8, +})); + +export const StyledTableRow = styled(TableRow)(({ theme }) => ({ + '&:nth-of-type(odd)': { + backgroundColor: theme.palette.action.hover, + }, + // hide last border + '&:last-child td, &:last-child th': { + border: 0, + }, +})); + +export const StyledSmartphone = styled(Smartphone)(() => ({ + marginRight: '8px', +})); +export const StyledEmail = styled(Email)(() => ({ + marginRight: '6px', +})); +export const StyledTask = styled(Task)(() => ({ + marginRight: '3px', +})); + +export const SelectAllBox = styled(Box)(() => ({ + width: 120, + margin: '0 0 0 auto', +})); + +type notificationType = Array< + Pick & { + notificationType: Pick< + Types.NotificationType, + 'descriptionTemplate' | 'type' + >; + } +>; + +export const NotificationsTable: React.FC = () => { + const { t } = useTranslation(); + const accountListId = useAccountListId(); + const { enqueueSnackbar } = useSnackbar(); + const [appSelectAll, setAppSelectAll] = useState(false); + const [emailSelectAll, setEmailSelectAll] = useState(false); + const [isSetup, _] = useState(false); + const [taskSelectAll, setTaskSelectAll] = useState(false); + + const [updateNotifications] = useUpdateNotificationPreferencesMutation(); + + const NotificationSchema: yup.SchemaOf<{ + notifications: notificationType; + }> = yup.object({ + notifications: yup.array( + yup.object({ + app: yup.boolean().required(), + email: yup.boolean().required(), + task: yup.boolean().required(), + notificationType: yup.object({ + descriptionTemplate: yup.string().required(), + type: yup + .mixed() + .oneOf(Object.values(Types.NotificationTypeTypeEnum)) + .required(), + }), + }), + ), + }); + + const { data, loading } = useGetPreferencesNotificationsQuery({ + variables: { + accountListId: accountListId ?? '', + }, + }); + + const defaultIfInSetup = ( + notificationPreference: any, + type: string, + ): boolean => { + // If Setup, show preference or default to TRUE + // If not setup, show preference or default to FALSE. + return notificationPreference[type] || isSetup; + }; + + // TODO: We need order the notifications like we do on the old MPDX. + // PR is https://github.com/CruGlobal/mpdx_api/pull/2743 + const notifications: notificationType = + data?.notificationPreferences?.nodes.reduce((result, notification) => { + return [ + ...result, + { + ...notification, + app: defaultIfInSetup(notification, 'app'), + email: defaultIfInSetup(notification, 'email'), + task: defaultIfInSetup(notification, 'task'), + }, + ]; + }, []); + + const selectAll = ( + type, + notifications, + setFieldValue, + selectAll, + setSelectAll, + ) => { + setSelectAll(!selectAll); + notifications.forEach((_, idx) => { + setFieldValue(`notifications.${idx}.${type}`, !selectAll); + }); + }; + + const onSubmit = async ({ + notifications, + }: { + notifications: Array< + Pick & { + notificationType: Pick< + Types.NotificationType, + 'descriptionTemplate' | 'type' + >; + } + >; + }) => { + const attributes = notifications.map((notification) => { + return { + app: notification.app, + email: notification.email, + task: notification.task, + notificationType: notification.notificationType.type, + }; + }); + + await updateNotifications({ + variables: { + input: { + accountListId: accountListId ?? '', + attributes, + }, + }, + }); + + enqueueSnackbar(t('Notifications updated successfully'), { + variant: 'success', + }); + }; + + return ( + + {loading && } + {!loading && ( + + {({ + values: { notifications }, + handleSubmit, + setFieldValue, + isSubmitting, + isValid, + }): ReactElement => ( +
+ + + {t('Save Changes')} + + + + ( + + + + + {t( + "Select the types of notifications you'd like to receive", + )} + + + + {t('In App')} + + + + {t('Email')} + + + + {t('Task')} + + + + + + selectAll( + notificationsEnum.App, + notifications, + setFieldValue, + appSelectAll, + setAppSelectAll, + ) + } + > + + {appSelectAll + ? t('deselect all') + : t('select all')} + + + + selectAll( + notificationsEnum.Email, + notifications, + setFieldValue, + emailSelectAll, + setEmailSelectAll, + ) + } + > + + {emailSelectAll + ? t('deselect all') + : t('select all')} + + + + selectAll( + notificationsEnum.Task, + notifications, + setFieldValue, + taskSelectAll, + setTaskSelectAll, + ) + } + > + + {taskSelectAll + ? t('deselect all') + : t('select all')} + + + + + + + <> + {notifications.map((notification, idx) => { + const { type, descriptionTemplate } = + notification.notificationType; + return ( + + + {descriptionTemplate} + + + { + setFieldValue( + `notifications.${idx}.app`, + value, + ); + }} + /> + + + { + setFieldValue( + `notifications.${idx}.email`, + value, + ); + }} + /> + + + { + setFieldValue( + `notifications.${idx}.task`, + value, + ); + }} + /> + + + ); + })} + + +
+ )} + /> +
+ + + {t('Save Changes')} + + +
+ )} +
+ )} +
+ ); +}; From 19ee9737805443fad978bcf36a2f03a9fb68808c Mon Sep 17 00:00:00 2001 From: Daniel Bisgrove Date: Tue, 14 Nov 2023 09:20:38 -0500 Subject: [PATCH 05/20] Hooking up new Notifications constants query. Notifications are now in same order as old mpdx --- .../notifications/GetNotifications.graphql | 11 +++++ .../notifications/NotificationsTable.tsx | 45 +++++++++++++------ 2 files changed, 42 insertions(+), 14 deletions(-) diff --git a/src/components/Settings/notifications/GetNotifications.graphql b/src/components/Settings/notifications/GetNotifications.graphql index 1b4814436..88f8a56e7 100644 --- a/src/components/Settings/notifications/GetNotifications.graphql +++ b/src/components/Settings/notifications/GetNotifications.graphql @@ -4,6 +4,7 @@ query getPreferencesNotifications($accountListId: ID!) { app email notificationType { + id descriptionTemplate type } @@ -11,3 +12,13 @@ query getPreferencesNotifications($accountListId: ID!) { } } } + +query getNotificationConstants { + constant { + notificationTranslatedHashes { + id + key + value + } + } +} diff --git a/src/components/Settings/notifications/NotificationsTable.tsx b/src/components/Settings/notifications/NotificationsTable.tsx index 252a88157..0677af373 100644 --- a/src/components/Settings/notifications/NotificationsTable.tsx +++ b/src/components/Settings/notifications/NotificationsTable.tsx @@ -1,8 +1,9 @@ -import * as yup from 'yup'; -import { Formik, FieldArray } from 'formik'; +import React, { useState, useEffect, ReactElement } from 'react'; import { useTranslation } from 'react-i18next'; +import { Formik, FieldArray } from 'formik'; +import * as yup from 'yup'; +import { v4 as uuidv4 } from 'uuid'; import { useSnackbar } from 'notistack'; -import React, { useState, ReactElement } from 'react'; import { styled } from '@mui/material/styles'; import { Box, @@ -16,12 +17,15 @@ import { Paper, } from '@mui/material'; import { Email, Smartphone, Task } from '@mui/icons-material'; -import * as Types from '../../../../graphql/types.generated'; import { useAccountListId } from 'src/hooks/useAccountListId'; +import { + useGetPreferencesNotificationsQuery, + useGetNotificationConstantsQuery, +} from './GetNotifications.generated'; +import { useUpdateNotificationPreferencesMutation } from './UpdateNotifications.generated'; +import * as Types from '../../../../graphql/types.generated'; import { SubmitButton } from 'src/components/common/Modal/ActionButtons/ActionButtons'; -import { useGetPreferencesNotificationsQuery } from './GetNotifications.generated'; import { NotificationsTableSkeleton } from './NotificationsTableSkeleton'; -import { useUpdateNotificationPreferencesMutation } from './UpdateNotifications.generated'; export enum notificationsEnum { App = 'app', @@ -86,6 +90,7 @@ export const NotificationsTable: React.FC = () => { const { t } = useTranslation(); const accountListId = useAccountListId(); const { enqueueSnackbar } = useSnackbar(); + const [notifications, setNotifications] = useState([]); const [appSelectAll, setAppSelectAll] = useState(false); const [emailSelectAll, setEmailSelectAll] = useState(false); const [isSetup, _] = useState(false); @@ -117,6 +122,7 @@ export const NotificationsTable: React.FC = () => { accountListId: accountListId ?? '', }, }); + const { data: notificationConstants } = useGetNotificationConstantsQuery(); const defaultIfInSetup = ( notificationPreference: any, @@ -127,21 +133,32 @@ export const NotificationsTable: React.FC = () => { return notificationPreference[type] || isSetup; }; - // TODO: We need order the notifications like we do on the old MPDX. - // PR is https://github.com/CruGlobal/mpdx_api/pull/2743 - const notifications: notificationType = - data?.notificationPreferences?.nodes.reduce((result, notification) => { + useEffect(() => { + const notificationsData = data?.notificationPreferences?.nodes || []; + const notificationsOrder = + notificationConstants?.constant?.notificationTranslatedHashes || []; + + if (!notificationsData.length || !notificationsOrder.length) return; + + const notifications = notificationsOrder.reduce((result, notification) => { + const notificationPreference = notificationsData.find( + (object) => object.notificationType.id === notification.key, + ); return [ ...result, { - ...notification, - app: defaultIfInSetup(notification, 'app'), - email: defaultIfInSetup(notification, 'email'), - task: defaultIfInSetup(notification, 'task'), + id: notificationPreference.id || uuidv4(), + notificationType: notificationPreference.notificationType, + app: defaultIfInSetup(notificationPreference, 'app'), + email: defaultIfInSetup(notificationPreference, 'email'), + task: defaultIfInSetup(notificationPreference, 'task'), }, ]; }, []); + setNotifications(notifications); + }, [data, notificationConstants]); + const selectAll = ( type, notifications, From 261f9c51ae5a6138c75cfa24f699e2fc349cd71c Mon Sep 17 00:00:00 2001 From: Daniel Bisgrove Date: Tue, 14 Nov 2023 10:39:45 -0500 Subject: [PATCH 06/20] Implementing notifications constants and cleaning TS errors --- .../notifications/NotificationsTable.tsx | 72 +++++++++---------- 1 file changed, 35 insertions(+), 37 deletions(-) diff --git a/src/components/Settings/notifications/NotificationsTable.tsx b/src/components/Settings/notifications/NotificationsTable.tsx index 0677af373..c0c12b383 100644 --- a/src/components/Settings/notifications/NotificationsTable.tsx +++ b/src/components/Settings/notifications/NotificationsTable.tsx @@ -2,7 +2,6 @@ import React, { useState, useEffect, ReactElement } from 'react'; import { useTranslation } from 'react-i18next'; import { Formik, FieldArray } from 'formik'; import * as yup from 'yup'; -import { v4 as uuidv4 } from 'uuid'; import { useSnackbar } from 'notistack'; import { styled } from '@mui/material/styles'; import { @@ -33,6 +32,17 @@ export enum notificationsEnum { Task = 'task', } +type Notification = Pick< + Types.NotificationPreference, + 'app' | 'email' | 'task' +> & { + notificationType: Pick< + Types.NotificationType, + 'id' | 'descriptionTemplate' | 'type' + >; +}; +type NotificationConstant = Pick; + export const StyledTableHeadCell = styled(TableCell)(({ theme }) => ({ backgroundColor: theme.palette.primary.main, color: theme.palette.common.white, @@ -77,20 +87,11 @@ export const SelectAllBox = styled(Box)(() => ({ margin: '0 0 0 auto', })); -type notificationType = Array< - Pick & { - notificationType: Pick< - Types.NotificationType, - 'descriptionTemplate' | 'type' - >; - } ->; - export const NotificationsTable: React.FC = () => { const { t } = useTranslation(); const accountListId = useAccountListId(); const { enqueueSnackbar } = useSnackbar(); - const [notifications, setNotifications] = useState([]); + const [notifications, setNotifications] = useState([]); const [appSelectAll, setAppSelectAll] = useState(false); const [emailSelectAll, setEmailSelectAll] = useState(false); const [isSetup, _] = useState(false); @@ -99,7 +100,7 @@ export const NotificationsTable: React.FC = () => { const [updateNotifications] = useUpdateNotificationPreferencesMutation(); const NotificationSchema: yup.SchemaOf<{ - notifications: notificationType; + notifications: Notification[]; }> = yup.object({ notifications: yup.array( yup.object({ @@ -107,6 +108,7 @@ export const NotificationsTable: React.FC = () => { email: yup.boolean().required(), task: yup.boolean().required(), notificationType: yup.object({ + id: yup.string().required(), descriptionTemplate: yup.string().required(), type: yup .mixed() @@ -134,27 +136,30 @@ export const NotificationsTable: React.FC = () => { }; useEffect(() => { - const notificationsData = data?.notificationPreferences?.nodes || []; - const notificationsOrder = + const notificationsData: Notification[] = + data?.notificationPreferences?.nodes || []; + const notificationsOrder: NotificationConstant[] = notificationConstants?.constant?.notificationTranslatedHashes || []; if (!notificationsData.length || !notificationsOrder.length) return; - const notifications = notificationsOrder.reduce((result, notification) => { - const notificationPreference = notificationsData.find( - (object) => object.notificationType.id === notification.key, - ); - return [ - ...result, - { - id: notificationPreference.id || uuidv4(), - notificationType: notificationPreference.notificationType, - app: defaultIfInSetup(notificationPreference, 'app'), - email: defaultIfInSetup(notificationPreference, 'email'), - task: defaultIfInSetup(notificationPreference, 'task'), - }, - ]; - }, []); + const notifications = notificationsOrder.reduce( + (result: Notification[], notification) => { + const notificationPreference = notificationsData.find( + (object) => object.notificationType.id === notification.key, + ); + return [ + ...result, + { + notificationType: notificationPreference?.notificationType || {}, + app: defaultIfInSetup(notificationPreference, 'app'), + email: defaultIfInSetup(notificationPreference, 'email'), + task: defaultIfInSetup(notificationPreference, 'task'), + } as Notification, + ]; + }, + [], + ); setNotifications(notifications); }, [data, notificationConstants]); @@ -175,14 +180,7 @@ export const NotificationsTable: React.FC = () => { const onSubmit = async ({ notifications, }: { - notifications: Array< - Pick & { - notificationType: Pick< - Types.NotificationType, - 'descriptionTemplate' | 'type' - >; - } - >; + notifications: Notification[]; }) => { const attributes = notifications.map((notification) => { return { From 91cda3531ffb6357ddab901a0527af192cf41f7e Mon Sep 17 00:00:00 2001 From: Daniel Bisgrove Date: Wed, 15 Nov 2023 11:36:08 -0500 Subject: [PATCH 07/20] Switching useMemo for useEffect to ensure it's run on initial render. Added disabled flags to checkboxes if submitting. --- .../notifications/NotificationsTable.tsx | 42 +++++++++---------- 1 file changed, 19 insertions(+), 23 deletions(-) diff --git a/src/components/Settings/notifications/NotificationsTable.tsx b/src/components/Settings/notifications/NotificationsTable.tsx index c0c12b383..be947cedf 100644 --- a/src/components/Settings/notifications/NotificationsTable.tsx +++ b/src/components/Settings/notifications/NotificationsTable.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, ReactElement } from 'react'; +import React, { useState, useMemo, ReactElement } from 'react'; import { useTranslation } from 'react-i18next'; import { Formik, FieldArray } from 'formik'; import * as yup from 'yup'; @@ -91,7 +91,6 @@ export const NotificationsTable: React.FC = () => { const { t } = useTranslation(); const accountListId = useAccountListId(); const { enqueueSnackbar } = useSnackbar(); - const [notifications, setNotifications] = useState([]); const [appSelectAll, setAppSelectAll] = useState(false); const [emailSelectAll, setEmailSelectAll] = useState(false); const [isSetup, _] = useState(false); @@ -135,33 +134,28 @@ export const NotificationsTable: React.FC = () => { return notificationPreference[type] || isSetup; }; - useEffect(() => { + const notifications = useMemo(() => { const notificationsData: Notification[] = data?.notificationPreferences?.nodes || []; const notificationsOrder: NotificationConstant[] = notificationConstants?.constant?.notificationTranslatedHashes || []; - if (!notificationsData.length || !notificationsOrder.length) return; + if (!notificationsData.length || !notificationsOrder.length) return []; - const notifications = notificationsOrder.reduce( - (result: Notification[], notification) => { - const notificationPreference = notificationsData.find( - (object) => object.notificationType.id === notification.key, - ); - return [ - ...result, - { - notificationType: notificationPreference?.notificationType || {}, - app: defaultIfInSetup(notificationPreference, 'app'), - email: defaultIfInSetup(notificationPreference, 'email'), - task: defaultIfInSetup(notificationPreference, 'task'), - } as Notification, - ]; - }, - [], - ); - - setNotifications(notifications); + return notificationsOrder.reduce((result: Notification[], notification) => { + const notificationPreference = notificationsData.find( + (object) => object.notificationType.id === notification.key, + ); + return [ + ...result, + { + notificationType: notificationPreference?.notificationType || {}, + app: defaultIfInSetup(notificationPreference, 'app'), + email: defaultIfInSetup(notificationPreference, 'email'), + task: defaultIfInSetup(notificationPreference, 'task'), + } as Notification, + ]; + }, []); }, [data, notificationConstants]); const selectAll = ( @@ -353,6 +347,7 @@ export const NotificationsTable: React.FC = () => { { setFieldValue( `notifications.${idx}.email`, @@ -365,6 +360,7 @@ export const NotificationsTable: React.FC = () => { { setFieldValue( `notifications.${idx}.task`, From 349bf95cdabb1b26eb616d505018b65bd3c96a08 Mon Sep 17 00:00:00 2001 From: Daniel Bisgrove Date: Wed, 15 Nov 2023 14:42:26 -0500 Subject: [PATCH 08/20] Fixing test errors. GraphQL should begin with captial letter --- .../notifications/GetNotifications.graphql | 4 +- .../notifications/NotificationsTable.test.tsx | 46 ++++++++++++++----- 2 files changed, 36 insertions(+), 14 deletions(-) diff --git a/src/components/Settings/notifications/GetNotifications.graphql b/src/components/Settings/notifications/GetNotifications.graphql index 88f8a56e7..921125899 100644 --- a/src/components/Settings/notifications/GetNotifications.graphql +++ b/src/components/Settings/notifications/GetNotifications.graphql @@ -1,4 +1,4 @@ -query getPreferencesNotifications($accountListId: ID!) { +query GetPreferencesNotifications($accountListId: ID!) { notificationPreferences(accountListId: $accountListId) { nodes { app @@ -13,7 +13,7 @@ query getPreferencesNotifications($accountListId: ID!) { } } -query getNotificationConstants { +query GetNotificationConstants { constant { notificationTranslatedHashes { id diff --git a/src/components/Settings/notifications/NotificationsTable.test.tsx b/src/components/Settings/notifications/NotificationsTable.test.tsx index 09a188306..ae4b1a1c2 100644 --- a/src/components/Settings/notifications/NotificationsTable.test.tsx +++ b/src/components/Settings/notifications/NotificationsTable.test.tsx @@ -28,22 +28,41 @@ const router = { query: { accountListId }, isReady: true, }; -const createNotification = (type) => ({ +const createNotification = (type, id) => ({ app: false, email: false, task: false, notificationType: { + id, descriptionTemplate: type, type, }, }); + +const createConstant = (type, id) => ({ + id: type, + key: id, + value: type, +}); const mocks = { - getPreferencesNotifications: { + GetPreferencesNotifications: { notificationPreferences: { nodes: [ - createNotification(NotificationTypeTypeEnum.CallPartnerOncePerYear), - createNotification(NotificationTypeTypeEnum.LargerGift), - createNotification(NotificationTypeTypeEnum.LongTimeFrameGift), + createNotification( + NotificationTypeTypeEnum.CallPartnerOncePerYear, + '111', + ), + createNotification(NotificationTypeTypeEnum.LargerGift, '222'), + createNotification(NotificationTypeTypeEnum.LongTimeFrameGift, '333'), + ], + }, + }, + GetNotificationConstants: { + constant: { + notificationTranslatedHashes: [ + createConstant(NotificationTypeTypeEnum.CallPartnerOncePerYear, '111'), + createConstant(NotificationTypeTypeEnum.LargerGift, '222'), + createConstant(NotificationTypeTypeEnum.LongTimeFrameGift, '333'), ], }, }, @@ -76,7 +95,10 @@ describe('NotificationsTable', () => { mutationSpy.mock.calls[0][0].operation.variables.accountListId, ).toEqual(accountListId); expect(mutationSpy.mock.calls[0][0].operation.operationName).toEqual( - 'getPreferencesNotifications', + 'GetPreferencesNotifications', + ); + expect(mutationSpy.mock.calls[1][0].operation.operationName).toEqual( + 'GetNotificationConstants', ); }); @@ -131,7 +153,7 @@ describe('NotificationsTable', () => { }); it('Should send data to server on submit', async () => { - const { queryByTestId, getByTestId, getByRole } = render(Components); + const { queryByTestId, getByTestId, getAllByRole } = render(Components); await waitFor(() => expect(queryByTestId('skeleton-notifications')).not.toBeInTheDocument(), @@ -140,18 +162,18 @@ describe('NotificationsTable', () => { userEvent.click(getByTestId('select-all-app')); userEvent.click( - getByRole('button', { - name: /save/i, - }), + getAllByRole('button', { + name: 'Save Changes', + })[0], ); await waitFor(() => { // mutationSpy.mock.calls[1][0].operation.variables.input - expect(mutationSpy.mock.calls[1][0].operation.operationName).toEqual( + expect(mutationSpy.mock.calls[2][0].operation.operationName).toEqual( 'UpdateNotificationPreferences', ); - expect(mutationSpy.mock.calls[1][0].operation.variables.input).toEqual({ + expect(mutationSpy.mock.calls[2][0].operation.variables.input).toEqual({ accountListId: accountListId, attributes: [ { From df7262ee9a8ea9715c66e0f8da050928b7ec4705 Mon Sep 17 00:00:00 2001 From: Daniel Bisgrove Date: Wed, 15 Nov 2023 15:25:12 -0500 Subject: [PATCH 09/20] Adding tests to up code coverage --- .../notifications/NotificationsTable.test.tsx | 29 +++++++++++++++++++ .../notifications/NotificationsTable.tsx | 12 ++++++-- 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/src/components/Settings/notifications/NotificationsTable.test.tsx b/src/components/Settings/notifications/NotificationsTable.test.tsx index ae4b1a1c2..0cfc28e93 100644 --- a/src/components/Settings/notifications/NotificationsTable.test.tsx +++ b/src/components/Settings/notifications/NotificationsTable.test.tsx @@ -152,6 +152,35 @@ describe('NotificationsTable', () => { expect(getAllByRole('checkbox')[8]).toBeChecked(); }); + it('Should select Call Partner Once Per Year checkboxes', async () => { + const { queryByTestId, getByTestId } = render(Components); + + await waitFor(() => + expect(queryByTestId('skeleton-notifications')).not.toBeInTheDocument(), + ); + + const appCheckbox = getByTestId('CALL_PARTNER_ONCE_PER_YEAR-app-checkbox'); + const emailCheckbox = getByTestId( + 'CALL_PARTNER_ONCE_PER_YEAR-email-checkbox', + ); + const taskCheckbox = getByTestId( + 'CALL_PARTNER_ONCE_PER_YEAR-task-checkbox', + ); + + expect(appCheckbox).not.toBeChecked(); + expect(emailCheckbox).not.toBeChecked(); + expect(taskCheckbox).not.toBeChecked(); + + // Check first row + userEvent.click(appCheckbox); + userEvent.click(emailCheckbox); + userEvent.click(taskCheckbox); + + expect(appCheckbox).toBeChecked(); + expect(emailCheckbox).toBeChecked(); + expect(taskCheckbox).toBeChecked(); + }); + it('Should send data to server on submit', async () => { const { queryByTestId, getByTestId, getAllByRole } = render(Components); diff --git a/src/components/Settings/notifications/NotificationsTable.tsx b/src/components/Settings/notifications/NotificationsTable.tsx index be947cedf..38cccd3ad 100644 --- a/src/components/Settings/notifications/NotificationsTable.tsx +++ b/src/components/Settings/notifications/NotificationsTable.tsx @@ -332,7 +332,6 @@ export const NotificationsTable: React.FC = () => { { @@ -341,11 +340,13 @@ export const NotificationsTable: React.FC = () => { value, ); }} + inputProps={{ + 'data-testid': `${type}-app-checkbox`, + }} /> { @@ -354,11 +355,13 @@ export const NotificationsTable: React.FC = () => { value, ); }} + inputProps={{ + 'data-testid': `${type}-email-checkbox`, + }} /> { @@ -367,6 +370,9 @@ export const NotificationsTable: React.FC = () => { value, ); }} + inputProps={{ + 'data-testid': `${type}-task-checkbox`, + }} /> From 1266a8797b666ee690d3f76bcfc038283cc1fb7f Mon Sep 17 00:00:00 2001 From: Daniel Bisgrove Date: Wed, 15 Nov 2023 15:32:07 -0500 Subject: [PATCH 10/20] reverted to having testId on as type error when passing it down to input --- .../notifications/NotificationsTable.test.tsx | 8 +++++--- .../Settings/notifications/NotificationsTable.tsx | 12 +++--------- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/src/components/Settings/notifications/NotificationsTable.test.tsx b/src/components/Settings/notifications/NotificationsTable.test.tsx index 0cfc28e93..3edd8172a 100644 --- a/src/components/Settings/notifications/NotificationsTable.test.tsx +++ b/src/components/Settings/notifications/NotificationsTable.test.tsx @@ -159,13 +159,15 @@ describe('NotificationsTable', () => { expect(queryByTestId('skeleton-notifications')).not.toBeInTheDocument(), ); - const appCheckbox = getByTestId('CALL_PARTNER_ONCE_PER_YEAR-app-checkbox'); + const appCheckbox = getByTestId( + 'CALL_PARTNER_ONCE_PER_YEAR-app-checkbox', + ).querySelectorAll("input[type='checkbox']")[0] as HTMLInputElement; const emailCheckbox = getByTestId( 'CALL_PARTNER_ONCE_PER_YEAR-email-checkbox', - ); + ).querySelectorAll("input[type='checkbox']")[0] as HTMLInputElement; const taskCheckbox = getByTestId( 'CALL_PARTNER_ONCE_PER_YEAR-task-checkbox', - ); + ).querySelectorAll("input[type='checkbox']")[0] as HTMLInputElement; expect(appCheckbox).not.toBeChecked(); expect(emailCheckbox).not.toBeChecked(); diff --git a/src/components/Settings/notifications/NotificationsTable.tsx b/src/components/Settings/notifications/NotificationsTable.tsx index 38cccd3ad..be947cedf 100644 --- a/src/components/Settings/notifications/NotificationsTable.tsx +++ b/src/components/Settings/notifications/NotificationsTable.tsx @@ -332,6 +332,7 @@ export const NotificationsTable: React.FC = () => { { @@ -340,13 +341,11 @@ export const NotificationsTable: React.FC = () => { value, ); }} - inputProps={{ - 'data-testid': `${type}-app-checkbox`, - }} /> { @@ -355,13 +354,11 @@ export const NotificationsTable: React.FC = () => { value, ); }} - inputProps={{ - 'data-testid': `${type}-email-checkbox`, - }} /> { @@ -370,9 +367,6 @@ export const NotificationsTable: React.FC = () => { value, ); }} - inputProps={{ - 'data-testid': `${type}-task-checkbox`, - }} /> From 37b287d489c361b48e108d850d351ad4b66d5762 Mon Sep 17 00:00:00 2001 From: Daniel Bisgrove Date: Mon, 20 Nov 2023 15:33:58 -0500 Subject: [PATCH 11/20] Small fixes to improve code quailty --- .../settings/notifications.page.tsx | 8 ++-- .../[accountListId]/settings/wrapper.tsx | 2 +- ...ications.graphql => Notifications.graphql} | 5 +- .../notifications/NotificationsTable.test.tsx | 46 +++++++++---------- .../notifications/NotificationsTable.tsx | 23 +++++----- .../notifications/UpdateNotifications.graphql | 1 + 6 files changed, 44 insertions(+), 41 deletions(-) rename src/components/Settings/notifications/{GetNotifications.graphql => Notifications.graphql} (75%) diff --git a/pages/accountLists/[accountListId]/settings/notifications.page.tsx b/pages/accountLists/[accountListId]/settings/notifications.page.tsx index a77c08e3d..f1cef96e0 100644 --- a/pages/accountLists/[accountListId]/settings/notifications.page.tsx +++ b/pages/accountLists/[accountListId]/settings/notifications.page.tsx @@ -14,18 +14,18 @@ const Notifications: React.FC = () => { >

- Based on an analysis of a partner's giving history, MPDX can + {t(`Based on an analysis of a partner's giving history, MPDX can notify you of events that you will probably want to follow up on. The detection logic is based on a set of rules that are right most of the time, but you will still want to verify an event manually before - contacting the partner. + contacting the partner.`)}

- For each event MPDX can notify you via email and also create a task + {t(`For each event MPDX can notify you via email and also create a task entry reminding you to do something about it. The options below allow - you to control that behavior. + you to control that behavior.`)}

diff --git a/pages/accountLists/[accountListId]/settings/wrapper.tsx b/pages/accountLists/[accountListId]/settings/wrapper.tsx index 558503ac6..84cee3b10 100644 --- a/pages/accountLists/[accountListId]/settings/wrapper.tsx +++ b/pages/accountLists/[accountListId]/settings/wrapper.tsx @@ -30,7 +30,7 @@ export const SettingsWrapper: React.FC = ({ children, }) => { const { appName } = useGetAppSettings(); - const [isNavListOpen, setNavListOpen] = useState(false); + const [isNavListOpen, setNavListOpen] = useState(false); const handleNavListToggle = () => { setNavListOpen(!isNavListOpen); }; diff --git a/src/components/Settings/notifications/GetNotifications.graphql b/src/components/Settings/notifications/Notifications.graphql similarity index 75% rename from src/components/Settings/notifications/GetNotifications.graphql rename to src/components/Settings/notifications/Notifications.graphql index 921125899..7b77f29b3 100644 --- a/src/components/Settings/notifications/GetNotifications.graphql +++ b/src/components/Settings/notifications/Notifications.graphql @@ -1,6 +1,7 @@ -query GetPreferencesNotifications($accountListId: ID!) { +query PreferencesNotifications($accountListId: ID!) { notificationPreferences(accountListId: $accountListId) { nodes { + id app email notificationType { @@ -13,7 +14,7 @@ query GetPreferencesNotifications($accountListId: ID!) { } } -query GetNotificationConstants { +query NotificationConstants { constant { notificationTranslatedHashes { id diff --git a/src/components/Settings/notifications/NotificationsTable.test.tsx b/src/components/Settings/notifications/NotificationsTable.test.tsx index 3edd8172a..a526c5a9c 100644 --- a/src/components/Settings/notifications/NotificationsTable.test.tsx +++ b/src/components/Settings/notifications/NotificationsTable.test.tsx @@ -45,7 +45,7 @@ const createConstant = (type, id) => ({ value: type, }); const mocks = { - GetPreferencesNotifications: { + PreferencesNotifications: { notificationPreferences: { nodes: [ createNotification( @@ -57,7 +57,7 @@ const mocks = { ], }, }, - GetNotificationConstants: { + NotificationConstants: { constant: { notificationTranslatedHashes: [ createConstant(NotificationTypeTypeEnum.CallPartnerOncePerYear, '111'), @@ -95,10 +95,10 @@ describe('NotificationsTable', () => { mutationSpy.mock.calls[0][0].operation.variables.accountListId, ).toEqual(accountListId); expect(mutationSpy.mock.calls[0][0].operation.operationName).toEqual( - 'GetPreferencesNotifications', + 'PreferencesNotifications', ); expect(mutationSpy.mock.calls[1][0].operation.operationName).toEqual( - 'GetNotificationConstants', + 'NotificationConstants', ); }); @@ -123,33 +123,34 @@ describe('NotificationsTable', () => { await waitFor(() => expect(queryByTestId('skeleton-notifications')).not.toBeInTheDocument(), ); - expect(getAllByRole('checkbox')[0]).not.toBeChecked(); - expect(getAllByRole('checkbox')[1]).not.toBeChecked(); - expect(getAllByRole('checkbox')[2]).not.toBeChecked(); - expect(getAllByRole('checkbox')[3]).not.toBeChecked(); - expect(getAllByRole('checkbox')[4]).not.toBeChecked(); - expect(getAllByRole('checkbox')[5]).not.toBeChecked(); - expect(getAllByRole('checkbox')[6]).not.toBeChecked(); - expect(getAllByRole('checkbox')[7]).not.toBeChecked(); - expect(getAllByRole('checkbox')[8]).not.toBeChecked(); + const checkboxes = getAllByRole('checkbox'); + expect(checkboxes[0]).not.toBeChecked(); + expect(checkboxes[1]).not.toBeChecked(); + expect(checkboxes[2]).not.toBeChecked(); + expect(checkboxes[3]).not.toBeChecked(); + expect(checkboxes[4]).not.toBeChecked(); + expect(checkboxes[5]).not.toBeChecked(); + expect(checkboxes[6]).not.toBeChecked(); + expect(checkboxes[7]).not.toBeChecked(); + expect(checkboxes[8]).not.toBeChecked(); // Select all app userEvent.click(getByTestId('select-all-app')); - expect(getAllByRole('checkbox')[0]).toBeChecked(); - expect(getAllByRole('checkbox')[3]).toBeChecked(); - expect(getAllByRole('checkbox')[6]).toBeChecked(); + expect(checkboxes[0]).toBeChecked(); + expect(checkboxes[3]).toBeChecked(); + expect(checkboxes[6]).toBeChecked(); // Select all email userEvent.click(getByTestId('select-all-email')); - expect(getAllByRole('checkbox')[1]).toBeChecked(); - expect(getAllByRole('checkbox')[4]).toBeChecked(); - expect(getAllByRole('checkbox')[7]).toBeChecked(); + expect(checkboxes[1]).toBeChecked(); + expect(checkboxes[4]).toBeChecked(); + expect(checkboxes[7]).toBeChecked(); // Select all tasks userEvent.click(getByTestId('select-all-task')); - expect(getAllByRole('checkbox')[2]).toBeChecked(); - expect(getAllByRole('checkbox')[5]).toBeChecked(); - expect(getAllByRole('checkbox')[8]).toBeChecked(); + expect(checkboxes[2]).toBeChecked(); + expect(checkboxes[5]).toBeChecked(); + expect(checkboxes[8]).toBeChecked(); }); it('Should select Call Partner Once Per Year checkboxes', async () => { @@ -199,7 +200,6 @@ describe('NotificationsTable', () => { ); await waitFor(() => { - // mutationSpy.mock.calls[1][0].operation.variables.input expect(mutationSpy.mock.calls[2][0].operation.operationName).toEqual( 'UpdateNotificationPreferences', ); diff --git a/src/components/Settings/notifications/NotificationsTable.tsx b/src/components/Settings/notifications/NotificationsTable.tsx index be947cedf..95d981f03 100644 --- a/src/components/Settings/notifications/NotificationsTable.tsx +++ b/src/components/Settings/notifications/NotificationsTable.tsx @@ -18,9 +18,9 @@ import { import { Email, Smartphone, Task } from '@mui/icons-material'; import { useAccountListId } from 'src/hooks/useAccountListId'; import { - useGetPreferencesNotificationsQuery, - useGetNotificationConstantsQuery, -} from './GetNotifications.generated'; + usePreferencesNotificationsQuery, + useNotificationConstantsQuery, +} from './Notifications.generated'; import { useUpdateNotificationPreferencesMutation } from './UpdateNotifications.generated'; import * as Types from '../../../../graphql/types.generated'; import { SubmitButton } from 'src/components/common/Modal/ActionButtons/ActionButtons'; @@ -118,12 +118,12 @@ export const NotificationsTable: React.FC = () => { ), }); - const { data, loading } = useGetPreferencesNotificationsQuery({ + const { data, loading } = usePreferencesNotificationsQuery({ variables: { accountListId: accountListId ?? '', }, }); - const { data: notificationConstants } = useGetNotificationConstantsQuery(); + const { data: notificationConstants } = useNotificationConstantsQuery(); const defaultIfInSetup = ( notificationPreference: any, @@ -158,7 +158,7 @@ export const NotificationsTable: React.FC = () => { }, []); }, [data, notificationConstants]); - const selectAll = ( + const handleSelectAll = ( type, notifications, setFieldValue, @@ -201,8 +201,9 @@ export const NotificationsTable: React.FC = () => { return ( - {loading && } - {!loading && ( + {loading ? ( + + ) : ( { align="right" data-testid="select-all-app" onClick={() => - selectAll( + handleSelectAll( notificationsEnum.App, notifications, setFieldValue, @@ -283,7 +284,7 @@ export const NotificationsTable: React.FC = () => { align="right" data-testid="select-all-email" onClick={() => - selectAll( + handleSelectAll( notificationsEnum.Email, notifications, setFieldValue, @@ -302,7 +303,7 @@ export const NotificationsTable: React.FC = () => { align="right" data-testid="select-all-task" onClick={() => - selectAll( + handleSelectAll( notificationsEnum.Task, notifications, setFieldValue, diff --git a/src/components/Settings/notifications/UpdateNotifications.graphql b/src/components/Settings/notifications/UpdateNotifications.graphql index ca7130898..42162cc26 100644 --- a/src/components/Settings/notifications/UpdateNotifications.graphql +++ b/src/components/Settings/notifications/UpdateNotifications.graphql @@ -3,6 +3,7 @@ mutation UpdateNotificationPreferences( ) { updateNotificationPreferences(input: $input) { notificationPreferences { + id notificationType { id } From 55e6038aacfa6a6b92c1e7c6ea15fa1342e2e9d2 Mon Sep 17 00:00:00 2001 From: Daniel Bisgrove Date: Thu, 30 Nov 2023 10:14:17 -0500 Subject: [PATCH 12/20] Typography changes --- .../settings/notifications.page.tsx | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/pages/accountLists/[accountListId]/settings/notifications.page.tsx b/pages/accountLists/[accountListId]/settings/notifications.page.tsx index f1cef96e0..a27e9069f 100644 --- a/pages/accountLists/[accountListId]/settings/notifications.page.tsx +++ b/pages/accountLists/[accountListId]/settings/notifications.page.tsx @@ -1,11 +1,13 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; import { Box } from '@mui/material'; -import { SettingsWrapper } from './wrapper'; +import useGetAppSettings from 'src/hooks/useGetAppSettings'; import { NotificationsTable } from 'src/components/Settings/notifications/NotificationsTable'; +import { SettingsWrapper } from './wrapper'; const Notifications: React.FC = () => { const { t } = useTranslation(); + const { appName } = useGetAppSettings(); return ( { >

- {t(`Based on an analysis of a partner's giving history, MPDX can + {t( + `Based on an analysis of a partner's giving history, {{appName}} can notify you of events that you will probably want to follow up on. The detection logic is based on a set of rules that are right most of the time, but you will still want to verify an event manually before - contacting the partner.`)} + contacting the partner.`, + { appName }, + )}

- {t(`For each event MPDX can notify you via email and also create a task + {t( + `For each event {{appName}} can notify you via email and also create a task entry reminding you to do something about it. The options below allow - you to control that behavior.`)} + you to control that behavior.`, + { appName }, + )}

From 60162beace9fa7faef01eae542fed5febf77e70b Mon Sep 17 00:00:00 2001 From: Daniel Bisgrove Date: Thu, 30 Nov 2023 10:15:28 -0500 Subject: [PATCH 13/20] Change tests to use render React components rather than function. --- .../notifications/NotificationsTable.test.tsx | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/components/Settings/notifications/NotificationsTable.test.tsx b/src/components/Settings/notifications/NotificationsTable.test.tsx index a526c5a9c..8bb05fa1e 100644 --- a/src/components/Settings/notifications/NotificationsTable.test.tsx +++ b/src/components/Settings/notifications/NotificationsTable.test.tsx @@ -68,7 +68,7 @@ const mocks = { }, }; const mutationSpy = jest.fn(); -const Components = ( +const Components: React.FC = () => ( @@ -85,15 +85,15 @@ describe('NotificationsTable', () => { mutationSpy.mockReset(); }); it('Should render the Table and request data', async () => { - const { getByTestId, queryByTestId, getByText } = render(Components); + const { getByTestId, queryByTestId, getByText } = render(); expect(getByTestId('skeleton-notifications')).toBeInTheDocument(); await waitFor(() => { - expect(queryByTestId('skeleton-notifications')).not.toBeInTheDocument(), - expect( - mutationSpy.mock.calls[0][0].operation.variables.accountListId, - ).toEqual(accountListId); + expect(queryByTestId('skeleton-notifications')).not.toBeInTheDocument(); + expect( + mutationSpy.mock.calls[0][0].operation.variables.accountListId, + ).toEqual(accountListId); expect(mutationSpy.mock.calls[0][0].operation.operationName).toEqual( 'PreferencesNotifications', ); @@ -118,7 +118,7 @@ describe('NotificationsTable', () => { }); it('Should select all', async () => { - const { queryByTestId, getByTestId, getAllByRole } = render(Components); + const { queryByTestId, getByTestId, getAllByRole } = render(); await waitFor(() => expect(queryByTestId('skeleton-notifications')).not.toBeInTheDocument(), @@ -154,7 +154,7 @@ describe('NotificationsTable', () => { }); it('Should select Call Partner Once Per Year checkboxes', async () => { - const { queryByTestId, getByTestId } = render(Components); + const { queryByTestId, getByTestId } = render(); await waitFor(() => expect(queryByTestId('skeleton-notifications')).not.toBeInTheDocument(), @@ -185,7 +185,7 @@ describe('NotificationsTable', () => { }); it('Should send data to server on submit', async () => { - const { queryByTestId, getByTestId, getAllByRole } = render(Components); + const { queryByTestId, getByTestId, getAllByRole } = render(); await waitFor(() => expect(queryByTestId('skeleton-notifications')).not.toBeInTheDocument(), From 31de4fbd881f7334628fa57895e84037230230a3 Mon Sep 17 00:00:00 2001 From: Daniel Bisgrove Date: Thu, 30 Nov 2023 10:16:11 -0500 Subject: [PATCH 14/20] Type fixes. Caching notificationConstants after first load. Changing .reduce to .map --- .../notifications/NotificationsTable.tsx | 31 +++++++++---------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/src/components/Settings/notifications/NotificationsTable.tsx b/src/components/Settings/notifications/NotificationsTable.tsx index 95d981f03..a5f45df9f 100644 --- a/src/components/Settings/notifications/NotificationsTable.tsx +++ b/src/components/Settings/notifications/NotificationsTable.tsx @@ -123,15 +123,17 @@ export const NotificationsTable: React.FC = () => { accountListId: accountListId ?? '', }, }); - const { data: notificationConstants } = useNotificationConstantsQuery(); + const { data: notificationConstants } = useNotificationConstantsQuery({ + fetchPolicy: 'cache-first', + }); const defaultIfInSetup = ( - notificationPreference: any, - type: string, + notificationPreference: Notification | undefined, + type: 'app' | 'email' | 'task', ): boolean => { // If Setup, show preference or default to TRUE // If not setup, show preference or default to FALSE. - return notificationPreference[type] || isSetup; + return notificationPreference?.[type] || isSetup; }; const notifications = useMemo(() => { @@ -142,19 +144,16 @@ export const NotificationsTable: React.FC = () => { if (!notificationsData.length || !notificationsOrder.length) return []; - return notificationsOrder.reduce((result: Notification[], notification) => { + return notificationsOrder.map((notification) => { const notificationPreference = notificationsData.find( (object) => object.notificationType.id === notification.key, ); - return [ - ...result, - { - notificationType: notificationPreference?.notificationType || {}, - app: defaultIfInSetup(notificationPreference, 'app'), - email: defaultIfInSetup(notificationPreference, 'email'), - task: defaultIfInSetup(notificationPreference, 'task'), - } as Notification, - ]; + return { + notificationType: notificationPreference?.notificationType || {}, + app: defaultIfInSetup(notificationPreference, 'app'), + email: defaultIfInSetup(notificationPreference, 'email'), + task: defaultIfInSetup(notificationPreference, 'task'), + } as Notification; }, []); }, [data, notificationConstants]); @@ -222,7 +221,7 @@ export const NotificationsTable: React.FC = () => { {t('Save Changes')} @@ -234,7 +233,7 @@ export const NotificationsTable: React.FC = () => { From a9265cd9f92bac36f253704254e327b01d7dca5c Mon Sep 17 00:00:00 2001 From: Daniel Bisgrove Date: Thu, 30 Nov 2023 10:43:00 -0500 Subject: [PATCH 15/20] Updating Organization query as conflicts with another PR yet to be merged. --- .../OrganizationAccordion.test.tsx | 43 ++++++++++--------- .../Organization/OrganizationAccordion.tsx | 4 +- .../Organization/Organizations.graphql | 2 +- 3 files changed, 25 insertions(+), 24 deletions(-) diff --git a/src/components/Settings/integrations/Organization/OrganizationAccordion.test.tsx b/src/components/Settings/integrations/Organization/OrganizationAccordion.test.tsx index 3e861ba6c..f8c334c4c 100644 --- a/src/components/Settings/integrations/Organization/OrganizationAccordion.test.tsx +++ b/src/components/Settings/integrations/Organization/OrganizationAccordion.test.tsx @@ -4,7 +4,7 @@ import userEvent from '@testing-library/user-event'; import { ThemeProvider } from '@mui/material/styles'; import { SnackbarProvider } from 'notistack'; import { - GetUsersOrganizationsQuery, + GetUsersOrganizationsAccountsQuery, GetOrganizationsQuery, } from './Organizations.generated'; import * as Types from '../../../../../graphql/types.generated'; @@ -64,7 +64,7 @@ const GetOrganizationsMock: Pick< }, ]; -const GetUsersOrganizationsMock: Array< +const GetUsersOrganizationsAccountsMock: Array< Pick< Types.OrganizationAccount, 'latestDonationDate' | 'lastDownloadedAt' | 'username' | 'id' @@ -93,8 +93,8 @@ const standardMocks = { GetOrganizations: { organizations: GetOrganizationsMock, }, - GetUsersOrganizations: { - userOrganizationAccounts: GetUsersOrganizationsMock, + GetUsersOrganizationsAccounts: { + userOrganizationAccounts: GetUsersOrganizationsAccountsMock, }, }; @@ -140,13 +140,13 @@ describe('OrganizationAccordion', () => { mocks={{ GetOrganizations: { organizations: [], }, - GetUsersOrganizations: { + GetUsersOrganizationsAccounts: { userOrganizationAccounts: [], }, }} @@ -180,7 +180,7 @@ describe('OrganizationAccordion', () => { mocks={mocks} > @@ -198,7 +198,7 @@ describe('OrganizationAccordion', () => { await waitFor(() => { expect( - getByText(GetUsersOrganizationsMock[0].organization.name), + getByText(GetUsersOrganizationsAccountsMock[0].organization.name), ).toBeInTheDocument(); expect(getByText('Last Updated')).toBeInTheDocument(); @@ -219,13 +219,13 @@ describe('OrganizationAccordion', () => { it('should render Ministry Account Organization', async () => { const mutationSpy = jest.fn(); - mocks.GetUsersOrganizations.userOrganizationAccounts[0].organization.apiClass = + mocks.GetUsersOrganizationsAccounts.userOrganizationAccounts[0].organization.apiClass = 'Siebel'; const { getByText, queryByText } = render( mocks={mocks} onCall={mutationSpy} @@ -261,19 +261,19 @@ describe('OrganizationAccordion', () => { 'SyncOrganizationAccount', ); expect(mutationSpy.mock.calls[1][0].operation.variables.input).toEqual({ - id: mocks.GetUsersOrganizations.userOrganizationAccounts[0].id, + id: mocks.GetUsersOrganizationsAccounts.userOrganizationAccounts[0].id, }); }); it('should render Login Organization', async () => { const mutationSpy = jest.fn(); - mocks.GetUsersOrganizations.userOrganizationAccounts[0].organization.apiClass = + mocks.GetUsersOrganizationsAccounts.userOrganizationAccounts[0].organization.apiClass = 'DataServer'; const { getByText, getByTestId } = render( mocks={mocks} onCall={mutationSpy} @@ -300,15 +300,15 @@ describe('OrganizationAccordion', () => { it('should render OAuth Organization', async () => { const mutationSpy = jest.fn(); - mocks.GetUsersOrganizations.userOrganizationAccounts[0].organization.apiClass = + mocks.GetUsersOrganizationsAccounts.userOrganizationAccounts[0].organization.apiClass = 'DataServer'; - mocks.GetUsersOrganizations.userOrganizationAccounts[0].organization.oauth = + mocks.GetUsersOrganizationsAccounts.userOrganizationAccounts[0].organization.oauth = true; const { getByText, queryByTestId } = render( mocks={mocks} onCall={mutationSpy} @@ -345,7 +345,7 @@ describe('OrganizationAccordion', () => { mocks={mocks} onCall={mutationSpy} @@ -376,7 +376,8 @@ describe('OrganizationAccordion', () => { 'DeleteOrganizationAccount', ); expect(mutationSpy.mock.calls[1][0].operation.variables.input).toEqual({ - id: mocks.GetUsersOrganizations.userOrganizationAccounts[0].id, + id: mocks.GetUsersOrganizationsAccounts.userOrganizationAccounts[0] + .id, }); expect(mockEnqueue).toHaveBeenCalledWith( '{{appName}} removed your organization integration', @@ -386,15 +387,15 @@ describe('OrganizationAccordion', () => { }); it("should not render Organization's download and last gift date", async () => { - mocks.GetUsersOrganizations.userOrganizationAccounts[0].lastDownloadedAt = + mocks.GetUsersOrganizationsAccounts.userOrganizationAccounts[0].lastDownloadedAt = null; - mocks.GetUsersOrganizations.userOrganizationAccounts[0].latestDonationDate = + mocks.GetUsersOrganizationsAccounts.userOrganizationAccounts[0].latestDonationDate = null; const { queryByText } = render( mocks={mocks} > diff --git a/src/components/Settings/integrations/Organization/OrganizationAccordion.tsx b/src/components/Settings/integrations/Organization/OrganizationAccordion.tsx index a3dcd2745..ddea21699 100644 --- a/src/components/Settings/integrations/Organization/OrganizationAccordion.tsx +++ b/src/components/Settings/integrations/Organization/OrganizationAccordion.tsx @@ -16,7 +16,7 @@ import Edit from '@mui/icons-material/Edit'; import { useAccountListId } from 'src/hooks/useAccountListId'; import useGetAppSettings from 'src/hooks/useGetAppSettings'; import { - useGetUsersOrganizationsQuery, + useGetUsersOrganizationsAccountsQuery, useDeleteOrganizationAccountMutation, useSyncOrganizationAccountMutation, } from './Organizations.generated'; @@ -100,7 +100,7 @@ export const OrganizationAccordion: React.FC = ({ data, loading, refetch: refetchOrganizations, - } = useGetUsersOrganizationsQuery(); + } = useGetUsersOrganizationsAccountsQuery(); const organizations = data?.userOrganizationAccounts; const handleReconnect = async (organizationId) => { diff --git a/src/components/Settings/integrations/Organization/Organizations.graphql b/src/components/Settings/integrations/Organization/Organizations.graphql index 9ab64a0d0..547043fa0 100644 --- a/src/components/Settings/integrations/Organization/Organizations.graphql +++ b/src/components/Settings/integrations/Organization/Organizations.graphql @@ -8,7 +8,7 @@ query getOrganizations { } } -query GetUsersOrganizations { +query GetUsersOrganizationsAccounts { userOrganizationAccounts { organization { apiClass From 436ba5c141df58a5b295be64b20f7696a4478228 Mon Sep 17 00:00:00 2001 From: Daniel Bisgrove Date: Tue, 12 Dec 2023 15:39:13 -0500 Subject: [PATCH 16/20] last edits on notifications --- src/components/Settings/notifications/NotificationsTable.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/Settings/notifications/NotificationsTable.tsx b/src/components/Settings/notifications/NotificationsTable.tsx index a5f45df9f..85c8c58a7 100644 --- a/src/components/Settings/notifications/NotificationsTable.tsx +++ b/src/components/Settings/notifications/NotificationsTable.tsx @@ -154,7 +154,7 @@ export const NotificationsTable: React.FC = () => { email: defaultIfInSetup(notificationPreference, 'email'), task: defaultIfInSetup(notificationPreference, 'task'), } as Notification; - }, []); + }); }, [data, notificationConstants]); const handleSelectAll = ( @@ -221,7 +221,7 @@ export const NotificationsTable: React.FC = () => { {t('Save Changes')} From f066704eed53164a661c3efda422198e2c1e3b1e Mon Sep 17 00:00:00 2001 From: Daniel Bisgrove Date: Fri, 15 Dec 2023 10:33:10 -0500 Subject: [PATCH 17/20] lint issues --- .../settings/notifications.page.tsx | 4 +-- .../notifications/NotificationsTable.test.tsx | 8 +++--- .../notifications/NotificationsTable.tsx | 28 +++++++++---------- .../NotificationsTableSkeleton.tsx | 16 +++++------ 4 files changed, 28 insertions(+), 28 deletions(-) diff --git a/pages/accountLists/[accountListId]/settings/notifications.page.tsx b/pages/accountLists/[accountListId]/settings/notifications.page.tsx index a27e9069f..43eeb296c 100644 --- a/pages/accountLists/[accountListId]/settings/notifications.page.tsx +++ b/pages/accountLists/[accountListId]/settings/notifications.page.tsx @@ -1,8 +1,8 @@ import React from 'react'; -import { useTranslation } from 'react-i18next'; import { Box } from '@mui/material'; -import useGetAppSettings from 'src/hooks/useGetAppSettings'; +import { useTranslation } from 'react-i18next'; import { NotificationsTable } from 'src/components/Settings/notifications/NotificationsTable'; +import useGetAppSettings from 'src/hooks/useGetAppSettings'; import { SettingsWrapper } from './wrapper'; const Notifications: React.FC = () => { diff --git a/src/components/Settings/notifications/NotificationsTable.test.tsx b/src/components/Settings/notifications/NotificationsTable.test.tsx index 8bb05fa1e..7c22ca383 100644 --- a/src/components/Settings/notifications/NotificationsTable.test.tsx +++ b/src/components/Settings/notifications/NotificationsTable.test.tsx @@ -1,13 +1,13 @@ import React from 'react'; +import { ThemeProvider } from '@mui/material/styles'; import { render, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { NotificationsTable } from './NotificationsTable'; -import { ThemeProvider } from '@mui/material/styles'; import { SnackbarProvider } from 'notistack'; -import { GqlMockedProvider } from '../../../../__tests__/util/graphqlMocking'; import TestRouter from '../../../../__tests__/util/TestRouter'; -import theme from '../../../../src/theme'; +import { GqlMockedProvider } from '../../../../__tests__/util/graphqlMocking'; import { NotificationTypeTypeEnum } from '../../../../graphql/types.generated'; +import theme from "../../../theme"; +import { NotificationsTable } from './NotificationsTable'; const mockEnqueue = jest.fn(); diff --git a/src/components/Settings/notifications/NotificationsTable.tsx b/src/components/Settings/notifications/NotificationsTable.tsx index 85c8c58a7..c23f55427 100644 --- a/src/components/Settings/notifications/NotificationsTable.tsx +++ b/src/components/Settings/notifications/NotificationsTable.tsx @@ -1,30 +1,30 @@ -import React, { useState, useMemo, ReactElement } from 'react'; -import { useTranslation } from 'react-i18next'; -import { Formik, FieldArray } from 'formik'; -import * as yup from 'yup'; -import { useSnackbar } from 'notistack'; -import { styled } from '@mui/material/styles'; +import React, { ReactElement, useMemo, useState } from 'react'; +import { Email, Smartphone, Task } from '@mui/icons-material'; import { Box, Checkbox, - TableContainer, + Paper, Table, + TableBody, TableCell, + TableContainer, TableHead, TableRow, - TableBody, - Paper, } from '@mui/material'; -import { Email, Smartphone, Task } from '@mui/icons-material'; +import { styled } from '@mui/material/styles'; +import { FieldArray, Formik } from 'formik'; +import { useSnackbar } from 'notistack'; +import { useTranslation } from 'react-i18next'; +import * as yup from 'yup'; +import { SubmitButton } from 'src/components/common/Modal/ActionButtons/ActionButtons'; import { useAccountListId } from 'src/hooks/useAccountListId'; +import * as Types from '../../../../graphql/types.generated'; import { - usePreferencesNotificationsQuery, useNotificationConstantsQuery, + usePreferencesNotificationsQuery, } from './Notifications.generated'; -import { useUpdateNotificationPreferencesMutation } from './UpdateNotifications.generated'; -import * as Types from '../../../../graphql/types.generated'; -import { SubmitButton } from 'src/components/common/Modal/ActionButtons/ActionButtons'; import { NotificationsTableSkeleton } from './NotificationsTableSkeleton'; +import { useUpdateNotificationPreferencesMutation } from './UpdateNotifications.generated'; export enum notificationsEnum { App = 'app', diff --git a/src/components/Settings/notifications/NotificationsTableSkeleton.tsx b/src/components/Settings/notifications/NotificationsTableSkeleton.tsx index 15f0635a5..9ef7c0eab 100644 --- a/src/components/Settings/notifications/NotificationsTableSkeleton.tsx +++ b/src/components/Settings/notifications/NotificationsTableSkeleton.tsx @@ -1,24 +1,24 @@ import React from 'react'; import { Box, - TableContainer, + Paper, + Skeleton, Table, + TableBody, + TableContainer, TableHead, TableRow, - TableBody, - Paper, - Skeleton, } from '@mui/material'; import { useTranslation } from 'react-i18next'; import { + SelectAllBox, + StyledEmail, + StyledSmartphone, + StyledTableCell, StyledTableHeadCell, StyledTableHeadSelectCell, - StyledTableCell, StyledTableRow, - StyledSmartphone, - StyledEmail, StyledTask, - SelectAllBox, } from './NotificationsTable'; export const NotificationsTableSkeleton: React.FC = () => { From c1997b58e4664e271668ad0e1f04c7fb845f7ea2 Mon Sep 17 00:00:00 2001 From: Daniel Bisgrove Date: Fri, 15 Dec 2023 11:08:21 -0500 Subject: [PATCH 18/20] prettier fixes --- .../Settings/notifications/NotificationsTable.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Settings/notifications/NotificationsTable.test.tsx b/src/components/Settings/notifications/NotificationsTable.test.tsx index 7c22ca383..35dd16431 100644 --- a/src/components/Settings/notifications/NotificationsTable.test.tsx +++ b/src/components/Settings/notifications/NotificationsTable.test.tsx @@ -6,7 +6,7 @@ import { SnackbarProvider } from 'notistack'; import TestRouter from '../../../../__tests__/util/TestRouter'; import { GqlMockedProvider } from '../../../../__tests__/util/graphqlMocking'; import { NotificationTypeTypeEnum } from '../../../../graphql/types.generated'; -import theme from "../../../theme"; +import theme from '../../../theme'; import { NotificationsTable } from './NotificationsTable'; const mockEnqueue = jest.fn(); From 54a118b5b5fe81e22a4b709aeb1d443c23696181 Mon Sep 17 00:00:00 2001 From: Daniel Bisgrove Date: Fri, 15 Dec 2023 15:16:49 -0500 Subject: [PATCH 19/20] Updating notifications to fix bug with main account viewing notifications and removed fetching constants and now fetching notification types --- .../notifications/Notifications.graphql | 14 +++-- .../notifications/NotificationsTable.tsx | 53 ++++++++++--------- 2 files changed, 34 insertions(+), 33 deletions(-) diff --git a/src/components/Settings/notifications/Notifications.graphql b/src/components/Settings/notifications/Notifications.graphql index 7b77f29b3..d05d45ae8 100644 --- a/src/components/Settings/notifications/Notifications.graphql +++ b/src/components/Settings/notifications/Notifications.graphql @@ -1,4 +1,4 @@ -query PreferencesNotifications($accountListId: ID!) { +query NotificationsPreferences($accountListId: ID!) { notificationPreferences(accountListId: $accountListId) { nodes { id @@ -14,12 +14,10 @@ query PreferencesNotifications($accountListId: ID!) { } } -query NotificationConstants { - constant { - notificationTranslatedHashes { - id - key - value - } +query NotificationTypes { + notificationTypes { + id + type + descriptionTemplate } } diff --git a/src/components/Settings/notifications/NotificationsTable.tsx b/src/components/Settings/notifications/NotificationsTable.tsx index c23f55427..13c2f3be2 100644 --- a/src/components/Settings/notifications/NotificationsTable.tsx +++ b/src/components/Settings/notifications/NotificationsTable.tsx @@ -20,8 +20,8 @@ import { SubmitButton } from 'src/components/common/Modal/ActionButtons/ActionBu import { useAccountListId } from 'src/hooks/useAccountListId'; import * as Types from '../../../../graphql/types.generated'; import { - useNotificationConstantsQuery, - usePreferencesNotificationsQuery, + useNotificationTypesQuery, + useNotificationsPreferencesQuery, } from './Notifications.generated'; import { NotificationsTableSkeleton } from './NotificationsTableSkeleton'; import { useUpdateNotificationPreferencesMutation } from './UpdateNotifications.generated'; @@ -32,7 +32,7 @@ export enum notificationsEnum { Task = 'task', } -type Notification = Pick< +type NotificationPreference = Pick< Types.NotificationPreference, 'app' | 'email' | 'task' > & { @@ -41,7 +41,10 @@ type Notification = Pick< 'id' | 'descriptionTemplate' | 'type' >; }; -type NotificationConstant = Pick; +type NotificationType = Pick< + Types.NotificationType, + 'id' | 'type' | 'descriptionTemplate' +>; export const StyledTableHeadCell = styled(TableCell)(({ theme }) => ({ backgroundColor: theme.palette.primary.main, @@ -99,7 +102,7 @@ export const NotificationsTable: React.FC = () => { const [updateNotifications] = useUpdateNotificationPreferencesMutation(); const NotificationSchema: yup.SchemaOf<{ - notifications: Notification[]; + notifications: NotificationPreference[]; }> = yup.object({ notifications: yup.array( yup.object({ @@ -118,17 +121,19 @@ export const NotificationsTable: React.FC = () => { ), }); - const { data, loading } = usePreferencesNotificationsQuery({ - variables: { - accountListId: accountListId ?? '', - }, - }); - const { data: notificationConstants } = useNotificationConstantsQuery({ + const { data: notificationsPreferences, loading } = + useNotificationsPreferencesQuery({ + variables: { + accountListId: accountListId ?? '', + }, + fetchPolicy: 'cache-and-network', + }); + const { data: notificationTypes } = useNotificationTypesQuery({ fetchPolicy: 'cache-first', }); const defaultIfInSetup = ( - notificationPreference: Notification | undefined, + notificationPreference: NotificationPreference | undefined, type: 'app' | 'email' | 'task', ): boolean => { // If Setup, show preference or default to TRUE @@ -137,25 +142,23 @@ export const NotificationsTable: React.FC = () => { }; const notifications = useMemo(() => { - const notificationsData: Notification[] = - data?.notificationPreferences?.nodes || []; - const notificationsOrder: NotificationConstant[] = - notificationConstants?.constant?.notificationTranslatedHashes || []; - - if (!notificationsData.length || !notificationsOrder.length) return []; + const notificationsPreferencesData: NotificationPreference[] = + notificationsPreferences?.notificationPreferences?.nodes || []; + const notificationTypesData: NotificationType[] = + notificationTypes?.notificationTypes || []; - return notificationsOrder.map((notification) => { - const notificationPreference = notificationsData.find( - (object) => object.notificationType.id === notification.key, + return notificationTypesData.map((notification) => { + const notificationPreference = notificationsPreferencesData.find( + (object) => object.notificationType.id === notification.id, ); return { - notificationType: notificationPreference?.notificationType || {}, + notificationType: notification, app: defaultIfInSetup(notificationPreference, 'app'), email: defaultIfInSetup(notificationPreference, 'email'), task: defaultIfInSetup(notificationPreference, 'task'), - } as Notification; + } as NotificationPreference; }); - }, [data, notificationConstants]); + }, [notificationsPreferences, notificationTypes]); const handleSelectAll = ( type, @@ -173,7 +176,7 @@ export const NotificationsTable: React.FC = () => { const onSubmit = async ({ notifications, }: { - notifications: Notification[]; + notifications: NotificationPreference[]; }) => { const attributes = notifications.map((notification) => { return { From 40f9e211b2ee986380fe296d5f2e6e1094781499 Mon Sep 17 00:00:00 2001 From: Daniel Bisgrove Date: Fri, 15 Dec 2023 15:22:03 -0500 Subject: [PATCH 20/20] tests for last commit --- .../notifications/NotificationsTable.test.tsx | 31 ++++++++++--------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/src/components/Settings/notifications/NotificationsTable.test.tsx b/src/components/Settings/notifications/NotificationsTable.test.tsx index 35dd16431..8e40638b0 100644 --- a/src/components/Settings/notifications/NotificationsTable.test.tsx +++ b/src/components/Settings/notifications/NotificationsTable.test.tsx @@ -39,13 +39,13 @@ const createNotification = (type, id) => ({ }, }); -const createConstant = (type, id) => ({ - id: type, - key: id, - value: type, +const createNotificationType = (type, id) => ({ + id: id, + type: type, + descriptionTemplate: type, }); const mocks = { - PreferencesNotifications: { + NotificationsPreferences: { notificationPreferences: { nodes: [ createNotification( @@ -57,14 +57,15 @@ const mocks = { ], }, }, - NotificationConstants: { - constant: { - notificationTranslatedHashes: [ - createConstant(NotificationTypeTypeEnum.CallPartnerOncePerYear, '111'), - createConstant(NotificationTypeTypeEnum.LargerGift, '222'), - createConstant(NotificationTypeTypeEnum.LongTimeFrameGift, '333'), - ], - }, + NotificationTypes: { + notificationTypes: [ + createNotificationType( + NotificationTypeTypeEnum.CallPartnerOncePerYear, + '111', + ), + createNotificationType(NotificationTypeTypeEnum.LargerGift, '222'), + createNotificationType(NotificationTypeTypeEnum.LongTimeFrameGift, '333'), + ], }, }; const mutationSpy = jest.fn(); @@ -95,10 +96,10 @@ describe('NotificationsTable', () => { mutationSpy.mock.calls[0][0].operation.variables.accountListId, ).toEqual(accountListId); expect(mutationSpy.mock.calls[0][0].operation.operationName).toEqual( - 'PreferencesNotifications', + 'NotificationsPreferences', ); expect(mutationSpy.mock.calls[1][0].operation.operationName).toEqual( - 'NotificationConstants', + 'NotificationTypes', ); });