Skip to content
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

Merged
merged 9 commits into from
Dec 19, 2024
60 changes: 60 additions & 0 deletions src/features/myAccounts/components/AccountsFilter/index.tsx
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} />
Copy link
Contributor

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

</Box>
</Paper>
)
}

export default AccountsFilter
55 changes: 55 additions & 0 deletions src/features/myAccounts/components/AccountsHeader/index.tsx
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
41 changes: 41 additions & 0 deletions src/features/myAccounts/components/AccountsList/index.tsx
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
83 changes: 83 additions & 0 deletions src/features/myAccounts/components/AllSafes/index.tsx
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
30 changes: 30 additions & 0 deletions src/features/myAccounts/components/FilteredSafes/index.tsx
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
43 changes: 43 additions & 0 deletions src/features/myAccounts/components/PinnedSafes/index.tsx
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
6 changes: 4 additions & 2 deletions src/features/myAccounts/hooks/useAllSafesGrouped.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,13 @@ export type MultiChainSafeItem = {
name: string | undefined
}

export type AllSafesGrouped = {
export type AllSafeItemsGrouped = {
allSingleSafes: SafeItems | undefined
allMultiChainSafes: MultiChainSafeItem[] | undefined
}

export type AllSafeItems = Array<SafeItem | MultiChainSafeItem>

export const _buildMultiChainSafeItem = (address: string, safes: SafeItems): MultiChainSafeItem => {
const isPinned = safes.some((safe) => safe.isPinned)
const lastVisited = safes.reduce((acc, safe) => Math.max(acc, safe.lastVisited || 0), 0)
Expand Down Expand Up @@ -43,7 +45,7 @@ export const _getSingleChainAccounts = (safes: SafeItems, allMultiChainSafes: Mu
export const useAllSafesGrouped = () => {
const allSafes = useAllSafes()

return useMemo<AllSafesGrouped>(() => {
return useMemo<AllSafeItemsGrouped>(() => {
if (!allSafes) {
return { allMultiChainSafes: undefined, allSingleSafes: undefined }
}
Expand Down
17 changes: 11 additions & 6 deletions src/features/myAccounts/hooks/useTrackedSafesCount.ts
Copy link
Member Author

Choose a reason for hiding this comment

The 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.

Copy link
Contributor

Choose a reason for hiding this comment

The 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
Expand Up @@ -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

Expand Down Expand Up @@ -46,6 +42,15 @@ const useTrackSafesCount = (
[safes, watchlistMultiChainSafes],
)

// TODO: This is computed here and inside PinnedSafes now. Find a way to optimize it
Copy link
Contributor

Choose a reason for hiding this comment

The 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
Expand Down
Loading
Loading