-
Notifications
You must be signed in to change notification settings - Fork 464
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
refactor: Split up MyAccounts component (#4671)
* refactor: Split up MyAccounts component * refactor: Move allSafes computation and sort comparator into AccountsList * fix: Add tests for AccountsList * fix: Add tests for PinnedSafes * fix: Add tests for FilteredSafes, adjust maybePlural to account for 0 value * fix: Failing maybePlural test * fix: Change QueueActions to a button instead of an a element * fix: Remove watchlist tracking and use pinned safes tracking instead * refactor: Rename AccountsFilter to AccountListFilters
- Loading branch information
1 parent
ce51771
commit 011e808
Showing
18 changed files
with
655 additions
and
323 deletions.
There are no files selected for viewing
60 changes: 60 additions & 0 deletions
60
src/features/myAccounts/components/AccountListFilters/index.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
import { useAppDispatch, useAppSelector } from '@/store' | ||
import { type OrderByOption, selectOrderByPreference, setOrderByPreference } from '@/store/orderByPreferenceSlice' | ||
import debounce from 'lodash/debounce' | ||
import { type Dispatch, type SetStateAction, useCallback } from 'react' | ||
import OrderByButton from '@/features/myAccounts/components/OrderByButton' | ||
import css from '@/features/myAccounts/styles.module.css' | ||
import SearchIcon from '@/public/images/common/search.svg' | ||
import { Box, InputAdornment, Paper, SvgIcon, TextField } from '@mui/material' | ||
|
||
const AccountListFilters = ({ setSearchQuery }: { setSearchQuery: Dispatch<SetStateAction<string>> }) => { | ||
const dispatch = useAppDispatch() | ||
const { orderBy } = useAppSelector(selectOrderByPreference) | ||
|
||
// eslint-disable-next-line react-hooks/exhaustive-deps | ||
const handleSearch = useCallback(debounce(setSearchQuery, 300), []) | ||
|
||
const handleOrderByChange = (orderBy: OrderByOption) => { | ||
dispatch(setOrderByPreference({ orderBy })) | ||
} | ||
|
||
return ( | ||
<Paper sx={{ px: 2, py: 1 }}> | ||
<Box display="flex" justifyContent="space-between" width="100%" gap={1}> | ||
<TextField | ||
id="search-by-name" | ||
placeholder="Search" | ||
aria-label="Search Safe list by name" | ||
variant="filled" | ||
hiddenLabel | ||
onChange={(e) => { | ||
handleSearch(e.target.value) | ||
}} | ||
className={css.search} | ||
InputProps={{ | ||
startAdornment: ( | ||
<InputAdornment position="start"> | ||
<SvgIcon | ||
component={SearchIcon} | ||
inheritViewBox | ||
fontWeight="bold" | ||
fontSize="small" | ||
sx={{ | ||
color: 'var(--color-border-main)', | ||
'.MuiInputBase-root.Mui-focused &': { color: 'var(--color-text-primary)' }, | ||
}} | ||
/> | ||
</InputAdornment> | ||
), | ||
disableUnderline: true, | ||
}} | ||
fullWidth | ||
size="small" | ||
/> | ||
<OrderByButton orderBy={orderBy} onOrderByChange={handleOrderByChange} /> | ||
</Box> | ||
</Paper> | ||
) | ||
} | ||
|
||
export default AccountListFilters |
55 changes: 55 additions & 0 deletions
55
src/features/myAccounts/components/AccountsHeader/index.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
import ConnectWalletButton from '@/components/common/ConnectWallet/ConnectWalletButton' | ||
import Track from '@/components/common/Track' | ||
import { AppRoutes } from '@/config/routes' | ||
import CreateButton from '@/features/myAccounts/components/CreateButton' | ||
import css from '@/features/myAccounts/styles.module.css' | ||
import useWallet from '@/hooks/wallets/useWallet' | ||
import AddIcon from '@/public/images/common/add.svg' | ||
import { OVERVIEW_EVENTS, OVERVIEW_LABELS } from '@/services/analytics' | ||
import { Box, Button, Link, SvgIcon, Typography } from '@mui/material' | ||
import classNames from 'classnames' | ||
import { useRouter } from 'next/router' | ||
|
||
const AccountsHeader = ({ isSidebar, onLinkClick }: { isSidebar: boolean; onLinkClick?: () => void }) => { | ||
const router = useRouter() | ||
const wallet = useWallet() | ||
|
||
const isLoginPage = router.pathname === AppRoutes.welcome.accounts | ||
const trackingLabel = isLoginPage ? OVERVIEW_LABELS.login_page : OVERVIEW_LABELS.sidebar | ||
|
||
return ( | ||
<Box className={classNames(css.header, { [css.sidebarHeader]: isSidebar })}> | ||
<Typography variant="h1" fontWeight={700} className={css.title}> | ||
My accounts | ||
</Typography> | ||
<Box className={css.headerButtons}> | ||
<Track {...OVERVIEW_EVENTS.ADD_TO_WATCHLIST} label={trackingLabel}> | ||
<Link href={AppRoutes.newSafe.load}> | ||
<Button | ||
disableElevation | ||
variant="outlined" | ||
size="small" | ||
onClick={onLinkClick} | ||
startIcon={<SvgIcon component={AddIcon} inheritViewBox fontSize="small" />} | ||
sx={{ height: '36px', width: '100%', px: 2 }} | ||
> | ||
Add | ||
</Button> | ||
</Link> | ||
</Track> | ||
|
||
{wallet ? ( | ||
<Track {...OVERVIEW_EVENTS.CREATE_NEW_SAFE} label={trackingLabel}> | ||
<CreateButton isPrimary /> | ||
</Track> | ||
) : ( | ||
<Box sx={{ '& button': { height: '36px' } }}> | ||
<ConnectWalletButton small={true} /> | ||
</Box> | ||
)} | ||
</Box> | ||
</Box> | ||
) | ||
} | ||
|
||
export default AccountsHeader |
113 changes: 113 additions & 0 deletions
113
src/features/myAccounts/components/AccountsList/index.test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,113 @@ | ||
import { OrderByOption } from '@/store/orderByPreferenceSlice' | ||
import { safeItemBuilder } from '@/tests/builders/safeItem' | ||
import { render } from '@/tests/test-utils' | ||
import React from 'react' | ||
import { screen } from '@testing-library/react' | ||
import AccountsList from './index' | ||
import FilteredSafes from '@/features/myAccounts/components/FilteredSafes' | ||
import PinnedSafes from '@/features/myAccounts/components/PinnedSafes' | ||
import AllSafes from '@/features/myAccounts/components/AllSafes' | ||
import type { AllSafeItemsGrouped } from '@/features/myAccounts/hooks/useAllSafesGrouped' | ||
|
||
// Mock child components to simplify tests, we just need to verify their rendering and props. | ||
jest.mock('@/features/myAccounts/components/FilteredSafes', () => jest.fn(() => <div>FilteredSafes Component</div>)) | ||
jest.mock('@/features/myAccounts/components/PinnedSafes', () => jest.fn(() => <div>PinnedSafes Component</div>)) | ||
jest.mock('@/features/myAccounts/components/AllSafes', () => jest.fn(() => <div>AllSafes Component</div>)) | ||
|
||
describe('AccountsList', () => { | ||
const baseSafes: AllSafeItemsGrouped = { | ||
allMultiChainSafes: [ | ||
{ name: 'MultiChainSafe1', address: '0xA', isPinned: false, lastVisited: 0, safes: [safeItemBuilder().build()] }, | ||
{ name: 'MultiChainSafe2', address: '0xB', isPinned: false, lastVisited: 1, safes: [safeItemBuilder().build()] }, | ||
], | ||
allSingleSafes: [ | ||
{ name: 'SingleSafe1', address: '0xC', isPinned: true, chainId: '3', isReadOnly: false, lastVisited: 2 }, | ||
], | ||
} | ||
|
||
afterEach(() => { | ||
jest.clearAllMocks() | ||
}) | ||
|
||
it('renders FilteredSafes when searchQuery is not empty', () => { | ||
render(<AccountsList searchQuery="Multi" safes={baseSafes} isSidebar={true} />, { | ||
initialReduxState: { orderByPreference: { orderBy: OrderByOption.NAME } }, | ||
}) | ||
|
||
expect(screen.getByText('FilteredSafes Component')).toBeInTheDocument() | ||
expect(screen.queryByText('PinnedSafes Component')).not.toBeInTheDocument() | ||
expect(screen.queryByText('AllSafes Component')).not.toBeInTheDocument() | ||
|
||
// Check that FilteredSafes is called with the correct props | ||
const filteredSafesMock = (FilteredSafes as jest.Mock).mock.calls[0][0] | ||
expect(filteredSafesMock.searchQuery).toBe('Multi') | ||
expect(filteredSafesMock.onLinkClick).toBeUndefined() | ||
|
||
// The combined allSafes array sorted by name | ||
const expectedSortedSafes = [ | ||
{ name: 'MultiChainSafe1', address: '0xA', isPinned: false, lastVisited: 0, safes: expect.anything() }, | ||
{ name: 'MultiChainSafe2', address: '0xB', isPinned: false, lastVisited: 1, safes: expect.anything() }, | ||
{ name: 'SingleSafe1', address: '0xC', isPinned: true, chainId: '3', isReadOnly: false, lastVisited: 2 }, | ||
] | ||
expect(filteredSafesMock.allSafes).toEqual(expectedSortedSafes) | ||
}) | ||
|
||
it('renders PinnedSafes and AllSafes when searchQuery is empty', () => { | ||
render(<AccountsList searchQuery="" safes={baseSafes} isSidebar={false} />, { | ||
initialReduxState: { orderByPreference: { orderBy: OrderByOption.NAME } }, | ||
}) | ||
|
||
expect(screen.queryByText('FilteredSafes Component')).not.toBeInTheDocument() | ||
expect(screen.getByText('PinnedSafes Component')).toBeInTheDocument() | ||
expect(screen.getByText('AllSafes Component')).toBeInTheDocument() | ||
|
||
// Check that PinnedSafes and AllSafes received the correct props | ||
const pinnedSafesMock = (PinnedSafes as jest.Mock).mock.calls[0][0] | ||
const allSafesMock = (AllSafes as jest.Mock).mock.calls[0][0] | ||
|
||
// Sorted array as in the previous test | ||
const expectedSortedSafes = [ | ||
{ name: 'MultiChainSafe1', address: '0xA', isPinned: false, lastVisited: 0, safes: expect.anything() }, | ||
{ name: 'MultiChainSafe2', address: '0xB', isPinned: false, lastVisited: 1, safes: expect.anything() }, | ||
{ name: 'SingleSafe1', address: '0xC', isPinned: true, chainId: '3', isReadOnly: false, lastVisited: 2 }, | ||
] | ||
|
||
expect(pinnedSafesMock.allSafes).toEqual(expectedSortedSafes) | ||
expect(allSafesMock.allSafes).toEqual(expectedSortedSafes) | ||
expect(allSafesMock.isSidebar).toBe(false) | ||
}) | ||
|
||
it('sorts by lastVisited', () => { | ||
render(<AccountsList searchQuery="" safes={baseSafes} isSidebar={false} />, { | ||
initialReduxState: { orderByPreference: { orderBy: OrderByOption.LAST_VISITED } }, | ||
}) | ||
|
||
expect(screen.queryByText('FilteredSafes Component')).not.toBeInTheDocument() | ||
expect(screen.getByText('PinnedSafes Component')).toBeInTheDocument() | ||
expect(screen.getByText('AllSafes Component')).toBeInTheDocument() | ||
|
||
// Check that PinnedSafes and AllSafes received the correct props | ||
const pinnedSafesMock = (PinnedSafes as jest.Mock).mock.calls[0][0] | ||
const allSafesMock = (AllSafes as jest.Mock).mock.calls[0][0] | ||
|
||
const expectedSortedSafes = [ | ||
{ name: 'SingleSafe1', address: '0xC', isPinned: true, chainId: '3', isReadOnly: false, lastVisited: 2 }, | ||
{ name: 'MultiChainSafe2', address: '0xB', isPinned: false, lastVisited: 1, safes: expect.anything() }, | ||
{ name: 'MultiChainSafe1', address: '0xA', isPinned: false, lastVisited: 0, safes: expect.anything() }, | ||
] | ||
|
||
expect(pinnedSafesMock.allSafes).toEqual(expectedSortedSafes) | ||
expect(allSafesMock.allSafes).toEqual(expectedSortedSafes) | ||
}) | ||
|
||
it('passes onLinkClick prop down to children', () => { | ||
const onLinkClickFn = jest.fn() | ||
|
||
render(<AccountsList searchQuery="" safes={baseSafes} onLinkClick={onLinkClickFn} isSidebar={true} />) | ||
|
||
const pinnedSafesMock = (PinnedSafes as jest.Mock).mock.calls[0][0] | ||
const allSafesMock = (AllSafes as jest.Mock).mock.calls[0][0] | ||
expect(pinnedSafesMock.onLinkClick).toBe(onLinkClickFn) | ||
expect(allSafesMock.onLinkClick).toBe(onLinkClickFn) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
import FilteredSafes from '@/features/myAccounts/components/FilteredSafes' | ||
import PinnedSafes from '@/features/myAccounts/components/PinnedSafes' | ||
import type { AllSafeItems, AllSafeItemsGrouped } from '@/features/myAccounts/hooks/useAllSafesGrouped' | ||
import AllSafes from '@/features/myAccounts/components/AllSafes' | ||
import { getComparator } from '@/features/myAccounts/utils/utils' | ||
import { useAppSelector } from '@/store' | ||
import { selectOrderByPreference } from '@/store/orderByPreferenceSlice' | ||
import { useMemo } from 'react' | ||
|
||
const AccountsList = ({ | ||
searchQuery, | ||
safes, | ||
onLinkClick, | ||
isSidebar, | ||
}: { | ||
searchQuery: string | ||
safes: AllSafeItemsGrouped | ||
onLinkClick?: () => void | ||
isSidebar: boolean | ||
}) => { | ||
const { orderBy } = useAppSelector(selectOrderByPreference) | ||
const sortComparator = getComparator(orderBy) | ||
|
||
const allSafes = useMemo<AllSafeItems>( | ||
() => [...(safes.allMultiChainSafes ?? []), ...(safes.allSingleSafes ?? [])].sort(sortComparator), | ||
[safes.allMultiChainSafes, safes.allSingleSafes, sortComparator], | ||
) | ||
|
||
if (searchQuery) { | ||
return <FilteredSafes searchQuery={searchQuery} allSafes={allSafes} onLinkClick={onLinkClick} /> | ||
} | ||
|
||
return ( | ||
<> | ||
<PinnedSafes allSafes={allSafes} onLinkClick={onLinkClick} /> | ||
<AllSafes allSafes={allSafes} onLinkClick={onLinkClick} isSidebar={isSidebar} /> | ||
</> | ||
) | ||
} | ||
|
||
export default AccountsList |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
import ConnectWalletButton from '@/components/common/ConnectWallet/ConnectWalletButton' | ||
import Track from '@/components/common/Track' | ||
import { AppRoutes } from '@/config/routes' | ||
import SafesList from '@/features/myAccounts/components/SafesList' | ||
import type { AllSafeItems } from '@/features/myAccounts/hooks/useAllSafesGrouped' | ||
import css from '@/features/myAccounts/styles.module.css' | ||
import useWallet from '@/hooks/wallets/useWallet' | ||
import { OVERVIEW_EVENTS, OVERVIEW_LABELS } from '@/services/analytics' | ||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore' | ||
import { Accordion, AccordionDetails, AccordionSummary, Box, Typography } from '@mui/material' | ||
import { useRouter } from 'next/router' | ||
|
||
const AllSafes = ({ | ||
allSafes, | ||
onLinkClick, | ||
isSidebar, | ||
}: { | ||
allSafes: AllSafeItems | ||
onLinkClick?: () => void | ||
isSidebar: boolean | ||
}) => { | ||
const wallet = useWallet() | ||
const router = useRouter() | ||
|
||
const isLoginPage = router.pathname === AppRoutes.welcome.accounts | ||
const trackingLabel = isLoginPage ? OVERVIEW_LABELS.login_page : OVERVIEW_LABELS.sidebar | ||
|
||
return ( | ||
<Accordion sx={{ border: 'none' }} defaultExpanded={!isSidebar} slotProps={{ transition: { unmountOnExit: true } }}> | ||
<AccordionSummary | ||
data-testid="expand-safes-list" | ||
expandIcon={<ExpandMoreIcon sx={{ '& path': { fill: 'var(--color-text-secondary)' } }} />} | ||
sx={{ | ||
padding: 0, | ||
'& .MuiAccordionSummary-content': { margin: '0 !important', mb: 1, flexGrow: 0 }, | ||
}} | ||
> | ||
<div className={css.listHeader}> | ||
<Typography variant="h5" fontWeight={700}> | ||
Accounts | ||
{allSafes && allSafes.length > 0 && ( | ||
<Typography component="span" color="text.secondary" fontSize="inherit" fontWeight="normal" mr={1}> | ||
{' '} | ||
({allSafes.length}) | ||
</Typography> | ||
)} | ||
</Typography> | ||
</div> | ||
</AccordionSummary> | ||
<AccordionDetails data-testid="accounts-list" sx={{ padding: 0 }}> | ||
{allSafes.length > 0 ? ( | ||
<Box mt={1}> | ||
<SafesList safes={allSafes} onLinkClick={onLinkClick} /> | ||
</Box> | ||
) : ( | ||
<Typography | ||
data-testid="empty-account-list" | ||
component="div" | ||
variant="body2" | ||
color="text.secondary" | ||
textAlign="center" | ||
py={3} | ||
mx="auto" | ||
width={250} | ||
> | ||
{!wallet ? ( | ||
<> | ||
<Box mb={2}>Connect a wallet to view your Safe Accounts or to create a new one</Box> | ||
<Track {...OVERVIEW_EVENTS.OPEN_ONBOARD} label={trackingLabel}> | ||
<ConnectWalletButton text="Connect a wallet" contained /> | ||
</Track> | ||
</> | ||
) : ( | ||
"You don't have any safes yet" | ||
)} | ||
</Typography> | ||
)} | ||
</AccordionDetails> | ||
</Accordion> | ||
) | ||
} | ||
|
||
export default AllSafes |
Oops, something went wrong.