diff --git a/pages/accountLists/[accountListId]/settings/integrations/index.page.test.tsx b/pages/accountLists/[accountListId]/settings/integrations/index.page.test.tsx new file mode 100644 index 000000000..bbd309d04 --- /dev/null +++ b/pages/accountLists/[accountListId]/settings/integrations/index.page.test.tsx @@ -0,0 +1,175 @@ +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 TestRouter from '__tests__/util/TestRouter'; +import { GqlMockedProvider } from '__tests__/util/graphqlMocking'; +import { GetUserOptionsQuery } from 'src/components/Contacts/ContactFlow/GetUserOptions.generated'; +import { MailchimpAccountQuery } from 'src/components/Settings/integrations/Mailchimp/MailchimpAccount.generated'; +import { GetUsersOrganizationsAccountsQuery } from 'src/components/Settings/integrations/Organization/Organizations.generated'; +import { PrayerlettersAccountQuery } from 'src/components/Settings/integrations/Prayerletters/PrayerlettersAccount.generated'; +import useGetAppSettings from 'src/hooks/useGetAppSettings'; +import theme from 'src/theme'; +import Integrations from './index.page'; + +const accountListId = 'account-list-1'; + +const mockEnqueue = jest.fn(); +const mutationSpy = jest.fn(); +const push = jest.fn(); + +const router = { + query: { accountListId }, + isReady: true, + push, +}; + +jest.mock('src/hooks/useGetAppSettings'); +jest.mock('notistack', () => ({ + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + ...jest.requireActual('notistack'), + useSnackbar: () => { + return { + enqueueSnackbar: mockEnqueue, + }; + }, +})); +interface MocksProvidersProps { + children: JSX.Element; + setup?: string; +} + +const MocksProviders: React.FC = ({ children, setup }) => ( + + + + mocks={{ + GetUsersOrganizationsAccounts: { + userOrganizationAccounts: [ + { + organization: {}, + }, + { + organization: {}, + }, + ], + }, + MailchimpAccount: { mailchimpAccount: [] }, + PrayerlettersAccount: { prayerlettersAccount: [] }, + GetUserOptions: { + userOptions: [ + { + id: '1', + key: 'setup_position', + value: setup || 'finish', + }, + ], + }, + }} + onCall={mutationSpy} + > + {children} + + + +); + +describe('Connect Services page', () => { + beforeEach(() => { + (useGetAppSettings as jest.Mock).mockReturnValue({ + appName: 'MPDX', + }); + }); + it('should render', async () => { + const { findByText } = render( + + + , + ); + expect(await findByText('Connect Services')).toBeInTheDocument(); + expect(await findByText('Organization')).toBeInTheDocument(); + }); + + describe('Setup Tour', () => { + it('should not show setup banner and accordions should not be disabled', async () => { + const { queryByText, queryByRole, findByText, getByText } = render( + + + , + ); + + await waitFor(() => { + expect( + queryByText('Make MPDX a part of your everyday life'), + ).not.toBeInTheDocument(); + expect( + queryByRole('button', { name: 'Next Step' }), + ).not.toBeInTheDocument(); + }); + + //Accordions should be clickable + userEvent.click(await findByText('Organization')); + await waitFor(() => { + expect( + getByText( + 'Add or change the organizations that sync donation information with this MPDX account. Removing an organization will not remove past information, but will prevent future donations and contacts from syncing.', + ), + ).toBeVisible(); + }); + }); + + it('should show setup banner and open google', async () => { + const { findByText, getByRole, getByText } = render( + + + , + ); + expect( + await findByText('Make MPDX a part of your everyday life'), + ).toBeInTheDocument(); + + //Accordions should be disabled + await waitFor(() => { + const label = getByText('Organization'); + expect(() => userEvent.click(label)).toThrow(); + }); + + const nextButton = getByRole('button', { name: 'Next Step' }); + + // Start with Google + expect(await findByText(/Add Account/i)).toBeInTheDocument(); + + // Moves to MailChimp + userEvent.click(nextButton); + expect(await findByText(/Connect MailChimp/i)).toBeInTheDocument(); + + // PrayerLetters.com + await waitFor(() => userEvent.click(nextButton)); + await waitFor(() => + expect( + getByText( + 'prayerletters.com is a significant way to save valuable ministry time while more effectively connecting with your partners. Keep your physical newsletter list up to date in MPDX and then sync it to your prayerletters.com account with this integration.', + ), + ).toBeInTheDocument(), + ); + + // Move to finish + userEvent.click(nextButton); + await waitFor(() => { + expect(mutationSpy).toHaveGraphqlOperation('UpdateUserOptions', { + key: 'setup_position', + value: 'finish', + }); + expect(push).toHaveBeenCalledWith( + '/accountLists/account-list-1/setup/finish', + ); + }); + }); + }); +}); diff --git a/pages/accountLists/[accountListId]/settings/integrations/index.page.tsx b/pages/accountLists/[accountListId]/settings/integrations/index.page.tsx index 0a19bfd6e..9a95f53ee 100644 --- a/pages/accountLists/[accountListId]/settings/integrations/index.page.tsx +++ b/pages/accountLists/[accountListId]/settings/integrations/index.page.tsx @@ -1,32 +1,85 @@ import { useRouter } from 'next/router'; import React, { useEffect, useState } from 'react'; +import { Button } from '@mui/material'; +import { useSnackbar } from 'notistack'; import { useTranslation } from 'react-i18next'; import { loadSession } from 'pages/api/utils/pagePropsHelpers'; +import { useUpdateUserOptionsMutation } from 'src/components/Contacts/ContactFlow/ContactFlowSetup/UpdateUserOptions.generated'; +import { useGetUserOptionsQuery } from 'src/components/Contacts/ContactFlow/GetUserOptions.generated'; import { ChalklineAccordion } from 'src/components/Settings/integrations/Chalkline/ChalklineAccordion'; import { GoogleAccordion } from 'src/components/Settings/integrations/Google/GoogleAccordion'; import { TheKeyAccordion } from 'src/components/Settings/integrations/Key/TheKeyAccordion'; import { MailchimpAccordion } from 'src/components/Settings/integrations/Mailchimp/MailchimpAccordion'; import { OrganizationAccordion } from 'src/components/Settings/integrations/Organization/OrganizationAccordion'; import { PrayerlettersAccordion } from 'src/components/Settings/integrations/Prayerletters/PrayerlettersAccordion'; +import { SetupBanner } from 'src/components/Settings/preferences/SetupBanner'; import { AccordionGroup } from 'src/components/Shared/Forms/Accordions/AccordionGroup'; +import { useAccountListId } from 'src/hooks/useAccountListId'; +import useGetAppSettings from 'src/hooks/useGetAppSettings'; import { suggestArticles } from 'src/lib/helpScout'; import { SettingsWrapper } from '../Wrapper'; +import { StickyBox } from '../preferences.page'; const Integrations: React.FC = () => { const { t } = useTranslation(); - const { query } = useRouter(); + const { push, query } = useRouter(); const [expandedPanel, setExpandedPanel] = useState( (query?.selectedTab as string | undefined) || '', ); + const accountListId = useAccountListId() || ''; + const { appName } = useGetAppSettings(); + const { enqueueSnackbar } = useSnackbar(); + const [setup, setSetup] = useState(0); - useEffect(() => { - suggestArticles('HS_SETTINGS_SERVICES_SUGGESTIONS'); - }, []); + const setupAccordions = ['google', 'mailchimp', 'prayerletters.com']; + + const { data: userOptions } = useGetUserOptionsQuery(); + const [updateUserOptions] = useUpdateUserOptionsMutation(); + + const isSettingUp = userOptions?.userOptions.some( + (option) => + option.key === 'setup_position' && + option.value === 'preferences.integrations', + ); + + const handleSetupChange = async () => { + if (!isSettingUp) { + return; + } + const nextNav = setup + 1; + + if (setupAccordions.length === nextNav) { + await updateUserOptions({ + variables: { + key: 'setup_position', + value: 'finish', + }, + onError: () => { + enqueueSnackbar(t('Saving setup phase failed.'), { + variant: 'error', + }); + }, + }); + push(`/accountLists/${accountListId}/setup/finish`); + } else { + setSetup(nextNav); + setExpandedPanel(setupAccordions[nextNav]); + } + }; const handleAccordionChange = (panel: string) => { const panelLowercase = panel.toLowerCase(); setExpandedPanel(expandedPanel === panelLowercase ? '' : panelLowercase); }; + useEffect(() => { + suggestArticles('HS_SETTINGS_SERVICES_SUGGESTIONS'); + }, []); + + useEffect(() => { + if (isSettingUp) { + setExpandedPanel(setupAccordions[0]); + } + }, [isSettingUp]); return ( { pageHeading={t('Connect Services')} selectedMenuId="integrations" > + {isSettingUp && ( + + + {t('Next Step')} + + } + title={t('Make {{appName}} a part of your everyday life', { + appName, + })} + /> + + )} diff --git a/pages/accountLists/[accountListId]/settings/notifications.page.test.tsx b/pages/accountLists/[accountListId]/settings/notifications.page.test.tsx new file mode 100644 index 000000000..5d2ab2a00 --- /dev/null +++ b/pages/accountLists/[accountListId]/settings/notifications.page.test.tsx @@ -0,0 +1,155 @@ +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 TestRouter from '__tests__/util/TestRouter'; +import { GqlMockedProvider } from '__tests__/util/graphqlMocking'; +import { GetUserOptionsQuery } from 'src/components/Contacts/ContactFlow/GetUserOptions.generated'; +import { + NotificationTypesQuery, + NotificationsPreferencesQuery, +} from 'src/components/Settings/notifications/Notifications.generated'; +import { notificationSettingsMocks } from 'src/components/Settings/notifications/notificationSettingsMocks'; +import theme from 'src/theme'; +import Notifications from './notifications.page'; + +const accountListId = 'account-list-1'; + +const mockEnqueue = jest.fn(); +const mutationSpy = jest.fn(); +const push = jest.fn(); + +const router = { + query: { accountListId }, + isReady: true, + push, +}; + +jest.mock('notistack', () => ({ + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + ...jest.requireActual('notistack'), + useSnackbar: () => { + return { + enqueueSnackbar: mockEnqueue, + }; + }, +})); + +interface MocksProvidersProps { + children: JSX.Element; + setup?: string; +} + +const MocksProviders: React.FC = ({ children, setup }) => ( + + + + mocks={{ + ...notificationSettingsMocks, + GetUserOptions: { + userOptions: [ + { + id: '1', + key: 'setup_position', + value: setup || 'finish', + }, + ], + }, + }} + onCall={mutationSpy} + > + {children} + + + +); + +describe('Notifications page', () => { + it('should render page', async () => { + const { findByText, getByTestId, queryByTestId } = render( + + + , + ); + await waitFor(() => { + expect(queryByTestId('skeleton-notifications')).not.toBeInTheDocument(); + }); + expect(await findByText('Notifications')).toBeInTheDocument(); + expect(getByTestId('select-all-app')).toBeInTheDocument(); + }); + + describe('Setup Tour', () => { + it('should not show setup banner', async () => { + const { queryByText, findByText } = render( + + + , + ); + + expect(await findByText('Notifications')).toBeInTheDocument(); + await waitFor(() => { + expect( + queryByText('Setup your notifications here'), + ).not.toBeInTheDocument(); + }); + }); + + it('should show setup banner move to the next part', async () => { + const { findByText, getByRole } = render( + + + , + ); + + expect( + await findByText('Setup your notifications here'), + ).toBeInTheDocument(); + + const skipButton = getByRole('button', { name: 'Skip Step' }); + userEvent.click(skipButton); + + // Move to Integrations + userEvent.click(skipButton); + await waitFor(() => { + expect(mutationSpy).toHaveGraphqlOperation('UpdateUserOptions', { + key: 'setup_position', + value: 'preferences.integrations', + }); + expect(push).toHaveBeenCalledWith( + '/accountLists/account-list-1/settings/integrations', + ); + }); + }); + + it('moves to the next section with Save Button', async () => { + const { getAllByRole, findByText } = render( + + + , + ); + + expect( + await findByText('Setup your notifications here'), + ).toBeInTheDocument(); + + const saveButton = getAllByRole('button', { name: 'Save Changes' })[0]; + + // Move to Integrations + userEvent.click(saveButton); + await waitFor(() => { + expect(mutationSpy).toHaveGraphqlOperation('UpdateUserOptions', { + key: 'setup_position', + value: 'preferences.integrations', + }); + expect(push).toHaveBeenCalledWith( + '/accountLists/account-list-1/settings/integrations', + ); + }); + }); + }); +}); diff --git a/pages/accountLists/[accountListId]/settings/notifications.page.tsx b/pages/accountLists/[accountListId]/settings/notifications.page.tsx index b89e3f5eb..0909ed279 100644 --- a/pages/accountLists/[accountListId]/settings/notifications.page.tsx +++ b/pages/accountLists/[accountListId]/settings/notifications.page.tsx @@ -1,14 +1,52 @@ +import { useRouter } from 'next/router'; import React from 'react'; -import { Box } from '@mui/material'; +import { Box, Button } from '@mui/material'; +import { useSnackbar } from 'notistack'; import { useTranslation } from 'react-i18next'; import { loadSession } from 'pages/api/utils/pagePropsHelpers'; +import { useUpdateUserOptionsMutation } from 'src/components/Contacts/ContactFlow/ContactFlowSetup/UpdateUserOptions.generated'; +import { useGetUserOptionsQuery } from 'src/components/Contacts/ContactFlow/GetUserOptions.generated'; import { NotificationsTable } from 'src/components/Settings/notifications/NotificationsTable'; +import { SetupBanner } from 'src/components/Settings/preferences/SetupBanner'; +import { useAccountListId } from 'src/hooks/useAccountListId'; import useGetAppSettings from 'src/hooks/useGetAppSettings'; import { SettingsWrapper } from './Wrapper'; +import { StickyBox } from './preferences.page'; const Notifications: React.FC = () => { const { t } = useTranslation(); const { appName } = useGetAppSettings(); + const accountListId = useAccountListId() || ''; + const { push } = useRouter(); + const { enqueueSnackbar } = useSnackbar(); + + const { data: userOptions } = useGetUserOptionsQuery(); + const [updateUserOptions] = useUpdateUserOptionsMutation(); + + const isSettingUp = userOptions?.userOptions.some( + (option) => + option.key === 'setup_position' && + option.value === 'preferences.notifications', + ); + + const handleSetupChange = async () => { + if (!isSettingUp) { + return; + } + + await updateUserOptions({ + variables: { + key: 'setup_position', + value: 'preferences.integrations', + }, + onError: () => { + enqueueSnackbar(t('Saving setup phase failed.'), { + variant: 'error', + }); + }, + }); + push(`/accountLists/${accountListId}/settings/integrations`); + }; return ( { pageHeading={t('Notifications')} selectedMenuId="notifications" > + {isSettingUp && ( + + + {t('Skip Step')} + + } + title={t('Setup your notifications here')} + /> + + )}

{t( @@ -38,7 +88,7 @@ const Notifications: React.FC = () => { )}

- +
); }; diff --git a/pages/accountLists/[accountListId]/settings/preferences.page.test.tsx b/pages/accountLists/[accountListId]/settings/preferences.page.test.tsx index 70c3d0ad5..8f0d60cad 100644 --- a/pages/accountLists/[accountListId]/settings/preferences.page.test.tsx +++ b/pages/accountLists/[accountListId]/settings/preferences.page.test.tsx @@ -1,20 +1,34 @@ 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 TestRouter from '__tests__/util/TestRouter'; import { GqlMockedProvider } from '__tests__/util/graphqlMocking'; +import { GetUserOptionsQuery } from 'src/components/Contacts/ContactFlow/GetUserOptions.generated'; +import { MailchimpAccountQuery } from 'src/components/Settings/integrations/Mailchimp/MailchimpAccount.generated'; +import { GetUsersOrganizationsAccountsQuery } from 'src/components/Settings/integrations/Organization/Organizations.generated'; +import { PrayerlettersAccountQuery } from 'src/components/Settings/integrations/Prayerletters/PrayerlettersAccount.generated'; +import { + CanUserExportDataQuery, + GetAccountPreferencesQuery, +} from 'src/components/Settings/preferences/GetAccountPreferences.generated'; +import { GetPersonalPreferencesQuery } from 'src/components/Settings/preferences/GetPersonalPreferences.generated'; +import { GetProfileInfoQuery } from 'src/components/Settings/preferences/GetProfileInfo.generated'; import theme from 'src/theme'; import Preferences from './preferences.page'; const accountListId = 'account-list-1'; +const mockEnqueue = jest.fn(); +const mutationSpy = jest.fn(); +const push = jest.fn(); + const router = { query: { accountListId }, isReady: true, + push, }; -const mockEnqueue = jest.fn(); - jest.mock('notistack', () => ({ // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore @@ -26,14 +40,31 @@ jest.mock('notistack', () => ({ }, })); -const MocksProviders = (props: { +interface MocksProvidersProps { children: JSX.Element; canUserExportData: boolean; singleOrg?: boolean; + setup?: string; +} + +const MocksProviders: React.FC = ({ + children, + canUserExportData, + singleOrg, + setup, }) => ( - mocks={{ GetAccountPreferences: { user: { @@ -87,7 +118,7 @@ const MocksProviders = (props: { }, }, GetUsersOrganizationsAccounts: { - userOrganizationAccounts: props.singleOrg + userOrganizationAccounts: singleOrg ? [ { organization: {}, @@ -104,13 +135,22 @@ const MocksProviders = (props: { }, CanUserExportData: { canUserExportData: { - allowed: props.canUserExportData, + allowed: canUserExportData, exportedAt: null, }, }, + GetUserOptions: { + userOptions: [ + { + key: 'setup_position', + value: setup || 'finish', + }, + ], + }, }} + onCall={mutationSpy} > - {props.children} + {children} @@ -166,4 +206,99 @@ describe('Preferences page', () => { expect(queryByText('Primary Organization')).not.toBeInTheDocument(), ); }); + + describe('Setup Tour', () => { + it('should not show setup banner and accordions should not be disabled', async () => { + const { queryByText, queryByRole, findByText, getByText, getByRole } = + render( + + + , + ); + + expect( + getByRole('button', { name: 'Reset Welcome Tour' }), + ).toBeInTheDocument(); + + await waitFor(() => { + expect(queryByText("Let's set your locale!")).not.toBeInTheDocument(); + expect( + queryByRole('button', { name: 'Skip Step' }), + ).not.toBeInTheDocument(); + }); + + //Accordions should be clickable + userEvent.click(await findByText('Language')); + await waitFor(() => { + expect( + getByText('The language determines your default language for .'), + ).toBeVisible(); + }); + }); + + it('should show setup banner and open locale', async () => { + const { findByText, getByRole, queryByText, getByText } = render( + + + , + ); + + //Accordions should be disabled + await waitFor(() => { + const label = getByText('Language'); + expect(() => userEvent.click(label)).toThrow(); + expect( + queryByText('The language determines your default language for .'), + ).not.toBeInTheDocument(); + }); + + // Start with Locale + expect(await findByText("Let's set your locale!")).toBeInTheDocument(); + expect( + await findByText( + 'The locale determines how numbers, dates and other information are formatted.', + ), + ).toBeInTheDocument(); + + // Moves to Monthly Goal + userEvent.click(getByRole('button', { name: 'Save' })); + expect( + await findByText('Great progress comes from great goals!'), + ).toBeInTheDocument(); + expect( + await findByText( + 'This amount should be set to the amount your organization has determined is your target monthly goal. If you do not know, make your best guess for now. You can change it at any time.', + ), + ).toBeInTheDocument(); + + // Home Country + const skipButton = getByRole('button', { name: 'Skip Step' }); + userEvent.click(skipButton); + expect( + await findByText( + 'This should be the place from which you are living and sending out physical communications. This will be used in exports for mailing address information.', + ), + ).toBeInTheDocument(); + + // Move to Notifications + userEvent.click(skipButton); + await waitFor(() => { + expect(mutationSpy).toHaveGraphqlOperation('UpdateUserOptions', { + key: 'setup_position', + value: 'preferences.notifications', + }); + expect(push).toHaveBeenCalledWith( + '/accountLists/account-list-1/settings/notifications', + ); + }); + }); + }); }); diff --git a/pages/accountLists/[accountListId]/settings/preferences.page.tsx b/pages/accountLists/[accountListId]/settings/preferences.page.tsx index bca5acd95..f9030508f 100644 --- a/pages/accountLists/[accountListId]/settings/preferences.page.tsx +++ b/pages/accountLists/[accountListId]/settings/preferences.page.tsx @@ -1,15 +1,19 @@ import { useRouter } from 'next/router'; import React, { useEffect, useState } from 'react'; -import { Skeleton } from '@mui/material'; +import { Box, Button, Skeleton } from '@mui/material'; import { styled } from '@mui/material/styles'; +import { useSnackbar } from 'notistack'; import { useTranslation } from 'react-i18next'; import { loadSession } from 'pages/api/utils/pagePropsHelpers'; +import { useUpdateUserOptionsMutation } from 'src/components/Contacts/ContactFlow/ContactFlowSetup/UpdateUserOptions.generated'; +import { useGetUserOptionsQuery } from 'src/components/Contacts/ContactFlow/GetUserOptions.generated'; import { useGetUsersOrganizationsAccountsQuery } from 'src/components/Settings/integrations/Organization/Organizations.generated'; import { useCanUserExportDataQuery, useGetAccountPreferencesQuery, } from 'src/components/Settings/preferences/GetAccountPreferences.generated'; import { useGetPersonalPreferencesQuery } from 'src/components/Settings/preferences/GetPersonalPreferences.generated'; +import { SetupBanner } from 'src/components/Settings/preferences/SetupBanner'; import { AccountNameAccordion } from 'src/components/Settings/preferences/accordions/AccountNameAccordion/AccountNameAccordion'; import { CurrencyAccordion } from 'src/components/Settings/preferences/accordions/CurrencyAccordion/CurrencyAccordion'; import { DefaultAccountAccordion } from 'src/components/Settings/preferences/accordions/DefaultAccountAccordion/DefaultAccountAccordion'; @@ -36,24 +40,35 @@ const AccordionLoading = styled(Skeleton)(() => ({ height: '48px', })); +export const StickyBox = styled(Box)(({ theme }) => ({ + position: 'sticky', + top: theme.spacing(10), + borderBottom: '1px solid', + borderBottomColor: theme.palette.grey[200], + height: theme.spacing(10), + zIndex: '700', + background: theme.palette.common.white, + paddingTop: theme.spacing(1), + paddingBottom: theme.spacing(1), + marginBottom: theme.spacing(2), +})); + const Preferences: React.FC = () => { const { t } = useTranslation(); const accountListId = useAccountListId() || ''; - const { query } = useRouter(); + const { push, query } = useRouter(); + const { enqueueSnackbar } = useSnackbar(); + + const setupAccordions = ['locale', 'monthly goal', 'home country']; + const [setup, setSetup] = useState(0); const [expandedPanel, setExpandedPanel] = useState( typeof query.selectedTab === 'string' ? query.selectedTab : '', ); const countries = getCountries(); const timeZones = useGetTimezones(); - useEffect(() => { - suggestArticles('HS_SETTINGS_PREFERENCES_SUGGESTIONS'); - }, []); - - const handleAccordionChange = (panel: string) => { - const panelLowercase = panel.toLowerCase(); - setExpandedPanel(expandedPanel === panelLowercase ? '' : panelLowercase); - }; + const { data: userOptions } = useGetUserOptionsQuery(); + const [updateUserOptions] = useUpdateUserOptionsMutation(); const { data: personalPreferencesData, loading: personalPreferencesLoading } = useGetPersonalPreferencesQuery({ @@ -77,12 +92,97 @@ const Preferences: React.FC = () => { const { data: userOrganizationAccountsData } = useGetUsersOrganizationsAccountsQuery(); + const savedSetupPosition = userOptions?.userOptions.find( + (option) => option.key === 'setup_position', + )?.value; + const isSettingUp = savedSetupPosition === 'preferences.personal'; + + useEffect(() => { + suggestArticles('HS_SETTINGS_PREFERENCES_SUGGESTIONS'); + }, []); + + useEffect(() => { + if (isSettingUp) { + setExpandedPanel(setupAccordions[0]); + } + }, [isSettingUp]); + + const handleAccordionChange = (panel: string) => { + const panelLowercase = panel.toLowerCase(); + setExpandedPanel(expandedPanel === panelLowercase ? '' : panelLowercase); + }; + + const resetWelcomeTour = async () => { + await updateUserOptions({ + variables: { + key: 'setup_position', + value: 'start', + }, + onError: () => { + enqueueSnackbar(t('Resetting the welcome tour failed.'), { + variant: 'error', + }); + }, + }); + push(`/accountLists/${accountListId}/setup/start`); + }; + + const handleSetupChange = async () => { + if (!isSettingUp) { + return; + } + const nextNav = setup + 1; + + if (setupAccordions.length === nextNav) { + await updateUserOptions({ + variables: { + key: 'setup_position', + value: 'preferences.notifications', + }, + onError: () => { + enqueueSnackbar(t('Saving setup phase failed.'), { + variant: 'error', + }); + }, + }); + push(`/accountLists/${accountListId}/settings/notifications`); + } else { + setSetup(nextNav); + setExpandedPanel(setupAccordions[nextNav]); + } + }; + + const getSetupMessage = (setup: number) => { + switch (setup) { + case 0: + return t("Let's set your locale!"); + case 1: + return t('Great progress comes from great goals!'); + case 2: + return t('What country are you in?'); + default: + return ''; + } + }; + return ( + {isSettingUp && ( + + + {t('Skip Step')} + + } + title={getSetupMessage(setup)} + /> + + )} {personalPreferencesLoading && ( @@ -100,6 +200,7 @@ const Preferences: React.FC = () => { handleAccordionChange={handleAccordionChange} expandedPanel={expandedPanel} locale={personalPreferencesData?.user?.preferences?.locale || ''} + disabled={isSettingUp} /> { localeDisplay={ personalPreferencesData?.user?.preferences?.localeDisplay || '' } + disabled={isSettingUp && setup !== 0} + handleSetupChange={handleSetupChange} /> { defaultAccountList={ personalPreferencesData?.user?.defaultAccountList || '' } + disabled={isSettingUp} /> { personalPreferencesData?.user?.preferences?.timeZone || '' } timeZones={timeZones} + disabled={isSettingUp} /> { personalPreferencesData?.user?.preferences ?.hourToSendNotifications || null } + disabled={isSettingUp} /> )} @@ -153,6 +259,7 @@ const Preferences: React.FC = () => { expandedPanel={expandedPanel} name={accountPreferencesData?.accountList?.name || ''} accountListId={accountListId} + disabled={isSettingUp} /> { currency={ accountPreferencesData?.accountList?.settings?.currency || '' } + disabled={isSettingUp && setup !== 1} + handleSetupChange={handleSetupChange} /> { } accountListId={accountListId} countries={countries} + disabled={isSettingUp && setup !== 2} + handleSetupChange={handleSetupChange} /> { accountPreferencesData?.accountList?.settings?.currency || '' } accountListId={accountListId} + disabled={isSettingUp} /> {userOrganizationAccountsData?.userOrganizationAccounts && userOrganizationAccountsData?.userOrganizationAccounts?.length > @@ -195,6 +307,7 @@ const Preferences: React.FC = () => { '' } accountListId={accountListId} + disabled={isSettingUp} /> )} { accountPreferencesData?.accountList?.settings?.tester || false } accountListId={accountListId} + disabled={isSettingUp} /> { accountPreferencesData?.accountList?.settings?.currency || '' } accountListId={accountListId} + disabled={isSettingUp} /> {canUserExportData?.canUserExportData.allowed && ( { } accountListId={accountListId} data={personalPreferencesData} + disabled={isSettingUp} /> )} )} + + + ); }; diff --git a/src/components/Settings/integrations/Chalkline/ChalklineAccordion.tsx b/src/components/Settings/integrations/Chalkline/ChalklineAccordion.tsx index 4dc4c8b6c..4e3ba3bfb 100644 --- a/src/components/Settings/integrations/Chalkline/ChalklineAccordion.tsx +++ b/src/components/Settings/integrations/Chalkline/ChalklineAccordion.tsx @@ -13,6 +13,7 @@ import { useSendToChalklineMutation } from './SendToChalkline.generated'; export const ChalklineAccordion: React.FC = ({ handleAccordionChange, expandedPanel, + disabled, }) => { const { t } = useTranslation(); const accordionName = t('Chalk Line'); @@ -54,6 +55,7 @@ export const ChalklineAccordion: React.FC = ({ expandedPanel={expandedPanel} label={accordionName} value={''} + disabled={disabled} image={ void; - expandedPanel: string; -} - const EditIconButton = styled(IconButton)(() => ({ color: theme.palette.primary.main, marginLeft: '10px', @@ -74,9 +70,10 @@ export type GoogleAccountAttributesSlimmed = Pick< 'id' | 'email' | 'primary' | 'remoteId' | 'tokenExpired' >; -export const GoogleAccordion: React.FC = ({ +export const GoogleAccordion: React.FC = ({ handleAccordionChange, expandedPanel, + disabled, }) => { const { t } = useTranslation(); const [openEditGoogleAccount, setOpenEditGoogleAccount] = useState(false); @@ -107,6 +104,7 @@ export const GoogleAccordion: React.FC = ({ expandedPanel={expandedPanel} label={t('Google')} value={''} + disabled={disabled} image={ void; - expandedPanel: string; -} - -export const TheKeyAccordion: React.FC = ({ +export const TheKeyAccordion: React.FC = ({ handleAccordionChange, expandedPanel, + disabled, }) => { const { t } = useTranslation(); const { data, loading } = useGetKeyAccountsQuery(); @@ -22,6 +19,7 @@ export const TheKeyAccordion: React.FC = ({ expandedPanel={expandedPanel} label={t('The Key / Relay')} value={''} + disabled={disabled} image={ void; - expandedPanel: string; -} - const StyledFormControlLabel = styled(FormControlLabel)(() => ({ flex: '0 1 50%', margin: '0 0 0 -11px', @@ -61,9 +57,10 @@ const StyledButton = styled(Button)(() => ({ marginLeft: '15px', })); -export const MailchimpAccordion: React.FC = ({ +export const MailchimpAccordion: React.FC = ({ handleAccordionChange, expandedPanel, + disabled, }) => { const { t } = useTranslation(); const [showSettings, setShowSettings] = useState(false); @@ -172,6 +169,7 @@ export const MailchimpAccordion: React.FC = ({ expandedPanel={expandedPanel} label={t('MailChimp')} value={''} + disabled={disabled} image={ void; - expandedPanel: string; -} - type OrganizationAccountPartial = GetUsersOrganizationsAccountsQuery['userOrganizationAccounts'][0]; @@ -83,9 +78,10 @@ export const getOrganizationType = ( return undefined; }; -export const OrganizationAccordion: React.FC = ({ +export const OrganizationAccordion: React.FC = ({ handleAccordionChange, expandedPanel, + disabled, }) => { const { t } = useTranslation(); const accountListId = useAccountListId(); @@ -183,6 +179,7 @@ export const OrganizationAccordion: React.FC = ({ expandedPanel={expandedPanel} label={t('Organization')} value={''} + disabled={disabled} image={ = ({ handleAccordionChange, expandedPanel, + disabled, }) => { const { t } = useTranslation(); const [isSaving, setIsSaving] = useState(false); @@ -91,6 +92,7 @@ export const PrayerlettersAccordion: React.FC = ({ expandedPanel={expandedPanel} label={accordionName} value={''} + disabled={disabled} image={ ({ export interface AccordionProps { handleAccordionChange: (panel: string) => void; expandedPanel: string; + disabled?: boolean; } diff --git a/src/components/Settings/notifications/NotificationsTable.test.tsx b/src/components/Settings/notifications/NotificationsTable.test.tsx index 9bb6b5cb0..82eb6fce6 100644 --- a/src/components/Settings/notifications/NotificationsTable.test.tsx +++ b/src/components/Settings/notifications/NotificationsTable.test.tsx @@ -5,9 +5,9 @@ import userEvent from '@testing-library/user-event'; import { SnackbarProvider } from 'notistack'; import TestRouter from '__tests__/util/TestRouter'; import { GqlMockedProvider } from '__tests__/util/graphqlMocking'; -import { NotificationTypeTypeEnum } from 'src/graphql/types.generated'; import theme from '../../../theme'; import { NotificationsTable } from './NotificationsTable'; +import { notificationSettingsMocks } from './notificationSettingsMocks'; const mockEnqueue = jest.fn(); @@ -28,53 +28,18 @@ const router = { query: { accountListId }, isReady: true, }; -const createNotification = (type, id) => ({ - app: false, - email: false, - task: false, - notificationType: { - id, - descriptionTemplate: type, - type, - }, -}); -const createNotificationType = (type, id) => ({ - id: id, - type: type, - descriptionTemplate: type, -}); -const mocks = { - NotificationsPreferences: { - notificationPreferences: { - nodes: [ - createNotification( - NotificationTypeTypeEnum.CallPartnerOncePerYear, - '111', - ), - createNotification(NotificationTypeTypeEnum.LargerGift, '222'), - createNotification(NotificationTypeTypeEnum.LongTimeFrameGift, '333'), - ], - }, - }, - NotificationTypes: { - notificationTypes: [ - createNotificationType( - NotificationTypeTypeEnum.CallPartnerOncePerYear, - '111', - ), - createNotificationType(NotificationTypeTypeEnum.LargerGift, '222'), - createNotificationType(NotificationTypeTypeEnum.LongTimeFrameGift, '333'), - ], - }, -}; const mutationSpy = jest.fn(); +const handleSetupChange = jest.fn(); const Components: React.FC = () => ( - - + + diff --git a/src/components/Settings/notifications/NotificationsTable.tsx b/src/components/Settings/notifications/NotificationsTable.tsx index 53a0a1310..32470a542 100644 --- a/src/components/Settings/notifications/NotificationsTable.tsx +++ b/src/components/Settings/notifications/NotificationsTable.tsx @@ -73,7 +73,13 @@ const notificationSchema: yup.SchemaOf<{ ), }); -export const NotificationsTable: React.FC = () => { +interface NotificationsTableProps { + handleSetupChange: () => Promise; +} + +export const NotificationsTable: React.FC = ({ + handleSetupChange, +}) => { const { t } = useTranslation(); const accountListId = useAccountListId(); const { enqueueSnackbar } = useSnackbar(); @@ -160,6 +166,7 @@ export const NotificationsTable: React.FC = () => { enqueueSnackbar(t('Notifications updated successfully'), { variant: 'success', }); + handleSetupChange(); }; return ( diff --git a/src/components/Settings/notifications/notificationSettingsMocks.ts b/src/components/Settings/notifications/notificationSettingsMocks.ts new file mode 100644 index 000000000..a454b0f2f --- /dev/null +++ b/src/components/Settings/notifications/notificationSettingsMocks.ts @@ -0,0 +1,43 @@ +import { NotificationTypeTypeEnum } from 'src/graphql/types.generated'; + +const createNotification = (type, id) => ({ + app: false, + email: false, + task: false, + notificationType: { + id, + descriptionTemplate: type, + type, + }, +}); + +const createNotificationType = (type, id) => ({ + id: id, + type: type, + descriptionTemplate: type, +}); + +export const notificationSettingsMocks = { + NotificationsPreferences: { + notificationPreferences: { + nodes: [ + createNotification( + NotificationTypeTypeEnum.CallPartnerOncePerYear, + '111', + ), + createNotification(NotificationTypeTypeEnum.LargerGift, '222'), + createNotification(NotificationTypeTypeEnum.LongTimeFrameGift, '333'), + ], + }, + }, + NotificationTypes: { + notificationTypes: [ + createNotificationType( + NotificationTypeTypeEnum.CallPartnerOncePerYear, + '111', + ), + createNotificationType(NotificationTypeTypeEnum.LargerGift, '222'), + createNotificationType(NotificationTypeTypeEnum.LongTimeFrameGift, '333'), + ], + }, +}; diff --git a/src/components/Settings/preferences/SetupBanner.tsx b/src/components/Settings/preferences/SetupBanner.tsx new file mode 100644 index 000000000..f3cc3b1b5 --- /dev/null +++ b/src/components/Settings/preferences/SetupBanner.tsx @@ -0,0 +1,26 @@ +import CampaignIcon from '@mui/icons-material/Campaign'; +import { Alert, Typography } from '@mui/material'; + +interface SetupBannerProps { + button?: React.ReactNode; + content?: React.ReactNode; + title?: string; +} + +export const SetupBanner: React.FC = ({ + button, + content, + title, +}) => { + return ( + } + action={button} + sx={{ marginBottom: 2 }} + > + {title && {title}} + {content} + + ); +}; diff --git a/src/components/Settings/preferences/accordions/AccountNameAccordion/AccountNameAccordion.tsx b/src/components/Settings/preferences/accordions/AccountNameAccordion/AccountNameAccordion.tsx index e6e9b9f43..33d1ab8fb 100644 --- a/src/components/Settings/preferences/accordions/AccountNameAccordion/AccountNameAccordion.tsx +++ b/src/components/Settings/preferences/accordions/AccountNameAccordion/AccountNameAccordion.tsx @@ -21,6 +21,7 @@ interface AccountNameAccordionProps { expandedPanel: string; accountListId: string; name: string; + disabled?: boolean; } export const AccountNameAccordion: React.FC = ({ @@ -28,6 +29,7 @@ export const AccountNameAccordion: React.FC = ({ expandedPanel, name, accountListId, + disabled, }) => { const { t } = useTranslation(); const { appName } = useGetAppSettings(); @@ -67,6 +69,7 @@ export const AccountNameAccordion: React.FC = ({ label={label} value={name} fullWidth + disabled={disabled} > = ({ @@ -29,6 +30,7 @@ export const CurrencyAccordion: React.FC = ({ expandedPanel, currency, accountListId, + disabled, }) => { const { t } = useTranslation(); const { enqueueSnackbar } = useSnackbar(); @@ -73,6 +75,7 @@ export const CurrencyAccordion: React.FC = ({ label={label} value={currency} fullWidth + disabled={disabled} > > = @@ -27,7 +28,13 @@ const preferencesSchema: yup.SchemaOf> = export const DefaultAccountAccordion: React.FC< DefaultAccountAccordionProps -> = ({ handleAccordionChange, expandedPanel, data, defaultAccountList }) => { +> = ({ + handleAccordionChange, + expandedPanel, + data, + defaultAccountList, + disabled, +}) => { const { t } = useTranslation(); const { appName } = useGetAppSettings(); const { enqueueSnackbar } = useSnackbar(); @@ -71,6 +78,7 @@ export const DefaultAccountAccordion: React.FC< label={label} value={selectedAccount} fullWidth + disabled={disabled} > = ({ @@ -30,6 +31,7 @@ export const EarlyAdopterAccordion: React.FC = ({ expandedPanel, tester, accountListId, + disabled, }) => { const { t } = useTranslation(); const { userId } = useUserPreferenceContext(); @@ -90,6 +92,7 @@ export const EarlyAdopterAccordion: React.FC = ({ label={label} value={tester ? t('Yes') : t('No')} fullWidth + disabled={disabled} > = ({ @@ -33,6 +34,7 @@ export const ExportAllDataAccordion: React.FC = ({ expandedPanel, exportedAt, accountListId, + disabled, }) => { const { t } = useTranslation(); const { enqueueSnackbar } = useSnackbar(); @@ -81,6 +83,7 @@ export const ExportAllDataAccordion: React.FC = ({ label={label} value={''} fullWidth + disabled={disabled} >
diff --git a/src/components/Settings/preferences/accordions/HomeCountryAccordion/HomeCountryAccordion.test.tsx b/src/components/Settings/preferences/accordions/HomeCountryAccordion/HomeCountryAccordion.test.tsx index d1dc8d272..f71a77e67 100644 --- a/src/components/Settings/preferences/accordions/HomeCountryAccordion/HomeCountryAccordion.test.tsx +++ b/src/components/Settings/preferences/accordions/HomeCountryAccordion/HomeCountryAccordion.test.tsx @@ -30,6 +30,7 @@ jest.mock('notistack', () => ({ })); const handleAccordionChange = jest.fn(); +const handleSetupChange = jest.fn(); const mutationSpy = jest.fn(); const countries = [ @@ -56,6 +57,7 @@ const Components: React.FC = ({ homeCountry={homeCountry} accountListId={accountListId} countries={countries} + handleSetupChange={handleSetupChange} /> @@ -146,6 +148,7 @@ describe('HomeCountryAccordion', () => { homeCountry={'USA'} accountListId={accountListId} countries={countries} + handleSetupChange={handleSetupChange} /> diff --git a/src/components/Settings/preferences/accordions/HomeCountryAccordion/HomeCountryAccordion.tsx b/src/components/Settings/preferences/accordions/HomeCountryAccordion/HomeCountryAccordion.tsx index 86c4c56f5..2d54b38bd 100644 --- a/src/components/Settings/preferences/accordions/HomeCountryAccordion/HomeCountryAccordion.tsx +++ b/src/components/Settings/preferences/accordions/HomeCountryAccordion/HomeCountryAccordion.tsx @@ -22,6 +22,8 @@ interface HomeCountryAccordionProps { homeCountry: string; accountListId: string; countries: { name: string; code: string }[]; + disabled?: boolean; + handleSetupChange: () => Promise; } export const HomeCountryAccordion: React.FC = ({ @@ -30,6 +32,8 @@ export const HomeCountryAccordion: React.FC = ({ homeCountry, accountListId, countries, + disabled, + handleSetupChange, }) => { const { t } = useTranslation(); const { enqueueSnackbar } = useSnackbar(); @@ -69,6 +73,7 @@ export const HomeCountryAccordion: React.FC = ({ }); }, }); + handleSetupChange(); }; return ( @@ -78,6 +83,7 @@ export const HomeCountryAccordion: React.FC = ({ label={label} value={selectedCountry} fullWidth + disabled={disabled} > void; expandedPanel: string; hourToSendNotifications: number | null; + disabled?: boolean; } export const HourToSendNotificationsAccordion: React.FC< HourToSendNotificationsAccordionProps -> = ({ handleAccordionChange, expandedPanel, hourToSendNotifications }) => { +> = ({ + handleAccordionChange, + expandedPanel, + hourToSendNotifications, + disabled, +}) => { const { t } = useTranslation(); const { appName } = useGetAppSettings(); const { enqueueSnackbar } = useSnackbar(); @@ -77,6 +83,7 @@ export const HourToSendNotificationsAccordion: React.FC< label={label} value={selectedHour || ''} fullWidth + disabled={disabled} > void; expandedPanel: string; locale: string; + disabled?: boolean; } export const LanguageAccordion: React.FC = ({ handleAccordionChange, expandedPanel, locale, + disabled, }) => { const { t } = useTranslation(); const { appName } = useGetAppSettings(); @@ -69,6 +71,7 @@ export const LanguageAccordion: React.FC = ({ label={label} value={selectedLanguage} fullWidth + disabled={disabled} > ({ })); const handleAccordionChange = jest.fn(); +const handleSetupChange = jest.fn(); const mutationSpy = jest.fn(); interface ComponentsProps { @@ -72,6 +73,7 @@ const Components: React.FC = ({ handleAccordionChange={handleAccordionChange} expandedPanel={expandedPanel} localeDisplay={localeDisplay} + handleSetupChange={handleSetupChange} /> diff --git a/src/components/Settings/preferences/accordions/LocaleAccordion/LocaleAccordion.tsx b/src/components/Settings/preferences/accordions/LocaleAccordion/LocaleAccordion.tsx index 30ed77f68..eee84b963 100644 --- a/src/components/Settings/preferences/accordions/LocaleAccordion/LocaleAccordion.tsx +++ b/src/components/Settings/preferences/accordions/LocaleAccordion/LocaleAccordion.tsx @@ -20,12 +20,16 @@ interface LocaleAccordionProps { handleAccordionChange: (panel: string) => void; expandedPanel: string; localeDisplay: string; + disabled?: boolean; + handleSetupChange: () => Promise; } export const LocaleAccordion: React.FC = ({ handleAccordionChange, expandedPanel, localeDisplay, + disabled, + handleSetupChange, }) => { const { t } = useTranslation(); const { enqueueSnackbar } = useSnackbar(); @@ -71,6 +75,7 @@ export const LocaleAccordion: React.FC = ({ }); }, }); + handleSetupChange(); }; return ( @@ -80,6 +85,7 @@ export const LocaleAccordion: React.FC = ({ label={label} value={selectedLocale || ''} fullWidth + disabled={disabled} > ({ })); const handleAccordionChange = jest.fn(); +const handleSetupChange = jest.fn(); const mutationSpy = jest.fn(); interface ComponentsProps { @@ -49,6 +50,7 @@ const Components: React.FC = ({ monthlyGoal={monthlyGoal} currency={'USD'} accountListId={accountListId} + handleSetupChange={handleSetupChange} /> diff --git a/src/components/Settings/preferences/accordions/MonthlyGoalAccordion/MonthlyGoalAccordion.tsx b/src/components/Settings/preferences/accordions/MonthlyGoalAccordion/MonthlyGoalAccordion.tsx index a9f6f4f0f..b944ed92e 100644 --- a/src/components/Settings/preferences/accordions/MonthlyGoalAccordion/MonthlyGoalAccordion.tsx +++ b/src/components/Settings/preferences/accordions/MonthlyGoalAccordion/MonthlyGoalAccordion.tsx @@ -24,6 +24,8 @@ interface MonthlyGoalAccordionProps { monthlyGoal: number | null; accountListId: string; currency: string; + disabled?: boolean; + handleSetupChange: () => Promise; } export const MonthlyGoalAccordion: React.FC = ({ @@ -32,6 +34,8 @@ export const MonthlyGoalAccordion: React.FC = ({ monthlyGoal, accountListId, currency, + disabled, + handleSetupChange, }) => { const { t } = useTranslation(); const { enqueueSnackbar } = useSnackbar(); @@ -72,6 +76,7 @@ export const MonthlyGoalAccordion: React.FC = ({ }); }, }); + handleSetupChange(); }; return ( @@ -81,6 +86,7 @@ export const MonthlyGoalAccordion: React.FC = ({ label={label} value={monthlyGoalString} fullWidth + disabled={disabled} > = ({ autoFocus label={label} sx={{ marginTop: 1 }} + id="monthlyGoalInput" /> diff --git a/src/components/Settings/preferences/accordions/MpdInfoAccordion/MpdInfoAccordion.tsx b/src/components/Settings/preferences/accordions/MpdInfoAccordion/MpdInfoAccordion.tsx index 0d38d3e79..40f720297 100644 --- a/src/components/Settings/preferences/accordions/MpdInfoAccordion/MpdInfoAccordion.tsx +++ b/src/components/Settings/preferences/accordions/MpdInfoAccordion/MpdInfoAccordion.tsx @@ -37,6 +37,7 @@ interface MpdInfoAccordionProps { activeMpdFinishAt: string | null; currency: string; accountListId: string; + disabled?: boolean; } export const MpdInfoAccordion: React.FC = ({ @@ -47,6 +48,7 @@ export const MpdInfoAccordion: React.FC = ({ activeMpdFinishAt, currency, accountListId, + disabled, }) => { const { t } = useTranslation(); const locale = useLocale(); @@ -111,6 +113,7 @@ export const MpdInfoAccordion: React.FC = ({ label={label} value={goalDateString} fullWidth + disabled={disabled} > = ({ @@ -31,6 +32,7 @@ export const PrimaryOrgAccordion: React.FC = ({ organizations, salaryOrganizationId, accountListId, + disabled, }) => { const { t } = useTranslation(); const { enqueueSnackbar } = useSnackbar(); @@ -79,6 +81,7 @@ export const PrimaryOrgAccordion: React.FC = ({ label={label} value={selectedOrgName} fullWidth + disabled={disabled} > >; + disabled?: boolean; } export const TimeZoneAccordion: React.FC = ({ @@ -27,6 +28,7 @@ export const TimeZoneAccordion: React.FC = ({ expandedPanel, timeZone, timeZones, + disabled, }) => { const { t } = useTranslation(); const { enqueueSnackbar } = useSnackbar(); @@ -72,6 +74,7 @@ export const TimeZoneAccordion: React.FC = ({ label={label} value={selectedTimeZone} fullWidth + disabled={disabled} > ({ '&:not(:last-child)': { borderBottom: 0, }, + '&.MuiAccordion-rounded.Mui-disabled': { + color: theme.palette.cruGrayDark, + backgroundColor: 'white', + }, ...accordionShared, })); @@ -121,6 +125,7 @@ interface AccordionItemProps { children?: React.ReactNode; fullWidth?: boolean; image?: React.ReactNode; + disabled?: boolean; } export const AccordionItem: React.FC = ({ @@ -131,9 +136,10 @@ export const AccordionItem: React.FC = ({ children, fullWidth = false, image, + disabled, }) => { const expanded = useMemo( - () => expandedPanel.toLowerCase() === label.toLowerCase(), + () => expandedPanel?.toLowerCase() === label.toLowerCase(), [expandedPanel, label], ); return ( @@ -141,6 +147,7 @@ export const AccordionItem: React.FC = ({ onChange={() => onAccordionChange(label)} expanded={expanded} disableGutters + disabled={disabled} > }>