diff --git a/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTableContainer/OrderRow/index.tsx b/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTableContainer/OrderRow/index.tsx index 28c433e3af..e78e8351d5 100644 --- a/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTableContainer/OrderRow/index.tsx +++ b/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTableContainer/OrderRow/index.tsx @@ -3,15 +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 { getAddress, getEtherscanLink, formatDateWithTimezone } 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 { UI, TokenAmount, Loader, HoverTooltip } from '@cowprotocol/ui' +import { PercentDisplay, percentIsAlmostHundred } from '@cowprotocol/ui' import { useIsSafeWallet } from '@cowprotocol/wallet' import { Currency, CurrencyAmount, Percent, Price } from '@uniswap/sdk-core' -import { Check, Clock, X, Zap } from 'react-feather' +import { Clock, Zap, Check, X } from 'react-feather' import SVG from 'react-inlinesvg' import { OrderStatus } from 'legacy/state/orders/actions' @@ -20,9 +21,9 @@ import { PendingOrderPrices } from 'modules/orders/state/pendingOrdersPricesAtom import { getIsEthFlowOrder } from 'modules/swap/containers/EthFlowStepper' import { - FAIR_PRICE_THRESHOLD_PERCENTAGE, - GOOD_PRICE_THRESHOLD_PERCENTAGE, PENDING_EXECUTION_THRESHOLD_PERCENTAGE, + GOOD_PRICE_THRESHOLD_PERCENTAGE, + FAIR_PRICE_THRESHOLD_PERCENTAGE, } from 'common/constants/common' import { useSafeMemo } from 'common/hooks/useSafeMemo' import { RateInfo } from 'common/pure/RateInfo' @@ -88,6 +89,7 @@ export interface OrderRowProps { onClick: Command orderActions: OrderActions children?: React.ReactNode + childOrders?: ParsedOrder[] isTwapTable?: boolean } @@ -105,6 +107,7 @@ export function OrderRow({ prices, spotPrice, children, + childOrders, isTwapTable, }: OrderRowProps) { const { buyAmount, rateInfoParams, hasEnoughAllowance, hasEnoughBalance, chainId } = orderParams @@ -170,19 +173,17 @@ export function OrderRow({ return 'Unfillable' } - const renderWarningTooltip = - (showIcon?: boolean) => - ({ children }: { children: React.ReactNode }) => ( - orderActions.approveOrderToken(order.inputToken)} - showIcon={showIcon} - children={children} - /> - ) + const renderWarningTooltip = (showIcon?: boolean) => (props: { children: React.ReactNode }) => ( + orderActions.approveOrderToken(order.inputToken)} + showIcon={showIcon} + {...props} + /> + ) const renderLimitPrice = () => ( @@ -198,19 +199,37 @@ export function OrderRow({ ) + const areAllChildOrdersCancelled = (orders: ParsedOrder[] | undefined): boolean => { + if (!orders || orders.length === 0) return false + return orders.every((order) => order.status === OrderStatus.CANCELLED) + } + const renderFillsAt = () => ( <> {getIsFinalizedOrder(order) ? ( - order.status === OrderStatus.CANCELLED ? ( - - - Order cancelled - - ) : order.status === OrderStatus.FULFILLED ? ( + order.executionData.partiallyFilled || order.status === OrderStatus.FULFILLED ? ( Order {order.partiallyFillable && Number(filledPercentDisplay) < 100 ? 'partially ' : ''}filled + ) : order.status === OrderStatus.CANCELLED ? ( + // For TWAP parent orders, show cancelled only when ALL child orders are cancelled + children ? ( + childOrders && areAllChildOrdersCancelled(childOrders) ? ( + + + Order cancelled + + ) : ( + '-' + ) + ) : ( + // For non-TWAP orders and TWAP child orders, show cancelled normally + + + Order cancelled + + ) ) : order.status === OrderStatus.EXPIRED ? ( @@ -291,24 +310,56 @@ export function OrderRow({ return renderFillsAt() } - // For TWAP parent orders, show the next scheduled child order's fills at price - if (children) { - // Get the next scheduled order from the children prop - const childrenArray = React.Children.toArray(children) as React.ReactElement<{ order: ParsedOrder }>[] - const nextScheduledOrder = childrenArray - .map((child) => child.props.order) - .find((childOrder) => { - return childOrder && childOrder.status === OrderStatus.SCHEDULED && !getIsFinalizedOrder(childOrder) - }) + // Handle warning states first, regardless of order type + if (withWarning) { + return ( + + orderActions.approveOrderToken(order.inputToken) : undefined} + /> + + ) + } + + // For TWAP parent orders + if (children && childOrders) { + // Check if all child orders are cancelled first + if (areAllChildOrdersCancelled(childOrders)) { + return ( + + + + + Order cancelled + + + + + ) + } + + const nextScheduledOrder = childOrders.find( + (childOrder) => childOrder.status === OrderStatus.SCHEDULED && !getIsFinalizedOrder(childOrder), + ) if (nextScheduledOrder) { - // Get the execution price from the next scheduled order - const nextOrderExecutionPrice = nextScheduledOrder.executionData.executedPrice - const nextOrderPriceDiffs = calculatePriceDifference({ - referencePrice: spotPrice, - targetPrice: nextOrderExecutionPrice, - isInverted: false, - }) + // For scheduled orders, use the execution price if available, otherwise use the estimated price from props + const nextOrderExecutionPrice = + nextScheduledOrder.executionData.executedPrice || prices?.estimatedExecutionPrice + const nextOrderPriceDiffs = nextOrderExecutionPrice + ? calculatePriceDifference({ + referencePrice: spotPrice, + targetPrice: nextOrderExecutionPrice, + isInverted: false, + }) + : null // Show the execution price for the next scheduled order let nextOrderFillsAtContent @@ -319,7 +370,7 @@ export function OrderRow({ } else { nextOrderFillsAtContent = ( @@ -381,13 +432,31 @@ export function OrderRow({ ) } - const renderMarketPrice = () => ( - <> - {children ? ( - '-' - ) : order.status === OrderStatus.CANCELLED || withWarning || order.status === OrderStatus.PRESIGNATURE_PENDING ? ( - '-' - ) : spotPrice ? ( + const renderMarketPrice = () => { + // Early return for warning states and non-active orders + if ( + withWarning || + order.status === OrderStatus.CREATING || + order.status === OrderStatus.PRESIGNATURE_PENDING || + getIsFinalizedOrder(order) + ) { + return '-' + } + + // Check children finalization status + if (children && childOrders) { + if (childOrders.every((childOrder) => getIsFinalizedOrder(childOrder))) { + return '-' + } + } + + // Handle spot price cases + if (spotPrice === null) { + return '-' + } + + if (spotPrice) { + return ( - ) : spotPrice === null ? ( - '-' - ) : ( - - )} - - ) + ) + } + + return + } return ( orderActions.selectReceiptOrder(parent)} isExpanded={!isCollapsed} + childOrders={children} > {isParentSigning ? undefined : ( ` +export const PageWrapper = styled.div<{ + isUnlocked: boolean + secondaryOnLeft?: boolean + maxWidth?: string + hideOrdersTable?: boolean +}>` width: 100%; display: grid; max-width: ${({ maxWidth = DEFAULT_MAX_WIDTH }) => maxWidth}; margin: 0 auto; grid-template-columns: 1fr; - grid-template-rows: auto auto; - grid-template-areas: 'primary' 'secondary'; + grid-template-rows: auto; + grid-template-areas: ${({ hideOrdersTable }) => (hideOrdersTable ? '"primary"' : '"primary" "secondary"')}; gap: 20px; ${Media.LargeAndUp()} { - grid-template-columns: ${({ isUnlocked, secondaryOnLeft }) => - isUnlocked + grid-template-columns: ${({ isUnlocked, hideOrdersTable, secondaryOnLeft }) => + isUnlocked && !hideOrdersTable ? secondaryOnLeft ? '1fr minmax(auto, ' + WIDGET_MAX_WIDTH.swap.replace('px', '') + 'px)' : 'minmax(auto, ' + WIDGET_MAX_WIDTH.swap.replace('px', '') + 'px) 1fr' : '1fr'}; grid-template-rows: 1fr; - grid-template-areas: ${({ secondaryOnLeft }) => (secondaryOnLeft ? '"secondary primary"' : '"primary secondary"')}; + grid-template-areas: ${({ secondaryOnLeft, hideOrdersTable }) => + hideOrdersTable ? '"primary"' : secondaryOnLeft ? '"secondary primary"' : '"primary secondary"'}; } > div:last-child { - display: ${({ isUnlocked }) => (isUnlocked ? '' : 'none')}; + display: ${({ isUnlocked }) => (!isUnlocked ? 'none' : '')}; } ` diff --git a/apps/cowswap-frontend/src/pages/AdvancedOrders/index.tsx b/apps/cowswap-frontend/src/pages/AdvancedOrders/index.tsx index 7c629bf4f4..c5b28c7a60 100644 --- a/apps/cowswap-frontend/src/pages/AdvancedOrders/index.tsx +++ b/apps/cowswap-frontend/src/pages/AdvancedOrders/index.tsx @@ -51,6 +51,7 @@ export default function AdvancedOrdersPage() { isUnlocked={isUnlocked} maxWidth={ADVANCED_ORDERS_MAX_WIDTH} secondaryOnLeft={ordersTableOnLeft} + hideOrdersTable={hideOrdersTable} > {isFallbackHandlerRequired && pendingOrders.length > 0 && } @@ -69,15 +70,15 @@ export default function AdvancedOrdersPage() { - - {!hideOrdersTable && ( + {!hideOrdersTable && ( + - )} - + + )} ) diff --git a/apps/cowswap-frontend/src/pages/LimitOrders/RegularLimitOrders.tsx b/apps/cowswap-frontend/src/pages/LimitOrders/RegularLimitOrders.tsx index 2254eb869b..24f37d3b9a 100644 --- a/apps/cowswap-frontend/src/pages/LimitOrders/RegularLimitOrders.tsx +++ b/apps/cowswap-frontend/src/pages/LimitOrders/RegularLimitOrders.tsx @@ -20,20 +20,25 @@ export function RegularLimitOrders() { const { ordersTableOnLeft } = useAtomValue(limitOrdersSettingsAtom) return ( - + - - {!hideOrdersTable && ( + {!hideOrdersTable && ( + - )} - + + )} ) }