Skip to content

Commit

Permalink
refactor: decompose OrderRow component (#5335)
Browse files Browse the repository at this point in the history
* refactor: extract utils from OrderRow

* refactor: get rid of CurrencySymbolItem

* refactor: move OrderRow component to containers

* refactor: extract custom hooks from OrderRow

* refactor: extract CurrencyAmountItem component

* refactor: get rid of renderLimitPrice

* refactor: remove orderRateInfo duplicate

* refactor: remove excessive showIcon prop from WarningTooltip
  • Loading branch information
shoom3301 authored Jan 24, 2025
1 parent 0dbc184 commit 220d233
Show file tree
Hide file tree
Showing 14 changed files with 214 additions and 217 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { Menu, MenuButton, MenuItem, MenuList } from '@reach/menu-button'
import { Edit, FileText, Link2, MoreVertical, Repeat, Trash2 } from 'react-feather'
import styled from 'styled-components/macro'

import { AlternativeOrderModalContext } from '../../../containers/OrdersReceiptModal/hooks'
import { AlternativeOrderModalContext } from '../OrdersReceiptModal/hooks'

export const ContextMenuButton = styled(MenuButton)`
background: none;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React from 'react'

import alertCircle from '@cowprotocol/assets/cow-swap/alert-circle.svg'
import { Command } from '@cowprotocol/types'
import { ButtonSecondary, HoverTooltip, TokenSymbol, UI } from '@cowprotocol/ui'
import { ButtonSecondary, TokenSymbol, UI } from '@cowprotocol/ui'

import SVG from 'react-inlinesvg'

Expand Down Expand Up @@ -92,7 +92,6 @@ interface WarningTooltipProps {
inputTokenSymbol: string
isOrderScheduled: boolean
onApprove: Command
showIcon?: boolean
}

export function WarningTooltip({
Expand All @@ -102,7 +101,6 @@ export function WarningTooltip({
inputTokenSymbol,
isOrderScheduled,
onApprove,
showIcon = false,
}: WarningTooltipProps) {
const withAllowanceWarning = !hasEnoughAllowance

Expand All @@ -115,29 +113,16 @@ export function WarningTooltip({
</styledEl.WarningContent>
)

if (showIcon) {
return (
<styledEl.WarningIndicator hasBackground={false}>
<styledEl.StyledQuestionHelper
text={tooltipContent}
placement="bottom"
bgColor={`var(${UI.COLOR_DANGER_BG})`}
color={`var(${UI.COLOR_DANGER_TEXT})`}
Icon={<SVG src={alertCircle} description="warning" width="14" height="14" />}
/>
{children}
</styledEl.WarningIndicator>
)
}

return (
<HoverTooltip
content={tooltipContent}
placement="bottom"
bgColor={`var(${UI.COLOR_DANGER})`}
color={`var(${UI.COLOR_DANGER_TEXT})`}
>
<styledEl.WarningIndicator hasBackground={false}>
<styledEl.StyledQuestionHelper
text={tooltipContent}
placement="bottom"
bgColor={`var(${UI.COLOR_DANGER_BG})`}
color={`var(${UI.COLOR_DANGER_TEXT})`}
Icon={<SVG src={alertCircle} description="warning" width="14" height="14" />}
/>
{children}
</HoverTooltip>
</styledEl.WarningIndicator>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,16 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'
import orderPresignaturePending from '@cowprotocol/assets/cow-swap/order-presignature-pending.svg'
import { ZERO_FRACTION } from '@cowprotocol/common-const'
import { useTimeAgo } from '@cowprotocol/common-hooks'
import { formatDateWithTimezone, getAddress, getEtherscanLink } from '@cowprotocol/common-utils'
import { formatDateWithTimezone, getAddress } from '@cowprotocol/common-utils'
import { SupportedChainId } from '@cowprotocol/cow-sdk'
import { TokenLogo } from '@cowprotocol/tokens'
import { Command, UiOrderType } from '@cowprotocol/types'
import { HoverTooltip, Loader, PercentDisplay, percentIsAlmostHundred, TokenAmount, UI } from '@cowprotocol/ui'
import { HoverTooltip, Loader, PercentDisplay, percentIsAlmostHundred, TokenAmount } from '@cowprotocol/ui'
import { useIsSafeWallet } from '@cowprotocol/wallet'
import { Currency, CurrencyAmount, Percent, Price } from '@uniswap/sdk-core'
import { Currency, Price } from '@uniswap/sdk-core'

import { Check, Clock, X, Zap } from 'react-feather'
import SVG from 'react-inlinesvg'
import { Nullish } from 'types'

import { OrderStatus } from 'legacy/state/orders/actions'
import { getEstimatedExecutionPrice } from 'legacy/state/orders/utils'
Expand All @@ -22,18 +21,12 @@ import { PendingOrderPrices } from 'modules/orders/state/pendingOrdersPricesAtom
import { getIsEthFlowOrder } from 'modules/swap/containers/EthFlowStepper'
import { BalancesAndAllowances } from 'modules/tokens'

import {
FAIR_PRICE_THRESHOLD_PERCENTAGE,
GOOD_PRICE_THRESHOLD_PERCENTAGE,
PENDING_EXECUTION_THRESHOLD_PERCENTAGE,
} from 'common/constants/common'
import { PENDING_EXECUTION_THRESHOLD_PERCENTAGE } from 'common/constants/common'
import { useSafeMemo } from 'common/hooks/useSafeMemo'
import { RateInfo } from 'common/pure/RateInfo'
import { getQuoteCurrency } from 'common/services/getQuoteCurrency'
import { isOrderCancellable } from 'common/utils/isOrderCancellable'
import { calculatePercentageInRelationToReference } from 'utils/orderUtils/calculatePercentageInRelationToReference'
import { calculatePriceDifference, PriceDifference } from 'utils/orderUtils/calculatePriceDifference'
import { getIsComposableCowParentOrder } from 'utils/orderUtils/getIsComposableCowParentOrder'
import { calculatePriceDifference } from 'utils/orderUtils/calculatePriceDifference'
import { getIsFinalizedOrder } from 'utils/orderUtils/getIsFinalizedOrder'
import { getSellAmountWithFee } from 'utils/orderUtils/getSellAmountWithFee'
import { getUiOrderType } from 'utils/orderUtils/getUiOrderType'
Expand All @@ -43,41 +36,24 @@ import { EstimatedExecutionPrice } from './EstimatedExecutionPrice'
import { OrderContextMenu } from './OrderContextMenu'
import { WarningTooltip } from './OrderWarning'
import * as styledEl from './styled'
import { getActivityUrl, getDistanceColor, shouldShowDashForExpiration } from './utils'

import { OrderParams } from '../../../utils/getOrderParams'
import { getOrderParams } from '../../../utils/getOrderParams'
import { OrderStatusBox } from '../../OrderStatusBox'
import { CheckboxCheckmark, TableRow, TableRowCheckbox, TableRowCheckboxWrapper } from '../styled'
import { OrderActions } from '../types'
import { useFeeAmountDifference } from '../../hooks/useFeeAmountDifference'
import { usePricesDifference } from '../../hooks/usePricesDifference'
import { CurrencyAmountItem } from '../../pure/CurrencyAmountItem'
import {
CheckboxCheckmark,
TableRow,
TableRowCheckbox,
TableRowCheckboxWrapper,
} from '../../pure/OrdersTableContainer/styled'
import { OrderActions } from '../../pure/OrdersTableContainer/types'
import { OrderStatusBox } from '../../pure/OrderStatusBox'
import { getOrderParams, OrderParams } from '../../utils/getOrderParams'

// Constants
const TIME_AGO_UPDATE_INTERVAL = 3000

// Helper to determine the color based on percentage
function getDistanceColor(percentage: number): string {
const absPercentage = Math.abs(percentage)

if (absPercentage <= GOOD_PRICE_THRESHOLD_PERCENTAGE) {
return `var(${UI.COLOR_SUCCESS})` // Green - good price
} else if (absPercentage <= FAIR_PRICE_THRESHOLD_PERCENTAGE) {
return `var(${UI.COLOR_PRIMARY})` // Blue - fair price
}

return 'inherit' // Default text color for larger differences
}

function CurrencyAmountItem({ amount }: { amount: CurrencyAmount<Currency> }) {
return (
<styledEl.AmountItem title={amount.toExact() + ' ' + amount.currency.symbol}>
<TokenAmount amount={amount} tokenSymbol={amount.currency} />
</styledEl.AmountItem>
)
}

function CurrencySymbolItem({ amount }: { amount: CurrencyAmount<Currency> }) {
return <TokenLogo token={amount.currency} size={28} />
}

export interface OrderRowProps {
order: ParsedOrder
prices: PendingOrderPrices | undefined | null
Expand Down Expand Up @@ -183,32 +159,17 @@ export function OrderRow({
return 'Unfillable'
}

const renderWarningTooltip = (showIcon?: boolean) => (props: { children: React.ReactNode }) => (
const renderWarningTooltip = () => (props: { children: React.ReactNode }) => (
<WarningTooltip
hasEnoughBalance={hasEnoughBalance ?? false}
hasEnoughAllowance={hasEnoughAllowance ?? false}
inputTokenSymbol={inputTokenSymbol}
isOrderScheduled={isOrderScheduled}
onApprove={() => orderActions.approveOrderToken(order.inputToken)}
showIcon={showIcon}
{...props}
/>
)

const renderLimitPrice = () => (
<styledEl.RateValue onClick={toggleIsInverted}>
<RateInfo
prependSymbol={false}
isInvertedState={[isInverted, setIsInverted]}
noLabel={true}
doNotUseSmartQuote
isInverted={isInverted}
rateInfoParams={rateInfoParams}
opacitySymbol={true}
/>
</styledEl.RateValue>
)

const areAllChildOrdersCancelled = (orders: ParsedOrder[] | undefined): boolean => {
if (!orders || orders.length === 0) return false
return orders.every((order) => order.status === OrderStatus.CANCELLED)
Expand Down Expand Up @@ -249,7 +210,7 @@ export function OrderRow({
: 'Unfillable'
: getWarningText()
}
WarningTooltip={renderWarningTooltip(true)}
WarningTooltip={renderWarningTooltip()}
onApprove={
warningChildWithParams?.params?.hasEnoughAllowance === false
? () => orderActions.approveOrderToken(warningChildWithParams.order.inputToken)
Expand Down Expand Up @@ -457,7 +418,7 @@ export function OrderRow({
canShowWarning={getUiOrderType(order) !== UiOrderType.SWAP && !isUnfillable}
isUnfillable={withWarning}
warningText={getWarningText()}
WarningTooltip={renderWarningTooltip(true)}
WarningTooltip={renderWarningTooltip()}
onApprove={withAllowanceWarning ? () => orderActions.approveOrderToken(order.inputToken) : undefined}
/>
)}
Expand Down Expand Up @@ -645,7 +606,7 @@ export function OrderRow({
? 'Insufficient allowance'
: 'Unfillable'
}
WarningTooltip={renderWarningTooltip(true)}
WarningTooltip={renderWarningTooltip()}
onApprove={withAllowanceWarning ? () => orderActions.approveOrderToken(order.inputToken) : undefined}
/>
</styledEl.ExecuteCellWrapper>
Expand Down Expand Up @@ -745,19 +706,31 @@ export function OrderRow({
{/* Order sell/buy tokens */}
<styledEl.CurrencyCell>
<styledEl.CurrencyLogoPair clickable onClick={onClick}>
<CurrencySymbolItem amount={getSellAmountWithFee(order)} />
<CurrencySymbolItem amount={buyAmount} />
<TokenLogo token={order.inputToken} size={28} />
<TokenLogo token={buyAmount.currency} size={28} />
</styledEl.CurrencyLogoPair>
<styledEl.CurrencyAmountWrapper clickable onClick={onClick}>
<CurrencyAmountItem amount={getSellAmountWithFee(order)} />
<CurrencyAmountItem amount={buyAmount} />
</styledEl.CurrencyAmountWrapper>
</styledEl.CurrencyCell>

{/* Limit price */}
<styledEl.PriceElement onClick={toggleIsInverted}>
<RateInfo
prependSymbol={false}
isInvertedState={[isInverted, setIsInverted]}
noLabel={true}
doNotUseSmartQuote
isInverted={isInverted}
rateInfoParams={rateInfoParams}
opacitySymbol={true}
/>
</styledEl.PriceElement>

{/* Non-history tab columns */}
{!isHistoryTab ? (
<>
<styledEl.PriceElement onClick={toggleIsInverted}>{renderLimitPrice()}</styledEl.PriceElement>
<styledEl.PriceElement onClick={toggleIsInverted}>{renderFillsAtWithDistance()}</styledEl.PriceElement>
<styledEl.PriceElement onClick={toggleIsInverted}>{renderMarketPrice()}</styledEl.PriceElement>

Expand All @@ -780,18 +753,6 @@ export function OrderRow({
) : (
<>
{/* History tab columns */}
{/* Limit price */}
<styledEl.PriceElement onClick={toggleIsInverted}>
<RateInfo
prependSymbol={false}
isInvertedState={[isInverted, setIsInverted]}
noLabel={true}
doNotUseSmartQuote
isInverted={isInverted}
rateInfoParams={rateInfoParams}
opacitySymbol={true}
/>
</styledEl.PriceElement>

{/* Execution price */}
<styledEl.PriceElement onClick={toggleIsInverted}>
Expand Down Expand Up @@ -842,7 +803,7 @@ export function OrderRow({
order={order}
withWarning={withWarning}
onClick={onClick}
WarningTooltip={withWarning ? renderWarningTooltip(true) : undefined}
WarningTooltip={withWarning ? renderWarningTooltip() : undefined}
/>
</styledEl.StatusBox>
</styledEl.CellElement>
Expand All @@ -869,80 +830,3 @@ export function OrderRow({
</TableRow>
)
}

/**
* Helper hook to prepare the parameters to calculate price difference
*/
function usePricesDifference(
estimatedExecutionPrice: Nullish<Price<Currency, Currency>>,
spotPrice: OrderRowProps['spotPrice'],
isInverted: boolean,
): PriceDifference {
return useSafeMemo(
() =>
calculatePriceDifference({
referencePrice: spotPrice,
targetPrice: estimatedExecutionPrice,
isInverted,
}),
[estimatedExecutionPrice, spotPrice, isInverted],
)
}

/**
* Helper hook to calculate fee amount percentage
*/
function useFeeAmountDifference(
{ inputCurrencyAmount }: OrderRowProps['orderParams']['rateInfoParams'],
prices: OrderRowProps['prices'],
): Percent | undefined {
const { feeAmount } = prices || {}

return useSafeMemo(
() => calculatePercentageInRelationToReference({ value: feeAmount, reference: inputCurrencyAmount }),
[feeAmount, inputCurrencyAmount],
)
}

function getActivityUrl(chainId: SupportedChainId, order: ParsedOrder): string | undefined {
const { activityId } = order.executionData

if (getIsComposableCowParentOrder(order)) {
return undefined
}

if (order.composableCowInfo?.isVirtualPart) {
return undefined
}

if (order.status === OrderStatus.SCHEDULED) {
return undefined
}

return chainId && activityId ? getEtherscanLink(chainId, 'transaction', activityId) : undefined
}

function shouldShowDashForExpiration(order: ParsedOrder): boolean {
// Show dash for finalized orders that are not expired
if (getIsFinalizedOrder(order) && order.status !== OrderStatus.EXPIRED) {
return true
}

// For TWAP parent orders, show dash when all child orders are in a final state
if (getIsComposableCowParentOrder(order)) {
// If the parent order is fulfilled or cancelled, all child orders are finalized
if (order.status === OrderStatus.FULFILLED || order.status === OrderStatus.CANCELLED) {
return true
}

// For mixed states (some filled, some expired), check either condition:
// 1. fullyFilled: true when all non-expired parts are filled
// 2. status === EXPIRED: true when all remaining parts are expired
// Either condition indicates all child orders are in a final state
if (order.executionData.fullyFilled || order.status === OrderStatus.EXPIRED) {
return true
}
}

return false
}
Loading

0 comments on commit 220d233

Please sign in to comment.