Skip to content

Commit

Permalink
Merge pull request #1021 from CruGlobal/8096-setup-account-page
Browse files Browse the repository at this point in the history
[MPDX-8096] Add default account setup page
  • Loading branch information
canac committed Aug 30, 2024
2 parents 9d0f572 + 983acac commit c09143f
Show file tree
Hide file tree
Showing 3 changed files with 330 additions and 0 deletions.
8 changes: 8 additions & 0 deletions pages/setup/Account.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
query AccountListOptions {
accountLists(first: 50) {
nodes {
id
name
}
}
}
168 changes: 168 additions & 0 deletions pages/setup/account.page.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import { GetServerSidePropsContext } from 'next';
import { ApolloClient, NormalizedCacheObject } from '@apollo/client';
import { render, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { getSession } from 'next-auth/react';
import { session } from '__tests__/fixtures/session';
import TestRouter from '__tests__/util/TestRouter';
import { GqlMockedProvider } from '__tests__/util/graphqlMocking';
import makeSsrClient from 'src/lib/apollo/ssrClient';
import AccountPage, { getServerSideProps } from './account.page';

jest.mock('src/lib/apollo/ssrClient');

const push = jest.fn();
const router = {
push,
};

const context = {
req: {},
} as unknown as GetServerSidePropsContext;

const mutationSpy = jest.fn();

const TestComponent: React.FC = () => {
const accountListOptions = {
accountLists: {
nodes: [1, 2, 3].map((id) => ({
id: `account-list-${id}`,
name: `Account List ${id}`,
})),
},
};

return (
<TestRouter router={router}>
<GqlMockedProvider onCall={mutationSpy}>
<AccountPage accountListOptions={accountListOptions} />
</GqlMockedProvider>
</TestRouter>
);
};

describe('Setup account page', () => {
it('renders account options, saves default account, and advances to the next page', async () => {
const { getByRole } = render(<TestComponent />);

expect(
getByRole('heading', { name: 'Set default account' }),
).toBeInTheDocument();

userEvent.click(getByRole('combobox', { name: 'Account' }));
userEvent.click(getByRole('option', { name: 'Account List 1' }));
const continueButton = getByRole('button', { name: 'Continue Tour' });
userEvent.click(continueButton);
expect(continueButton).toBeDisabled();

await waitFor(() =>
expect(mutationSpy).toHaveGraphqlOperation('UpdateUserDefaultAccount', {
input: { attributes: { defaultAccountList: 'account-list-1' } },
}),
);
expect(push).toHaveBeenCalledWith(
'/accountLists/account-list-1/settings/preferences',
);
});

it('disables save button until the user selects an account', () => {
const { getByRole } = render(<TestComponent />);

expect(getByRole('button', { name: 'Continue Tour' })).toBeDisabled();

userEvent.click(getByRole('combobox', { name: 'Account' }));
userEvent.click(getByRole('option', { name: 'Account List 1' }));
expect(getByRole('button', { name: 'Continue Tour' })).not.toBeDisabled();
});
});

describe('getServerSideProps', () => {
const query = jest.fn();
const mutate = jest.fn();

beforeEach(() => {
(makeSsrClient as jest.MockedFn<typeof makeSsrClient>).mockReturnValue({
query,
mutate,
} as unknown as ApolloClient<NormalizedCacheObject>);
});

it('redirects to the login page if the session is missing', async () => {
(getSession as jest.MockedFn<typeof getSession>).mockResolvedValueOnce(
null,
);

await expect(getServerSideProps(context)).resolves.toEqual({
redirect: {
destination: '/login',
permanent: false,
},
});
});

it('sets the single account list as the default', async () => {
query.mockResolvedValue({
data: {
accountLists: {
nodes: [{ id: 'account-list-1' }],
},
},
});

await expect(getServerSideProps(context)).resolves.toEqual({
redirect: {
destination: '/accountLists/account-list-1/settings/preferences',
permanent: false,
},
});
expect(mutate).toHaveBeenCalledWith(
expect.objectContaining({
variables: {
input: {
attributes: {
defaultAccountList: 'account-list-1',
},
},
},
}),
);
});

it('swallows server-side mutation errors', async () => {
const accountListOptions = {
accountLists: {
nodes: [{ id: 'account-list-1' }],
},
};
query.mockResolvedValue({
data: accountListOptions,
});
mutate.mockRejectedValue(new Error('Failed'));

await expect(getServerSideProps(context)).resolves.toEqual({
props: {
accountListOptions,
session,
},
});
});

it('does not set an account list as the default when there are multiple', async () => {
const accountListOptions = {
accountLists: {
nodes: [{ id: 'account-list-1' }, { id: 'account-list-2' }],
},
};
query.mockResolvedValue({
data: accountListOptions,
});

await expect(getServerSideProps(context)).resolves.toEqual({
props: {
accountListOptions,
session,
},
});
expect(mutate).not.toHaveBeenCalled();
});
});
154 changes: 154 additions & 0 deletions pages/setup/account.page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import { GetServerSideProps } from 'next';
import Head from 'next/head';
import { useRouter } from 'next/router';
import React, { useState } from 'react';
import { Autocomplete, TextField } from '@mui/material';
import { getSession } from 'next-auth/react';
import { useTranslation } from 'react-i18next';
import {
UpdateUserDefaultAccountDocument,
UpdateUserDefaultAccountMutation,
UpdateUserDefaultAccountMutationVariables,
useUpdateUserDefaultAccountMutation,
} from 'src/components/Settings/preferences/accordions/DefaultAccountAccordion/UpdateDefaultAccount.generated';
import { SetupPage } from 'src/components/Setup/SetupPage';
import { LargeButton } from 'src/components/Setup/styledComponents';
import useGetAppSettings from 'src/hooks/useGetAppSettings';
import makeSsrClient from 'src/lib/apollo/ssrClient';
import {
AccountListOptionsDocument,
AccountListOptionsQuery,
} from './Account.generated';

type AccountList = AccountListOptionsQuery['accountLists']['nodes'][number];

interface PageProps {
accountListOptions: AccountListOptionsQuery;
}

// This is the third page page of the setup tour. It lets users choose their
// default account list. It will be shown if the user has more than one account
// list and don't have a default chosen yet.
const AccountPage: React.FC<PageProps> = ({ accountListOptions }) => {
const { t } = useTranslation();
const { appName } = useGetAppSettings();
const { push } = useRouter();
const [updateUserDefaultAccount, { loading: isSubmitting }] =
useUpdateUserDefaultAccountMutation();

const [defaultAccountList, setDefaultAccountList] =
useState<AccountList | null>(null);

const handleSave = async () => {
if (!defaultAccountList) {
return;
}

await updateUserDefaultAccount({
variables: {
input: {
attributes: {
defaultAccountList: defaultAccountList.id,
},
},
},
});
push(`/accountLists/${defaultAccountList.id}/settings/preferences`);
};

return (
<>
<Head>
<title>
{appName} | {t('Setup - Default Account')}
</title>
</Head>
<SetupPage title={t('Set default account')}>
{t(
'Which account would you like to see by default when you open {{appName}}?',
{ appName },
)}
<Autocomplete
autoHighlight
value={defaultAccountList}
onChange={(_, value) => setDefaultAccountList(value)}
options={accountListOptions.accountLists.nodes}
getOptionLabel={(accountList) => accountList.name ?? ''}
fullWidth
renderInput={(params) => (
<TextField {...params} label={t('Account')} />
)}
/>
<LargeButton
variant="contained"
fullWidth
onClick={handleSave}
disabled={!defaultAccountList || isSubmitting}
>
{t('Continue Tour')}
</LargeButton>
</SetupPage>
</>
);
};

export const getServerSideProps: GetServerSideProps<PageProps> = async (
context,
) => {
const session = await getSession(context);
const apiToken = session?.user?.apiToken;
if (!apiToken) {
return {
redirect: {
destination: '/login',
permanent: false,
},
};
}

const ssrClient = makeSsrClient(apiToken);
const { data: accountListOptions } =
await ssrClient.query<AccountListOptionsQuery>({
query: AccountListOptionsDocument,
});

if (accountListOptions.accountLists.nodes.length === 1) {
// The user has exactly one account list, so set it as the default and go to preferences
const defaultAccountListId = accountListOptions.accountLists.nodes[0].id;
try {
await ssrClient.mutate<
UpdateUserDefaultAccountMutation,
UpdateUserDefaultAccountMutationVariables
>({
mutation: UpdateUserDefaultAccountDocument,
variables: {
input: {
attributes: {
defaultAccountList: defaultAccountListId,
},
},
},
});
return {
redirect: {
destination: `/accountLists/${defaultAccountListId}/settings/preferences`,
permanent: false,
},
};
} catch {
// If setting the account list failed, silently swallow the error and let
// the user view the page. If the error is persistent, the mutation will
// fail there when they try to choose a default account list, and they
// will at least get an error message.
}
}

return {
props: {
session,
accountListOptions,
},
};
};

export default AccountPage;

0 comments on commit c09143f

Please sign in to comment.