(Fix) Transctions infinite scroll (#1931)
* install `react-intersection-observer` dependency - also, remove `react-infinite-scroll-component` * refactor `InfiniteScroll` to be used with `react-intersection-observer` * build an infinite scroll wrapper for transactions based on `InfiniteScroll` * recover `TxsInfiniteScrollContext` information to identify the last item in a list - a new component was created for History transactions: `HistoryTransactions` as a wrapper * refactor lists to use `TxsInfiniteScrollContext` and identify the last item in the list * allow to pass config to the InfiniteScroll component - also changed default bottom margin so the txs loading starts a bit earlier * fix memory consumption issue based on nft retrieval/update data * delay `lastItemId` set to next tick, to prevent multiple updates during the same render phase * Set triggerOnce to infinitescroll Co-authored-by: nicosampler <nf.dominguez.87@gmail.com> Co-authored-by: nicolas <nicosampler@users.noreply.github.com> Co-authored-by: Daniel Sanchez <daniel.sanchez@gnosis.pm>
This commit is contained in:
parent
16c347e97e
commit
ae8175aae2
|
@ -215,7 +215,7 @@
|
||||||
"react-final-form-listeners": "^1.0.2",
|
"react-final-form-listeners": "^1.0.2",
|
||||||
"react-ga": "3.3.0",
|
"react-ga": "3.3.0",
|
||||||
"react-hot-loader": "4.13.0",
|
"react-hot-loader": "4.13.0",
|
||||||
"react-infinite-scroll-component": "^5.1.0",
|
"react-intersection-observer": "^8.31.0",
|
||||||
"react-qr-reader": "^2.2.1",
|
"react-qr-reader": "^2.2.1",
|
||||||
"react-redux": "7.2.2",
|
"react-redux": "7.2.2",
|
||||||
"react-router-dom": "5.2.0",
|
"react-router-dom": "5.2.0",
|
||||||
|
|
|
@ -1,39 +1,53 @@
|
||||||
import { Loader } from '@gnosis.pm/safe-react-components'
|
import React, { createContext, forwardRef, MutableRefObject, ReactElement, ReactNode, useEffect, useState } from 'react'
|
||||||
import React, { ReactElement } from 'react'
|
import { InViewHookResponse, useInView } from 'react-intersection-observer'
|
||||||
import { default as ReactInfiniteScroll, Props as ReactInfiniteScrollProps } from 'react-infinite-scroll-component'
|
|
||||||
import styled from 'styled-components'
|
|
||||||
|
|
||||||
import { Overwrite } from 'src/types/helpers'
|
export const INFINITE_SCROLL_CONTAINER = 'infinite-scroll-container'
|
||||||
|
|
||||||
export const Centered = styled.div<{ padding?: number }>`
|
export const InfiniteScrollContext = createContext<{
|
||||||
width: 100%;
|
ref: MutableRefObject<HTMLDivElement | null> | ((instance: HTMLDivElement | null) => void) | null
|
||||||
height: 100%;
|
lastItemId?: string
|
||||||
display: flex;
|
setLastItemId: (itemId?: string) => void
|
||||||
padding: ${({ padding }) => `${padding}px`};
|
}>({ setLastItemId: () => {}, ref: null })
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
`
|
|
||||||
|
|
||||||
export const SCROLLABLE_TARGET_ID = 'scrollableDiv'
|
export const InfiniteScrollProvider = forwardRef<HTMLDivElement, { children: ReactNode }>(
|
||||||
|
({ children }, ref): ReactElement => {
|
||||||
|
const [lastItemId, _setLastItemId] = useState<string>()
|
||||||
|
|
||||||
type InfiniteScrollProps = Overwrite<ReactInfiniteScrollProps, { loader?: ReactInfiniteScrollProps['loader'] }>
|
const setLastItemId = (itemId?: string) => {
|
||||||
|
setTimeout(() => _setLastItemId(itemId), 0)
|
||||||
|
}
|
||||||
|
|
||||||
export const InfiniteScroll = ({ dataLength, next, hasMore, ...props }: InfiniteScrollProps): ReactElement => {
|
return (
|
||||||
return (
|
<InfiniteScrollContext.Provider value={{ ref, lastItemId, setLastItemId }}>
|
||||||
<ReactInfiniteScroll
|
{children}
|
||||||
style={{ overflow: 'hidden' }}
|
</InfiniteScrollContext.Provider>
|
||||||
dataLength={dataLength}
|
)
|
||||||
next={next}
|
},
|
||||||
hasMore={hasMore}
|
)
|
||||||
loader={
|
|
||||||
<Centered>
|
InfiniteScrollProvider.displayName = 'InfiniteScrollProvider'
|
||||||
<Loader size="md" />
|
|
||||||
</Centered>
|
type InfiniteScrollProps = {
|
||||||
}
|
children: ReactNode
|
||||||
scrollThreshold="120px"
|
hasMore: boolean
|
||||||
scrollableTarget={SCROLLABLE_TARGET_ID}
|
next: () => Promise<void>
|
||||||
>
|
config?: InViewHookResponse
|
||||||
{props.children}
|
}
|
||||||
</ReactInfiniteScroll>
|
|
||||||
)
|
export const InfiniteScroll = ({ children, hasMore, next, config }: InfiniteScrollProps): ReactElement => {
|
||||||
|
const { ref, inView } = useInView({
|
||||||
|
threshold: 0,
|
||||||
|
root: document.querySelector(`#${INFINITE_SCROLL_CONTAINER}`),
|
||||||
|
rootMargin: '0px 0px 200px 0px',
|
||||||
|
triggerOnce: true,
|
||||||
|
...config,
|
||||||
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (inView && hasMore) {
|
||||||
|
next()
|
||||||
|
}
|
||||||
|
}, [inView, hasMore, next])
|
||||||
|
|
||||||
|
return <InfiniteScrollProvider ref={ref}>{children}</InfiniteScrollProvider>
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import { RateLimit } from 'async-sema'
|
|
||||||
|
|
||||||
import { Collectibles, NFTAsset, NFTAssets, NFTTokens } from 'src/logic/collectibles/sources/collectibles.d'
|
import { Collectibles, NFTAsset, NFTAssets, NFTTokens } from 'src/logic/collectibles/sources/collectibles.d'
|
||||||
|
import { sameAddress } from 'src/logic/wallets/ethAddresses'
|
||||||
import NFTIcon from 'src/routes/safe/components/Balances/assets/nft_icon.png'
|
import NFTIcon from 'src/routes/safe/components/Balances/assets/nft_icon.png'
|
||||||
import { fetchErc20AndErc721AssetsList, fetchSafeCollectibles } from 'src/logic/tokens/api'
|
import { fetchErc20AndErc721AssetsList, fetchSafeCollectibles } from 'src/logic/tokens/api'
|
||||||
import { TokenResult } from 'src/logic/tokens/api/fetchErc20AndErc721AssetsList'
|
import { TokenResult } from 'src/logic/tokens/api/fetchErc20AndErc721AssetsList'
|
||||||
import { CollectibleResult } from 'src/logic/tokens/api/fetchSafeCollectibles'
|
import { CollectibleResult } from 'src/logic/tokens/api/fetchSafeCollectibles'
|
||||||
|
import { sameString } from 'src/utils/strings'
|
||||||
|
|
||||||
type FetchResult = {
|
type FetchResult = {
|
||||||
erc721Assets: TokenResult[]
|
erc721Assets: TokenResult[]
|
||||||
|
@ -12,67 +12,70 @@ type FetchResult = {
|
||||||
}
|
}
|
||||||
|
|
||||||
class Gnosis {
|
class Gnosis {
|
||||||
_rateLimit = async (): Promise<void> => {}
|
|
||||||
|
|
||||||
_fetch = async (safeAddress: string): Promise<FetchResult> => {
|
_fetch = async (safeAddress: string): Promise<FetchResult> => {
|
||||||
const collectibles: FetchResult = {
|
const collectibles: FetchResult = {
|
||||||
erc721Assets: [],
|
erc721Assets: [],
|
||||||
erc721Tokens: [],
|
erc721Tokens: [],
|
||||||
}
|
}
|
||||||
|
const [assets, tokens] = await Promise.allSettled([
|
||||||
|
fetchErc20AndErc721AssetsList(),
|
||||||
|
fetchSafeCollectibles(safeAddress),
|
||||||
|
])
|
||||||
|
|
||||||
try {
|
switch (assets.status) {
|
||||||
const {
|
case 'fulfilled':
|
||||||
data: { results: assets = [] },
|
const {
|
||||||
} = await fetchErc20AndErc721AssetsList()
|
data: { results = [] },
|
||||||
collectibles.erc721Assets = assets.filter((token) => token.type.toLowerCase() === 'erc721')
|
} = assets.value
|
||||||
} catch (e) {
|
collectibles.erc721Assets = results.filter((token) => sameString(token.type, 'erc721'))
|
||||||
console.error('no erc721 assets could be fetched', e)
|
break
|
||||||
|
case 'rejected':
|
||||||
|
console.error('no erc721 assets could be fetched', assets.reason)
|
||||||
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
switch (tokens.status) {
|
||||||
const { data: tokens = [] } = await fetchSafeCollectibles(safeAddress)
|
case 'fulfilled':
|
||||||
collectibles.erc721Tokens = tokens
|
const {
|
||||||
} catch (e) {
|
data: { results = [] },
|
||||||
console.error('no erc721 tokens for the current safe', e)
|
} = tokens.value
|
||||||
|
collectibles.erc721Tokens = results
|
||||||
|
break
|
||||||
|
case 'rejected':
|
||||||
|
console.error('no erc721 tokens for the current safe', tokens.reason)
|
||||||
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
return collectibles
|
return collectibles
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
static extractNFTAsset = (asset: TokenResult, nftTokens: NFTTokens): NFTAsset => {
|
||||||
* OpenSea class constructor
|
const mainAssetAddress = asset.address
|
||||||
* @param {object} options
|
const numberOfTokens = nftTokens.filter(({ assetAddress }) => sameAddress(assetAddress, mainAssetAddress)).length
|
||||||
* @param {number} options.rps - requests per second
|
|
||||||
*/
|
return {
|
||||||
constructor(options: { rps: number }) {
|
address: mainAssetAddress,
|
||||||
// eslint-disable-next-line no-underscore-dangle
|
description: asset.name,
|
||||||
this._rateLimit = RateLimit(options.rps, { timeUnit: 60 * 1000, uniformDistribution: true })
|
image: asset.logoUri || NFTIcon,
|
||||||
|
name: asset.name,
|
||||||
|
numberOfTokens,
|
||||||
|
slug: `${mainAssetAddress}_${asset.name}`,
|
||||||
|
symbol: asset.symbol,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static extractAssets(assets: TokenResult[], nftTokens: NFTTokens): NFTAssets {
|
static extractAssets(assets: TokenResult[], nftTokens: NFTTokens): NFTAssets {
|
||||||
const extractNFTAsset = (asset: TokenResult): NFTAsset => {
|
const extractedAssets = {}
|
||||||
const numberOfTokens = nftTokens.filter(({ assetAddress }) => assetAddress === asset.address).length
|
|
||||||
|
|
||||||
return {
|
assets.forEach((asset) => {
|
||||||
address: asset.address,
|
|
||||||
description: asset.name,
|
|
||||||
image: asset.logoUri || NFTIcon,
|
|
||||||
name: asset.name,
|
|
||||||
numberOfTokens,
|
|
||||||
slug: `${asset.address}_${asset.name}`,
|
|
||||||
symbol: asset.symbol,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return assets.reduce((acc, asset) => {
|
|
||||||
const address = asset.address
|
const address = asset.address
|
||||||
|
|
||||||
if (acc[address] === undefined) {
|
if (extractedAssets[address] === undefined) {
|
||||||
acc[address] = extractNFTAsset(asset)
|
extractedAssets[address] = Gnosis.extractNFTAsset(asset, nftTokens)
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
|
||||||
return acc
|
return extractedAssets
|
||||||
}, {})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static extractTokens(tokens: CollectibleResult[]): NFTTokens {
|
static extractTokens(tokens: CollectibleResult[]): NFTTokens {
|
||||||
|
@ -94,12 +97,11 @@ class Gnosis {
|
||||||
*/
|
*/
|
||||||
async fetchCollectibles(safeAddress: string): Promise<Collectibles> {
|
async fetchCollectibles(safeAddress: string): Promise<Collectibles> {
|
||||||
const { erc721Assets, erc721Tokens } = await this._fetch(safeAddress)
|
const { erc721Assets, erc721Tokens } = await this._fetch(safeAddress)
|
||||||
const nftTokens = Gnosis.extractTokens(erc721Tokens)
|
|
||||||
|
|
||||||
return {
|
const nftTokens = Gnosis.extractTokens(erc721Tokens)
|
||||||
nftTokens,
|
const nftAssets = Gnosis.extractAssets(erc721Assets, nftTokens)
|
||||||
nftAssets: Gnosis.extractAssets(erc721Assets, nftTokens),
|
|
||||||
}
|
return { nftTokens, nftAssets }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { COLLECTIBLES_SOURCE } from 'src/utils/constants'
|
||||||
|
|
||||||
const SOURCES = {
|
const SOURCES = {
|
||||||
opensea: new OpenSea({ rps: 4 }),
|
opensea: new OpenSea({ rps: 4 }),
|
||||||
gnosis: new Gnosis({ rps: 4 }),
|
gnosis: new Gnosis(),
|
||||||
mockedopensea: new MockedOpenSea({ rps: 4 }),
|
mockedopensea: new MockedOpenSea({ rps: 4 }),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import { batch } from 'react-redux'
|
|
||||||
import { Dispatch } from 'redux'
|
import { Dispatch } from 'redux'
|
||||||
|
|
||||||
import { getConfiguredSource } from 'src/logic/collectibles/sources'
|
import { getConfiguredSource } from 'src/logic/collectibles/sources'
|
||||||
|
@ -9,10 +8,8 @@ export const fetchCollectibles = (safeAddress: string) => async (dispatch: Dispa
|
||||||
const source = getConfiguredSource()
|
const source = getConfiguredSource()
|
||||||
const collectibles = await source.fetchCollectibles(safeAddress)
|
const collectibles = await source.fetchCollectibles(safeAddress)
|
||||||
|
|
||||||
batch(() => {
|
dispatch(addNftAssets(collectibles.nftAssets))
|
||||||
dispatch(addNftAssets(collectibles.nftAssets))
|
dispatch(addNftTokens(collectibles.nftTokens))
|
||||||
dispatch(addNftTokens(collectibles.nftTokens))
|
|
||||||
})
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log('Error fetching collectibles:', error)
|
console.log('Error fetching collectibles:', error)
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,7 @@ export const gatewayTransactions = (state: AppReduxState): AppReduxState['gatewa
|
||||||
return state[GATEWAY_TRANSACTIONS_ID]
|
return state[GATEWAY_TRANSACTIONS_ID]
|
||||||
}
|
}
|
||||||
|
|
||||||
export const historyTransactions = createSelector(
|
export const historyTransactions = createHashBasedSelector(
|
||||||
gatewayTransactions,
|
gatewayTransactions,
|
||||||
safeParamAddressFromStateSelector,
|
safeParamAddressFromStateSelector,
|
||||||
(gatewayTransactions, safeAddress): StoreStructure['history'] | undefined => {
|
(gatewayTransactions, safeAddress): StoreStructure['history'] | undefined => {
|
||||||
|
|
|
@ -8,6 +8,6 @@ import { AppReduxState } from 'src/store'
|
||||||
export const createIsEqualSelector = createSelectorCreator(defaultMemoize, isEqual)
|
export const createIsEqualSelector = createSelectorCreator(defaultMemoize, isEqual)
|
||||||
|
|
||||||
const hashFn = (gatewayTransactions: AppReduxState['gatewayTransactions'], safeAddress: string): string =>
|
const hashFn = (gatewayTransactions: AppReduxState['gatewayTransactions'], safeAddress: string): string =>
|
||||||
hash(gatewayTransactions[safeAddress])
|
hash(gatewayTransactions[safeAddress] ?? {})
|
||||||
|
|
||||||
export const createHashBasedSelector = createSelectorCreator(memoize as any, hashFn)
|
export const createHashBasedSelector = createSelectorCreator(memoize as any, hashFn)
|
||||||
|
|
|
@ -11,10 +11,10 @@ export type TokenResult = {
|
||||||
type: string
|
type: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const fetchErc20AndErc721AssetsList = async (): Promise<AxiosResponse<{ results: TokenResult[] }>> => {
|
export const fetchErc20AndErc721AssetsList = (): Promise<AxiosResponse<{ results: TokenResult[] }>> => {
|
||||||
const url = getTokensServiceBaseUrl()
|
const url = getTokensServiceBaseUrl()
|
||||||
|
|
||||||
return axios.get<{ results: TokenResult[] }>(`${url}/`, {
|
return axios.get<{ results: TokenResult[] }, AxiosResponse<{ results: TokenResult[] }>>(`${url}/`, {
|
||||||
params: {
|
params: {
|
||||||
limit: 3000,
|
limit: 3000,
|
||||||
},
|
},
|
||||||
|
|
|
@ -16,9 +16,11 @@ export type CollectibleResult = {
|
||||||
uri: string | null
|
uri: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export const fetchSafeCollectibles = async (safeAddress: string): Promise<AxiosResponse<CollectibleResult[]>> => {
|
export const fetchSafeCollectibles = async (
|
||||||
|
safeAddress: string,
|
||||||
|
): Promise<AxiosResponse<{ results: CollectibleResult[] }>> => {
|
||||||
const address = checksumAddress(safeAddress)
|
const address = checksumAddress(safeAddress)
|
||||||
const url = `${getSafeServiceBaseUrl(address)}/collectibles/`
|
const url = `${getSafeServiceBaseUrl(address)}/collectibles/`
|
||||||
|
|
||||||
return axios.get(url)
|
return axios.get<CollectibleResult[], AxiosResponse<{ results: CollectibleResult[] }>>(url)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import { backOff } from 'exponential-backoff'
|
import { backOff } from 'exponential-backoff'
|
||||||
import { List, Map } from 'immutable'
|
import { List, Map } from 'immutable'
|
||||||
import { batch } from 'react-redux'
|
|
||||||
import { Dispatch } from 'redux'
|
import { Dispatch } from 'redux'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
@ -84,11 +83,9 @@ const fetchSafeTokens = (safeAddress: string) => async (
|
||||||
balances.keySeq().toSet().subtract(blacklistedTokens),
|
balances.keySeq().toSet().subtract(blacklistedTokens),
|
||||||
)
|
)
|
||||||
|
|
||||||
batch(() => {
|
dispatch(updateSafe({ address: safeAddress, activeTokens, balances, ethBalance }))
|
||||||
dispatch(updateSafe({ address: safeAddress, activeTokens, balances, ethBalance }))
|
dispatch(setCurrencyBalances(safeAddress, currencyList))
|
||||||
dispatch(setCurrencyBalances(safeAddress, currencyList))
|
dispatch(addTokens(tokens))
|
||||||
dispatch(addTokens(tokens))
|
|
||||||
})
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error fetching active token list', err)
|
console.error('Error fetching active token list', err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { Loader } from '@gnosis.pm/safe-react-components'
|
||||||
|
import React, { ReactElement } from 'react'
|
||||||
|
|
||||||
|
import { usePagedHistoryTransactions } from './hooks/usePagedHistoryTransactions'
|
||||||
|
import { Centered } from './styled'
|
||||||
|
import { HistoryTxList } from './HistoryTxList'
|
||||||
|
import { TxsInfiniteScroll } from './TxsInfiniteScroll'
|
||||||
|
|
||||||
|
export const HistoryTransactions = (): ReactElement => {
|
||||||
|
const { count, hasMore, next, transactions, isLoading } = usePagedHistoryTransactions()
|
||||||
|
|
||||||
|
if (count === 0) {
|
||||||
|
return (
|
||||||
|
<Centered>
|
||||||
|
<Loader size="md" />
|
||||||
|
</Centered>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TxsInfiniteScroll next={next} hasMore={hasMore} isLoading={isLoading}>
|
||||||
|
<HistoryTxList transactions={transactions} />
|
||||||
|
</TxsInfiniteScroll>
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,46 +1,35 @@
|
||||||
import { Loader } from '@gnosis.pm/safe-react-components'
|
import React, { ReactElement, useContext } from 'react'
|
||||||
import React, { ReactElement } from 'react'
|
|
||||||
|
|
||||||
import { InfiniteScroll, SCROLLABLE_TARGET_ID } from 'src/components/InfiniteScroll'
|
import { TransactionDetails } from 'src/logic/safe/store/models/types/gateway.d'
|
||||||
import { usePagedHistoryTransactions } from './hooks/usePagedHistoryTransactions'
|
import { TxsInfiniteScrollContext } from 'src/routes/safe/components/Transactions/GatewayTransactions/TxsInfiniteScroll'
|
||||||
import {
|
import { formatWithSchema } from 'src/utils/date'
|
||||||
SubTitle,
|
import { sameString } from 'src/utils/strings'
|
||||||
ScrollableTransactionsContainer,
|
import { StyledTransactions, StyledTransactionsGroup, SubTitle } from './styled'
|
||||||
StyledTransactions,
|
|
||||||
StyledTransactionsGroup,
|
|
||||||
Centered,
|
|
||||||
} from './styled'
|
|
||||||
import { TxHistoryRow } from './TxHistoryRow'
|
import { TxHistoryRow } from './TxHistoryRow'
|
||||||
import { TxLocationContext } from './TxLocationProvider'
|
import { TxLocationContext } from './TxLocationProvider'
|
||||||
import { formatWithSchema } from 'src/utils/date'
|
|
||||||
|
|
||||||
export const HistoryTxList = (): ReactElement => {
|
export const HistoryTxList = ({ transactions }: { transactions: TransactionDetails['transactions'] }): ReactElement => {
|
||||||
const { count, hasMore, next, transactions } = usePagedHistoryTransactions()
|
const { lastItemId, setLastItemId } = useContext(TxsInfiniteScrollContext)
|
||||||
|
|
||||||
if (count === 0) {
|
const [, lastTransactionsGroup] = transactions[transactions.length - 1]
|
||||||
return (
|
const lastTransaction = lastTransactionsGroup[lastTransactionsGroup.length - 1]
|
||||||
<Centered>
|
|
||||||
<Loader size="md" />
|
if (!sameString(lastItemId, lastTransaction.id)) {
|
||||||
</Centered>
|
setLastItemId(lastTransaction.id)
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TxLocationContext.Provider value={{ txLocation: 'history' }}>
|
<TxLocationContext.Provider value={{ txLocation: 'history' }}>
|
||||||
<ScrollableTransactionsContainer id={SCROLLABLE_TARGET_ID}>
|
{transactions?.map(([timestamp, txs]) => (
|
||||||
<InfiniteScroll dataLength={transactions.length} next={next} hasMore={hasMore}>
|
<StyledTransactionsGroup key={timestamp}>
|
||||||
{transactions?.map(([timestamp, txs]) => (
|
<SubTitle size="lg">{formatWithSchema(Number(timestamp), 'MMM d, yyyy')}</SubTitle>
|
||||||
<StyledTransactionsGroup key={timestamp}>
|
<StyledTransactions>
|
||||||
<SubTitle size="lg">{formatWithSchema(Number(timestamp), 'MMM d, yyyy')}</SubTitle>
|
{txs.map((transaction) => (
|
||||||
<StyledTransactions>
|
<TxHistoryRow key={transaction.id} transaction={transaction} />
|
||||||
{txs.map((transaction) => (
|
))}
|
||||||
<TxHistoryRow key={transaction.id} transaction={transaction} />
|
</StyledTransactions>
|
||||||
))}
|
</StyledTransactionsGroup>
|
||||||
</StyledTransactions>
|
))}
|
||||||
</StyledTransactionsGroup>
|
|
||||||
))}
|
|
||||||
</InfiniteScroll>
|
|
||||||
</ScrollableTransactionsContainer>
|
|
||||||
</TxLocationContext.Provider>
|
</TxLocationContext.Provider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,15 +2,15 @@ import { Loader, Title } from '@gnosis.pm/safe-react-components'
|
||||||
import React, { ReactElement } from 'react'
|
import React, { ReactElement } from 'react'
|
||||||
import style from 'styled-components'
|
import style from 'styled-components'
|
||||||
|
|
||||||
import { InfiniteScroll, SCROLLABLE_TARGET_ID } from 'src/components/InfiniteScroll'
|
|
||||||
import Img from 'src/components/layout/Img'
|
import Img from 'src/components/layout/Img'
|
||||||
import { usePagedQueuedTransactions } from './hooks/usePagedQueuedTransactions'
|
|
||||||
import { ActionModal } from './ActionModal'
|
import { ActionModal } from './ActionModal'
|
||||||
import { TxActionProvider } from './TxActionProvider'
|
|
||||||
import { TxLocationContext } from './TxLocationProvider'
|
|
||||||
import { QueueTxList } from './QueueTxList'
|
|
||||||
import { ScrollableTransactionsContainer, Centered } from './styled'
|
|
||||||
import NoTransactionsImage from './assets/no-transactions.svg'
|
import NoTransactionsImage from './assets/no-transactions.svg'
|
||||||
|
import { usePagedQueuedTransactions } from './hooks/usePagedQueuedTransactions'
|
||||||
|
import { QueueTxList } from './QueueTxList'
|
||||||
|
import { Centered } from './styled'
|
||||||
|
import { TxActionProvider } from './TxActionProvider'
|
||||||
|
import { TxsInfiniteScroll } from './TxsInfiniteScroll'
|
||||||
|
import { TxLocationContext } from './TxLocationProvider'
|
||||||
|
|
||||||
const NoTransactions = style.div`
|
const NoTransactions = style.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -19,11 +19,11 @@ const NoTransactions = style.div`
|
||||||
`
|
`
|
||||||
|
|
||||||
export const QueueTransactions = (): ReactElement => {
|
export const QueueTransactions = (): ReactElement => {
|
||||||
const { count, loading, hasMore, next, transactions } = usePagedQueuedTransactions()
|
const { count, isLoading, hasMore, next, transactions } = usePagedQueuedTransactions()
|
||||||
|
|
||||||
// `loading` is, actually `!transactions`
|
// `loading` is, actually `!transactions`
|
||||||
// added the `transaction` verification to prevent `Object is possibly 'undefined'` error
|
// added the `transaction` verification to prevent `Object is possibly 'undefined'` error
|
||||||
if (loading || !transactions) {
|
if (isLoading || !transactions) {
|
||||||
return (
|
return (
|
||||||
<Centered>
|
<Centered>
|
||||||
<Loader size="md" />
|
<Loader size="md" />
|
||||||
|
@ -42,19 +42,17 @@ export const QueueTransactions = (): ReactElement => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TxActionProvider>
|
<TxActionProvider>
|
||||||
<ScrollableTransactionsContainer id={SCROLLABLE_TARGET_ID}>
|
<TxsInfiniteScroll next={next} hasMore={hasMore} isLoading={isLoading}>
|
||||||
<InfiniteScroll dataLength={count} next={next} hasMore={hasMore}>
|
{/* Next list */}
|
||||||
{/* Next list */}
|
<TxLocationContext.Provider value={{ txLocation: 'queued.next' }}>
|
||||||
<TxLocationContext.Provider value={{ txLocation: 'queued.next' }}>
|
{transactions.next.count !== 0 && <QueueTxList transactions={transactions.next.transactions} />}
|
||||||
{transactions.next.count !== 0 && <QueueTxList transactions={transactions.next.transactions} />}
|
</TxLocationContext.Provider>
|
||||||
</TxLocationContext.Provider>
|
|
||||||
|
|
||||||
{/* Queue list */}
|
{/* Queue list */}
|
||||||
<TxLocationContext.Provider value={{ txLocation: 'queued.queued' }}>
|
<TxLocationContext.Provider value={{ txLocation: 'queued.queued' }}>
|
||||||
{transactions.queue.count !== 0 && <QueueTxList transactions={transactions.queue.transactions} />}
|
{transactions.queue.count !== 0 && <QueueTxList transactions={transactions.queue.transactions} />}
|
||||||
</TxLocationContext.Provider>
|
</TxLocationContext.Provider>
|
||||||
</InfiniteScroll>
|
</TxsInfiniteScroll>
|
||||||
</ScrollableTransactionsContainer>
|
|
||||||
<ActionModal />
|
<ActionModal />
|
||||||
</TxActionProvider>
|
</TxActionProvider>
|
||||||
)
|
)
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { Icon, Link, Text } from '@gnosis.pm/safe-react-components'
|
||||||
import React, { Fragment, ReactElement, useContext } from 'react'
|
import React, { Fragment, ReactElement, useContext } from 'react'
|
||||||
|
|
||||||
import { Transaction, TransactionDetails } from 'src/logic/safe/store/models/types/gateway.d'
|
import { Transaction, TransactionDetails } from 'src/logic/safe/store/models/types/gateway.d'
|
||||||
|
import { sameString } from 'src/utils/strings'
|
||||||
import {
|
import {
|
||||||
DisclaimerContainer,
|
DisclaimerContainer,
|
||||||
GroupedTransactions,
|
GroupedTransactions,
|
||||||
|
@ -14,6 +15,7 @@ import {
|
||||||
import { TxHoverProvider } from './TxHoverProvider'
|
import { TxHoverProvider } from './TxHoverProvider'
|
||||||
import { TxLocationContext } from './TxLocationProvider'
|
import { TxLocationContext } from './TxLocationProvider'
|
||||||
import { TxQueueRow } from './TxQueueRow'
|
import { TxQueueRow } from './TxQueueRow'
|
||||||
|
import { TxsInfiniteScrollContext } from './TxsInfiniteScroll'
|
||||||
|
|
||||||
const TreeView = ({ firstElement }: { firstElement: boolean }): ReactElement => {
|
const TreeView = ({ firstElement }: { firstElement: boolean }): ReactElement => {
|
||||||
return <p className="tree-lines">{firstElement ? <span className="first-node" /> : null}</p>
|
return <p className="tree-lines">{firstElement ? <span className="first-node" /> : null}</p>
|
||||||
|
@ -52,8 +54,8 @@ type QueueTransactionProps = {
|
||||||
transactions: Transaction[]
|
transactions: Transaction[]
|
||||||
}
|
}
|
||||||
|
|
||||||
const QueueTransaction = ({ nonce, transactions }: QueueTransactionProps): ReactElement => {
|
const QueueTransaction = ({ nonce, transactions }: QueueTransactionProps): ReactElement =>
|
||||||
return transactions.length > 1 ? (
|
transactions.length > 1 ? (
|
||||||
<GroupedTransactionsCard>
|
<GroupedTransactionsCard>
|
||||||
<TxHoverProvider>
|
<TxHoverProvider>
|
||||||
<Disclaimer nonce={nonce} />
|
<Disclaimer nonce={nonce} />
|
||||||
|
@ -70,7 +72,6 @@ const QueueTransaction = ({ nonce, transactions }: QueueTransactionProps): React
|
||||||
) : (
|
) : (
|
||||||
<TxQueueRow transaction={transactions[0]} />
|
<TxQueueRow transaction={transactions[0]} />
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
|
||||||
type QueueTxListProps = {
|
type QueueTxListProps = {
|
||||||
transactions: TransactionDetails['transactions']
|
transactions: TransactionDetails['transactions']
|
||||||
|
@ -80,6 +81,14 @@ export const QueueTxList = ({ transactions }: QueueTxListProps): ReactElement =>
|
||||||
const { txLocation } = useContext(TxLocationContext)
|
const { txLocation } = useContext(TxLocationContext)
|
||||||
const title = txLocation === 'queued.next' ? 'NEXT TRANSACTION' : 'QUEUE'
|
const title = txLocation === 'queued.next' ? 'NEXT TRANSACTION' : 'QUEUE'
|
||||||
|
|
||||||
|
const { lastItemId, setLastItemId } = useContext(TxsInfiniteScrollContext)
|
||||||
|
const [, lastTransactionsGroup] = transactions[transactions.length - 1]
|
||||||
|
const lastTransaction = lastTransactionsGroup[lastTransactionsGroup.length - 1]
|
||||||
|
|
||||||
|
if (txLocation === 'queued.queued' && !sameString(lastItemId, lastTransaction.id)) {
|
||||||
|
setLastItemId(lastTransaction.id)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledTransactionsGroup>
|
<StyledTransactionsGroup>
|
||||||
<SubTitle size="lg">{title}</SubTitle>
|
<SubTitle size="lg">{title}</SubTitle>
|
||||||
|
|
|
@ -11,7 +11,7 @@ import {
|
||||||
isSettingsChangeTxInfo,
|
isSettingsChangeTxInfo,
|
||||||
Transaction,
|
Transaction,
|
||||||
} from 'src/logic/safe/store/models/types/gateway.d'
|
} from 'src/logic/safe/store/models/types/gateway.d'
|
||||||
import { TxCollapsedActions } from 'src/routes/safe/components/Transactions/GatewayTransactions/TxCollapsedActions'
|
import { TxCollapsedActions } from './TxCollapsedActions'
|
||||||
import { formatDateTime, formatTime, formatTimeInWords } from 'src/utils/date'
|
import { formatDateTime, formatTime, formatTimeInWords } from 'src/utils/date'
|
||||||
import { KNOWN_MODULES } from 'src/utils/constants'
|
import { KNOWN_MODULES } from 'src/utils/constants'
|
||||||
import { sameString } from 'src/utils/strings'
|
import { sameString } from 'src/utils/strings'
|
||||||
|
@ -21,6 +21,7 @@ import { TransactionStatusProps } from './hooks/useTransactionStatus'
|
||||||
import { TxTypeProps } from './hooks/useTransactionType'
|
import { TxTypeProps } from './hooks/useTransactionType'
|
||||||
import { StyledGroupedTransactions, StyledTransaction } from './styled'
|
import { StyledGroupedTransactions, StyledTransaction } from './styled'
|
||||||
import { TokenTransferAmount } from './TokenTransferAmount'
|
import { TokenTransferAmount } from './TokenTransferAmount'
|
||||||
|
import { TxsInfiniteScrollContext } from './TxsInfiniteScroll'
|
||||||
import { TxLocationContext } from './TxLocationProvider'
|
import { TxLocationContext } from './TxLocationProvider'
|
||||||
import { CalculatedVotes } from './TxQueueCollapsed'
|
import { CalculatedVotes } from './TxQueueCollapsed'
|
||||||
|
|
||||||
|
@ -89,7 +90,7 @@ const TooltipContent = styled.div`
|
||||||
`
|
`
|
||||||
|
|
||||||
type TxCollapsedProps = {
|
type TxCollapsedProps = {
|
||||||
transaction?: Transaction
|
transaction: Transaction
|
||||||
isGrouped?: boolean
|
isGrouped?: boolean
|
||||||
nonce?: number
|
nonce?: number
|
||||||
type: TxTypeProps
|
type: TxTypeProps
|
||||||
|
@ -112,6 +113,7 @@ export const TxCollapsed = ({
|
||||||
status,
|
status,
|
||||||
}: TxCollapsedProps): ReactElement => {
|
}: TxCollapsedProps): ReactElement => {
|
||||||
const { txLocation } = useContext(TxLocationContext)
|
const { txLocation } = useContext(TxLocationContext)
|
||||||
|
const { ref, lastItemId } = useContext(TxsInfiniteScrollContext)
|
||||||
|
|
||||||
const willBeReplaced = transaction?.txStatus === 'WILL_BE_REPLACED' ? ' will-be-replaced' : ''
|
const willBeReplaced = transaction?.txStatus === 'WILL_BE_REPLACED' ? ' will-be-replaced' : ''
|
||||||
|
|
||||||
|
@ -161,8 +163,9 @@ export const TxCollapsed = ({
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// attaching ref to a div element as it was causing troubles to add a `ref` to a FunctionComponent
|
||||||
const txCollapsedStatus = (
|
const txCollapsedStatus = (
|
||||||
<div className="tx-status">
|
<div className="tx-status" ref={sameString(lastItemId, transaction.id) ? ref : null}>
|
||||||
{transaction?.txStatus === 'PENDING' || transaction?.txStatus === 'PENDING_FAILED' ? (
|
{transaction?.txStatus === 'PENDING' || transaction?.txStatus === 'PENDING_FAILED' ? (
|
||||||
<CircularProgressPainter color={status.color}>
|
<CircularProgressPainter color={status.color}>
|
||||||
<CircularProgress size={14} color="inherit" />
|
<CircularProgress size={14} color="inherit" />
|
||||||
|
|
|
@ -32,22 +32,26 @@ export const TxCollapsedActions = ({ transaction }: TxCollapsedActionsProps): Re
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Tooltip title={transaction.txStatus === 'AWAITING_EXECUTION' ? 'Execute' : 'Confirm'} placement="top">
|
<Tooltip title={transaction.txStatus === 'AWAITING_EXECUTION' ? 'Execute' : 'Confirm'} placement="top">
|
||||||
<IconButton
|
<span>
|
||||||
size="small"
|
<IconButton
|
||||||
type="button"
|
size="small"
|
||||||
onClick={handleConfirmButtonClick}
|
type="button"
|
||||||
disabled={disabledActions}
|
onClick={handleConfirmButtonClick}
|
||||||
onMouseEnter={handleOnMouseEnter}
|
disabled={disabledActions}
|
||||||
onMouseLeave={handleOnMouseLeave}
|
onMouseEnter={handleOnMouseEnter}
|
||||||
>
|
onMouseLeave={handleOnMouseLeave}
|
||||||
<Icon type={transaction.txStatus === 'AWAITING_EXECUTION' ? 'rocket' : 'check'} color="primary" size="sm" />
|
>
|
||||||
</IconButton>
|
<Icon type={transaction.txStatus === 'AWAITING_EXECUTION' ? 'rocket' : 'check'} color="primary" size="sm" />
|
||||||
|
</IconButton>
|
||||||
|
</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{canCancel && (
|
{canCancel && (
|
||||||
<Tooltip title="Cancel" placement="top">
|
<Tooltip title="Cancel" placement="top">
|
||||||
<IconButton size="small" type="button" onClick={handleCancelButtonClick} disabled={isPending}>
|
<span>
|
||||||
<Icon type="circleCross" color="error" size="sm" />
|
<IconButton size="small" type="button" onClick={handleCancelButtonClick} disabled={isPending}>
|
||||||
</IconButton>
|
<Icon type="circleCross" color="error" size="sm" />
|
||||||
|
</IconButton>
|
||||||
|
</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -12,5 +12,14 @@ export const TxHistoryCollapsed = ({ transaction }: { transaction: Transaction }
|
||||||
const info = useAssetInfo(transaction.txInfo)
|
const info = useAssetInfo(transaction.txInfo)
|
||||||
const status = useTransactionStatus(transaction)
|
const status = useTransactionStatus(transaction)
|
||||||
|
|
||||||
return <TxCollapsed nonce={nonce} type={type} info={info} time={transaction.timestamp} status={status} />
|
return (
|
||||||
|
<TxCollapsed
|
||||||
|
nonce={nonce}
|
||||||
|
type={type}
|
||||||
|
info={info}
|
||||||
|
time={transaction.timestamp}
|
||||||
|
status={status}
|
||||||
|
transaction={transaction}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
import { Loader } from '@gnosis.pm/safe-react-components'
|
||||||
|
import React, { ReactElement, ReactNode } from 'react'
|
||||||
|
|
||||||
|
import { INFINITE_SCROLL_CONTAINER, InfiniteScroll } from 'src/components/InfiniteScroll'
|
||||||
|
import { HorizontallyCentered, ScrollableTransactionsContainer } from './styled'
|
||||||
|
|
||||||
|
type TxsInfiniteScrollProps = {
|
||||||
|
children: ReactNode
|
||||||
|
next: () => Promise<void>
|
||||||
|
hasMore: boolean
|
||||||
|
isLoading: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TxsInfiniteScroll = ({ children, next, hasMore, isLoading }: TxsInfiniteScrollProps): ReactElement => {
|
||||||
|
return (
|
||||||
|
<InfiniteScroll next={next} hasMore={hasMore}>
|
||||||
|
<ScrollableTransactionsContainer id={INFINITE_SCROLL_CONTAINER}>
|
||||||
|
{children}
|
||||||
|
<HorizontallyCentered isVisible={isLoading}>
|
||||||
|
<Loader size="md" />
|
||||||
|
</HorizontallyCentered>
|
||||||
|
</ScrollableTransactionsContainer>
|
||||||
|
</InfiniteScroll>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { InfiniteScrollContext as TxsInfiniteScrollContext } from 'src/components/InfiniteScroll'
|
|
@ -1,4 +1,4 @@
|
||||||
import { useState } from 'react'
|
import { useCallback, useRef, useState } from 'react'
|
||||||
import { useDispatch, useSelector } from 'react-redux'
|
import { useDispatch, useSelector } from 'react-redux'
|
||||||
|
|
||||||
import { loadPagedHistoryTransactions } from 'src/logic/safe/store/actions/transactions/fetchTransactions/loadGatewayTransactions'
|
import { loadPagedHistoryTransactions } from 'src/logic/safe/store/actions/transactions/fetchTransactions/loadGatewayTransactions'
|
||||||
|
@ -12,17 +12,20 @@ type PagedTransactions = {
|
||||||
transactions: TransactionDetails['transactions']
|
transactions: TransactionDetails['transactions']
|
||||||
hasMore: boolean
|
hasMore: boolean
|
||||||
next: () => Promise<void>
|
next: () => Promise<void>
|
||||||
|
isLoading: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const usePagedHistoryTransactions = (): PagedTransactions => {
|
export const usePagedHistoryTransactions = (): PagedTransactions => {
|
||||||
const { count, transactions } = useHistoryTransactions()
|
const { count, transactions } = useHistoryTransactions()
|
||||||
|
|
||||||
const dispatch = useDispatch()
|
const dispatch = useRef(useDispatch())
|
||||||
const safeAddress = useSelector(safeParamAddressFromStateSelector)
|
const safeAddress = useRef(useSelector(safeParamAddressFromStateSelector))
|
||||||
const [hasMore, setHasMore] = useState(true)
|
const [hasMore, setHasMore] = useState(true)
|
||||||
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
|
||||||
const next = async () => {
|
const next = useCallback(async () => {
|
||||||
const results = await loadPagedHistoryTransactions(safeAddress)
|
setIsLoading(true)
|
||||||
|
const results = await loadPagedHistoryTransactions(safeAddress.current)
|
||||||
|
|
||||||
if (!results) {
|
if (!results) {
|
||||||
setHasMore(false)
|
setHasMore(false)
|
||||||
|
@ -36,11 +39,12 @@ export const usePagedHistoryTransactions = (): PagedTransactions => {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (values) {
|
if (values) {
|
||||||
dispatch(addHistoryTransactions({ safeAddress, values, isTail: true }))
|
dispatch.current(addHistoryTransactions({ safeAddress: safeAddress.current, values, isTail: true }))
|
||||||
} else {
|
} else {
|
||||||
setHasMore(false)
|
setHasMore(false)
|
||||||
}
|
}
|
||||||
}
|
setIsLoading(false)
|
||||||
|
}, [])
|
||||||
|
|
||||||
return { count, transactions, hasMore, next }
|
return { count, transactions, hasMore, next, isLoading }
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@ import { QueueTransactionsInfo, useQueueTransactions } from './useQueueTransacti
|
||||||
|
|
||||||
type PagedQueuedTransactions = {
|
type PagedQueuedTransactions = {
|
||||||
count: number
|
count: number
|
||||||
loading: boolean
|
isLoading: boolean
|
||||||
transactions?: QueueTransactionsInfo
|
transactions?: QueueTransactionsInfo
|
||||||
hasMore: boolean
|
hasMore: boolean
|
||||||
next: () => Promise<void>
|
next: () => Promise<void>
|
||||||
|
@ -46,7 +46,7 @@ export const usePagedQueuedTransactions = (): PagedQueuedTransactions => {
|
||||||
count = transactions.next.count + transactions.queue.count
|
count = transactions.next.count + transactions.queue.count
|
||||||
}
|
}
|
||||||
|
|
||||||
const loading = typeof transactions === 'undefined' || typeof count === 'undefined'
|
const isLoading = typeof transactions === 'undefined' || typeof count === 'undefined'
|
||||||
|
|
||||||
return { count, loading, transactions, hasMore, next: nextPage }
|
return { count, isLoading, transactions, hasMore, next: nextPage }
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { Item } from '@gnosis.pm/safe-react-components/dist/navigation/Tab'
|
||||||
import React, { ReactElement, useState } from 'react'
|
import React, { ReactElement, useState } from 'react'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
import { HistoryTxList } from './HistoryTxList'
|
import { HistoryTransactions } from './HistoryTransactions'
|
||||||
import { QueueTransactions } from './QueueTransactions'
|
import { QueueTransactions } from './QueueTransactions'
|
||||||
import { Breadcrumb, ContentWrapper, Wrapper } from './styled'
|
import { Breadcrumb, ContentWrapper, Wrapper } from './styled'
|
||||||
|
|
||||||
|
@ -27,7 +27,7 @@ const GatewayTransactions = (): ReactElement => {
|
||||||
<Tab items={items} onChange={setTab} selectedTab={tab} />
|
<Tab items={items} onChange={setTab} selectedTab={tab} />
|
||||||
<ContentWrapper>
|
<ContentWrapper>
|
||||||
{tab === 'queue' && <QueueTransactions />}
|
{tab === 'queue' && <QueueTransactions />}
|
||||||
{tab === 'history' && <HistoryTxList />}
|
{tab === 'history' && <HistoryTransactions />}
|
||||||
</ContentWrapper>
|
</ContentWrapper>
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
)
|
)
|
||||||
|
|
|
@ -91,7 +91,6 @@ export const StyledTransactions = styled.div`
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
// '^' to prevent applying rules to the 'Actions' accordion components
|
|
||||||
& > .MuiAccordion-root {
|
& > .MuiAccordion-root {
|
||||||
&:first-child {
|
&:first-child {
|
||||||
border-top: none;
|
border-top: none;
|
||||||
|
@ -485,6 +484,11 @@ export const Centered = styled.div<{ padding?: number }>`
|
||||||
align-items: center;
|
align-items: center;
|
||||||
`
|
`
|
||||||
|
|
||||||
|
export const HorizontallyCentered = styled(Centered)<{ isVisible: boolean }>`
|
||||||
|
visibility: ${({ isVisible }) => (isVisible ? 'visible' : 'hidden')};
|
||||||
|
height: auto;
|
||||||
|
`
|
||||||
|
|
||||||
export const StyledAccordionSummary = styled(AccordionSummary)`
|
export const StyledAccordionSummary = styled(AccordionSummary)`
|
||||||
height: 52px;
|
height: 52px;
|
||||||
.tx-nonce {
|
.tx-nonce {
|
||||||
|
|
12
yarn.lock
12
yarn.lock
|
@ -17191,13 +17191,6 @@ react-hotkeys@2.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
prop-types "^15.6.1"
|
prop-types "^15.6.1"
|
||||||
|
|
||||||
react-infinite-scroll-component@^5.1.0:
|
|
||||||
version "5.1.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/react-infinite-scroll-component/-/react-infinite-scroll-component-5.1.0.tgz#3c0043cd17c6857c25d3ca7aa4171e0c33ce8023"
|
|
||||||
integrity sha512-ZQ7lYlLByil1ZVdvt57UJeBIj/tSOwKyds+B0LIm/xGmxzJJZwW/hYGKKs74sHO3LoIDwcq2ci6YXqrkWSalLQ==
|
|
||||||
dependencies:
|
|
||||||
throttle-debounce "^2.1.0"
|
|
||||||
|
|
||||||
react-inspector@^4.0.0:
|
react-inspector@^4.0.0:
|
||||||
version "4.0.1"
|
version "4.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/react-inspector/-/react-inspector-4.0.1.tgz#0f888f78ff7daccbc7be5d452b20c96dc6d5fbb8"
|
resolved "https://registry.yarnpkg.com/react-inspector/-/react-inspector-4.0.1.tgz#0f888f78ff7daccbc7be5d452b20c96dc6d5fbb8"
|
||||||
|
@ -17207,6 +17200,11 @@ react-inspector@^4.0.0:
|
||||||
is-dom "^1.0.9"
|
is-dom "^1.0.9"
|
||||||
prop-types "^15.6.1"
|
prop-types "^15.6.1"
|
||||||
|
|
||||||
|
react-intersection-observer@^8.31.0:
|
||||||
|
version "8.31.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-intersection-observer/-/react-intersection-observer-8.31.0.tgz#0ed21aaf93c4c0475b22b0ccaba6169076d01605"
|
||||||
|
integrity sha512-XraIC/tkrD9JtrmVA7ypEN1QIpKc52mXBH1u/bz/aicRLo8QQEJQAMUTb8mz4B6dqpPwyzgjrr7Ljv/2ACDtqw==
|
||||||
|
|
||||||
react-is@^16.13.1, react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1, react-is@^16.9.0:
|
react-is@^16.13.1, react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1, react-is@^16.9.0:
|
||||||
version "16.13.1"
|
version "16.13.1"
|
||||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
|
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
|
||||||
|
|
Loading…
Reference in New Issue