-
Notifications
You must be signed in to change notification settings - Fork 465
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
refactor: Split up MyAccounts component #4671
Changes from 2 commits
ab87fbd
23e3e3f
7bc4284
e0ebec0
d571ab8
c3ef09f
524eb00
e3fd2da
43cee1d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 AccountsFilter = ({ 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 AccountsFilter |
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 |
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 |
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 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
import SafesList from '@/features/myAccounts/components/SafesList' | ||
import type { AllSafeItems } from '@/features/myAccounts/hooks/useAllSafesGrouped' | ||
import { useSafesSearch } from '@/features/myAccounts/hooks/useSafesSearch' | ||
import { maybePlural } from '@/utils/formatters' | ||
import { Box, Typography } from '@mui/material' | ||
|
||
const FilteredSafes = ({ | ||
searchQuery, | ||
allSafes, | ||
onLinkClick, | ||
}: { | ||
searchQuery: string | ||
allSafes: AllSafeItems | ||
onLinkClick?: () => void | ||
}) => { | ||
const filteredSafes = useSafesSearch(allSafes ?? [], searchQuery) | ||
|
||
return ( | ||
<> | ||
<Typography variant="h5" fontWeight="normal" mb={2} color="primary.light"> | ||
Found {filteredSafes.length} result{maybePlural(filteredSafes)} | ||
</Typography> | ||
<Box mt={1}> | ||
<SafesList safes={filteredSafes} onLinkClick={onLinkClick} useTransitions={false} /> | ||
</Box> | ||
</> | ||
) | ||
} | ||
|
||
export default FilteredSafes |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
import SafesList from '@/features/myAccounts/components/SafesList' | ||
import type { SafeItem } from '@/features/myAccounts/hooks/useAllSafes' | ||
import type { AllSafeItems, MultiChainSafeItem } from '@/features/myAccounts/hooks/useAllSafesGrouped' | ||
import css from '@/features/myAccounts/styles.module.css' | ||
import BookmarkIcon from '@/public/images/apps/bookmark.svg' | ||
import { Box, SvgIcon, Typography } from '@mui/material' | ||
import { useMemo } from 'react' | ||
|
||
const PinnedSafes = ({ allSafes, onLinkClick }: { allSafes: AllSafeItems; onLinkClick?: () => void }) => { | ||
const pinnedSafes = useMemo<(MultiChainSafeItem | SafeItem)[]>( | ||
() => [...(allSafes?.filter(({ isPinned }) => isPinned) ?? [])], | ||
[allSafes], | ||
) | ||
|
||
return ( | ||
<Box data-testid="pinned-accounts" mb={2} minHeight="170px"> | ||
<div className={css.listHeader}> | ||
<SvgIcon component={BookmarkIcon} inheritViewBox fontSize="small" sx={{ mt: '2px', mr: 1, strokeWidth: 2 }} /> | ||
<Typography variant="h5" fontWeight={700} mb={2}> | ||
Pinned | ||
</Typography> | ||
</div> | ||
{pinnedSafes.length > 0 ? ( | ||
<SafesList safes={pinnedSafes} onLinkClick={onLinkClick} /> | ||
) : ( | ||
<Box data-testid="empty-pinned-list" className={css.noPinnedSafesMessage}> | ||
<Typography color="text.secondary" variant="body2" maxWidth="350px" textAlign="center"> | ||
Personalize your account list by clicking the | ||
<SvgIcon | ||
component={BookmarkIcon} | ||
inheritViewBox | ||
fontSize="small" | ||
sx={{ mx: '4px', color: 'text.secondary', position: 'relative', top: '2px' }} | ||
/> | ||
icon on the accounts most important to you. | ||
</Typography> | ||
</Box> | ||
)} | ||
</Box> | ||
) | ||
} | ||
|
||
export default PinnedSafes |
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @jmealy I remember we talked about how we handle pinned and watchlist safes. In this hook we say watchlist safes are safes that are not pinned and read-only. I think this doesn't work anymore since an unpinned safe that is not owned or undeployed is removed from the list so watchlist === pinned. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Makes sense! It was a good idea to equate pinned and watchlist/added, having them as two separate concepts was confusing |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,19 +4,15 @@ import { useRouter } from 'next/router' | |
import { useEffect, useMemo } from 'react' | ||
import type { ConnectedWallet } from '@/hooks/wallets/useOnboard' | ||
import { type SafeItem } from './useAllSafes' | ||
import type { AllSafesGrouped } from './useAllSafesGrouped' | ||
import type { AllSafeItemsGrouped } from './useAllSafesGrouped' | ||
import { type MultiChainSafeItem } from './useAllSafesGrouped' | ||
import { isMultiChainSafeItem } from '@/features/multichain/utils/utils' | ||
|
||
let isOwnedSafesTracked = false | ||
let isPinnedSafesTracked = false | ||
let isWatchlistTracked = false | ||
|
||
const useTrackSafesCount = ( | ||
safes: AllSafesGrouped, | ||
pinnedSafes: (MultiChainSafeItem | SafeItem)[], | ||
wallet: ConnectedWallet | null, | ||
) => { | ||
const useTrackSafesCount = (safes: AllSafeItemsGrouped, wallet: ConnectedWallet | null) => { | ||
const router = useRouter() | ||
const isLoginPage = router.pathname === AppRoutes.welcome.accounts | ||
|
||
|
@@ -46,6 +42,15 @@ const useTrackSafesCount = ( | |
[safes, watchlistMultiChainSafes], | ||
) | ||
|
||
// TODO: This is computed here and inside PinnedSafes now. Find a way to optimize it | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A small improvement could be to extract the calculations of allSafes and pinnedSafes into helper functions since they are both done twice, but nbd. IMO it's ok as is |
||
const pinnedSafes = useMemo<(MultiChainSafeItem | SafeItem)[]>( | ||
() => [ | ||
...(safes.allSingleSafes?.filter(({ isPinned }) => isPinned) ?? []), | ||
...(safes.allMultiChainSafes?.filter(({ isPinned }) => isPinned) ?? []), | ||
], | ||
[safes], | ||
) | ||
|
||
// Reset tracking for new wallet | ||
useEffect(() => { | ||
isOwnedSafesTracked = false | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Took me a minute to find the OrderByButton component here. To me
AccountsFilter
, suggests it only contains the search. Could we rename to reflect that it has both search and sort?AccountListFilters
or something