diff --git a/apps/cowswap-frontend/src/common/hooks/useConvertUsdToTokenValue.ts b/apps/cowswap-frontend/src/common/hooks/useConvertUsdToTokenValue.ts
new file mode 100644
index 0000000000..aa5d4c7cc6
--- /dev/null
+++ b/apps/cowswap-frontend/src/common/hooks/useConvertUsdToTokenValue.ts
@@ -0,0 +1,33 @@
+import { TokenWithLogo, USDC } from '@cowprotocol/common-const'
+import { getWrappedToken, tryParseCurrencyAmount } from '@cowprotocol/common-utils'
+import { SupportedChainId } from '@cowprotocol/cow-sdk'
+import { Currency, CurrencyAmount } from '@uniswap/sdk-core'
+
+import { useUsdPrice } from 'modules/usdAmount'
+
+export function useConvertUsdToTokenValue(
+ currency: Currency | null,
+): (typedValue: string, isUsdMode: boolean) => string {
+ const currencyUsdcPrice = useUsdPrice(currency ? getWrappedToken(currency) : null)
+
+ return (typedValue: string, isUsdMode: boolean) => {
+ if (isUsdMode && currencyUsdcPrice?.price) {
+ const usdcToken = USDC[currencyUsdcPrice.currency.chainId as SupportedChainId]
+ const usdAmount = tryParseCurrencyAmount(typedValue, usdcToken)
+
+ const tokenAmount = currencyUsdcPrice.price.invert().quote(hackyAdjustAmountDust(usdAmount))
+
+ return tokenAmount.toExact()
+ }
+
+ return typedValue
+ }
+}
+
+/**
+ * TODO: this is a hacky way to adjust the amount to avoid dust
+ * For some reason, when you enter for example $366, price.quote() returns 365,9999999999
+ */
+function hackyAdjustAmountDust(amount: CurrencyAmount): typeof amount {
+ return amount.add(tryParseCurrencyAmount('0.000001', amount.currency))
+}
diff --git a/apps/cowswap-frontend/src/common/pure/CurrencyInputPanel/CurrencyInputPanel.tsx b/apps/cowswap-frontend/src/common/pure/CurrencyInputPanel/CurrencyInputPanel.tsx
index 9020f7c004..5074642baa 100644
--- a/apps/cowswap-frontend/src/common/pure/CurrencyInputPanel/CurrencyInputPanel.tsx
+++ b/apps/cowswap-frontend/src/common/pure/CurrencyInputPanel/CurrencyInputPanel.tsx
@@ -6,14 +6,13 @@ import { SupportedChainId } from '@cowprotocol/cow-sdk'
import { TokenAmount, HoverTooltip } from '@cowprotocol/ui'
import { Currency, CurrencyAmount } from '@uniswap/sdk-core'
-import { Trans } from '@lingui/macro'
-
import { BalanceAndSubsidy } from 'legacy/hooks/useCowBalanceAndSubsidy'
import { PriceImpact } from 'legacy/hooks/usePriceImpact'
import { Field } from 'legacy/state/types'
import { setMaxSellTokensAnalytics } from 'modules/analytics'
import { ReceiveAmount } from 'modules/swap/pure/ReceiveAmount'
+import { useUsdAmount } from 'modules/usdAmount'
import { CurrencyInfo } from 'common/pure/CurrencyInputPanel/types'
import { CurrencySelectButton } from 'common/pure/CurrencySelectButton'
@@ -21,6 +20,8 @@ import { FiatValue } from 'common/pure/FiatValue'
import * as styledEl from './styled'
+import { useConvertUsdToTokenValue } from '../../hooks/useConvertUsdToTokenValue'
+
interface BuiltItProps {
className: string
}
@@ -84,26 +85,47 @@ export function CurrencyInputPanel(props: CurrencyInputPanelProps) {
customSelectTokenButton,
} = props
- const { field, currency, balance, fiatAmount, amount, isIndependent, receiveAmountInfo } = currencyInfo
+ const {
+ field,
+ currency,
+ balance,
+ fiatAmount,
+ amount,
+ isIndependent,
+ receiveAmountInfo,
+ isUsdValuesMode = false,
+ } = currencyInfo
const disabled = !!props.disabled || isChainIdUnsupported
- const viewAmount = formatInputAmount(amount, balance, isIndependent)
+
+ const { value: usdAmount } = useUsdAmount(amount)
+ const { value: maxBalanceUsdAmount } = useUsdAmount(maxBalance)
+ const { value: balanceUsdAmount } = useUsdAmount(balance)
+ const viewAmount = isUsdValuesMode ? formatInputAmount(usdAmount) : formatInputAmount(amount, balance, isIndependent)
const [typedValue, setTypedValue] = useState(viewAmount)
+ const convertUsdToTokenValue = useConvertUsdToTokenValue(currency)
+
const onUserInputDispatch = useCallback(
(typedValue: string) => {
- setTypedValue(typedValue)
- onUserInput(field, typedValue)
+ const value = convertUsdToTokenValue(typedValue, isUsdValuesMode)
+
+ setTypedValue(value)
+ onUserInput(field, value)
},
- [onUserInput, field],
+ [onUserInput, field, viewAmount, convertUsdToTokenValue, isUsdValuesMode],
)
const handleMaxInput = useCallback(() => {
if (!maxBalance) {
return
}
- onUserInputDispatch(maxBalance.toExact())
- setMaxSellTokensAnalytics()
- }, [maxBalance, onUserInputDispatch])
+ const value = isUsdValuesMode ? maxBalanceUsdAmount : maxBalance
+
+ if (value) {
+ onUserInputDispatch(value.toExact())
+ setMaxSellTokensAnalytics()
+ }
+ }, [maxBalance, onUserInputDispatch, convertUsdToTokenValue, isUsdValuesMode, maxBalanceUsdAmount])
useEffect(() => {
const areValuesSame = parseFloat(viewAmount) === parseFloat(typedValue)
@@ -128,13 +150,31 @@ export function CurrencyInputPanel(props: CurrencyInputPanelProps) {
const numericalInput = (
)
+ const balanceView = (
+
+ {balance && !disabled && (
+
+ {isUsdValuesMode ? (
+
+ ) : (
+
+ )}
+ {showSetMax && balance.greaterThan(0) && (
+ Max
+ )}
+
+ )}
+
+ )
+
const priceImpactParams: typeof _priceImpactParams = useMemo(() => {
if (!_priceImpactParams) return undefined
@@ -159,10 +199,28 @@ export function CurrencyInputPanel(props: CurrencyInputPanelProps) {
pointerDisabled={disabled}
readOnly={inputDisabled}
>
- {topLabel && {topLabel} }
+
+ {topLabel && (
+
+ {topLabel}{' '}
+ {isUsdValuesMode ? : ''}
+
+ )}
+
+ {isUsdValuesMode && balanceView}
+
{topContent}
+
+ {inputTooltip ? (
+
+ {numericalInput}
+
+ ) : (
+ numericalInput
+ )}
+
-
- {inputTooltip ? (
-
- {numericalInput}
-
- ) : (
- numericalInput
- )}
-
- {balance && !disabled && (
-
- Balance :
- {showSetMax && balance.greaterThan(0) && (
- Max
- )}
-
- )}
-
-
- {amount && (
+ {amount && !isUsdValuesMode && (
)}
+ {!isUsdValuesMode && balanceView}
diff --git a/apps/cowswap-frontend/src/common/pure/CurrencyInputPanel/styled.tsx b/apps/cowswap-frontend/src/common/pure/CurrencyInputPanel/styled.tsx
index 81d5492220..2df15709f0 100644
--- a/apps/cowswap-frontend/src/common/pure/CurrencyInputPanel/styled.tsx
+++ b/apps/cowswap-frontend/src/common/pure/CurrencyInputPanel/styled.tsx
@@ -72,6 +72,15 @@ export const CurrencyTopLabel = styled.div`
}
`
+export const TopRow = styled.div`
+ display: flex;
+ flex-flow: row wrap;
+ align-items: center;
+ justify-content: space-between;
+ gap: 16px;
+ width: 100%;
+`
+
export const NumericalInput = styled(Input)<{ $loading: boolean }>`
width: 100%;
height: 100%;
@@ -81,6 +90,7 @@ export const NumericalInput = styled(Input)<{ $loading: boolean }>`
font-size: 28px;
font-weight: 500;
color: inherit;
+ text-align: left;
&::placeholder {
opacity: 0.7;
@@ -106,7 +116,7 @@ export const TokenAmountStyled = styled(TokenAmount)`
export const BalanceText = styled.span`
font-weight: inherit;
- font-size: inherit;
+ font-size: 13px;
gap: 5px;
display: flex;
align-items: center;
@@ -150,7 +160,9 @@ export const SetMaxBtn = styled.button`
border-radius: 6px;
padding: 3px 4px;
text-transform: uppercase;
- transition: background var(${UI.ANIMATION_DURATION}) ease-in-out, color var(${UI.ANIMATION_DURATION}) ease-in-out;
+ transition:
+ background var(${UI.ANIMATION_DURATION}) ease-in-out,
+ color var(${UI.ANIMATION_DURATION}) ease-in-out;
&:hover {
background: var(${UI.COLOR_PRIMARY});
diff --git a/apps/cowswap-frontend/src/common/pure/CurrencyInputPanel/types.ts b/apps/cowswap-frontend/src/common/pure/CurrencyInputPanel/types.ts
index 076ecfb355..079c5e532a 100644
--- a/apps/cowswap-frontend/src/common/pure/CurrencyInputPanel/types.ts
+++ b/apps/cowswap-frontend/src/common/pure/CurrencyInputPanel/types.ts
@@ -16,4 +16,5 @@ export interface CurrencyInfo {
balance: CurrencyAmount | null
fiatAmount: CurrencyAmount | null
topContent?: ReactNode
+ isUsdValuesMode?: boolean
}
diff --git a/apps/cowswap-frontend/src/common/pure/OrderExecutionStatusList/RateTooltipHeader.tsx b/apps/cowswap-frontend/src/common/pure/OrderExecutionStatusList/RateTooltipHeader.tsx
deleted file mode 100644
index b179c8bad8..0000000000
--- a/apps/cowswap-frontend/src/common/pure/OrderExecutionStatusList/RateTooltipHeader.tsx
+++ /dev/null
@@ -1,54 +0,0 @@
-import styled from 'styled-components/macro'
-
-import { OrderExecutionStatusList } from './index'
-
-const Content = styled.div`
- display: flex;
- flex-flow: column wrap;
- align-items: center;
- justify-content: center;
- width: 100%;
- padding: 5px 10px;
- color: inherit;
-
- > p {
- font-size: 13px;
- font-weight: 400;
- line-height: 1.5;
- padding: 0;
- margin: 0;
- color: inherit;
- }
-
- > h3 {
- font-size: 14px;
- font-weight: 600;
- margin: 21px 0 0;
- padding: 0;
- text-align: left;
- width: 100%;
- color: inherit;
- }
-`
-
-interface RateTooltipHeaderProps {
- isOpenOrdersTab?: boolean
-}
-
-export function RateTooltipHeader({ isOpenOrdersTab }: RateTooltipHeaderProps) {
- return (
-
-
- Network costs (incl. gas) are covered by filling your order when the market price is better than your limit
- price.
-
-
- {isOpenOrdersTab && (
- <>
- How close is my order to executing?
-
- >
- )}
-
- )
-}
diff --git a/apps/cowswap-frontend/src/common/pure/OrderExecutionStatusList/calculateOrderExecutionStatus.test.ts b/apps/cowswap-frontend/src/common/pure/OrderExecutionStatusList/calculateOrderExecutionStatus.test.ts
deleted file mode 100644
index 8927078786..0000000000
--- a/apps/cowswap-frontend/src/common/pure/OrderExecutionStatusList/calculateOrderExecutionStatus.test.ts
+++ /dev/null
@@ -1,42 +0,0 @@
-import { Percent } from '@uniswap/sdk-core'
-
-import { calculateOrderExecutionStatus } from './index'
-
-describe('calculateOrderExecutionStatus', () => {
- it('returns `undefined` when any parameter is missing', () => {
- expect(calculateOrderExecutionStatus(undefined)).toBe(undefined)
- })
-
- describe('veryClose', () => {
- test('-1%', () => {
- expect(calculateOrderExecutionStatus(new Percent(-1, 100))).toBe('veryClose')
- })
- test('0.1%', () => {
- expect(calculateOrderExecutionStatus(new Percent(1, 1_000))).toBe('veryClose')
- })
- test('0.49%', () => {
- expect(calculateOrderExecutionStatus(new Percent(49, 100_000))).toBe('veryClose')
- })
- })
-
- describe('close', () => {
- test('0.5%', () => {
- expect(calculateOrderExecutionStatus(new Percent(5, 1_000))).toBe('close')
- })
- test('1%', () => {
- expect(calculateOrderExecutionStatus(new Percent(1, 100))).toBe('close')
- })
- test('5%', () => {
- expect(calculateOrderExecutionStatus(new Percent(5, 100))).toBe('close')
- })
- })
-
- describe('notClose', () => {
- test('5.01%', () => {
- expect(calculateOrderExecutionStatus(new Percent(501, 10_000))).toBe('notClose')
- })
- test('10%', () => {
- expect(calculateOrderExecutionStatus(new Percent(1, 10))).toBe('notClose')
- })
- })
-})
diff --git a/apps/cowswap-frontend/src/common/pure/OrderExecutionStatusList/index.tsx b/apps/cowswap-frontend/src/common/pure/OrderExecutionStatusList/index.tsx
deleted file mode 100644
index bae8e6ac94..0000000000
--- a/apps/cowswap-frontend/src/common/pure/OrderExecutionStatusList/index.tsx
+++ /dev/null
@@ -1,78 +0,0 @@
-import { UI } from '@cowprotocol/ui'
-import { Percent } from '@uniswap/sdk-core'
-
-import styled from 'styled-components/macro'
-
-const StatusList = styled.ol`
- display: flex;
- flex-flow: row wrap;
- list-style: none;
- font-size: 12px;
- font-weight: 400;
- gap: 5px;
- padding: 0;
-
- > li {
- display: flex;
- gap: 5px;
- align-items: center;
- width: 100%;
- }
-`
-
-const LOWER_PERCENTAGE_DIFFERENCE = new Percent(5, 1000) // 0.5%
-const UPPER_PERCENTAGE_DIFFERENCE = new Percent(5, 100) // 5%
-export type OrderExecutionStatus = 'notClose' | 'close' | 'veryClose'
-
-export function calculateOrderExecutionStatus(difference: Percent | undefined): OrderExecutionStatus | undefined {
- if (!difference) {
- return undefined
- }
-
- if (difference.lessThan(LOWER_PERCENTAGE_DIFFERENCE)) {
- return 'veryClose'
- } else if (difference.greaterThan(UPPER_PERCENTAGE_DIFFERENCE)) {
- return 'notClose'
- } else {
- return 'close'
- }
-}
-
-export const ExecuteIndicator = styled.div<{ status?: OrderExecutionStatus }>`
- --size: 6px;
- width: var(--size);
- height: var(--size);
- min-width: var(--size);
- min-height: var(--size);
- border-radius: var(--size);
- display: block;
- margin: 0 3px 0 0;
- background: ${({ status, theme }) => {
- switch (status) {
- case 'veryClose':
- return theme.success
- case 'close':
- return theme.info
- case 'notClose':
- default:
- return `var(${UI.COLOR_TEXT})`
- }
- }};
-`
-
-export function OrderExecutionStatusList() {
- return (
-
-
- Very close (<0.5% from market price)
-
-
- Close (0.5% - 5% from market price)
-
-
- Not yet close (>5% from market price)
-
-
- )
-}
-export { RateTooltipHeader } from './RateTooltipHeader'
diff --git a/apps/cowswap-frontend/src/modules/advancedOrders/containers/AdvancedOrdersSettings/index.tsx b/apps/cowswap-frontend/src/modules/advancedOrders/containers/AdvancedOrdersSettings/index.tsx
index 96f9769fc1..62d6a56521 100644
--- a/apps/cowswap-frontend/src/modules/advancedOrders/containers/AdvancedOrdersSettings/index.tsx
+++ b/apps/cowswap-frontend/src/modules/advancedOrders/containers/AdvancedOrdersSettings/index.tsx
@@ -1,10 +1,11 @@
import { useAtomValue, useSetAtom } from 'jotai'
import { useCallback } from 'react'
-import { Menu, MenuItem } from '@reach/menu-button'
+import { Menu, MenuItem, MenuPopover, MenuItems } from '@reach/menu-button'
+import styled from 'styled-components/macro'
import { Settings } from 'modules/advancedOrders/pure/Settings'
-import { MenuContent, SettingsButton, SettingsIcon } from 'modules/trade/pure/Settings'
+import { SettingsButton, SettingsIcon } from 'modules/trade/pure/Settings'
import {
advancedOrdersSettingsAtom,
@@ -12,6 +13,15 @@ import {
updateAdvancedOrdersSettingsAtom,
} from '../../state/advancedOrdersSettingsAtom'
+const MenuWrapper = styled.div`
+ [data-reach-menu-popover] {
+ position: absolute;
+ width: 100%;
+ left: 0;
+ top: 0;
+ }
+`
+
export function AdvancedOrdersSettings() {
const settingsState = useAtomValue(advancedOrdersSettingsAtom)
const updateSettingsState = useSetAtom(updateAdvancedOrdersSettingsAtom)
@@ -20,19 +30,23 @@ export function AdvancedOrdersSettings() {
(state: Partial) => {
updateSettingsState(state)
},
- [updateSettingsState]
+ [updateSettingsState],
)
return (
-
-
-
-
-
- void 0}>
-
-
-
-
+
+
+
+
+
+
+
+ void 0}>
+
+
+
+
+
+
)
}
diff --git a/apps/cowswap-frontend/src/modules/advancedOrders/pure/Settings/index.tsx b/apps/cowswap-frontend/src/modules/advancedOrders/pure/Settings/index.tsx
index 51ec93bc1f..77f295a4b1 100644
--- a/apps/cowswap-frontend/src/modules/advancedOrders/pure/Settings/index.tsx
+++ b/apps/cowswap-frontend/src/modules/advancedOrders/pure/Settings/index.tsx
@@ -1,6 +1,14 @@
-import { AdvancedOrdersSettingsState } from 'modules/advancedOrders/state/advancedOrdersSettingsAtom'
+import { useAtomValue, useSetAtom } from 'jotai'
+
+import {
+ limitOrdersSettingsAtom,
+ updateLimitOrdersSettingsAtom,
+} from 'modules/limitOrders/state/limitOrdersSettingsAtom'
+import { ORDERS_TABLE_SETTINGS } from 'modules/trade/const/common'
import { SettingsBox, SettingsContainer, SettingsTitle } from 'modules/trade/pure/Settings'
+import { AdvancedOrdersSettingsState } from '../../state/advancedOrdersSettingsAtom'
+
export interface SettingsProps {
state: AdvancedOrdersSettingsState
onStateChanged: (state: Partial) => void
@@ -8,10 +16,12 @@ export interface SettingsProps {
export function Settings({ state, onStateChanged }: SettingsProps) {
const { showRecipient } = state
+ const limitOrdersSettings = useAtomValue(limitOrdersSettingsAtom)
+ const updateLimitOrdersSettings = useSetAtom(updateLimitOrdersSettingsAtom)
return (
- Interface Settings
+ Advanced Order Settings
onStateChanged({ showRecipient: !showRecipient })}
/>
+
+ updateLimitOrdersSettings({ ordersTableOnLeft: !limitOrdersSettings.ordersTableOnLeft })}
+ />
)
}
diff --git a/apps/cowswap-frontend/src/modules/limitOrders/containers/LimitOrdersWidget/index.tsx b/apps/cowswap-frontend/src/modules/limitOrders/containers/LimitOrdersWidget/index.tsx
index a2420948e3..3a8970856b 100644
--- a/apps/cowswap-frontend/src/modules/limitOrders/containers/LimitOrdersWidget/index.tsx
+++ b/apps/cowswap-frontend/src/modules/limitOrders/containers/LimitOrdersWidget/index.tsx
@@ -78,7 +78,7 @@ export function LimitOrdersWidget() {
const widgetActions = useLimitOrdersWidgetActions()
const isWrapOrUnwrap = useIsWrapOrUnwrap()
- const { showRecipient: showRecipientSetting } = settingsState
+ const { showRecipient: showRecipientSetting, isUsdValuesMode } = settingsState
const showRecipient = showRecipientSetting || !!recipient
const priceImpact = useTradePriceImpact()
@@ -91,13 +91,14 @@ export function LimitOrdersWidget() {
const inputCurrencyInfo: CurrencyInfo = {
field: Field.INPUT,
- label: isSell ? 'Sell amount' : 'You sell at most',
+ label: isSell ? 'Sell' : 'You sell at most',
currency: inputCurrency,
amount: inputCurrencyAmount,
isIndependent: isSell,
balance: inputCurrencyBalance,
fiatAmount: inputCurrencyFiatAmount,
receiveAmountInfo: null,
+ isUsdValuesMode,
}
const outputCurrencyInfo: CurrencyInfo = {
field: Field.OUTPUT,
@@ -108,6 +109,7 @@ export function LimitOrdersWidget() {
balance: outputCurrencyBalance,
fiatAmount: outputCurrencyFiatAmount,
receiveAmountInfo: null,
+ isUsdValuesMode,
}
const props: LimitOrdersProps = {
@@ -176,7 +178,7 @@ const LimitOrders = React.memo((props: LimitOrdersProps) => {
handleUnlock={() => updateLimitOrdersState({ isUnlocked: true })}
/>
),
- middleContent: (
+ topContent: (
<>
{!isWrapOrUnwrap &&
ClosableBanner(ZERO_BANNER_STORAGE_KEY, (onClose) => (
@@ -194,17 +196,33 @@ const LimitOrders = React.memo((props: LimitOrdersProps) => {
))}
-
-
-
-
+ {props.settingsState.limitPricePosition === 'top' && (
+
+
+
+ )}
+ >
+ ),
+ middleContent: (
+ <>
+ {props.settingsState.limitPricePosition === 'between' && (
+
+
+
+ )}
>
),
bottomContent(warnings) {
return (
<>
+ {props.settingsState.limitPricePosition === 'bottom' && (
+
+
+
+ )}
-
+
+
@@ -220,7 +238,7 @@ const LimitOrders = React.memo((props: LimitOrdersProps) => {
}
const params = {
- compactView: false,
+ compactView: true,
recipient,
showRecipient,
isTradePriceUpdating: isRateLoading,
diff --git a/apps/cowswap-frontend/src/modules/limitOrders/containers/LimitOrdersWidget/styled.tsx b/apps/cowswap-frontend/src/modules/limitOrders/containers/LimitOrdersWidget/styled.tsx
index c23762aa09..1c0c34d7df 100644
--- a/apps/cowswap-frontend/src/modules/limitOrders/containers/LimitOrdersWidget/styled.tsx
+++ b/apps/cowswap-frontend/src/modules/limitOrders/containers/LimitOrdersWidget/styled.tsx
@@ -1,9 +1,7 @@
-import { Media } from '@cowprotocol/ui'
+import { Media, UI } from '@cowprotocol/ui'
import styled from 'styled-components/macro'
-import { NumericalInput } from 'modules/limitOrders/containers/RateInput/styled'
-
export const TradeButtonBox = styled.div`
margin: 10px 0 0;
display: flex;
@@ -20,20 +18,18 @@ export const FooterBox = styled.div`
`
export const RateWrapper = styled.div`
- display: grid;
- grid-template-columns: auto 151px;
- grid-template-rows: max-content;
+ display: flex;
+ flex-flow: column wrap;
+ width: 100%;
max-width: 100%;
gap: 6px;
text-align: right;
color: inherit;
+ background: var(${UI.COLOR_PAPER_DARKER});
+ border-radius: 16px;
${Media.upToSmall()} {
display: flex;
flex-flow: column wrap;
}
-
- ${NumericalInput} {
- font-size: 21px;
- }
`
diff --git a/apps/cowswap-frontend/src/modules/limitOrders/containers/RateInput/hooks/useExecutionPriceUsdValue.ts b/apps/cowswap-frontend/src/modules/limitOrders/containers/RateInput/hooks/useExecutionPriceUsdValue.ts
new file mode 100644
index 0000000000..53d9e68a2a
--- /dev/null
+++ b/apps/cowswap-frontend/src/modules/limitOrders/containers/RateInput/hooks/useExecutionPriceUsdValue.ts
@@ -0,0 +1,23 @@
+import { useAtomValue } from 'jotai/index'
+
+import { tryParseCurrencyAmount } from '@cowprotocol/common-utils'
+import { Currency, Price } from '@uniswap/sdk-core'
+
+import { useUsdAmount } from 'modules/usdAmount'
+
+import { limitRateAtom } from '../../../state/limitRateAtom'
+
+export function useExecutionPriceUsdValue(executionPrice: Price | null) {
+ const { isInverted } = useAtomValue(limitRateAtom)
+ const executionPriceToDisplay = isInverted ? executionPrice?.invert() : executionPrice
+ const executionPriceBaseAmount = tryParseCurrencyAmount(
+ '1',
+ isInverted ? executionPrice?.quoteCurrency : executionPrice?.baseCurrency,
+ )
+
+ const { value: rateAsUsdAmount } = useUsdAmount(
+ executionPriceBaseAmount ? executionPriceToDisplay?.quote(executionPriceBaseAmount) : null,
+ )
+
+ return rateAsUsdAmount
+}
diff --git a/apps/cowswap-frontend/src/modules/limitOrders/containers/RateInput/hooks/useRateDisplayedValue.ts b/apps/cowswap-frontend/src/modules/limitOrders/containers/RateInput/hooks/useRateDisplayedValue.ts
new file mode 100644
index 0000000000..63cbe2d99a
--- /dev/null
+++ b/apps/cowswap-frontend/src/modules/limitOrders/containers/RateInput/hooks/useRateDisplayedValue.ts
@@ -0,0 +1,36 @@
+import { useAtomValue } from 'jotai/index'
+import { useMemo } from 'react'
+
+import { formatInputAmount, tryParseCurrencyAmount } from '@cowprotocol/common-utils'
+import { Currency } from '@uniswap/sdk-core'
+
+import { useUsdAmount } from 'modules/usdAmount'
+
+import { useLimitOrdersDerivedState } from '../../../hooks/useLimitOrdersDerivedState'
+import { limitRateAtom } from '../../../state/limitRateAtom'
+
+export function useRateDisplayedValue(currency: Currency | null, isUsdMode: boolean): string {
+ const { isInverted, activeRate, typedValue, isTypedValue } = useAtomValue(limitRateAtom)
+ const { inputCurrency, outputCurrency } = useLimitOrdersDerivedState()
+ const areBothCurrencies = !!inputCurrency && !!outputCurrency
+
+ const rawRate = useMemo(() => {
+ if (isTypedValue) return typedValue || ''
+
+ if (!activeRate || !areBothCurrencies || activeRate.equalTo(0)) return ''
+
+ const rate = isInverted ? activeRate.invert() : activeRate
+
+ return formatInputAmount(rate)
+ }, [activeRate, areBothCurrencies, isInverted, isTypedValue, typedValue])
+
+ const rateAsCurrencyAmount = !currency || !rawRate ? null : tryParseCurrencyAmount(rawRate, currency)
+
+ const { value: rateAsUsdAmount } = useUsdAmount(rateAsCurrencyAmount)
+
+ if (isUsdMode) {
+ return formatInputAmount(rateAsUsdAmount)
+ }
+
+ return rawRate
+}
diff --git a/apps/cowswap-frontend/src/modules/limitOrders/containers/RateInput/index.tsx b/apps/cowswap-frontend/src/modules/limitOrders/containers/RateInput/index.tsx
index e1d21cab06..e68bdaab89 100644
--- a/apps/cowswap-frontend/src/modules/limitOrders/containers/RateInput/index.tsx
+++ b/apps/cowswap-frontend/src/modules/limitOrders/containers/RateInput/index.tsx
@@ -1,46 +1,53 @@
import { useAtomValue, useSetAtom } from 'jotai'
import { useCallback, useEffect, useMemo, useState } from 'react'
-import { formatInputAmount, getAddress, isFractionFalsy } from '@cowprotocol/common-utils'
-import { HelpTooltip, Loader, TokenSymbol } from '@cowprotocol/ui'
+import LockedIcon from '@cowprotocol/assets/images/icon-locked.svg'
+import UnlockedIcon from '@cowprotocol/assets/images/icon-unlocked.svg'
+import UsdIcon from '@cowprotocol/assets/images/icon-USD.svg'
+import { formatInputAmount, getAddress, isFractionFalsy, tryParseCurrencyAmount } from '@cowprotocol/common-utils'
+import { TokenLogo } from '@cowprotocol/tokens'
+import { FiatAmount, HelpTooltip, HoverTooltip, TokenSymbol } from '@cowprotocol/ui'
import { useWalletInfo } from '@cowprotocol/wallet'
-import { RefreshCw } from 'react-feather'
-
+import SVG from 'react-inlinesvg'
import { useLimitOrdersDerivedState } from 'modules/limitOrders/hooks/useLimitOrdersDerivedState'
import { useRateImpact } from 'modules/limitOrders/hooks/useRateImpact'
import { useUpdateActiveRate } from 'modules/limitOrders/hooks/useUpdateActiveRate'
-import { ExecutionPriceTooltip } from 'modules/limitOrders/pure/ExecutionPriceTooltip'
import { HeadingText } from 'modules/limitOrders/pure/RateInput/HeadingText'
import { executionPriceAtom } from 'modules/limitOrders/state/executionPriceAtom'
+import {
+ limitOrdersSettingsAtom,
+ updateLimitOrdersSettingsAtom,
+} from 'modules/limitOrders/state/limitOrdersSettingsAtom'
import { limitRateAtom, updateLimitRateAtom } from 'modules/limitOrders/state/limitRateAtom'
import { toFraction } from 'modules/limitOrders/utils/toFraction'
+import { useUsdAmount } from 'modules/usdAmount'
-import { ordersTableFeatures } from 'common/constants/featureFlags'
+import { useConvertUsdToTokenValue } from 'common/hooks/useConvertUsdToTokenValue'
import { ExecutionPrice } from 'common/pure/ExecutionPrice'
import { getQuoteCurrency, getQuoteCurrencyByStableCoin } from 'common/services/getQuoteCurrency'
+import { useExecutionPriceUsdValue } from './hooks/useExecutionPriceUsdValue'
+import { useRateDisplayedValue } from './hooks/useRateDisplayedValue'
import * as styledEl from './styled'
export function RateInput() {
const { chainId } = useWalletInfo()
// Rate state
- const {
- isInverted,
- activeRate,
- isLoading,
- marketRate,
- feeAmount,
- isLoadingMarketRate,
- typedValue,
- isTypedValue,
- initialRate,
- } = useAtomValue(limitRateAtom)
+ const { isInverted, activeRate, isLoading, marketRate, isLoadingMarketRate, initialRate } =
+ useAtomValue(limitRateAtom)
const updateRate = useUpdateActiveRate()
const updateLimitRateState = useSetAtom(updateLimitRateAtom)
const executionPrice = useAtomValue(executionPriceAtom)
+ const { limitPriceLocked, isUsdValuesMode } = useAtomValue(limitOrdersSettingsAtom)
+ const updateLimitOrdersSettings = useSetAtom(updateLimitOrdersSettingsAtom)
+
+ const executionPriceUsdValue = useExecutionPriceUsdValue(executionPrice)
+
const [isQuoteCurrencySet, setIsQuoteCurrencySet] = useState(false)
+ const [typedTrailingZeros, setTypedTrailingZeros] = useState('')
+ const [isUsdRateMode, setIsUsdRateMode] = useState(isUsdValuesMode)
// Limit order state
const { inputCurrency, outputCurrency, inputCurrencyAmount, outputCurrencyAmount } = useLimitOrdersDerivedState()
@@ -52,45 +59,76 @@ export function RateInput() {
const primaryCurrency = isInverted ? outputCurrency : inputCurrency
const secondaryCurrency = isInverted ? inputCurrency : outputCurrency
- // Handle rate display
- const displayedRate = useMemo(() => {
- if (isTypedValue) return typedValue || ''
+ const marketRateRaw =
+ marketRate && !marketRate.equalTo(0) ? formatInputAmount(isInverted ? marketRate.invert() : marketRate) : null
- if (!activeRate || !areBothCurrencies || activeRate.equalTo(0)) return ''
+ const { value: marketRateUsd } = useUsdAmount(
+ marketRateRaw && secondaryCurrency ? tryParseCurrencyAmount(marketRateRaw, secondaryCurrency) : null,
+ )
- const rate = isInverted ? activeRate.invert() : activeRate
+ const marketRateDisplay = isUsdRateMode ? '$' + formatInputAmount(marketRateUsd) : marketRateRaw
- return formatInputAmount(rate)
- }, [activeRate, areBothCurrencies, isInverted, isTypedValue, typedValue])
-
- // Handle set market price
- const handleSetMarketPrice = useCallback(() => {
- updateRate({
- activeRate: isFractionFalsy(marketRate) ? initialRate : marketRate,
- isTypedValue: false,
- isRateFromUrl: false,
- isAlternativeOrderRate: false,
- })
- }, [marketRate, initialRate, updateRate])
+ const displayedRate = useRateDisplayedValue(secondaryCurrency, isUsdRateMode)
+ const convertUsdToTokenValue = useConvertUsdToTokenValue(secondaryCurrency)
// Handle rate input
const handleUserInput = useCallback(
(typedValue: string) => {
- updateLimitRateState({ typedValue })
+ const trailing = typedValue.slice(displayedRate.length)
+ const onlyTrailingZeroAdded = typedValue.includes('.') && /^0+$/.test(trailing)
+
+ /**
+ * Since we convert USD to token value, we need to handle trailing zeros separately, otherwise we will lose them
+ */
+ if (onlyTrailingZeroAdded) {
+ setTypedTrailingZeros(trailing)
+ return
+ }
+
+ setTypedTrailingZeros('')
+
+ const value = convertUsdToTokenValue(typedValue, isUsdRateMode)
+
+ updateLimitRateState({ typedValue: value })
updateRate({
- activeRate: toFraction(typedValue, isInverted),
+ activeRate: toFraction(value, isInverted),
isTypedValue: true,
isRateFromUrl: false,
isAlternativeOrderRate: false,
})
},
- [isInverted, updateRate, updateLimitRateState]
+ [isUsdRateMode, isInverted, updateRate, updateLimitRateState, displayedRate, convertUsdToTokenValue],
)
+ // Handle set market price
+ const handleSetMarketPrice = useCallback(() => {
+ updateRate({
+ activeRate: isFractionFalsy(marketRate) ? initialRate : marketRate,
+ isTypedValue: false,
+ isRateFromUrl: false,
+ isAlternativeOrderRate: false,
+ })
+ }, [marketRate, initialRate, updateRate])
+
// Handle toggle primary field
const handleToggle = useCallback(() => {
- updateLimitRateState({ isInverted: !isInverted, isTypedValue: false })
- }, [isInverted, updateLimitRateState])
+ if (isUsdRateMode) {
+ // When in USD mode, just switch to token mode without toggling tokens
+ setIsUsdRateMode(false)
+ } else {
+ // When already in token mode, toggle between tokens
+ updateLimitRateState({ isInverted: !isInverted, isTypedValue: false })
+ }
+ }, [isInverted, updateLimitRateState, isUsdRateMode])
+
+ // Handle toggle price lock
+ const handleTogglePriceLock = useCallback(
+ (event: React.MouseEvent) => {
+ event.stopPropagation()
+ updateLimitOrdersSettings({ limitPriceLocked: !limitPriceLocked })
+ },
+ [limitPriceLocked, updateLimitOrdersSettings],
+ )
const isDisabledMPrice = useMemo(() => {
if (isLoadingMarketRate) return true
@@ -140,17 +178,41 @@ export function RateInput() {
setIsQuoteCurrencySet(false)
}, [inputCurrency, outputCurrency])
+ // Depend rate USD mode on settings
+ useEffect(() => {
+ setIsUsdRateMode(isUsdValuesMode)
+ }, [isUsdValuesMode])
+
return (
<>
-
-
-
- Set to market
-
+
+
+
+
+
+ }
+ onToggle={handleToggle}
+ />
+ {areBothCurrencies && (isLoadingMarketRate || marketRateDisplay) && (
+
+ Market: {' '}
+
+ {isLoadingMarketRate ? : marketRateDisplay}
+
+
+ )}
-
{isLoading && areBothCurrencies ? (
@@ -158,47 +220,48 @@ export function RateInput() {
)}
-
-
-
-
-
-
-
-
+ {secondaryCurrency && (
+
+
+
+
+
+
+
+
+ setIsUsdRateMode((state) => !state)} $active={isUsdRateMode}>
+
+
+
+ )}
- {ordersTableFeatures.DISPLAY_EST_EXECUTION_PRICE && (
-
-
- Order executes at{' '}
- {isLoadingMarketRate ? (
-
- ) : executionPrice ? (
-
- }
- />
- ) : null}
-
- {!isLoadingMarketRate && executionPrice && (
-
+
+
+ {isLoadingMarketRate ? (
+
+ ) : executionPrice ? (
+ isUsdRateMode ? (
+
+ ) : (
+
+ )
+ ) : (
+ '-'
)}
-
- )}
+
+
+ Estimated fill price
+
+
+
>
)
}
diff --git a/apps/cowswap-frontend/src/modules/limitOrders/containers/RateInput/styled.ts b/apps/cowswap-frontend/src/modules/limitOrders/containers/RateInput/styled.ts
index 08d19b62ea..c56abdaaa8 100644
--- a/apps/cowswap-frontend/src/modules/limitOrders/containers/RateInput/styled.ts
+++ b/apps/cowswap-frontend/src/modules/limitOrders/containers/RateInput/styled.ts
@@ -1,4 +1,4 @@
-import { Loader, Media } from '@cowprotocol/ui'
+import { Loader, Media, TokenSymbol } from '@cowprotocol/ui'
import { UI } from '@cowprotocol/ui'
import styled from 'styled-components/macro'
@@ -6,12 +6,9 @@ import styled from 'styled-components/macro'
import Input from 'legacy/components/NumericalInput'
export const Wrapper = styled.div`
- background: var(${UI.COLOR_PAPER_DARKER});
- border-radius: 16px;
- padding: 10px 16px;
- flex: 1 1 70%;
- min-height: 80px;
- justify-content: space-between;
+ padding: 16px 16px 0;
+ width: 100%;
+ max-width: 100%;
display: flex;
flex-flow: row wrap;
color: inherit;
@@ -29,28 +26,53 @@ export const Header = styled.div`
font-weight: 500;
width: 100%;
color: inherit;
+
+ > span > i {
+ font-style: normal;
+ color: var(${UI.COLOR_TEXT});
+ }
+`
+
+export const MarketRateWrapper = styled.div`
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ font-size: 12px;
+ font-weight: 400;
+
+ > i {
+ font-style: normal;
+ color: var(${UI.COLOR_TEXT_OPACITY_70});
+ }
`
export const MarketPriceButton = styled.button`
- background: var(${UI.COLOR_PAPER});
color: inherit;
white-space: nowrap;
border: none;
font-weight: 500;
cursor: pointer;
- border-radius: 9px;
- padding: 5px 8px;
- font-size: 11px;
- transition: background var(${UI.ANIMATION_DURATION}) ease-in-out, color var(${UI.ANIMATION_DURATION}) ease-in-out;
+ font-size: inherit;
+ background: transparent;
+ padding: 0;
+ color: var(${UI.COLOR_TEXT});
+ transition:
+ background var(${UI.ANIMATION_DURATION}) ease-in-out,
+ color var(${UI.ANIMATION_DURATION}) ease-in-out;
+ text-decoration: underline;
+ text-decoration-style: dashed;
+ text-decoration-thickness: 1px;
+ text-underline-offset: 2px;
+ text-decoration-color: var(${UI.COLOR_TEXT_OPACITY_70});
- &:disabled {
- cursor: default;
- opacity: 0.6;
+ > svg {
+ margin: 0 0 -2px 7px;
}
- &:not(:disabled):hover {
- background: var(${UI.COLOR_PRIMARY});
- color: var(${UI.COLOR_BUTTON_TEXT});
+ &:disabled {
+ cursor: default;
+ opacity: 0.7;
+ text-decoration: none;
}
`
@@ -59,7 +81,9 @@ export const Body = styled.div`
align-items: center;
justify-content: space-between;
width: 100%;
+ max-width: 100%;
gap: 8px;
+ padding: 12px 0 4px;
color: inherit;
`
@@ -68,9 +92,11 @@ export const NumericalInput = styled(Input)<{ $loading: boolean }>`
align-items: center;
background: none;
border: none;
- width: 100%;
text-align: left;
color: inherit;
+ font-size: 32px;
+ letter-spacing: -1.5px;
+ flex: 1 1 auto;
&::placeholder {
opacity: 0.7;
@@ -78,32 +104,126 @@ export const NumericalInput = styled(Input)<{ $loading: boolean }>`
}
`
-export const ActiveCurrency = styled.button`
+export const CurrencyToggleGroup = styled.div`
+ display: flex;
+ align-items: center;
+ background: transparent;
+ overflow: hidden;
+`
+
+export const ActiveCurrency = styled.button<{ $active?: boolean }>`
+ --height: 25px;
+ --skew-width: 6px;
+ --skew-offset: -3px;
+ --skew-angle: -10deg;
+ --padding: 10px;
+ --gap: 6px;
+ --font-size: 13px;
+ --border-radius: 8px;
+
display: flex;
align-items: center;
- justify-content: flex-end;
+ gap: var(--gap);
+ font-size: var(--font-size);
+ font-weight: var(${UI.FONT_WEIGHT_MEDIUM});
border: none;
- background: none;
- padding: 0;
- margin: 0 0 0 auto;
- gap: 8px;
- max-width: 130px;
- width: auto;
- color: inherit;
cursor: pointer;
+ position: relative;
+ height: var(--height);
+ border-radius: var(--border-radius);
+ transition: all 0.2s ease-in-out;
+ background: ${({ $active }) => ($active ? 'var(' + UI.COLOR_PAPER + ')' : 'var(' + UI.COLOR_PAPER_DARKEST + ')')};
+ color: ${({ $active }) => ($active ? 'var(' + UI.COLOR_TEXT + ')' : 'var(' + UI.COLOR_TEXT_OPACITY_50 + ')')};
+ padding: 0 10px;
+
+ &:first-child {
+ padding-right: var(--padding);
+ border-top-right-radius: 0;
+ border-bottom-right-radius: 0;
+
+ ${({ $active }) =>
+ $active &&
+ `
+ &::after {
+ content: '';
+ position: absolute;
+ right: var(--skew-offset);
+ top: 0;
+ bottom: 0;
+ width: var(--skew-width);
+ background: var(${UI.COLOR_PAPER_DARKER});
+ transform: skew(var(--skew-angle));
+ z-index: 5;
+ }
+ `}
+ }
+
+ &:last-child {
+ padding-left: var(--padding);
+ border-top-left-radius: 0;
+ border-bottom-left-radius: 0;
+
+ ${({ $active }) =>
+ $active &&
+ `
+ &::before {
+ content: '';
+ position: absolute;
+ left: var(--skew-offset);
+ top: 0;
+ bottom: 0;
+ width: var(--skew-width);
+ background: var(${UI.COLOR_PAPER_DARKER});
+ transform: skew(var(--skew-angle));
+ z-index: 5;
+ }
+ `}
+ }
+
+ &:hover {
+ color: var(${UI.COLOR_TEXT});
+ }
`
-export const ActiveSymbol = styled.span`
+export const UsdButton = styled(ActiveCurrency)`
+ font-weight: var(${UI.FONT_WEIGHT_BOLD});
+ min-width: 40px;
+ justify-content: center;
+
+ > svg {
+ width: 10px;
+ height: 16px;
+ color: inherit;
+ }
+`
+
+export const ActiveSymbol = styled.span<{ $active?: boolean }>`
color: inherit;
font-size: 13px;
font-weight: 500;
text-align: right;
padding: 10px 0;
+ display: flex;
+ gap: 4px;
+
+ ${({ $active }) =>
+ !$active &&
+ `
+ > div {
+ background: transparent;
+ }
+ > div > img {
+ opacity: 0.5;
+ }
+
+ > ${TokenSymbol} {
+ color: var(${UI.COLOR_TEXT_OPACITY_50});
+ }
+ `}
`
export const ActiveIcon = styled.div`
- --size: 20px;
- background-color: var(${UI.COLOR_PAPER});
+ --size: 19px;
color: inherit;
width: var(--size);
min-width: var(--size);
@@ -113,6 +233,19 @@ export const ActiveIcon = styled.div`
display: flex;
align-items: center;
justify-content: center;
+ cursor: pointer;
+ margin: 0 2px 0 0;
+ transition:
+ color var(${UI.ANIMATION_DURATION}) ease-in-out,
+ background var(${UI.ANIMATION_DURATION}) ease-in-out;
+ background: transparent;
+ border: 1px solid var(${UI.COLOR_PAPER_DARKEST});
+
+ &:hover {
+ color: var(${UI.COLOR_TEXT});
+ background: var(${UI.COLOR_PAPER});
+ border-color: var(${UI.COLOR_PAPER});
+ }
`
export const RateLoader = styled(Loader)`
@@ -123,33 +256,29 @@ export const EstimatedRate = styled.div`
display: flex;
width: 100%;
justify-content: space-between;
- min-height: 42px;
+ min-height: 36px;
margin: 0;
padding: 12px 10px 14px;
- font-size: 13px;
+ font-size: 16px;
border-radius: 0 0 16px 16px;
- font-weight: 400;
+ font-weight: 500;
+ color: var(${UI.COLOR_TEXT});
background: var(${UI.COLOR_PAPER});
border: 2px solid var(${UI.COLOR_PAPER_DARKER});
- background: red;
> b {
display: flex;
flex-flow: row nowrap;
- font-weight: normal;
+ font-weight: inherit;
text-align: left;
- opacity: 0.7;
transition: opacity var(${UI.ANIMATION_DURATION}) ease-in-out;
-
- &:hover {
- opacity: 1;
- }
+ color: inherit;
}
- // TODO: Make the question helper icon transparent through a prop instead
> b svg {
opacity: 0.7;
transition: opacity var(${UI.ANIMATION_DURATION}) ease-in-out;
+ color: inherit;
&:hover {
opacity: 1;
@@ -161,4 +290,36 @@ export const EstimatedRate = styled.div`
color: inherit;
opacity: 0.7;
}
+
+ > span {
+ display: flex;
+ align-items: center;
+ font-size: 13px;
+ font-weight: 400;
+ color: var(${UI.COLOR_TEXT_OPACITY_70});
+ }
+`
+
+export const LockIcon = styled.span`
+ --size: 19px;
+ position: relative;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 4px;
+ color: inherit;
+ border-radius: var(--size);
+ width: var(--size);
+ min-width: var(--size);
+ height: var(--size);
+ border: 1px solid var(${UI.COLOR_PAPER_DARKEST});
+ cursor: pointer;
+ transition:
+ border-color var(${UI.ANIMATION_DURATION}) ease-in-out,
+ background var(${UI.ANIMATION_DURATION}) ease-in-out;
+
+ &:hover {
+ border-color: var(${UI.COLOR_PAPER});
+ background: var(${UI.COLOR_PAPER});
+ }
`
diff --git a/apps/cowswap-frontend/src/modules/limitOrders/containers/SettingsWidget/index.tsx b/apps/cowswap-frontend/src/modules/limitOrders/containers/SettingsWidget/index.tsx
index 00267f9fb2..f4222a4efc 100644
--- a/apps/cowswap-frontend/src/modules/limitOrders/containers/SettingsWidget/index.tsx
+++ b/apps/cowswap-frontend/src/modules/limitOrders/containers/SettingsWidget/index.tsx
@@ -1,9 +1,11 @@
import { useAtomValue, useSetAtom } from 'jotai'
-import React from 'react'
-import { Menu, MenuItem } from '@reach/menu-button'
+import UsdIcon from '@cowprotocol/assets/images/icon-USD.svg'
-import { MenuContent, SettingsButton, SettingsIcon } from 'modules/trade/pure/Settings'
+import { Menu, MenuItem, MenuPopover, MenuItems } from '@reach/menu-button'
+import SVG from 'react-inlinesvg'
+
+import { ButtonsContainer, SettingsButton, SettingsIcon, UsdButton } from 'modules/trade/pure/Settings'
import { Settings } from '../../pure/Settings'
import { limitOrdersSettingsAtom, updateLimitOrdersSettingsAtom } from '../../state/limitOrdersSettingsAtom'
@@ -11,19 +13,25 @@ import { limitOrdersSettingsAtom, updateLimitOrdersSettingsAtom } from '../../st
export function SettingsWidget() {
const settingsState = useAtomValue(limitOrdersSettingsAtom)
const updateSettingsState = useSetAtom(updateLimitOrdersSettingsAtom)
+ const isUsdValuesMode = settingsState.isUsdValuesMode
return (
- <>
+
+ updateSettingsState({ isUsdValuesMode: !isUsdValuesMode })} active={isUsdValuesMode}>
+
+
-
- void 0}>
-
-
-
+
+
+ void 0}>
+
+
+
+
- >
+
)
}
diff --git a/apps/cowswap-frontend/src/modules/limitOrders/containers/TradeRateDetails/index.tsx b/apps/cowswap-frontend/src/modules/limitOrders/containers/TradeRateDetails/index.tsx
index 8817f5a27a..0647846b5f 100644
--- a/apps/cowswap-frontend/src/modules/limitOrders/containers/TradeRateDetails/index.tsx
+++ b/apps/cowswap-frontend/src/modules/limitOrders/containers/TradeRateDetails/index.tsx
@@ -1,6 +1,8 @@
import React, { useState, useCallback } from 'react'
import { TradeTotalCostsDetails, PartnerFeeRow } from 'modules/trade'
+import { StyledRateInfo } from 'modules/trade/containers/TradeTotalCostsDetails/styled'
+import { Box } from 'modules/trade/containers/TradeTotalCostsDetails/styled'
import { useUsdAmount } from 'modules/usdAmount'
import { useVolumeFee, useVolumeFeeTooltip } from 'modules/volumeFee'
@@ -10,10 +12,11 @@ import { useLimitOrderPartnerFeeAmount } from '../../hooks/useLimitOrderPartnerF
interface TradeRateDetailsProps {
rateInfoParams?: RateInfoParams
+ alwaysExpanded?: boolean
}
-export function TradeRateDetails({ rateInfoParams }: TradeRateDetailsProps) {
- const [isFeeDetailsOpen, setFeeDetailsOpen] = useState(false)
+export function TradeRateDetails({ rateInfoParams, alwaysExpanded = false }: TradeRateDetailsProps) {
+ const [isFeeDetailsOpen, setFeeDetailsOpen] = useState(alwaysExpanded)
const volumeFee = useVolumeFee()
const partnerFeeAmount = useLimitOrderPartnerFeeAmount()
const volumeFeeTooltip = useVolumeFeeTooltip()
@@ -21,8 +24,9 @@ export function TradeRateDetails({ rateInfoParams }: TradeRateDetailsProps) {
const partnerFeeBps = volumeFee?.bps
const toggleAccordion = useCallback(() => {
+ if (alwaysExpanded) return
setFeeDetailsOpen((prev) => !prev)
- }, [])
+ }, [alwaysExpanded])
const partnerFeeRow = (
+
+ {partnerFeeRow}
+ >
+ )
+ }
+
return (
{
const { activeRate, amount, orderKind } = params
const field = isSellOrder(orderKind) ? Field.INPUT : Field.OUTPUT
+ const isBuyAmountChange = field === Field.OUTPUT
+ if (isBuyAmountChange) {
+ const update: Partial> = {
+ orderKind,
+ outputCurrencyAmount: FractionUtils.serializeFractionToJSON(amount),
+ }
+
+ updateLimitOrdersState(update)
+
+ // If price is unlocked, update the rate based on the new amounts
+ if (!limitPriceLocked) {
+ // Calculate and update the new rate
+ if (amount && currentInputAmount) {
+ const newRate = new Price(
+ currentInputAmount.currency,
+ amount.currency,
+ currentInputAmount.quotient,
+ amount.quotient,
+ )
+ updateLimitRateState({
+ activeRate: FractionUtils.fractionLikeToFraction(newRate),
+ isTypedValue: false,
+ isRateFromUrl: false,
+ isAlternativeOrderRate: false,
+ })
+ }
+ }
+ return
+ }
+
+ // Normal flow for SELL amount changes
const calculatedAmount = calculateAmountForRate({
activeRate,
amount,
@@ -37,21 +73,21 @@ export function useUpdateCurrencyAmount() {
outputCurrency,
})
- const inputCurrencyAmount = FractionUtils.serializeFractionToJSON(
- field === Field.INPUT ? amount : calculatedAmount
- )
- const outputCurrencyAmount = FractionUtils.serializeFractionToJSON(
- field === Field.OUTPUT ? amount : calculatedAmount
- )
+ const newInputAmount = (field as Field) === Field.INPUT ? amount : calculatedAmount
+ const newOutputAmount = (field as Field) === Field.OUTPUT ? amount : calculatedAmount
const update: Partial> = {
orderKind,
- ...(inputCurrencyAmount ? { inputCurrencyAmount } : undefined),
- ...(outputCurrencyAmount ? { outputCurrencyAmount } : undefined),
+ ...(newInputAmount
+ ? { inputCurrencyAmount: FractionUtils.serializeFractionToJSON(newInputAmount) }
+ : undefined),
+ ...(newOutputAmount
+ ? { outputCurrencyAmount: FractionUtils.serializeFractionToJSON(newOutputAmount) }
+ : undefined),
}
updateLimitOrdersState(update)
},
- [inputCurrency, outputCurrency, updateLimitOrdersState]
+ [inputCurrency, outputCurrency, updateLimitOrdersState, limitPriceLocked, updateLimitRateState, currentInputAmount],
)
}
diff --git a/apps/cowswap-frontend/src/modules/limitOrders/pure/DeadlineSelector/deadlines.ts b/apps/cowswap-frontend/src/modules/limitOrders/pure/DeadlineSelector/deadlines.ts
index b2fd186d29..7364eae9f3 100644
--- a/apps/cowswap-frontend/src/modules/limitOrders/pure/DeadlineSelector/deadlines.ts
+++ b/apps/cowswap-frontend/src/modules/limitOrders/pure/DeadlineSelector/deadlines.ts
@@ -11,18 +11,35 @@ export interface LimitOrderDeadline {
export const MIN_CUSTOM_DEADLINE = ms`30min`
export const MAX_CUSTOM_DEADLINE = MAX_ORDER_DEADLINE
-export const defaultLimitOrderDeadline: LimitOrderDeadline = { title: '7 Days', value: ms`7d` }
-
-export const LIMIT_ORDERS_DEADLINES: LimitOrderDeadline[] = [
- { title: '5 Minutes', value: ms`5m` },
- { title: '30 Minutes', value: ms`30m` },
- { title: '1 Hour', value: ms`1 hour` },
- { title: '1 Day', value: ms`1d` },
- { title: '3 Days', value: ms`3d` },
- defaultLimitOrderDeadline,
- { title: '1 Month', value: ms`30d` },
- { title: '1 Year (max)', value: MAX_CUSTOM_DEADLINE },
-]
+export enum LimitOrderDeadlinePreset {
+ FIVE_MINUTES = '5 Minutes',
+ THIRTY_MINUTES = '30 Minutes',
+ ONE_HOUR = '1 Hour',
+ ONE_DAY = '1 Day',
+ THREE_DAYS = '3 Days',
+ ONE_MONTH = '1 Month',
+ SIX_MONTHS = '6 Months (max)',
+}
+
+const DEADLINE_VALUES: Record = {
+ [LimitOrderDeadlinePreset.FIVE_MINUTES]: ms`5m`,
+ [LimitOrderDeadlinePreset.THIRTY_MINUTES]: ms`30m`,
+ [LimitOrderDeadlinePreset.ONE_HOUR]: ms`1 hour`,
+ [LimitOrderDeadlinePreset.ONE_DAY]: ms`1d`,
+ [LimitOrderDeadlinePreset.THREE_DAYS]: ms`3d`,
+ [LimitOrderDeadlinePreset.ONE_MONTH]: ms`30d`,
+ [LimitOrderDeadlinePreset.SIX_MONTHS]: MAX_CUSTOM_DEADLINE,
+}
+
+export const defaultLimitOrderDeadline: LimitOrderDeadline = {
+ title: LimitOrderDeadlinePreset.SIX_MONTHS,
+ value: DEADLINE_VALUES[LimitOrderDeadlinePreset.SIX_MONTHS],
+}
+
+export const LIMIT_ORDERS_DEADLINES: LimitOrderDeadline[] = Object.entries(DEADLINE_VALUES).map(([title, value]) => ({
+ title,
+ value,
+}))
/**
* Get limit order deadlines and optionally adds
diff --git a/apps/cowswap-frontend/src/modules/limitOrders/pure/DeadlineSelector/index.tsx b/apps/cowswap-frontend/src/modules/limitOrders/pure/DeadlineSelector/index.tsx
index 247d86c01f..32d1fdb777 100644
--- a/apps/cowswap-frontend/src/modules/limitOrders/pure/DeadlineSelector/index.tsx
+++ b/apps/cowswap-frontend/src/modules/limitOrders/pure/DeadlineSelector/index.tsx
@@ -126,7 +126,7 @@ export function DeadlineSelector(props: DeadlineSelectorProps) {
return (
- Expiry
+ Order expires in
{isDeadlineDisabled ? (
diff --git a/apps/cowswap-frontend/src/modules/limitOrders/pure/DeadlineSelector/styled.tsx b/apps/cowswap-frontend/src/modules/limitOrders/pure/DeadlineSelector/styled.tsx
index 8f07cfb6cc..73272e0ef7 100644
--- a/apps/cowswap-frontend/src/modules/limitOrders/pure/DeadlineSelector/styled.tsx
+++ b/apps/cowswap-frontend/src/modules/limitOrders/pure/DeadlineSelector/styled.tsx
@@ -5,52 +5,32 @@ import { transparentize } from 'color2k'
import { X } from 'react-feather'
import styled from 'styled-components/macro'
-export const Wrapper = styled.div<{ inline?: boolean; minHeight?: string }>`
- background: var(${UI.COLOR_PAPER_DARKER});
+export const Wrapper = styled.div`
color: inherit;
- border-radius: 16px;
- padding: 10px 16px;
- min-height: ${({ minHeight }) => minHeight || '80px'};
+ padding: 0;
justify-content: space-between;
display: flex;
flex-flow: row wrap;
-
- ${({ inline }) =>
- inline &&
- `
- justify-content: space-between;
-
- > span {
- flex: 1;
- }
-
- > button {
- width: auto;
- }
- `}
+ width: 100%;
+ font-size: 13px;
+ font-weight: inherit;
+ min-height: 24px;
`
export const Label = styled.span`
display: flex;
- align-items: center;
- justify-content: space-between;
- font-size: 13px;
- font-weight: 500;
- width: 100%;
+ font-size: inherit;
+ font-weight: inherit;
color: inherit;
- opacity: 0.7;
- transition: opacity var(${UI.ANIMATION_DURATION}) ease-in-out;
-
- &:hover {
- opacity: 1;
- }
+ align-self: center;
+ justify-self: center;
`
export const Current = styled(MenuButton)<{ $custom?: boolean }>`
color: inherit;
font-size: ${({ $custom }) => ($custom ? '12px' : '100%')};
letter-spacing: ${({ $custom }) => ($custom ? '-0.3px' : '0')};
- font-weight: 500;
+ font-weight: inherit;
display: flex;
align-items: center;
justify-content: space-between;
@@ -61,7 +41,6 @@ export const Current = styled(MenuButton)<{ $custom?: boolean }>`
padding: 0;
white-space: nowrap;
cursor: pointer;
- width: 100%;
text-overflow: ellipsis;
overflow: hidden;
diff --git a/apps/cowswap-frontend/src/modules/limitOrders/pure/EstimatedFillPrice/index.tsx b/apps/cowswap-frontend/src/modules/limitOrders/pure/EstimatedFillPrice/index.tsx
new file mode 100644
index 0000000000..5c9b4e47c9
--- /dev/null
+++ b/apps/cowswap-frontend/src/modules/limitOrders/pure/EstimatedFillPrice/index.tsx
@@ -0,0 +1,82 @@
+import { TokenAmount, HoverTooltip } from '@cowprotocol/ui'
+import { UI } from '@cowprotocol/ui'
+import { FractionLike, Nullish } from '@cowprotocol/ui'
+import { Currency, Price, CurrencyAmount, Fraction } from '@uniswap/sdk-core'
+
+import styled from 'styled-components/macro'
+
+import { ExecutionPriceTooltip } from '../ExecutionPriceTooltip'
+
+const EstimatedFillPriceBox = styled.div`
+ display: flex;
+ justify-content: space-between;
+ padding: 10px;
+ border-radius: 0 0 16px 16px;
+ font-size: 14px;
+ font-weight: 600;
+ background: var(${UI.COLOR_PAPER});
+ border: 1px solid var(${UI.COLOR_PAPER_DARKER});
+`
+
+const Label = styled.div`
+ display: flex;
+ align-items: center;
+ gap: 5px;
+ font-weight: 500;
+`
+
+const Value = styled.div`
+ font-size: 16px;
+ display: flex;
+ align-items: center;
+ gap: 4px;
+`
+
+const QuestionWrapper = styled.div`
+ display: flex;
+ align-items: center;
+`
+
+export interface EstimatedFillPriceProps {
+ currency: Currency
+ estimatedFillPrice: Nullish
+ executionPrice: Price
+ isInverted: boolean
+ feeAmount: CurrencyAmount | null
+ marketRate: Fraction | null
+}
+
+export function EstimatedFillPrice({
+ currency,
+ estimatedFillPrice,
+ executionPrice,
+ isInverted,
+ feeAmount,
+ marketRate,
+}: EstimatedFillPriceProps) {
+ return (
+
+
+ Estimated fill price
+
+
+ }
+ >
+ ❔
+
+
+
+
+ ≈
+
+
+ )
+}
diff --git a/apps/cowswap-frontend/src/modules/limitOrders/pure/ExecutionPriceTooltip/index.tsx b/apps/cowswap-frontend/src/modules/limitOrders/pure/ExecutionPriceTooltip/index.tsx
index f31603ba88..1e06c44654 100644
--- a/apps/cowswap-frontend/src/modules/limitOrders/pure/ExecutionPriceTooltip/index.tsx
+++ b/apps/cowswap-frontend/src/modules/limitOrders/pure/ExecutionPriceTooltip/index.tsx
@@ -4,7 +4,6 @@ import { Currency, CurrencyAmount, Fraction, Price } from '@uniswap/sdk-core'
import { useUsdAmount } from 'modules/usdAmount'
import { ExecutionPrice } from 'common/pure/ExecutionPrice'
-import { RateTooltipHeader } from 'common/pure/OrderExecutionStatusList'
import { convertAmountToCurrency } from 'utils/orderUtils/calculateExecutionPrice'
import * as styledEl from './styled'
@@ -30,13 +29,13 @@ function formatFeeAmount({
return !isInverted && invertedFee && currency && feeAmount
? convertAmountToCurrency(
CurrencyAmount.fromFractionalAmount(feeAmount.currency, invertedFee.numerator, invertedFee.denominator),
- currency
+ currency,
)
: feeAmount
}
export function ExecutionPriceTooltip(props: ExecutionPriceTooltipProps) {
- const { isInverted, displayedRate, executionPrice, isOpenOrdersTab } = props
+ const { isInverted, displayedRate, executionPrice } = props
const currentCurrency = isInverted ? executionPrice?.baseCurrency : executionPrice?.quoteCurrency
const formattedFeeAmount = formatFeeAmount(props)
@@ -45,8 +44,6 @@ export function ExecutionPriceTooltip(props: ExecutionPriceTooltipProps) {
return (
-
-
Limit price
diff --git a/apps/cowswap-frontend/src/modules/limitOrders/pure/RateInput/HeadingText.tsx b/apps/cowswap-frontend/src/modules/limitOrders/pure/RateInput/HeadingText.tsx
index 822e847b32..88ed3dee08 100644
--- a/apps/cowswap-frontend/src/modules/limitOrders/pure/RateInput/HeadingText.tsx
+++ b/apps/cowswap-frontend/src/modules/limitOrders/pure/RateInput/HeadingText.tsx
@@ -1,4 +1,5 @@
-import { UI } from '@cowprotocol/ui'
+import { TokenLogo } from '@cowprotocol/tokens'
+import { TokenSymbol, UI } from '@cowprotocol/ui'
import { Currency } from '@uniswap/sdk-core'
import styled from 'styled-components/macro'
@@ -9,33 +10,65 @@ type Props = {
currency: Currency | null
inputCurrency: Currency | null
rateImpact: number
+ toggleIcon?: React.ReactNode
+ onToggle?: () => void
}
const Wrapper = styled.span`
display: flex;
flex-flow: row wrap;
- align-items: flex-start;
+ align-items: center;
justify-content: flex-start;
text-align: left;
gap: 0 3px;
- opacity: 0.7;
+ font-size: 13px;
+ font-weight: 400;
+ margin: auto 0;
+ color: var(${UI.COLOR_TEXT_OPACITY_70});
+`
+
+const TokenWrapper = styled.div`
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ font-weight: 600;
+ color: var(${UI.COLOR_TEXT});
+`
+
+const TextWrapper = styled.span<{ clickable: boolean }>`
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ cursor: ${({ clickable }) => (clickable ? 'pointer' : 'default')};
transition: opacity var(${UI.ANIMATION_DURATION}) ease-in-out;
&:hover {
- opacity: 1;
+ text-decoration: underline;
+ text-decoration-style: dashed;
+ text-decoration-thickness: 1px;
+ text-underline-offset: 2px;
+ text-decoration-color: var(${UI.COLOR_TEXT_OPACITY_70});
}
`
-export function HeadingText({ inputCurrency, currency, rateImpact }: Props) {
+export function HeadingText({ inputCurrency, currency, rateImpact, toggleIcon, onToggle }: Props) {
if (!currency) {
return Select input and output
}
return (
- {/* Price of */}
- Limit price
- { }
+ {toggleIcon}
+
+ When
+
+ 1
+
+
+
+ is worth
+ { }
+
)
}
diff --git a/apps/cowswap-frontend/src/modules/limitOrders/pure/Settings/index.cosmos.tsx b/apps/cowswap-frontend/src/modules/limitOrders/pure/Settings/index.cosmos.tsx
index f1d4599a3c..b03535f199 100644
--- a/apps/cowswap-frontend/src/modules/limitOrders/pure/Settings/index.cosmos.tsx
+++ b/apps/cowswap-frontend/src/modules/limitOrders/pure/Settings/index.cosmos.tsx
@@ -6,6 +6,11 @@ const defaultProps: SettingsProps = {
partialFillsEnabled: true,
deadlineMilliseconds: 200_000,
customDeadlineTimestamp: null,
+ limitPricePosition: 'between',
+ limitPriceLocked: false,
+ columnLayout: 'DEFAULT',
+ ordersTableOnLeft: false,
+ isUsdValuesMode: false,
},
onStateChanged(state) {
console.log('Settings state changed: ', state)
diff --git a/apps/cowswap-frontend/src/modules/limitOrders/pure/Settings/index.tsx b/apps/cowswap-frontend/src/modules/limitOrders/pure/Settings/index.tsx
index a12ec03174..4b756b6253 100644
--- a/apps/cowswap-frontend/src/modules/limitOrders/pure/Settings/index.tsx
+++ b/apps/cowswap-frontend/src/modules/limitOrders/pure/Settings/index.tsx
@@ -1,18 +1,155 @@
+import { useCallback, useState } from 'react'
+
+import { UI } from '@cowprotocol/ui'
+import { HelpTooltip } from '@cowprotocol/ui'
+
+import styled from 'styled-components/macro'
+
+import { ORDERS_TABLE_SETTINGS } from 'modules/trade/const/common'
import { SettingsBox, SettingsContainer, SettingsTitle } from 'modules/trade/pure/Settings'
import { LimitOrdersSettingsState } from '../../state/limitOrdersSettingsAtom'
+const DropdownButton = styled.button`
+ background: var(${UI.COLOR_PAPER_DARKER});
+ color: inherit;
+ border: 1px solid var(${UI.COLOR_BORDER});
+ border-radius: 12px;
+ padding: 10px 34px 10px 12px;
+ min-width: 140px;
+ cursor: pointer;
+ font-size: 14px;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ position: relative;
+ transition: all 0.2s ease-in-out;
+
+ &::after {
+ content: '▼';
+ position: absolute;
+ right: 12px;
+ top: 50%;
+ transform: translateY(-50%);
+ font-size: 10px;
+ transition: transform 0.2s ease-in-out;
+ color: var(${UI.COLOR_PRIMARY_OPACITY_50});
+ }
+
+ &:hover {
+ border-color: var(${UI.COLOR_PRIMARY_OPACITY_25});
+ background: var(${UI.COLOR_PRIMARY_OPACITY_10});
+ }
+
+ &:focus {
+ outline: none;
+ }
+`
+
+const DropdownList = styled.div<{ isOpen: boolean }>`
+ display: ${({ isOpen }) => (isOpen ? 'block' : 'none')};
+ position: absolute;
+ top: calc(100% + 8px);
+ right: 0;
+ background: var(${UI.COLOR_PAPER});
+ border: 1px solid var(${UI.COLOR_BORDER});
+ border-radius: 12px;
+ padding: 6px;
+ min-width: 140px;
+ z-index: 100;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+`
+
+const DropdownItem = styled.div`
+ padding: 10px 12px;
+ border-radius: 8px;
+ cursor: pointer;
+ font-size: 14px;
+ transition: all 0.15s ease-in-out;
+ color: inherit;
+
+ &:hover {
+ background: var(${UI.COLOR_PRIMARY_OPACITY_10});
+ transform: translateX(2px);
+ }
+
+ &:active {
+ transform: translateX(2px) scale(0.98);
+ }
+`
+
+const DropdownContainer = styled.div`
+ position: relative;
+ user-select: none;
+`
+
+const SettingsRow = styled.div`
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ gap: 15px;
+ font-weight: 400;
+ color: inherit;
+ font-size: 14px;
+`
+
+const SettingsLabel = styled.div`
+ display: flex;
+ align-items: center;
+ gap: 5px;
+`
+
export interface SettingsProps {
state: LimitOrdersSettingsState
onStateChanged: (state: Partial) => void
}
+const POSITION_LABELS = {
+ top: 'Top',
+ between: 'Between currencies',
+ bottom: 'Bottom',
+}
+
+const COLUMN_LAYOUT_LABELS = {
+ DEFAULT: 'Default view',
+ VIEW_2: 'Limit price / Fills at / Distance',
+ VIEW_3: 'Limit price / Fills at + Distance / Market',
+}
+
export function Settings({ state, onStateChanged }: SettingsProps) {
- const { showRecipient, partialFillsEnabled } = state
+ const { showRecipient, partialFillsEnabled, limitPricePosition, limitPriceLocked, columnLayout, ordersTableOnLeft } =
+ state
+ const [isOpen, setIsOpen] = useState(false)
+ const [isColumnLayoutOpen, setIsColumnLayoutOpen] = useState(false)
+
+ const handleSelect = useCallback(
+ (value: LimitOrdersSettingsState['limitPricePosition']) => (e: React.MouseEvent) => {
+ e.stopPropagation()
+ onStateChanged({ limitPricePosition: value })
+ setIsOpen(false)
+ },
+ [onStateChanged],
+ )
+
+ const handleColumnLayoutSelect = (value: LimitOrdersSettingsState['columnLayout']) => (e: React.MouseEvent) => {
+ e.stopPropagation()
+ onStateChanged({ columnLayout: value })
+ setIsColumnLayoutOpen(false)
+ }
+
+ const toggleDropdown = (e: React.MouseEvent) => {
+ e.stopPropagation()
+ setIsOpen(!isOpen)
+ }
+
+ const toggleColumnLayoutDropdown = (e: React.MouseEvent) => {
+ e.stopPropagation()
+ setIsColumnLayoutOpen(!isColumnLayoutOpen)
+ }
return (
- Interface Settings
+ Limit Order Settings
onStateChanged({ partialFillsEnabled: !partialFillsEnabled })}
/>
+
+ onStateChanged({ limitPriceLocked: !limitPriceLocked })}
+ />
+
+ onStateChanged({ ordersTableOnLeft: !ordersTableOnLeft })}
+ />
+
+
+
+ Limit Price Position
+
+
+
+ {POSITION_LABELS[limitPricePosition]}
+
+ {Object.entries(POSITION_LABELS).map(([value, label]) => (
+
+ {label}
+
+ ))}
+
+
+
+
+
+
+ Column Layout
+
+
+
+ {COLUMN_LAYOUT_LABELS[columnLayout]}
+
+ {Object.entries(COLUMN_LAYOUT_LABELS).map(([value, label]) => (
+
+ {label}
+
+ ))}
+
+
+
)
}
diff --git a/apps/cowswap-frontend/src/modules/limitOrders/state/limitOrdersSettingsAtom.ts b/apps/cowswap-frontend/src/modules/limitOrders/state/limitOrdersSettingsAtom.ts
index b4df5c2ef4..7f1d8c4b11 100644
--- a/apps/cowswap-frontend/src/modules/limitOrders/state/limitOrdersSettingsAtom.ts
+++ b/apps/cowswap-frontend/src/modules/limitOrders/state/limitOrdersSettingsAtom.ts
@@ -12,11 +12,18 @@ import {
alternativeOrderReadWriteAtomFactory,
} from 'modules/trade/state/alternativeOrder'
+export type ColumnLayoutType = 'DEFAULT' | 'VIEW_2' | 'VIEW_3'
+
export interface LimitOrdersSettingsState {
readonly showRecipient: boolean
readonly partialFillsEnabled: boolean
readonly deadlineMilliseconds: Milliseconds
readonly customDeadlineTimestamp: Timestamp | null
+ readonly limitPricePosition: 'top' | 'between' | 'bottom'
+ readonly limitPriceLocked: boolean
+ readonly columnLayout: ColumnLayoutType
+ readonly ordersTableOnLeft: boolean
+ readonly isUsdValuesMode: boolean
}
export const defaultLimitOrdersSettings: LimitOrdersSettingsState = {
@@ -24,40 +31,45 @@ export const defaultLimitOrdersSettings: LimitOrdersSettingsState = {
partialFillsEnabled: true,
deadlineMilliseconds: defaultLimitOrderDeadline.value,
customDeadlineTimestamp: null,
+ limitPricePosition: 'top',
+ limitPriceLocked: true,
+ columnLayout: 'DEFAULT',
+ ordersTableOnLeft: false,
+ isUsdValuesMode: false,
}
// regular
const regularLimitOrdersSettingsAtom = atomWithStorage(
- 'limit-orders-settings-atom:v2',
+ 'limit-orders-settings-atom:v3',
defaultLimitOrdersSettings,
- getJotaiIsolatedStorage()
+ getJotaiIsolatedStorage(),
)
const regularUpdateLimitOrdersSettingsAtom = atom(
null,
- partialFillsOverrideSetterFactory(regularLimitOrdersSettingsAtom)
+ partialFillsOverrideSetterFactory(regularLimitOrdersSettingsAtom),
)
// alternative
const alternativeLimitOrdersSettingsAtom = atom(defaultLimitOrdersSettings)
const alternativeUpdateLimitOrdersSettingsAtom = atom(
null,
- partialFillsOverrideSetterFactory(alternativeLimitOrdersSettingsAtom)
+ partialFillsOverrideSetterFactory(alternativeLimitOrdersSettingsAtom),
)
// export
export const limitOrdersSettingsAtom = alternativeOrderReadWriteAtomFactory(
regularLimitOrdersSettingsAtom,
- alternativeLimitOrdersSettingsAtom
+ alternativeLimitOrdersSettingsAtom,
)
export const updateLimitOrdersSettingsAtom = atom(
null,
- alternativeOrderAtomSetterFactory(regularUpdateLimitOrdersSettingsAtom, alternativeUpdateLimitOrdersSettingsAtom)
+ alternativeOrderAtomSetterFactory(regularUpdateLimitOrdersSettingsAtom, alternativeUpdateLimitOrdersSettingsAtom),
)
// utils
function partialFillsOverrideSetterFactory(
- atomToUpdate: typeof regularLimitOrdersSettingsAtom | typeof alternativeLimitOrdersSettingsAtom
+ atomToUpdate: typeof regularLimitOrdersSettingsAtom | typeof alternativeLimitOrdersSettingsAtom,
) {
return (get: Getter, set: Setter, nextState: Partial) => {
set(atomToUpdate, () => {
diff --git a/apps/cowswap-frontend/src/modules/ordersTable/const/tabs.ts b/apps/cowswap-frontend/src/modules/ordersTable/const/tabs.ts
index 72e324a5be..fe4fdacbf4 100644
--- a/apps/cowswap-frontend/src/modules/ordersTable/const/tabs.ts
+++ b/apps/cowswap-frontend/src/modules/ordersTable/const/tabs.ts
@@ -5,9 +5,21 @@ export interface OrderTab {
isActive?: boolean
}
+export const ALL_ORDERS_TAB: OrderTab = {
+ id: 'all',
+ title: 'All orders',
+ count: 0,
+}
+
+export const UNFILLABLE_TAB: OrderTab = {
+ id: 'unfillable',
+ title: 'Unfillable',
+ count: 0,
+}
+
export const OPEN_TAB: OrderTab = {
id: 'open',
- title: 'Open orders',
+ title: 'Open',
count: 0,
}
@@ -17,7 +29,7 @@ export const HISTORY_TAB: OrderTab = {
count: 0,
}
-export const ORDERS_TABLE_TABS: OrderTab[] = [OPEN_TAB, HISTORY_TAB]
+export const ORDERS_TABLE_TABS: OrderTab[] = [ALL_ORDERS_TAB, UNFILLABLE_TAB, OPEN_TAB, HISTORY_TAB]
export const ORDERS_TABLE_PAGE_SIZE = 10
diff --git a/apps/cowswap-frontend/src/modules/ordersTable/containers/MultipleCancellationMenu/index.tsx b/apps/cowswap-frontend/src/modules/ordersTable/containers/MultipleCancellationMenu/index.tsx
index befa8c25a9..ea38bb0bb6 100644
--- a/apps/cowswap-frontend/src/modules/ordersTable/containers/MultipleCancellationMenu/index.tsx
+++ b/apps/cowswap-frontend/src/modules/ordersTable/containers/MultipleCancellationMenu/index.tsx
@@ -22,8 +22,7 @@ const Wrapper = styled.div<{ hasSelectedItems: boolean }>`
align-items: center;
justify-content: space-between;
gap: 6px;
- margin-left: ${({ hasSelectedItems }) => (hasSelectedItems ? '' : 'auto')};
- margin: 0 10px 0 0;
+ margin: 0 0 0 ${({ hasSelectedItems }) => (hasSelectedItems ? '' : 'auto')};
${Media.upToSmall()} {
width: 100%;
@@ -39,13 +38,15 @@ const ActionButton = styled.button`
font-weight: 600;
text-decoration: none;
font-size: 13px;
- padding: 10px 15px;
+ padding: 7px 12px;
margin: 0;
gap: 5px;
border: 0;
outline: none;
cursor: pointer;
- transition: background var(${UI.ANIMATION_DURATION}) ease-in-out, color var(${UI.ANIMATION_DURATION}) ease-in-out;
+ transition:
+ background var(${UI.ANIMATION_DURATION}) ease-in-out,
+ color var(${UI.ANIMATION_DURATION}) ease-in-out;
border-radius: 24px;
vertical-align: center;
@@ -61,9 +62,10 @@ const ActionButton = styled.button`
const TextButton = styled.button`
display: inline-block;
- color: inherit;
- font-size: 13px;
- padding: 5px 10px;
+ color: var(${UI.COLOR_TEXT_OPACITY_70});
+ font-size: 12px;
+ font-weight: 500;
+ padding: 5px;
cursor: pointer;
background: none;
outline: none;
diff --git a/apps/cowswap-frontend/src/modules/ordersTable/containers/OrdersTableWidget/hooks/useOrdersTableList.ts b/apps/cowswap-frontend/src/modules/ordersTable/containers/OrdersTableWidget/hooks/useOrdersTableList.ts
index 6f9e37bee4..853aab2b91 100644
--- a/apps/cowswap-frontend/src/modules/ordersTable/containers/OrdersTableWidget/hooks/useOrdersTableList.ts
+++ b/apps/cowswap-frontend/src/modules/ordersTable/containers/OrdersTableWidget/hooks/useOrdersTableList.ts
@@ -6,12 +6,15 @@ import { getIsComposableCowOrder } from 'utils/orderUtils/getIsComposableCowOrde
import { getIsNotComposableCowOrder } from 'utils/orderUtils/getIsNotComposableCowOrder'
import { TabOrderTypes } from '../../../types'
+import { getOrderParams } from '../../../utils/getOrderParams'
import { groupOrdersTable } from '../../../utils/groupOrdersTable'
import { getParsedOrderFromTableItem, isParsedOrder, OrderTableItem } from '../../../utils/orderTableGroupUtils'
export interface OrdersTableList {
pending: OrderTableItem[]
history: OrderTableItem[]
+ unfillable: OrderTableItem[]
+ all: OrderTableItem[]
}
const ordersSorter = (a: OrderTableItem, b: OrderTableItem) => {
@@ -23,13 +26,18 @@ const ordersSorter = (a: OrderTableItem, b: OrderTableItem) => {
const ORDERS_LIMIT = 100
-export function useOrdersTableList(allOrders: Order[], orderType: TabOrderTypes): OrdersTableList {
+export function useOrdersTableList(
+ allOrders: Order[],
+ orderType: TabOrderTypes,
+ chainId: number,
+ balancesAndAllowances: any,
+): OrdersTableList {
const allSortedOrders = useMemo(() => {
return groupOrdersTable(allOrders).sort(ordersSorter)
}, [allOrders])
return useMemo(() => {
- const { pending, history } = allSortedOrders.reduce(
+ const { pending, history, unfillable, all } = allSortedOrders.reduce(
(acc, item) => {
const order = isParsedOrder(item) ? item : item.parent
@@ -41,7 +49,22 @@ export function useOrdersTableList(allOrders: Order[], orderType: TabOrderTypes)
return acc
}
- if (PENDING_STATES.includes(order.status)) {
+ // Add to 'all' list regardless of status
+ acc.all.push(item)
+
+ const isPending = PENDING_STATES.includes(order.status)
+
+ // Check if order is unfillable (insufficient balance or allowance)
+ const params = getOrderParams(chainId, balancesAndAllowances, order)
+ const isUnfillable = params.hasEnoughBalance === false || params.hasEnoughAllowance === false
+
+ // Only add to unfillable if the order is both pending and unfillable
+ if (isPending && isUnfillable) {
+ acc.unfillable.push(item)
+ }
+
+ // Add to pending or history based on status
+ if (isPending) {
acc.pending.push(item)
} else {
acc.history.push(item)
@@ -49,9 +72,14 @@ export function useOrdersTableList(allOrders: Order[], orderType: TabOrderTypes)
return acc
},
- { pending: [], history: [] } as OrdersTableList
+ { pending: [], history: [], unfillable: [], all: [] } as OrdersTableList,
)
- return { pending: pending.slice(0, ORDERS_LIMIT), history: history.slice(0, ORDERS_LIMIT) }
- }, [allSortedOrders, orderType])
+ return {
+ pending: pending.slice(0, ORDERS_LIMIT),
+ history: history.slice(0, ORDERS_LIMIT),
+ unfillable: unfillable.slice(0, ORDERS_LIMIT),
+ all: all.slice(0, ORDERS_LIMIT),
+ }
+ }, [allSortedOrders, orderType, chainId, balancesAndAllowances])
}
diff --git a/apps/cowswap-frontend/src/modules/ordersTable/containers/OrdersTableWidget/index.tsx b/apps/cowswap-frontend/src/modules/ordersTable/containers/OrdersTableWidget/index.tsx
index 509dd0ff75..2f727fdaf0 100644
--- a/apps/cowswap-frontend/src/modules/ordersTable/containers/OrdersTableWidget/index.tsx
+++ b/apps/cowswap-frontend/src/modules/ordersTable/containers/OrdersTableWidget/index.tsx
@@ -1,15 +1,18 @@
import { useAtomValue, useSetAtom } from 'jotai'
-import { ReactNode, useCallback, useEffect, useMemo } from 'react'
+import { ReactNode, useCallback, useEffect, useMemo, useState } from 'react'
import { useTokensAllowances, useTokensBalances } from '@cowprotocol/balances-and-allowances'
+import { UI } from '@cowprotocol/ui'
import { useIsSafeViaWc, useWalletDetails, useWalletInfo } from '@cowprotocol/wallet'
+import { Search } from 'react-feather'
import { useLocation } from 'react-router-dom'
import styled from 'styled-components/macro'
import { Order } from 'legacy/state/orders/actions'
import { useInjectedWidgetParams } from 'modules/injectedWidget'
+import { limitOrdersSettingsAtom } from 'modules/limitOrders/state/limitOrdersSettingsAtom'
import { pendingOrdersPricesAtom } from 'modules/orders/state/pendingOrdersPricesAtom'
import { useGetSpotPrice } from 'modules/orders/state/spotPricesAtom'
import { BalancesAndAllowances } from 'modules/tokens'
@@ -25,19 +28,67 @@ import { OrdersTableList, useOrdersTableList } from './hooks/useOrdersTableList'
import { useOrdersTableTokenApprove } from './hooks/useOrdersTableTokenApprove'
import { useValidatePageUrlParams } from './hooks/useValidatePageUrlParams'
-import { OPEN_TAB, ORDERS_TABLE_TABS } from '../../const/tabs'
+import { OPEN_TAB, ORDERS_TABLE_TABS, ALL_ORDERS_TAB } from '../../const/tabs'
import { OrdersTableContainer } from '../../pure/OrdersTableContainer'
+import { ColumnLayout, LAYOUT_MAP } from '../../pure/OrdersTableContainer/tableHeaders'
import { OrderActions } from '../../pure/OrdersTableContainer/types'
import { TabOrderTypes } from '../../types'
import { buildOrdersTableUrl } from '../../utils/buildOrdersTableUrl'
-import { OrderTableItem, tableItemsToOrders } from '../../utils/orderTableGroupUtils'
+import { OrderTableItem, tableItemsToOrders, getParsedOrderFromTableItem } from '../../utils/orderTableGroupUtils'
import { parseOrdersTableUrl } from '../../utils/parseOrdersTableUrl'
import { MultipleCancellationMenu } from '../MultipleCancellationMenu'
import { OrdersReceiptModal } from '../OrdersReceiptModal'
import { useGetAlternativeOrderModalContextCallback, useSelectReceiptOrder } from '../OrdersReceiptModal/hooks'
+const SearchInputContainer = styled.div`
+ margin: 0;
+ padding: 0 0 0 16px;
+ position: relative;
+`
+
+const SearchIcon = styled(Search)`
+ position: absolute;
+ left: 28px;
+ top: 50%;
+ transform: translateY(-50%);
+ color: var(${UI.COLOR_TEXT_OPACITY_50});
+ width: 16px;
+ height: 16px;
+`
+
+const SearchInput = styled.input`
+ width: 100%;
+ padding: 8px 12px 8px 36px;
+ border: 1px solid var(${UI.COLOR_TEXT_OPACITY_10});
+ background: var(${UI.COLOR_PAPER});
+ color: var(${UI.COLOR_TEXT});
+ border-radius: 8px;
+ font-size: 13px;
+ font-weight: 500;
+
+ &::placeholder {
+ color: var(${UI.COLOR_TEXT_OPACITY_50});
+ }
+
+ &:focus {
+ outline: none;
+ border-color: var(${UI.COLOR_TEXT_OPACITY_50});
+ }
+`
+
function getOrdersListByIndex(ordersList: OrdersTableList, id: string): OrderTableItem[] {
- return id === OPEN_TAB.id ? ordersList.pending : ordersList.history
+ switch (id) {
+ case 'all':
+ return ordersList.all
+ case 'unfillable':
+ return ordersList.unfillable
+ case 'open':
+ return ordersList.pending
+ case 'history':
+ return ordersList.history
+ default:
+ return ordersList.pending
+ }
}
function toggleOrderInCancellationList(state: CancellableOrder[], order: CancellableOrder): CancellableOrder[] {
@@ -50,10 +101,6 @@ function toggleOrderInCancellationList(state: CancellableOrder[], order: Cancell
return [...state, order]
}
-const ContentWrapper = styled.div`
- width: 100%;
-`
-
interface OrdersTableWidgetProps {
displayOrdersOnlyForSafeApp: boolean
orders: Order[]
@@ -71,7 +118,6 @@ export function OrdersTableWidget({
const location = useLocation()
const navigate = useNavigate()
const cancelOrder = useCancelOrder()
- const ordersList = useOrdersTableList(allOrders, orderType)
const { allowsOffchainSigning } = useWalletDetails()
const pendingOrdersPrices = useAtomValue(pendingOrdersPricesAtom)
const ordersToCancel = useAtomValue(ordersToCancelAtom)
@@ -80,12 +126,33 @@ export function OrdersTableWidget({
const selectReceiptOrder = useSelectReceiptOrder()
const isSafeViaWc = useIsSafeViaWc()
const injectedWidgetParams = useInjectedWidgetParams()
+ const [searchTerm, setSearchTerm] = useState('')
+ const limitOrdersSettings = useAtomValue(limitOrdersSettingsAtom)
+ const columnLayout = useMemo(
+ () => LAYOUT_MAP[limitOrdersSettings.columnLayout] || ColumnLayout.DEFAULT,
+ [limitOrdersSettings.columnLayout],
+ )
+
+ const balancesState = useTokensBalances()
+ const allowancesState = useTokensAllowances()
+
+ const balancesAndAllowances: BalancesAndAllowances = useMemo(() => {
+ const { isLoading: balancesLoading, values: balances } = balancesState
+ const { isLoading: allowancesLoading, values: allowances } = allowancesState
+ return {
+ isLoading: balancesLoading || allowancesLoading,
+ balances,
+ allowances,
+ }
+ }, [balancesState, allowancesState])
+
+ const ordersList = useOrdersTableList(allOrders, orderType, chainId, balancesAndAllowances)
const { currentTabId, currentPageNumber } = useMemo(() => {
const params = parseOrdersTableUrl(location.search)
return {
- currentTabId: params.tabId || OPEN_TAB.id,
+ currentTabId: params.tabId || ALL_ORDERS_TAB.id,
currentPageNumber: params.pageNumber || 1,
}
}, [location.search])
@@ -100,21 +167,6 @@ export function OrdersTableWidget({
})
}, [currentTabId, ordersList])
- const isOpenOrdersTab = useMemo(() => OPEN_TAB.id === currentTabId, [currentTabId])
-
- const balancesState = useTokensBalances()
- const allowancesState = useTokensAllowances()
-
- const balancesAndAllowances: BalancesAndAllowances = useMemo(() => {
- const { isLoading: balancesLoading, values: balances } = balancesState
- const { isLoading: allowancesLoading, values: allowances } = allowancesState
- return {
- isLoading: balancesLoading || allowancesLoading,
- balances,
- allowances,
- }
- }, [balancesState, allowancesState])
-
const { pendingActivity } = useCategorizeRecentActivity()
const toggleOrdersForCancellation = useCallback(
@@ -160,32 +212,104 @@ export function OrdersTableWidget({
useValidatePageUrlParams(orders.length, currentTabId, currentPageNumber)
+ const filteredOrders = useMemo(() => {
+ if (!searchTerm) return orders
+
+ const searchTermLower = searchTerm.toLowerCase().trim()
+
+ // First try exact symbol matches (case-insensitive)
+ const exactMatches = orders.filter((order) => {
+ const parsedOrder = getParsedOrderFromTableItem(order)
+ const inputToken = parsedOrder.inputToken
+ const outputToken = parsedOrder.outputToken
+
+ return [inputToken.symbol, outputToken.symbol].some((symbol) => {
+ return symbol?.toLowerCase() === searchTermLower
+ })
+ })
+
+ // If we have exact matches, return those
+ if (exactMatches.length > 0) {
+ return exactMatches
+ }
+
+ // Otherwise, fall back to partial matches and address search
+ return orders.filter((order) => {
+ const parsedOrder = getParsedOrderFromTableItem(order)
+ const inputToken = parsedOrder.inputToken
+ const outputToken = parsedOrder.outputToken
+
+ // Check for partial symbol matches (case-insensitive)
+ const symbolMatch = [inputToken.symbol, outputToken.symbol].some((symbol) => {
+ return symbol?.toLowerCase().includes(searchTermLower)
+ })
+
+ if (symbolMatch) return true
+
+ // If not a symbol match, check for address matches
+ // Clean up the search term but preserve '0x' prefix if present
+ const hasPrefix = searchTermLower.startsWith('0x')
+ const cleanedSearch = searchTermLower.replace(/[^0-9a-fx]/g, '')
+
+ // For exact address matches (40 or 42 chars), do strict comparison
+ if (cleanedSearch.length === 40 || cleanedSearch.length === 42) {
+ const searchTermNormalized = hasPrefix ? cleanedSearch : `0x${cleanedSearch}`
+ return [inputToken.address, outputToken.address].some(
+ (address) => address.toLowerCase() === searchTermNormalized.toLowerCase(),
+ )
+ }
+
+ // For partial address matches
+ const searchWithoutPrefix = hasPrefix ? cleanedSearch.slice(2) : cleanedSearch
+ if (searchWithoutPrefix.length >= 2) {
+ // Only search if we have at least 2 characters
+ return [inputToken.address, outputToken.address].some((address) => {
+ const addressWithoutPrefix = address.slice(2).toLowerCase()
+ return addressWithoutPrefix.includes(searchWithoutPrefix.toLowerCase())
+ })
+ }
+
+ return false
+ })
+ }, [orders, searchTerm])
+
return (
<>
-
- {children}
-
- {isOpenOrdersTab && orders.length && }
-
-
+ {children}
+
+ {(currentTabId === OPEN_TAB.id || currentTabId === 'all' || currentTabId === 'unfillable') &&
+ orders.length > 0 && }
+
+
+
+ setSearchTerm(e.target.value)}
+ />
+
+
+
>
)
diff --git a/apps/cowswap-frontend/src/modules/ordersTable/pure/OrderStatusBox/index.tsx b/apps/cowswap-frontend/src/modules/ordersTable/pure/OrderStatusBox/index.tsx
index f066c74d6b..5e40b59f38 100644
--- a/apps/cowswap-frontend/src/modules/ordersTable/pure/OrderStatusBox/index.tsx
+++ b/apps/cowswap-frontend/src/modules/ordersTable/pure/OrderStatusBox/index.tsx
@@ -39,32 +39,49 @@ const Wrapper = styled.div<{
left: 0;
top: 0;
background: var(--statusBackground);
- /* opacity: 0.14; */
z-index: 1;
border-radius: ${({ withWarning }) => (withWarning ? '9px 0 0 9px' : '9px')};
}
`
+const StatusContent = styled.div`
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ position: relative;
+ z-index: 2;
+`
+
type OrderStatusBoxProps = {
order: ParsedOrder
widthAuto?: boolean
withWarning?: boolean
onClick?: Command
+ WarningTooltip?: React.ComponentType<{ children: React.ReactNode }>
}
-export function OrderStatusBox({ order, widthAuto, withWarning, onClick }: OrderStatusBoxProps) {
+export function OrderStatusBox({ order, widthAuto, withWarning, onClick, WarningTooltip }: OrderStatusBoxProps) {
const { title, color, background } = getOrderStatusTitleAndColor(order)
+
+ const content = {title}
+
return (
-
- {/* Status overrides for special cases */}
- {title}
-
+ <>
+
+ {content}
+
+ {withWarning && WarningTooltip && (
+
+ <>>
+
+ )}
+ >
)
}
diff --git a/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTableContainer/OrderRow/EstimatedExecutionPrice.tsx b/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTableContainer/OrderRow/EstimatedExecutionPrice.tsx
index e4c71dda24..cda6d61cd5 100644
--- a/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTableContainer/OrderRow/EstimatedExecutionPrice.tsx
+++ b/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTableContainer/OrderRow/EstimatedExecutionPrice.tsx
@@ -1,5 +1,6 @@
import AlertTriangle from '@cowprotocol/assets/cow-swap/alert.svg'
import { ZERO_FRACTION } from '@cowprotocol/common-const'
+import { Command } from '@cowprotocol/types'
import { UI } from '@cowprotocol/ui'
import { SymbolElement, TokenAmount, TokenAmountProps } from '@cowprotocol/ui'
import { HoverTooltip } from '@cowprotocol/ui'
@@ -10,7 +11,6 @@ import SVG from 'react-inlinesvg'
import styled from 'styled-components/macro'
import { HIGH_FEE_WARNING_PERCENTAGE } from 'common/constants/common'
-import { calculateOrderExecutionStatus, ExecuteIndicator } from 'common/pure/OrderExecutionStatusList'
import * as styledEl from './styled'
@@ -28,19 +28,6 @@ export const EstimatedExecutionPriceWrapper = styled.span<{ hasWarning: boolean;
color: inherit;
}
- // Triangle warning icon override
- ${styledEl.WarningIndicator} {
- padding: 0 0 0 3px;
-
- svg {
- --size: 18px;
- width: var(--size);
- height: var(--size);
- min-width: var(--size);
- min-height: var(--size);
- }
- }
-
// Popover container override
> div > div,
> span {
@@ -50,21 +37,36 @@ export const EstimatedExecutionPriceWrapper = styled.span<{ hasWarning: boolean;
`
const UnfillableLabel = styled.span`
- width: 100%;
- max-width: 90px;
- background: var(${UI.COLOR_DANGER_BG});
- color: var(${UI.COLOR_DANGER_TEXT});
+ width: auto;
+ color: var(${UI.COLOR_DANGER});
position: relative;
- border-radius: 9px;
display: flex;
align-items: center;
justify-content: center;
- font-size: 11px;
- font-weight: 600;
- padding: 6px 2px;
- margin: 0 4px 0 0;
- letter-spacing: 0.2px;
- text-transform: uppercase;
+ font-size: inherit;
+ font-weight: 500;
+ line-height: 1.1;
+ flex-flow: row wrap;
+ align-items: center;
+ justify-content: flex-start;
+ gap: 3px;
+`
+
+const ApprovalLink = styled.button`
+ background: none;
+ border: none;
+ padding: 0;
+ margin: 0;
+ cursor: pointer;
+ font-size: inherit;
+ color: inherit;
+ text-decoration: underline;
+ color: var(${UI.COLOR_PRIMARY});
+ font-weight: 500;
+
+ &:hover {
+ opacity: 1;
+ }
`
export type EstimatedExecutionPriceProps = TokenAmountProps & {
@@ -75,10 +77,15 @@ export type EstimatedExecutionPriceProps = TokenAmountProps & {
amountDifference?: CurrencyAmount
percentageFee?: Percent
amountFee?: CurrencyAmount
+ warningText?: string
+ WarningTooltip?: React.FC<{ children: React.ReactNode; showIcon: boolean }>
+ onApprove?: Command
}
export function EstimatedExecutionPrice(props: EstimatedExecutionPriceProps) {
const {
+ amount,
+ tokenSymbol,
isInverted,
isUnfillable,
canShowWarning,
@@ -86,7 +93,9 @@ export function EstimatedExecutionPrice(props: EstimatedExecutionPriceProps) {
amountDifference,
percentageFee,
amountFee,
- amount,
+ warningText,
+ WarningTooltip,
+ onApprove,
...rest
} = props
@@ -98,22 +107,31 @@ export function EstimatedExecutionPrice(props: EstimatedExecutionPriceProps) {
const absoluteDifferenceAmount = amountDifference?.lessThan(ZERO_FRACTION)
? amountDifference.multiply(MINUS_ONE_FRACTION)
: amountDifference
- const orderExecutionStatus = calculateOrderExecutionStatus(percentageDifferenceInverted)
+
const feeWarning = canShowWarning && percentageFee?.greaterThan(HIGH_FEE_WARNING_PERCENTAGE)
const isNegativeDifference = percentageDifferenceInverted?.lessThan(ZERO_FRACTION)
const marketPriceNeedsToGoDown = isInverted ? !isNegativeDifference : isNegativeDifference
const content = (
<>
-
-
+
>
)
+ const unfillableLabel = (
+
+ {warningText}
+ {warningText === 'Insufficient allowance' && onApprove && (
+ Set approval
+ )}
+ {WarningTooltip && {null} }
+
+ )
+
return (
{isUnfillable ? (
- UNFILLABLE
+ {unfillableLabel}
) : !absoluteDifferenceAmount ? (
{content}
) : (
@@ -121,7 +139,9 @@ export function EstimatedExecutionPrice(props: EstimatedExecutionPriceProps) {
wrapInContainer={true}
content={
- {!isNegativeDifference ? (
+ {isNegativeDifference && Math.abs(Number(percentageDifferenceInverted?.toFixed(4) ?? 0)) <= 0.01 ? (
+ <>Will execute soon!>
+ ) : (
<>
Market price needs to go {marketPriceNeedsToGoDown ? 'down 📉' : 'up 📈'} by
@@ -133,8 +153,6 @@ export function EstimatedExecutionPrice(props: EstimatedExecutionPriceProps) {
to execute your order.
>
- ) : (
- <>Will execute soon!>
)}
}
diff --git a/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTableContainer/OrderRow/OrderWarning.tsx b/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTableContainer/OrderRow/OrderWarning.tsx
new file mode 100644
index 0000000000..3f50ba73aa
--- /dev/null
+++ b/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTableContainer/OrderRow/OrderWarning.tsx
@@ -0,0 +1,143 @@
+import React from 'react'
+
+import AlertTriangle from '@cowprotocol/assets/cow-swap/alert.svg'
+import { Command } from '@cowprotocol/types'
+import { ButtonSecondary, TokenSymbol, UI, HoverTooltip } from '@cowprotocol/ui'
+
+import SVG from 'react-inlinesvg'
+
+import * as styledEl from './styled'
+
+interface WarningProps {
+ symbol: string
+ isScheduled: boolean
+}
+
+interface AllowanceWarningProps extends WarningProps {
+ approve: Command
+}
+
+function BalanceWarning({ symbol, isScheduled }: WarningProps) {
+ return (
+
+ Insufficient token balance
+
+ Insufficient{' '}
+
+
+ {' '}
+ balance detected.
+
+
+ {isScheduled ? (
+ <>
+ If the balance remains insufficient at creation time, this order portion will not be created. Add more{' '}
+
+
+ {' '}
+ before that time.
+ >
+ ) : (
+ <>
+ The order remains open. Execution requires sufficient{' '}
+
+
+ {' '}
+ balance.
+ >
+ )}
+
+
+ )
+}
+
+function AllowanceWarning({ symbol, isScheduled, approve }: AllowanceWarningProps) {
+ return (
+
+ Insufficient token allowance
+
+ {isScheduled ? (
+ <>
+ Insufficient allowance granted for{' '}
+
+
+
+ . If allowance remains insufficient at creation time, this portion will not be created. Approve the{' '}
+
+
+ {' '}
+ token before creation.
+ >
+ ) : (
+ <>
+ The order remains open. Execution requires adequate allowance for{' '}
+
+
+
+ . Approve the token to proceed.
+ >
+ )}
+
+
+ Set approval
+
+
+ )
+}
+
+interface WarningTooltipProps {
+ children: React.ReactNode
+ hasEnoughBalance: boolean
+ hasEnoughAllowance: boolean
+ inputTokenSymbol: string
+ isOrderScheduled: boolean
+ onApprove: Command
+ showIcon?: boolean
+}
+
+export function WarningTooltip({
+ children,
+ hasEnoughBalance,
+ hasEnoughAllowance,
+ inputTokenSymbol,
+ isOrderScheduled,
+ onApprove,
+ showIcon = false,
+}: WarningTooltipProps) {
+ const withAllowanceWarning = hasEnoughAllowance === false
+
+ const tooltipContent = (
+
+ {hasEnoughBalance === false && }
+ {withAllowanceWarning && (
+
+ )}
+
+ )
+
+ if (showIcon) {
+ return (
+
+ }
+ />
+ {children}
+
+ )
+ }
+
+ return (
+
+ {children}
+
+ )
+}
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 8bd52a248f..cbd63f9469 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
@@ -1,18 +1,15 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react'
-import AlertTriangle from '@cowprotocol/assets/cow-swap/alert.svg'
import { ZERO_FRACTION } from '@cowprotocol/common-const'
import { useTimeAgo } from '@cowprotocol/common-hooks'
import { getAddress, getEtherscanLink } from '@cowprotocol/common-utils'
import { SupportedChainId } from '@cowprotocol/cow-sdk'
import { TokenLogo } from '@cowprotocol/tokens'
import { Command, UiOrderType } from '@cowprotocol/types'
-import { ButtonSecondary, Loader, TokenAmount, TokenSymbol, UI } from '@cowprotocol/ui'
+import { Loader, TokenAmount, UI } from '@cowprotocol/ui'
import { PercentDisplay, percentIsAlmostHundred } from '@cowprotocol/ui'
import { Currency, CurrencyAmount, Percent, Price } from '@uniswap/sdk-core'
-import SVG from 'react-inlinesvg'
-
import { CREATING_STATES, OrderStatus } from 'legacy/state/orders/actions'
import { PendingOrderPrices } from 'modules/orders/state/pendingOrdersPricesAtom'
@@ -25,20 +22,40 @@ 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 { getIsFinalizedOrder } from 'utils/orderUtils/getIsFinalizedOrder'
import { getSellAmountWithFee } from 'utils/orderUtils/getSellAmountWithFee'
import { getUiOrderType } from 'utils/orderUtils/getUiOrderType'
import { ParsedOrder } from 'utils/orderUtils/parseOrder'
import { EstimatedExecutionPrice } from './EstimatedExecutionPrice'
import { OrderContextMenu } from './OrderContextMenu'
+import { WarningTooltip } from './OrderWarning'
import * as styledEl from './styled'
import { OrderParams } from '../../../utils/getOrderParams'
import { OrderStatusBox } from '../../OrderStatusBox'
import { CheckboxCheckmark, TableRow, TableRowCheckbox, TableRowCheckboxWrapper } from '../styled'
+import { ColumnLayout } from '../tableHeaders'
import { OrderActions } from '../types'
+// Constants
const TIME_AGO_UPDATE_INTERVAL = 3000
+const MIN_PERCENTAGE_TO_DISPLAY = 0.01 // Minimum percentage to display (show dash below this)
+const GOOD_PRICE_THRESHOLD = 1.0 // 1% or less difference - good price
+const FAIR_PRICE_THRESHOLD = 5.0 // 5% or less difference - fair price
+
+// Helper to determine the color based on percentage
+function getDistanceColor(percentage: number): string {
+ const absPercentage = Math.abs(percentage)
+
+ if (absPercentage <= GOOD_PRICE_THRESHOLD) {
+ return `var(${UI.COLOR_SUCCESS})` // Green - good price
+ } else if (absPercentage <= FAIR_PRICE_THRESHOLD) {
+ return `var(${UI.COLOR_PRIMARY})` // Blue - fair price
+ }
+
+ return 'inherit' // Default text color for larger differences
+}
function CurrencyAmountItem({ amount }: { amount: CurrencyAmount }) {
return (
@@ -52,104 +69,28 @@ function CurrencySymbolItem({ amount }: { amount: CurrencyAmount }) {
return
}
-function BalanceWarning(params: { symbol: string; isScheduled: boolean }) {
- const { symbol, isScheduled } = params
-
- return (
-
- Insufficient balance
-
- Your wallet currently has insufficient{' '}
-
-
- {' '}
- balance to execute this order.
-
-
- {isScheduled ? (
- <>
- If there are not enough funds for this order by creation time, this part won't be created. Top up your{' '}
-
-
- {' '}
- balance before then to have it created.
- >
- ) : (
- <>
- The order is still open and will become executable when you top up your{' '}
-
-
- {' '}
- balance.
- >
- )}
-
-
- )
-}
-function AllowanceWarning(params: { symbol: string; isScheduled: boolean; approve: Command }) {
- const { symbol, isScheduled } = params
-
- return (
-
- Insufficient approval for this order
-
- {isScheduled ? (
- <>
- You haven't given CoW Swap sufficient allowance to spend{' '}
-
-
-
- .
-
- If there's not enough allowance for this order by creation time, this part won't be created. Approve{' '}
-
-
- {' '}
- in your account token page before then to have it created.
- >
- ) : (
- <>
- This order is still open and valid, but you haven't given CoW Swap sufficient allowance to spend{' '}
-
-
-
- .
-
- The order will become executable when you approve{' '}
-
-
- {' '}
- in your account token page.
- >
- )}
-
-
- Approve
-
-
- )
-}
-
export interface OrderRowProps {
order: ParsedOrder
prices: PendingOrderPrices | undefined | null
spotPrice: Price | undefined | null
isRateInverted: boolean
- isOpenOrdersTab: boolean
+ showLimitPrice: boolean
+ isHistoryTab: boolean
isRowSelectable: boolean
isRowSelected: boolean
isChild?: boolean
orderParams: OrderParams
onClick: Command
orderActions: OrderActions
- children?: JSX.Element
+ children?: React.ReactNode
+ columnLayout?: ColumnLayout
}
export function OrderRow({
order,
isRateInverted: isGloballyInverted,
- isOpenOrdersTab,
+ showLimitPrice,
+ isHistoryTab,
isRowSelectable,
isRowSelected,
isChild,
@@ -159,6 +100,7 @@ export function OrderRow({
prices,
spotPrice,
children,
+ columnLayout = ColumnLayout.DEFAULT,
}: OrderRowProps) {
const { buyAmount, rateInfoParams, hasEnoughAllowance, hasEnoughBalance, chainId } = orderParams
const { creationTime, expirationTime, status } = order
@@ -184,9 +126,10 @@ export function OrderRow({
const isScheduledCreating = isOrderScheduled && Date.now() > creationTime.getTime()
const expirationTimeAgo = useTimeAgo(expirationTime, TIME_AGO_UPDATE_INTERVAL)
const creationTimeAgo = useTimeAgo(creationTime, TIME_AGO_UPDATE_INTERVAL)
-
- // TODO: set the real value when API returns it
- // const executedTimeAgo = useTimeAgo(expirationTime, TIME_AGO_UPDATE_INTERVAL)
+ const fulfillmentTimeAgo = useTimeAgo(
+ order.fulfillmentTime ? new Date(order.fulfillmentTime) : undefined,
+ TIME_AGO_UPDATE_INTERVAL,
+ )
const activityUrl = chainId ? getActivityUrl(chainId, order) : undefined
const [isInverted, setIsInverted] = useState(() => {
@@ -205,7 +148,7 @@ export function OrderRow({
const executedPriceInverted = isInverted ? executedPrice?.invert() : executedPrice
const spotPriceInverted = isInverted ? spotPrice?.invert() : spotPrice
- const priceDiffs = usePricesDifference(prices, spotPrice, isInverted)
+ const priceDiffs = usePricesDifference(prices, spotPrice)
const feeDifference = useFeeAmountDifference(rateInfoParams, prices)
const isExecutedPriceZero = executedPriceInverted !== undefined && executedPriceInverted?.equalTo(ZERO_FRACTION)
@@ -215,15 +158,131 @@ export function OrderRow({
const inputTokenSymbol = order.inputToken.symbol || ''
+ const getWarningText = () => {
+ if (hasEnoughBalance === false) return 'Insufficient balance'
+ if (hasEnoughAllowance === false) return 'Insufficient allowance'
+ return 'Unfillable'
+ }
+
+ const renderWarningTooltip = (showIcon?: boolean) => (props: { children: React.ReactNode }) => (
+ orderActions.approveOrderToken(order.inputToken)}
+ showIcon={showIcon}
+ {...props}
+ />
+ )
+
+ const renderLimitPrice = () => (
+
+
+
+ )
+
+ const renderFillsAt = () => (
+ <>
+ {getIsFinalizedOrder(order) ? (
+ '-'
+ ) : prices && estimatedExecutionPrice ? (
+
+ {!isUnfillable &&
+ priceDiffs?.percentage &&
+ Math.abs(Number(priceDiffs.percentage.toFixed(4))) <= MIN_PERCENTAGE_TO_DISPLAY ? (
+ ⚡️ Pending execution
+ ) : (
+ orderActions.approveOrderToken(order.inputToken)}
+ WarningTooltip={renderWarningTooltip()}
+ />
+ )}
+
+ ) : prices === null || !estimatedExecutionPrice || isOrderCreating ? (
+ '-'
+ ) : (
+
+ )}
+ >
+ )
+
+ const renderFillsAtWithDistance = () => {
+ const fillsAtContent = renderFillsAt()
+ const distance =
+ !isUnfillable && priceDiffs?.percentage && Number(priceDiffs?.percentage.toFixed(4)) >= MIN_PERCENTAGE_TO_DISPLAY
+ ? `${priceDiffs?.percentage.toFixed(2)}%`
+ : '-'
+
+ return (
+
+ {fillsAtContent}
+
+ {distance}
+
+
+ )
+ }
+
+ const renderDistanceToMarket = () => (
+ <>
+ {isUnfillable ? (
+ '-'
+ ) : priceDiffs?.percentage && Number(priceDiffs.percentage.toFixed(4)) >= MIN_PERCENTAGE_TO_DISPLAY ? (
+
+ {priceDiffs.percentage.toFixed(2)}%
+
+ ) : (
+ '-'
+ )}
+ >
+ )
+
+ const renderMarketPrice = () => (
+ <>
+ {spotPrice ? (
+
+ ) : spotPrice === null ? (
+ '-'
+ ) : (
+
+ )}
+ >
+ )
+
return (
{/*Checkbox for multiple cancellation*/}
- {isRowSelectable && isOpenOrdersTab && (
+ {isRowSelectable && !isHistoryTab && (
- {/* Limit price */}
-
-
-
-
-
+ {/* Non-history tab columns */}
+ {!isHistoryTab ? (
+ <>
+ {/* Price columns based on layout */}
+ {columnLayout === ColumnLayout.DEFAULT && (
+ <>
+
+ {showLimitPrice ? renderLimitPrice() : renderFillsAt()}
+
+ {renderDistanceToMarket()}
+ {renderMarketPrice()}
+ >
+ )}
- {/* Market price */}
- {/* {isOpenOrdersTab && ordersTableFeatures.DISPLAY_EST_EXECUTION_PRICE && ( */}
- {isOpenOrdersTab && (
-
- {/*// TODO: gray out the price when it was updated too long ago*/}
- {spotPrice ? (
-
- ) : spotPrice === null ? (
- '-'
- ) : (
-
+ {columnLayout === ColumnLayout.VIEW_2 && (
+ <>
+ {renderLimitPrice()}
+ {renderFillsAt()}
+ {renderDistanceToMarket()}
+ >
)}
-
- )}
- {/* Execution price */}
- {!isOpenOrdersTab && (
-
- {executedPriceInverted ? (
-
- ) : (
- '-'
+ {columnLayout === ColumnLayout.VIEW_3 && (
+ <>
+ {renderLimitPrice()}
+ {renderFillsAtWithDistance()}
+ {renderMarketPrice()}
+ >
)}
-
- )}
- {/* Executes at */}
- {isOpenOrdersTab && (
-
- {/*// TODO: gray out the price when it was updated too long ago*/}
- {prices && estimatedExecutionPrice ? (
-
-
+ {expirationTimeAgo}
+ {isScheduledCreating ? 'Creating...' : creationTimeAgo}
+
+ >
+ ) : (
+ <>
+ {/* History tab columns */}
+ {/* Limit price */}
+
+
+
+
+ {/* Execution price */}
+
+ {executedPriceInverted ? (
+
-
- ) : prices === null || !estimatedExecutionPrice || isOrderCreating ? (
- '-'
- ) : (
-
- )}
-
- )}
+ ) : (
+ '-'
+ )}
+
- {/* Expires */}
- {/* Created */}
- {isOpenOrdersTab && (
-
- {expirationTimeAgo}
- {isScheduledCreating ? 'Creating...' : creationTimeAgo}
-
- )}
+
+ {order.status === OrderStatus.FULFILLED && fulfillmentTimeAgo ? fulfillmentTimeAgo : '-'}
+
- {/* TODO: Enable once there is back-end support */}
- {/* {!isOpenOrdersTab && ordersTableFeatures.DISPLAY_EXECUTION_TIME && (
-
- {order.status === OrderStatus.FULFILLED ? executedTimeAgo : '-'}
-
- )} */}
+ {creationTimeAgo}
+ >
+ )}
{/* Filled % */}
-
-
-
-
+
+
+
+
+
+
{/* Status label */}
- {children ? (
- children
- ) : (
- <>
-
- {withWarning && (
-
- }
- text={
-
- {hasEnoughBalance === false && (
-
- )}
- {withAllowanceWarning && (
- orderActions.approveOrderToken(order.inputToken)}
- symbol={inputTokenSymbol}
- isScheduled={isOrderScheduled}
- />
- )}
-
- }
- />
-
- )}
- >
- )}
+
+ {/* Children (e.g. ToggleExpandButton for parent orders) */}
+ {children}
+
{/* Action content menu */}
{
- return calculatePriceDifference({ referencePrice: spotPrice, targetPrice: estimatedExecutionPrice, isInverted })
- }, [estimatedExecutionPrice, spotPrice, isInverted])
+ if (!spotPrice || !estimatedExecutionPrice) return null
+
+ // Calculate price difference using original (non-inverted) prices
+ // The percentage should stay the same regardless of display inversion
+ return calculatePriceDifference({
+ referencePrice: spotPrice,
+ targetPrice: estimatedExecutionPrice,
+ isInverted: false,
+ })
+ }, [estimatedExecutionPrice, spotPrice]) // Remove isInverted from dependencies since it shouldn't affect the calculation
}
/**
diff --git a/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTableContainer/OrderRow/styled.tsx b/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTableContainer/OrderRow/styled.tsx
index 3e137e4363..fde0b5b2ea 100644
--- a/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTableContainer/OrderRow/styled.tsx
+++ b/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTableContainer/OrderRow/styled.tsx
@@ -96,23 +96,22 @@ export const AmountItem = styled.div`
export const CellElement = styled.div<{
clickable?: boolean
doubleRow?: boolean
- hasBackground?: boolean
}>`
- padding: 0 ${({ hasBackground }) => (hasBackground ? '10px' : '0')};
+ padding: 0;
font-size: 12px;
font-weight: 500;
gap: 5px;
height: 100%;
display: flex;
- flex-direction: row;
+ flex-flow: row nowrap;
align-items: ${({ doubleRow }) => (doubleRow ? 'flex-start' : 'center')};
text-align: left;
- background: ${({ hasBackground }) =>
- hasBackground ? `linear-gradient(90deg, var(${UI.COLOR_TEXT_OPACITY_10}) 0%, transparent 100%)` : 'transparent'};
cursor: ${({ clickable }) => (clickable ? 'pointer' : '')};
> b {
font-weight: 500;
+ width: 100%;
+ text-align: left;
}
${({ doubleRow }) =>
@@ -177,24 +176,20 @@ export const CurrencyAmountWrapper = styled.div<{ clickable?: boolean }>`
`
export const ProgressBarWrapper = styled.div`
- width: 100%;
- max-width: 50%;
+ flex: 1;
align-items: center;
flex-flow: row nowrap;
gap: 8px;
- flex-direction: row-reverse;
padding: 0;
font-size: 12px;
font-weight: 500;
height: 100%;
+ width: 100%;
+ max-width: 190px;
display: flex;
text-align: left;
background: transparent;
- justify-content: center;
-
- > b {
- line-height: 1;
- }
+ justify-content: flex-start;
`
export const ProgressBar = styled.div<{ value: string }>`
@@ -215,6 +210,14 @@ export const ProgressBar = styled.div<{ value: string }>`
}
`
+export const FilledPercentageContainer = styled.div`
+ display: grid;
+ grid-template-columns: 50px 36px;
+ gap: 4px;
+ align-items: center;
+ width: 100%;
+`
+
export const ExecuteCellWrapper = styled.div`
width: 100%;
display: flex;
@@ -327,3 +330,7 @@ export const ToggleExpandButton = styled.div<{ isCollapsed?: boolean }>`
transform: ${({ isCollapsed }) => (isCollapsed ? 'rotate(90deg)' : 'rotate(0deg)')};
}
`
+
+export const DistanceToMarket = styled.span<{ $color: string }>`
+ color: ${({ $color }: { $color: string }) => $color};
+`
diff --git a/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTableContainer/OrdersTable.tsx b/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTableContainer/OrdersTable.tsx
index d2051d80d7..a34144ee2c 100644
--- a/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTableContainer/OrdersTable.tsx
+++ b/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTableContainer/OrdersTable.tsx
@@ -1,32 +1,25 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
-import iconOrderExecution from '@cowprotocol/assets/cow-swap/orderExecution.svg'
import { SupportedChainId } from '@cowprotocol/cow-sdk'
-import { Media, QuestionTooltipIconWrapper, UI } from '@cowprotocol/ui'
-import { HelpTooltip } from '@cowprotocol/ui'
+import { Media, UI } from '@cowprotocol/ui'
import { Currency, Price } from '@uniswap/sdk-core'
-import { Trans } from '@lingui/macro'
-import { X } from 'react-feather'
-import SVG from 'react-inlinesvg'
import styled from 'styled-components/macro'
import { PendingOrdersPrices } from 'modules/orders/state/pendingOrdersPricesAtom'
import { SpotPricesKeyParams } from 'modules/orders/state/spotPricesAtom'
import { BalancesAndAllowances } from 'modules/tokens'
-import { ordersTableFeatures } from 'common/constants/featureFlags'
-import { OrderExecutionStatusList, RateTooltipHeader } from 'common/pure/OrderExecutionStatusList'
-import { InvertRateControl } from 'common/pure/RateInfo'
import { CancellableOrder } from 'common/utils/isOrderCancellable'
import { isOrderOffChainCancellable } from 'common/utils/isOrderOffChainCancellable'
import { OrderRow } from './OrderRow'
import { CheckboxCheckmark, TableHeader, TableRowCheckbox, TableRowCheckboxWrapper } from './styled'
import { TableGroup } from './TableGroup'
+import { ColumnLayout, createTableHeaders } from './tableHeaders'
import { OrderActions } from './types'
-import { ORDERS_TABLE_PAGE_SIZE } from '../../const/tabs'
+import { HISTORY_TAB, ORDERS_TABLE_PAGE_SIZE } from '../../const/tabs'
import { useGetBuildOrdersTableUrl } from '../../hooks/useGetBuildOrdersTableUrl'
import { getOrderParams } from '../../utils/getOrderParams'
import {
@@ -41,11 +34,9 @@ import { OrdersTablePagination } from '../OrdersTablePagination'
const TableBox = styled.div`
display: block;
- border-radius: 16px;
border: none;
padding: 0;
position: relative;
- overflow: hidden;
background: var(${UI.COLOR_PAPER});
${Media.upToLargeAlt()} {
@@ -65,16 +56,14 @@ const TableInner = styled.div`
${({ theme }) => theme.colorScrollbar};
`
-const HeaderElement = styled.div<{ doubleRow?: boolean; hasBackground?: boolean }>`
+const HeaderElement = styled.div<{ doubleRow?: boolean }>`
height: 100%;
- padding: 0 ${({ hasBackground }) => (hasBackground ? '10px' : '0')};
+ padding: 0;
font-size: 12px;
line-height: 1.1;
font-weight: 500;
display: flex;
align-items: ${({ doubleRow }) => (doubleRow ? 'flex-start' : 'center')};
- background: ${({ hasBackground }) =>
- hasBackground ? `linear-gradient(90deg, var(${UI.COLOR_TEXT_OPACITY_10}) 0%, transparent 100%)` : 'transparent'};
> span {
display: flex;
@@ -93,26 +82,6 @@ const HeaderElement = styled.div<{ doubleRow?: boolean; hasBackground?: boolean
opacity: 0.7;
}
`}
-
- ${QuestionTooltipIconWrapper} {
- opacity: 0.5;
- transition: opacity var(${UI.ANIMATION_DURATION}) ease-in-out;
-
- &:hover {
- opacity: 1;
- }
- }
-
- ${({ doubleRow }) =>
- doubleRow &&
- `
- flex-flow: column wrap;
- gap: 2px;
-
- > i {
- opacity: 0.7;
- }
- `}
`
const Rows = styled.div`
@@ -125,71 +94,8 @@ const Rows = styled.div`
}
`
-const StyledInvertRateControl = styled(InvertRateControl)`
- display: inline-flex;
- margin-left: 5px;
-`
-
-const StyledCloseIcon = styled(X)`
- height: 24px;
- width: 24px;
- opacity: 0.6;
- transition: opacity var(${UI.ANIMATION_DURATION}) ease-in-out;
-
- &:hover {
- cursor: pointer;
- opacity: 1;
- }
-
- > line {
- stroke: var(${UI.COLOR_TEXT});
- }
-`
-
-const OrdersExplainerBanner = styled.div`
- display: grid;
- background: ${`linear-gradient(90deg, var(${UI.COLOR_PAPER}) 0%, var(${UI.COLOR_PAPER_DARKER}) 100%)`};
- width: 100%;
- gap: 16px;
- grid-template-columns: 6.2fr 5.5fr 24px;
- grid-template-rows: minmax(90px, 1fr);
- align-items: center;
- border-top: 1px solid transparent;
- border-bottom: 1px solid var(${UI.COLOR_TEXT_OPACITY_10});
- padding: 0 16px;
- color: inherit;
-
- ${Media.upToLargeAlt()} {
- width: fit-content;
- grid-template-columns: minmax(462px, 4fr) minmax(426px, 3.8fr) 24px;
- }
-
- /* 1st section */
- > div {
- display: flex;
- align-items: center;
- gap: 12px;
- color: inherit;
-
- > svg > path {
- fill: currentColor;
- }
-
- > b {
- font-size: 18px;
- font-weight: 500;
- }
- }
-
- /* 2nd section */
- > span {
- display: flex;
- flex-flow: column wrap;
- }
-`
-
export interface OrdersTableProps {
- isOpenOrdersTab: boolean
+ currentTab: string
allowsOffchainSigning: boolean
currentPageNumber: number
chainId: SupportedChainId
@@ -199,10 +105,11 @@ export interface OrdersTableProps {
balancesAndAllowances: BalancesAndAllowances
getSpotPrice: (params: SpotPricesKeyParams) => Price | null
orderActions: OrderActions
+ columnLayout?: ColumnLayout
}
export function OrdersTable({
- isOpenOrdersTab,
+ currentTab,
selectedOrders,
allowsOffchainSigning,
chainId,
@@ -212,9 +119,10 @@ export function OrdersTable({
getSpotPrice,
orderActions,
currentPageNumber,
+ columnLayout,
}: OrdersTableProps) {
const buildOrdersTableUrl = useGetBuildOrdersTableUrl()
- const [isRateInverted, setIsRateInverted] = useState(false)
+ const [showLimitPrice, setShowLimitPrice] = useState(false)
const checkboxRef = useRef(null)
const step = currentPageNumber * ORDERS_TABLE_PAGE_SIZE
@@ -234,28 +142,12 @@ export function OrdersTable({
return selectedOrders.reduce(
(acc, val) => {
acc[val.id] = true
-
return acc
},
{} as { [key: string]: true },
)
}, [selectedOrders])
- // Explainer banner for orders
- const [showOrdersExplainerBanner, setShowOrdersExplainerBanner] = useState(() => {
- const item = localStorage.getItem('showOrdersExplainerBanner')
- return item !== null ? item === 'true' : true
- })
-
- const closeOrdersExplainerBanner = (): void => {
- setShowOrdersExplainerBanner(false)
- localStorage.setItem('showOrdersExplainerBanner', 'false')
- }
-
- useEffect(() => {
- localStorage.setItem('showOrdersExplainerBanner', showOrdersExplainerBanner.toString())
- }, [showOrdersExplainerBanner])
-
const cancellableOrders = useMemo(
() => ordersPage.filter((item) => isOrderOffChainCancellable(getParsedOrderFromTableItem(item))),
[ordersPage],
@@ -280,119 +172,63 @@ export function OrdersTable({
checkbox.checked = allOrdersSelected
}, [allOrdersSelected, selectedOrders.length])
+ const tableHeaders = useMemo(
+ () => createTableHeaders(showLimitPrice, setShowLimitPrice, columnLayout),
+ [showLimitPrice, columnLayout],
+ )
+
+ const visibleHeaders = useMemo(() => {
+ const isHistoryTab = currentTab === HISTORY_TAB.id
+ return tableHeaders.filter((header) => {
+ // If showInHistory is not defined, show the header in all tabs
+ if (header.showInHistory === undefined) return true
+ // Otherwise, show based on the showInHistory value
+ return header.showInHistory === isHistoryTab
+ })
+ }, [tableHeaders, currentTab])
+
return (
<>
-
- {isRowSelectable && isOpenOrdersTab && (
-
-
-
- orderActions.toggleOrdersForCancellation(
- event.target.checked ? tableItemsToOrders(ordersPage) : [],
- )
- }
- />
-
-
-
- )}
-
-
- Sell → Buy
-
-
-
-
- Limit price
-
- setIsRateInverted(!isRateInverted)} />
-
-
- {isOpenOrdersTab && ordersTableFeatures.DISPLAY_EST_EXECUTION_PRICE && (
-
-
-
- Order executes at } />
-
-
-
- Market price
-
-
- )}
-
- {isOpenOrdersTab && (
-
-
- Market price
-
-
- )}
-
- {isOpenOrdersTab && (
-
-
-
- Executes at } />
-
-
-
- )}
-
- {!isOpenOrdersTab && (
-
-
-
- Execution price } />
-
-
-
- )}
-
- {isOpenOrdersTab && (
-
- Expiration
-
- Creation
-
-
- )}
-
- {/* {!isOpenOrdersTab && ordersTableFeatures.DISPLAY_EXECUTION_TIME && (
-
- Execution time
-
- )} */}
-
-
- Filled
-
-
-
- Status
-
- {/*Cancel order column*/}
-
+
+ {visibleHeaders.map((header) => {
+ if (header.id === 'checkbox' && (!isRowSelectable || currentTab === HISTORY_TAB.id)) {
+ return null
+ }
- {/* Show explainer modal if user hasn't closed it */}
- {isOpenOrdersTab && showOrdersExplainerBanner && (
-
-
-
-
- How close is my order to executing?
-
-
- {OrderExecutionStatusList()}
-
-
- )}
+ if (header.id === 'checkbox') {
+ return (
+
+
+
+ orderActions.toggleOrdersForCancellation(
+ event.target.checked ? tableItemsToOrders(ordersPage) : [],
+ )
+ }
+ />
+
+
+
+ )
+ }
+
+ return (
+
+ {header.content}
+ {header.extraComponent}
+
+ )
+ })}
+
{ordersPage.map((item) => {
@@ -406,21 +242,21 @@ export function OrdersTable({
if (isParsedOrder(item)) {
const order = item
- const orderParams = getOrderParams(chainId, balancesAndAllowances, order)
-
return (
orderActions.selectReceiptOrder(order)}
+ orderActions={orderActions}
+ columnLayout={columnLayout}
/>
)
} else {
@@ -432,10 +268,11 @@ export function OrdersTable({
key={item.parent.id}
isRowSelectable={isRowSelectable}
isRowSelected={!!selectedOrdersMap[item.parent.id]}
- isOpenOrdersTab={isOpenOrdersTab}
+ isHistoryTab={currentTab === HISTORY_TAB.id}
spotPrice={spotPrice}
prices={pendingOrdersPrices[item.parent.id]}
- isRateInverted={isRateInverted}
+ isRateInverted={false}
+ showLimitPrice={showLimitPrice}
orderActions={orderActions}
/>
)
diff --git a/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTableContainer/OrdersTabs.tsx b/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTableContainer/OrdersTabs.tsx
index f746ab82e4..ce129d5d03 100644
--- a/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTableContainer/OrdersTabs.tsx
+++ b/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTableContainer/OrdersTabs.tsx
@@ -8,18 +8,10 @@ import { OrderTab } from '../../const/tabs'
import { useGetBuildOrdersTableUrl } from '../../hooks/useGetBuildOrdersTableUrl'
const Tabs = styled.div`
- display: inline-block;
- border-radius: 9px;
- overflow: hidden;
+ display: flex;
+ flex-flow: row wrap;
+ gap: 4px;
margin: 0;
- border: 1px solid var(${UI.COLOR_TEXT_OPACITY_10});
-
- ${Media.upToMedium()} {
- width: 100%;
- display: grid;
- grid-template-columns: 1fr 1fr;
- grid-template-rows: max-content;
- }
`
const TabButton = styled(Link)<{ active: string }>`
@@ -27,13 +19,17 @@ const TabButton = styled(Link)<{ active: string }>`
background: ${({ active }) => (active === 'true' ? `var(${UI.COLOR_TEXT_OPACITY_10})` : 'transparent')};
color: ${({ active }) => (active === 'true' ? `var(${UI.COLOR_TEXT_PAPER})` : 'inherit')};
font-weight: ${({ active }) => (active === 'true' ? '600' : '400')};
+ border-radius: 14px;
+ border: 1px solid var(${UI.COLOR_TEXT_OPACITY_10});
text-decoration: none;
font-size: 13px;
- padding: 10px 24px;
+ padding: 10px;
border: 0;
outline: none;
cursor: pointer;
- transition: background var(${UI.ANIMATION_DURATION}) ease-in-out, color var(${UI.ANIMATION_DURATION}) ease-in-out;
+ transition:
+ background var(${UI.ANIMATION_DURATION}) ease-in-out,
+ color var(${UI.ANIMATION_DURATION}) ease-in-out;
${Media.upToMedium()} {
text-align: center;
@@ -54,7 +50,7 @@ export function OrdersTabs({ tabs }: OrdersTabsProps) {
const buildOrdersTableUrl = useGetBuildOrdersTableUrl()
const activeTabIndex = Math.max(
tabs.findIndex((i) => i.isActive),
- 0
+ 0,
)
return (
diff --git a/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTableContainer/TableGroup.tsx b/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTableContainer/TableGroup.tsx
index 8f582d5164..d545967624 100644
--- a/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTableContainer/TableGroup.tsx
+++ b/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTableContainer/TableGroup.tsx
@@ -33,7 +33,8 @@ export interface TableGroupProps {
prices: PendingOrderPrices | undefined | null
spotPrice: Price | undefined | null
isRateInverted: boolean
- isOpenOrdersTab: boolean
+ showLimitPrice: boolean
+ isHistoryTab: boolean
isRowSelectable: boolean
isRowSelected: boolean
orderActions: OrderActions
@@ -47,7 +48,8 @@ export function TableGroup(props: TableGroupProps) {
prices,
spotPrice,
isRateInverted,
- isOpenOrdersTab,
+ showLimitPrice,
+ isHistoryTab,
isRowSelectable,
isRowSelected,
orderActions,
@@ -68,10 +70,11 @@ export function TableGroup(props: TableGroupProps) {
const commonProps = {
isRowSelectable,
- isOpenOrdersTab,
+ isHistoryTab,
spotPrice,
prices,
isRateInverted,
+ showLimitPrice,
orderActions,
}
@@ -111,7 +114,6 @@ export function TableGroup(props: TableGroupProps) {
/>
))}
{/* Only show pagination if more than 1 page available */}
- {/*TODO: add styled to the paginator*/}
{childrenLength > ORDERS_TABLE_PAGE_SIZE && (
(theme.isInjectedWidgetMode ? `var(${UI.COLOR_PAPER})` : 'transparent')};
- color: inherit;
- border: none;
- border-radius: var(${UI.BORDER_RADIUS_NORMAL});
- box-shadow: none;
- position: relative;
- padding: ${({ theme }) => (theme.isInjectedWidgetMode ? '16px' : '0')};
- padding: ${({ theme }) => (theme.isInjectedWidgetMode ? '16px' : '0')};
- min-height: 200px;
+const Wrapper = styled.div`
+ display: flex;
+ flex-flow: column wrap;
+ gap: 16px;
width: 100%;
- margin: 0 0 76px;
`
const Content = styled.div`
@@ -36,8 +32,6 @@ const Content = styled.div`
flex-flow: column wrap;
align-items: center;
justify-content: center;
- border-radius: 16px;
- border: 1px solid var(${UI.COLOR_TEXT_OPACITY_10});
color: inherit;
min-height: 490px;
padding: 0;
@@ -113,14 +107,12 @@ const MeditatingCowImg = styled.img`
padding: 16px;
`
-const Header = styled.div`
- display: grid;
- grid-template-columns: 150px 1fr;
- grid-template-rows: max-content;
+const TopContainer = styled.div`
+ display: flex;
align-items: center;
- gap: 3px;
+ justify-content: space-between;
width: 100%;
- margin: 0 0 24px;
+ gap: 3px;
${Media.upToMedium()} {
display: block;
@@ -140,7 +132,8 @@ const Header = styled.div`
const TabsContainer = styled.div<{ withSingleChild: boolean }>`
display: flex;
align-items: center;
- justify-content: flex-end;
+ justify-content: space-between;
+ width: 100%;
${Media.upToMedium()} {
${({ withSingleChild }) =>
@@ -168,15 +161,32 @@ const ExternalArrow = styled.span`
font-size: 11px;
}
`
-interface OrdersProps extends OrdersTabsProps, OrdersTableProps {
+
+const RightContainer = styled.div`
+ display: flex;
+ flex-flow: row wrap;
+`
+
+interface OrdersProps {
isWalletConnected: boolean
- isOpenOrdersTab: boolean
isSafeViaWc: boolean
displayOrdersOnlyForSafeApp: boolean
pendingActivities: string[]
children?: ReactNode
orderType: TabOrderTypes
injectedWidgetParams: Partial
+ tabs: Array<{ id: string; title: string; count: number; isActive?: boolean }>
+ chainId: number
+ orders: any[]
+ selectedOrders: any[]
+ allowsOffchainSigning: boolean
+ balancesAndAllowances: any
+ orderActions: OrderActions
+ currentPageNumber: number
+ pendingOrdersPrices: any
+ getSpotPrice: any
+ searchTerm?: string
+ columnLayout?: ColumnLayout
}
export function OrdersTableContainer({
@@ -187,7 +197,6 @@ export function OrdersTableContainer({
isSafeViaWc,
displayOrdersOnlyForSafeApp,
selectedOrders,
- isOpenOrdersTab,
allowsOffchainSigning,
balancesAndAllowances,
orderActions,
@@ -198,7 +207,14 @@ export function OrdersTableContainer({
orderType,
pendingActivities,
injectedWidgetParams,
+ searchTerm,
+ columnLayout,
}: OrdersProps) {
+ const currentTab = useMemo(() => {
+ const activeTab = tabs.find((tab) => tab.isActive)
+ return activeTab?.id || ALL_ORDERS_TAB.id
+ }, [tabs])
+
const content = () => {
const emptyOrdersImage = injectedWidgetParams.images?.emptyOrders
@@ -238,23 +254,44 @@ export function OrdersTableContainer({
)}
- {isOpenOrdersTab ? 'No open orders' : 'No orders history'}
+
+ {searchTerm
+ ? 'No matching orders found'
+ : currentTab === ALL_ORDERS_TAB.id
+ ? 'No orders'
+ : currentTab === UNFILLABLE_TAB.id
+ ? 'No unfillable orders'
+ : currentTab === OPEN_TAB.id
+ ? 'No open orders'
+ : 'No order history'}
+
{displayOrdersOnlyForSafeApp && isSafeViaWc ? (
- Use the to see {isOpenOrdersTab ? 'open orders' : 'orders history'}
+ Use the to see {currentTab === HISTORY_TAB.id ? 'orders history' : 'your orders'}
+ ) : searchTerm ? (
+ Try adjusting your search term or clearing the filter
) : (
<>
- You don't have any {isOpenOrdersTab ? 'open' : ''} orders at the moment.
- Time to create a new one! {/* TODO: add link for Advanced orders also */}
- {orderType === TabOrderTypes.LIMIT ? (
-
- Learn more
-
-
- ) : null}
+
+ You don't have any{' '}
+ {currentTab === UNFILLABLE_TAB.id ? 'unfillable' : currentTab === OPEN_TAB.id ? 'open' : ''} orders at
+ the moment.
+ {' '}
+ {(currentTab === OPEN_TAB.id || currentTab === ALL_ORDERS_TAB.id) && (
+ <>
+
+ Time to create a new one! {' '}
+ {orderType === TabOrderTypes.LIMIT ? (
+
+ Learn more
+
+
+ ) : null}
+ >
+ )}
>
)}
@@ -264,31 +301,30 @@ export function OrdersTableContainer({
return (
)
}
return (
-
-
- Your Orders
+
+
- {children ||
}
+ {children && {children} }
-
-
+
{content()}
-
+
)
}
diff --git a/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTableContainer/styled.tsx b/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTableContainer/styled.tsx
index f09d90c309..cd161df303 100644
--- a/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTableContainer/styled.tsx
+++ b/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTableContainer/styled.tsx
@@ -1,35 +1,107 @@
import { Media, UI } from '@cowprotocol/ui'
import { transparentize } from 'color2k'
-import styled, { css } from 'styled-components/macro'
+import styled from 'styled-components/macro'
import { RateWrapper } from 'common/pure/RateInfo'
-export const TableHeader = styled.div<{ isOpenOrdersTab: boolean; isRowSelectable: boolean }>`
- --height: 50px;
+import { ColumnLayout } from './tableHeaders'
+
+export const SettingsContainer = styled.div`
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ margin-right: 16px;
+`
+
+export const SettingsSelect = styled.select`
+ background: var(${UI.COLOR_PAPER_DARKER});
+ color: inherit;
+ border: 1px solid var(${UI.COLOR_TEXT_OPACITY_10});
+ border-radius: 8px;
+ padding: 8px 12px;
+ font-size: 13px;
+ cursor: pointer;
+
+ &:focus {
+ outline: none;
+ border-color: var(${UI.COLOR_TEXT});
+ }
+`
+
+export const SettingsLabel = styled.span`
+ font-size: 13px;
+ color: inherit;
+ opacity: 0.7;
+`
+
+export const LayoutSelector = styled.select`
+ background: var(${UI.COLOR_PAPER_DARKER});
+ color: inherit;
+ border: 1px solid var(${UI.COLOR_TEXT_OPACITY_10});
+ border-radius: 8px;
+ padding: 4px 8px;
+ font-size: 12px;
+ cursor: pointer;
+ margin-left: 8px;
+
+ &:focus {
+ outline: none;
+ border-color: var(${UI.COLOR_TEXT});
+ }
+`
+
+export const TableWrapper = styled.div`
+ width: 100%;
+ overflow-x: auto;
+ -webkit-overflow-scrolling: touch; /* Smooth scrolling on iOS */
+
+ ${Media.upToSmall()} {
+ margin: 0 -12px; /* Negative margin to allow full-width scrolling */
+ padding: 0 12px;
+ width: calc(100% + 24px);
+ }
+`
+
+export const TableHeader = styled.div<{ isHistoryTab: boolean; isRowSelectable: boolean; columnLayout?: ColumnLayout }>`
+ --header-height: 26px;
+ --row-height: 41px;
--checkboxSize: 16px;
--checkBoxBorderRadius: 3px;
display: grid;
gap: 14px;
- grid-template-columns: ${({ isOpenOrdersTab, isRowSelectable }) =>
- `${isRowSelectable && isOpenOrdersTab ? 'var(--checkboxSize) 3fr' : '3.2fr'} repeat(2,2fr) ${
- isOpenOrdersTab ? '2.5fr 1.4fr' : ''
- } 0.7fr 108px 24px`};
- grid-template-rows: minmax(var(--height), 1fr);
+ grid-template-columns: ${({ isHistoryTab, isRowSelectable, columnLayout }) => {
+ if (isHistoryTab) {
+ return `minmax(200px, 2.5fr)
+ repeat(4, minmax(110px, 1fr))
+ minmax(80px, 0.8fr)
+ minmax(100px, 1fr)
+ 24px`
+ }
+
+ const checkboxColumn = isRowSelectable ? 'var(--checkboxSize)' : ''
+ const baseColumns = `${checkboxColumn}`
+
+ switch (columnLayout) {
+ case ColumnLayout.VIEW_2:
+ return `${baseColumns} minmax(180px,2fr) minmax(120px,1fr) minmax(120px,1fr) 60px minmax(120px,1fr) minmax(80px,90px) minmax(80px,0.8fr) 24px`
+ case ColumnLayout.VIEW_3:
+ return `${baseColumns} minmax(160px,2fr) minmax(120px,1fr) minmax(140px,1fr) minmax(120px,1fr) minmax(120px,1fr) minmax(80px,90px) minmax(80px,0.8fr) 24px`
+ default:
+ return `${baseColumns} minmax(200px, 2.5fr) minmax(140px,1fr) 60px minmax(110px,1fr) minmax(110px,1fr) minmax(80px,90px) minmax(80px,0.8fr) 24px`
+ }
+ }};
+ grid-template-rows: minmax(var(--header-height), 1fr);
align-items: center;
border: none;
- border-bottom: 1px solid var(${UI.COLOR_TEXT_OPACITY_10});
- padding: 0 12px;
-
- ${Media.upToLargeAlt()} {
- ${({ isRowSelectable, isOpenOrdersTab }) => css`
- grid-template-columns: ${`${
- isRowSelectable && isOpenOrdersTab ? 'var(--checkboxSize) minmax(200px,2fr)' : 'minmax(200px,2fr)'
- } repeat(2,minmax(110px,2fr)) ${
- isOpenOrdersTab ? 'minmax(140px,2.2fr) minmax(100px,1fr)' : ''
- } minmax(50px,1fr) 108px 24px`};
- `}
- }
+ padding: 5px 12px;
+ background: var(${UI.COLOR_PAPER_DARKER});
+ border-top: none;
+ border-right: none;
+ border-left: none;
+ border-image: initial;
+ border-bottom: 1px solid var(--cow-color-text-opacity-10);
+ min-width: 888px; /* Minimum width to prevent too much squeezing */
${Media.upToSmall()} {
--checkboxSize: 24px;
@@ -37,7 +109,13 @@ export const TableHeader = styled.div<{ isOpenOrdersTab: boolean; isRowSelectabl
}
`
-export const TableRow = styled(TableHeader)<{ isChildOrder?: boolean }>`
+export const TableRow = styled(TableHeader)<{
+ isChildOrder?: boolean
+ isHistoryTab: boolean
+ isRowSelectable: boolean
+ columnLayout?: ColumnLayout
+}>`
+ grid-template-rows: minmax(var(--row-height), 1fr);
background: ${({ isChildOrder }) => (isChildOrder ? `var(${UI.COLOR_PAPER_DARKER})` : 'transparent')};
transition: background var(${UI.ANIMATION_DURATION}) ease-in-out;
display: grid;
@@ -46,17 +124,6 @@ export const TableRow = styled(TableHeader)<{ isChildOrder?: boolean }>`
background: var(${UI.COLOR_PAPER_DARKER});
}
- > div:first-child {
- margin: 0;
-
- &::before {
- display: ${({ isChildOrder }) => (isChildOrder ? 'inline-block' : 'none')};
- color: ${({ theme }) => transparentize(theme.text, 0.5)};
- content: '↳';
- text-decoration: none !important;
- }
- }
-
> div:first-child {
margin-left: ${({ isChildOrder }) => (isChildOrder ? '5px' : '')};
@@ -118,26 +185,32 @@ export const TableRowCheckbox = styled.input`
background: transparent;
border: 2px solid var(${UI.COLOR_TEXT});
border-radius: var(--checkBoxBorderRadius);
- transition: background var(${UI.ANIMATION_DURATION}) ease-in-out, opacity var(${UI.ANIMATION_DURATION}) ease-in-out,
+ transition:
+ background var(${UI.ANIMATION_DURATION}) ease-in-out,
+ opacity var(${UI.ANIMATION_DURATION}) ease-in-out,
border-color var(${UI.ANIMATION_DURATION}) ease-in-out;
appearance: none;
margin: 0;
outline: 0;
opacity: 0.5;
+ z-index: 5;
&:checked {
border-color: var(${UI.COLOR_PRIMARY});
background: var(${UI.COLOR_PRIMARY});
opacity: 1;
+ z-index: 6;
}
&:checked + ${CheckboxCheckmark}::after {
display: block;
+ z-index: 6;
}
&:indeterminate {
background: var(${UI.COLOR_PRIMARY});
border-color: var(${UI.COLOR_PRIMARY});
+ z-index: 6;
}
&:indeterminate + ${CheckboxCheckmark}::after {
@@ -146,15 +219,18 @@ export const TableRowCheckbox = styled.input`
border-width: 2px 0 0 0;
top: calc(50% + 3px);
transform: none;
+ z-index: 6;
${Media.upToSmall()} {
top: calc(50% + 4px);
}
}
- &[disabled],
- &[disabled] + ${CheckboxCheckmark} {
- cursor: default;
+ &:disabled {
+ cursor: not-allowed;
+ opacity: 0.1;
+ background: var(${UI.COLOR_TEXT});
+ z-index: 6;
}
`
@@ -171,10 +247,12 @@ export const TableRowCheckboxWrapper = styled.label`
background: var(${UI.COLOR_PRIMARY});
border-color: var(${UI.COLOR_PRIMARY});
opacity: 0.5;
+ z-index: 6;
+ ${CheckboxCheckmark}::after {
display: block;
border-color: var(${UI.COLOR_BUTTON_TEXT});
+ z-index: 6;
}
}
`
diff --git a/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTableContainer/tableHeaders.tsx b/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTableContainer/tableHeaders.tsx
new file mode 100644
index 0000000000..11a148ce3a
--- /dev/null
+++ b/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTableContainer/tableHeaders.tsx
@@ -0,0 +1,237 @@
+import { ReactNode } from 'react'
+
+import { Trans } from '@lingui/macro'
+import { Repeat } from 'react-feather'
+import styled from 'styled-components/macro'
+
+export enum ColumnLayout {
+ DEFAULT = 'DEFAULT',
+ VIEW_2 = 'VIEW_2',
+ VIEW_3 = 'VIEW_3',
+}
+
+export const LAYOUT_MAP: Record = {
+ DEFAULT: ColumnLayout.DEFAULT,
+ VIEW_2: ColumnLayout.VIEW_2,
+ VIEW_3: ColumnLayout.VIEW_3,
+} as const
+
+const StyledArrowControl = styled.div`
+ display: inline-flex;
+ margin-left: 5px;
+ cursor: pointer;
+ opacity: 0.7;
+ transition: opacity 0.2s ease-in-out;
+
+ &:hover {
+ opacity: 1;
+ }
+
+ > svg {
+ width: 14px;
+ height: 14px;
+ }
+`
+
+const HeaderElement = styled.div<{ doubleRow?: boolean }>`
+ display: flex;
+ flex-direction: column;
+ gap: 3px;
+
+ i {
+ font-style: normal;
+ opacity: 0.7;
+ font-size: 0.85em;
+ }
+`
+
+export interface TableHeaderConfig {
+ id: string
+ content: ReactNode
+ doubleRow?: boolean
+ showInHistory?: boolean
+ width?: string
+ extraComponent?: ReactNode
+ order: number
+}
+
+// Core columns that appear in all views
+const CORE_COLUMNS = {
+ CHECKBOX: {
+ id: 'checkbox',
+ content: null,
+ order: 1,
+ },
+ TRADE: {
+ id: 'trade',
+ content: Sell → Buy ,
+ order: 2,
+ },
+}
+
+// Price-related columns for different layouts
+const PRICE_COLUMNS = {
+ DEFAULT: (showLimitPrice: boolean, setShowLimitPrice: (value: boolean) => void): TableHeaderConfig[] => [
+ {
+ id: 'fillsAt',
+ content: showLimitPrice ? Limit price : Fills at ,
+ showInHistory: false,
+ order: 3,
+ extraComponent: (
+ setShowLimitPrice(!showLimitPrice)}>
+
+
+ ),
+ },
+ {
+ id: 'distanceToMarket',
+ content: (
+
+ Distance
+ to market
+
+ ),
+ showInHistory: false,
+ order: 4,
+ },
+ {
+ id: 'marketPrice',
+ content: Market price ,
+ showInHistory: false,
+ order: 5,
+ },
+ ],
+ VIEW_2: (): TableHeaderConfig[] => [
+ {
+ id: 'limitPrice',
+ content: Limit price ,
+ showInHistory: false,
+ order: 3,
+ },
+ {
+ id: 'fillsAt',
+ content: Fills at ,
+ showInHistory: false,
+ order: 4,
+ },
+ {
+ id: 'distanceToMarket',
+ content: (
+
+ Distance
+ to market
+
+ ),
+ showInHistory: false,
+ order: 5,
+ },
+ ],
+ VIEW_3: (): TableHeaderConfig[] => [
+ {
+ id: 'limitPrice',
+ content: Limit price ,
+ showInHistory: false,
+ order: 3,
+ },
+ {
+ id: 'fillsAtWithDistance',
+ content: (
+
+ Fills at
+
+ Distance to market
+
+
+ ),
+ showInHistory: false,
+ doubleRow: true,
+ order: 4,
+ },
+ {
+ id: 'marketPrice',
+ content: Market price ,
+ showInHistory: false,
+ order: 5,
+ },
+ ],
+}
+
+// Columns that appear after price columns
+const DETAIL_COLUMNS: TableHeaderConfig[] = [
+ {
+ id: 'limitPrice',
+ content: Limit price ,
+ showInHistory: true,
+ order: 6,
+ },
+ {
+ id: 'expiration',
+ content: (
+
+ Expiration
+
+ Creation
+
+
+ ),
+ showInHistory: false,
+ doubleRow: true,
+ order: 7,
+ },
+ {
+ id: 'executionPrice',
+ content: Execution price ,
+ showInHistory: true,
+ order: 8,
+ },
+ {
+ id: 'executionTime',
+ content: Execution time ,
+ showInHistory: true,
+ order: 9,
+ },
+ {
+ id: 'creationTime',
+ content: Creation time ,
+ showInHistory: true,
+ order: 10,
+ },
+ {
+ id: 'filled',
+ content: Filled ,
+ order: 11,
+ },
+ {
+ id: 'status',
+ content: Status ,
+ order: 12,
+ },
+ {
+ id: 'actions',
+ content: null,
+ order: 13,
+ },
+]
+
+export const createTableHeaders = (
+ showLimitPrice: boolean,
+ setShowLimitPrice: (value: boolean) => void,
+ columnLayout: ColumnLayout = ColumnLayout.DEFAULT,
+): TableHeaderConfig[] => {
+ // Get the appropriate price columns based on layout
+ const priceColumns = (() => {
+ switch (columnLayout) {
+ case ColumnLayout.VIEW_2:
+ return PRICE_COLUMNS.VIEW_2()
+ case ColumnLayout.VIEW_3:
+ return PRICE_COLUMNS.VIEW_3()
+ default:
+ return PRICE_COLUMNS.DEFAULT(showLimitPrice, setShowLimitPrice)
+ }
+ })()
+
+ // Combine all columns and sort by order
+ return [CORE_COLUMNS.CHECKBOX, CORE_COLUMNS.TRADE, ...priceColumns, ...DETAIL_COLUMNS].sort(
+ (a, b) => a.order - b.order,
+ )
+}
diff --git a/apps/cowswap-frontend/src/modules/ordersTable/pure/ReceiptModal/FilledField.tsx b/apps/cowswap-frontend/src/modules/ordersTable/pure/ReceiptModal/FilledField.tsx
index 767dfd7d01..7addde60e4 100644
--- a/apps/cowswap-frontend/src/modules/ordersTable/pure/ReceiptModal/FilledField.tsx
+++ b/apps/cowswap-frontend/src/modules/ordersTable/pure/ReceiptModal/FilledField.tsx
@@ -23,10 +23,10 @@ export function FilledField({ order }: Props) {
+
-
diff --git a/apps/cowswap-frontend/src/modules/ordersTable/pure/ReceiptModal/styled.ts b/apps/cowswap-frontend/src/modules/ordersTable/pure/ReceiptModal/styled.ts
index 392f8680b1..0051874ba3 100644
--- a/apps/cowswap-frontend/src/modules/ordersTable/pure/ReceiptModal/styled.ts
+++ b/apps/cowswap-frontend/src/modules/ordersTable/pure/ReceiptModal/styled.ts
@@ -193,7 +193,8 @@ export const LightButton = styled.button`
padding: 6px 14px;
border-radius: var(${UI.BORDER_RADIUS_NORMAL});
background-color: var(${UI.COLOR_PAPER_DARKER});
- transition: border var(${UI.ANIMATION_DURATION}) ease-in-out,
+ transition:
+ border var(${UI.ANIMATION_DURATION}) ease-in-out,
background-color var(${UI.ANIMATION_DURATION}) ease-in-out;
cursor: pointer;
color: inherit;
diff --git a/apps/cowswap-frontend/src/modules/trade/const/common.ts b/apps/cowswap-frontend/src/modules/trade/const/common.ts
index 503f40f539..851ff6d027 100644
--- a/apps/cowswap-frontend/src/modules/trade/const/common.ts
+++ b/apps/cowswap-frontend/src/modules/trade/const/common.ts
@@ -8,3 +8,11 @@ export const TradeTypeToUiOrderType: Record = {
[TradeType.ADVANCED_ORDERS]: UiOrderType.TWAP,
[TradeType.YIELD]: UiOrderType.YIELD,
}
+
+export const ORDERS_TABLE_SETTINGS = {
+ LEFT_ALIGNED: {
+ title: 'Desktop: Left-Aligned Orders Table',
+ tooltip:
+ 'When enabled, the orders table will be displayed on the left side on desktop screens. On mobile, the orders table will always be stacked below.',
+ },
+} as const
diff --git a/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/TradeWidgetForm.tsx b/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/TradeWidgetForm.tsx
index 6866a903ba..a2f3738b46 100644
--- a/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/TradeWidgetForm.tsx
+++ b/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/TradeWidgetForm.tsx
@@ -193,6 +193,7 @@ export function TradeWidgetForm(props: TradeWidgetProps) {
/>
{!isWrapOrUnwrap && middleContent}
+
+ {slots.limitPriceInput}
+
{withRecipient && }
{isWrapOrUnwrap ? (
diff --git a/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/types.ts b/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/types.ts
index 0cf4606b84..f9fe79e55b 100644
--- a/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/types.ts
+++ b/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/types.ts
@@ -38,6 +38,7 @@ export interface TradeWidgetSlots {
lockScreen?: ReactNode
topContent?: ReactNode
middleContent?: ReactNode
+ limitPriceInput?: ReactNode
bottomContent?(warnings: ReactNode | null): ReactNode
outerContent?: ReactNode
updaters?: ReactNode
diff --git a/apps/cowswap-frontend/src/modules/trade/pure/Settings/SettingsIcon.tsx b/apps/cowswap-frontend/src/modules/trade/pure/Settings/SettingsIcon.tsx
index a3a5694823..007cf8e99c 100644
--- a/apps/cowswap-frontend/src/modules/trade/pure/Settings/SettingsIcon.tsx
+++ b/apps/cowswap-frontend/src/modules/trade/pure/Settings/SettingsIcon.tsx
@@ -1,23 +1,13 @@
import IMAGE_ICON_SETTINGS_ALT from '@cowprotocol/assets/images/icon-settings-alt.svg'
-import { UI } from '@cowprotocol/ui'
import SVG from 'react-inlinesvg'
-import styled from 'styled-components/macro'
-const StyledMenuIcon = styled.span`
- height: var(${UI.ICON_SIZE_NORMAL});
- width: var(${UI.ICON_SIZE_NORMAL});
- opacity: 0.6;
-
- &:hover {
- opacity: 1;
- }
-`
+import { SettingsButtonIcon } from './styled'
export function SettingsIcon() {
return (
-
+
-
+
)
}
diff --git a/apps/cowswap-frontend/src/modules/trade/pure/Settings/styled.ts b/apps/cowswap-frontend/src/modules/trade/pure/Settings/styled.ts
index 8ae70bc993..69c680c9c2 100644
--- a/apps/cowswap-frontend/src/modules/trade/pure/Settings/styled.ts
+++ b/apps/cowswap-frontend/src/modules/trade/pure/Settings/styled.ts
@@ -1,38 +1,50 @@
import { UI } from '@cowprotocol/ui'
import { MenuButton, MenuList } from '@reach/menu-button'
-import { transparentize } from 'color2k'
-import styled from 'styled-components/macro'
-
-export const SettingsTitle = styled.h3`
- font-weight: 600;
- font-size: 14px;
- color: inherit;
- margin: 0 0 12px 0;
-`
+import styled, { css } from 'styled-components/macro'
export const SettingsContainer = styled.div`
- margin: 12px 0 0;
- padding: 16px;
+ --padding: 10px;
+ display: flex;
+ flex-direction: column;
+ margin: 0;
+ padding: var(--padding);
border-radius: 12px;
- box-shadow: ${({ theme }) => theme.boxShadow2};
- border: 1px solid ${({ theme }) => transparentize(theme.white, 0.95)};
+ box-shadow: var(${UI.BOX_SHADOW_2});
+ border: 1px solid var(${UI.COLOR_PAPER_DARKER});
background: var(${UI.COLOR_PAPER});
color: inherit;
+ gap: 10px;
+ position: absolute;
+ right: 9px;
+ margin: auto;
+ top: 48px;
+ z-index: 100;
+ min-width: 330px;
+ max-width: calc(100% - var(--padding) * 2);
+`
+
+export const SettingsTitle = styled.h3`
+ font-weight: 600;
+ font-size: 15px;
+ color: inherit;
+ width: 100%;
+ text-align: left;
+ margin: 0;
`
export const SettingsBoxWrapper = styled.div<{ disabled: boolean }>`
display: flex;
justify-content: space-between;
- margin: 0 0 10px;
+ margin: 0;
+ gap: 10px;
color: inherit;
+ opacity: ${({ disabled }) => (disabled ? '0.7' : '1')};
+ pointer-events: ${({ disabled }) => (disabled ? 'none' : '')};
:last-child {
margin-bottom: 0;
}
-
- opacity: ${({ disabled }) => (disabled ? '0.7' : '1')};
- pointer-events: ${({ disabled }) => (disabled ? 'none' : '')};
`
export const SettingsBoxTitle = styled.div`
@@ -41,8 +53,6 @@ export const SettingsBoxTitle = styled.div`
font-weight: 400;
color: inherit;
font-size: 14px;
- opacity: 0.85;
- margin-right: 2rem;
`
export const SettingsButton = styled(MenuButton)`
@@ -56,8 +66,63 @@ export const SettingsButton = styled(MenuButton)`
cursor: pointer;
`
+const iconButtonStyles = css<{ active?: boolean; iconSize?: string }>`
+ --maxSize: 28px;
+ --iconSize: ${({ iconSize }) => iconSize || `var(${UI.ICON_SIZE_NORMAL})`};
+ background: ${({ active }) => (active ? `var(${UI.COLOR_PAPER_DARKER})` : 'none')};
+ border: none;
+ padding: 4px;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: var(${UI.COLOR_TEXT});
+ opacity: ${({ active }) => (active ? '1' : '0.6')};
+ transition: all var(${UI.ANIMATION_DURATION}) ease-in-out;
+ border-radius: 8px;
+ max-width: var(--maxSize);
+ max-height: var(--maxSize);
+ width: var(--maxSize);
+ height: var(--maxSize);
+
+ &:hover {
+ opacity: 1;
+ background: var(${UI.COLOR_PAPER_DARKER});
+ }
+
+ > svg {
+ width: var(--iconSize);
+ height: var(--iconSize);
+ color: inherit;
+ object-fit: contain;
+ }
+`
+
+export const SettingsButtonIcon = styled.span<{ active?: boolean; iconSize?: string }>`
+ ${iconButtonStyles}
+ --iconSize: 18px;
+ margin: auto;
+`
+
export const MenuContent = styled(MenuList)`
position: relative;
z-index: 2;
color: var(${UI.COLOR_TEXT_PAPER});
`
+
+export const ButtonsContainer = styled.div`
+ display: flex;
+ gap: 4px;
+
+ [data-reach-menu-popover] {
+ position: absolute;
+ width: 100%;
+ left: 0;
+ top: 0;
+ }
+`
+
+export const UsdButton = styled.button<{ active?: boolean }>`
+ ${iconButtonStyles}
+ --iconSize: 20px;
+`
diff --git a/apps/cowswap-frontend/src/modules/trade/pure/TradePageLayout/index.tsx b/apps/cowswap-frontend/src/modules/trade/pure/TradePageLayout/index.tsx
index b269c147ba..7e48603ae6 100644
--- a/apps/cowswap-frontend/src/modules/trade/pure/TradePageLayout/index.tsx
+++ b/apps/cowswap-frontend/src/modules/trade/pure/TradePageLayout/index.tsx
@@ -1,26 +1,36 @@
-import { Media } from '@cowprotocol/ui'
+import { Media, UI } from '@cowprotocol/ui'
import styled from 'styled-components/macro'
import { WIDGET_MAX_WIDTH } from 'theme'
-export const PageWrapper = styled.div<{ isUnlocked: boolean }>`
+const DEFAULT_MAX_WIDTH = '1500px'
+
+export const PageWrapper = styled.div<{ isUnlocked: boolean; secondaryOnLeft?: boolean; maxWidth?: string }>`
width: 100%;
display: grid;
- max-width: 1500px;
+ max-width: ${({ maxWidth = DEFAULT_MAX_WIDTH }) => maxWidth};
margin: 0 auto;
- grid-template-columns: ${({ isUnlocked }) => (isUnlocked ? WIDGET_MAX_WIDTH.swap : '')} 1fr;
- grid-template-rows: max-content;
- grid-column-gap: 20px;
+ grid-template-columns: 1fr;
+ grid-template-rows: auto auto;
+ grid-template-areas: 'primary' 'secondary';
+ gap: 20px;
- ${Media.upToLarge()} {
- display: flex;
- flex-flow: column wrap;
+ ${Media.LargeAndUp()} {
+ grid-template-columns: ${({ isUnlocked, secondaryOnLeft }) =>
+ isUnlocked
+ ? 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"')};
}
> div:last-child {
display: ${({ isUnlocked }) => (isUnlocked ? '' : 'none')};
}
`
+
// Form + banner
export const PrimaryWrapper = styled.div`
display: flex;
@@ -30,16 +40,29 @@ export const PrimaryWrapper = styled.div`
max-width: ${WIDGET_MAX_WIDTH.swap};
margin: 0 auto;
color: inherit;
+ grid-area: primary;
`
// Graph + orders table
export const SecondaryWrapper = styled.div`
display: flex;
+ flex-flow: column wrap;
width: 100%;
overflow: hidden;
+ border-radius: var(${UI.BORDER_RADIUS_NORMAL});
+ background: var(${UI.COLOR_PAPER});
+ color: inherit;
+ border: none;
+ box-shadow: none;
+ position: relative;
+ padding: 10px;
+ min-height: 200px;
+ width: 100%;
+ margin: 0 0 76px;
+ grid-area: secondary;
${Media.upToLargeAlt()} {
flex-flow: column wrap;
- margin: 56px 0;
+ margin: 0 0 20px;
}
`
diff --git a/apps/cowswap-frontend/src/modules/trade/types/TradeRawState.ts b/apps/cowswap-frontend/src/modules/trade/types/TradeRawState.ts
index 7e4ab675fd..8094aff7b0 100644
--- a/apps/cowswap-frontend/src/modules/trade/types/TradeRawState.ts
+++ b/apps/cowswap-frontend/src/modules/trade/types/TradeRawState.ts
@@ -1,4 +1,4 @@
-import { WRAPPED_NATIVE_CURRENCIES as WETH } from '@cowprotocol/common-const'
+import { USDC, WRAPPED_NATIVE_CURRENCIES as WETH } from '@cowprotocol/common-const'
import { OrderKind, SupportedChainId } from '@cowprotocol/cow-sdk'
export interface TradeUrlParams {
@@ -30,7 +30,7 @@ export function getDefaultTradeRawState(chainId: SupportedChainId | null): Trade
return {
chainId,
inputCurrencyId: chainId ? WETH[chainId]?.symbol || null : null,
- outputCurrencyId: null,
+ outputCurrencyId: chainId ? USDC[chainId].symbol || null : null,
recipient: null,
recipientAddress: null,
}
diff --git a/apps/cowswap-frontend/src/pages/AdvancedOrders/index.tsx b/apps/cowswap-frontend/src/pages/AdvancedOrders/index.tsx
index 9443164f31..7c629bf4f4 100644
--- a/apps/cowswap-frontend/src/pages/AdvancedOrders/index.tsx
+++ b/apps/cowswap-frontend/src/pages/AdvancedOrders/index.tsx
@@ -9,6 +9,7 @@ import {
SetupAdvancedOrderAmountsFromUrlUpdater,
} from 'modules/advancedOrders'
import { useInjectedWidgetParams } from 'modules/injectedWidget'
+import { limitOrdersSettingsAtom } from 'modules/limitOrders/state/limitOrdersSettingsAtom'
import { OrdersTableWidget, TabOrderTypes } from 'modules/ordersTable'
import * as styledEl from 'modules/trade/pure/TradePageLayout'
import {
@@ -24,8 +25,11 @@ import {
} from 'modules/twap'
import { TwapFormState } from 'modules/twap/pure/PrimaryActionButton/getTwapFormState'
+const ADVANCED_ORDERS_MAX_WIDTH = '1800px'
+
export default function AdvancedOrdersPage() {
const { isUnlocked } = useAtomValue(advancedOrdersAtom)
+ const { ordersTableOnLeft } = useAtomValue(limitOrdersSettingsAtom)
const allEmulatedOrders = useAllEmulatedOrders()
const isFallbackHandlerRequired = useIsFallbackHandlerRequired()
@@ -43,7 +47,11 @@ export default function AdvancedOrdersPage() {
<>
-
+
{isFallbackHandlerRequired && pendingOrders.length > 0 && }
+
diff --git a/apps/cowswap-frontend/src/utils/orderUtils/parseOrder.ts b/apps/cowswap-frontend/src/utils/orderUtils/parseOrder.ts
index f6a3d0b4e2..d85b05113f 100644
--- a/apps/cowswap-frontend/src/utils/orderUtils/parseOrder.ts
+++ b/apps/cowswap-frontend/src/utils/orderUtils/parseOrder.ts
@@ -49,6 +49,7 @@ export interface ParsedOrder {
partiallyFillable: boolean
creationTime: Date
expirationTime: Date
+ fulfillmentTime: string | undefined
composableCowInfo?: ComposableCowInfo
fullAppData: Order['fullAppData']
signingScheme: SigningScheme
@@ -66,6 +67,7 @@ export const parseOrder = (order: Order): ParsedOrder => {
const executedFeeToken = order.apiAdditionalInfo?.executedFeeToken || null
const totalFee = order.apiAdditionalInfo?.totalFee || null
const creationTime = new Date(order.creationTime)
+ const fulfillmentTime = order.fulfillmentTime
const fullyFilled = isOrderFilled(order)
const partiallyFilled = isPartiallyFilled(order)
const filledPercentDisplay = filledPercentage.times(100).toString()
@@ -119,6 +121,7 @@ export const parseOrder = (order: Order): ParsedOrder => {
receiver: order.receiver || undefined,
creationTime,
expirationTime,
+ fulfillmentTime,
fullAppData: order.fullAppData,
executionData,
signingScheme: order.signingScheme,
diff --git a/libs/assets/src/images/icon-USD.svg b/libs/assets/src/images/icon-USD.svg
new file mode 100644
index 0000000000..eaaacd1af3
--- /dev/null
+++ b/libs/assets/src/images/icon-USD.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/libs/assets/src/images/icon-locked.svg b/libs/assets/src/images/icon-locked.svg
new file mode 100644
index 0000000000..debfee8ade
--- /dev/null
+++ b/libs/assets/src/images/icon-locked.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/libs/assets/src/images/icon-switch-arrows.svg b/libs/assets/src/images/icon-switch-arrows.svg
new file mode 100644
index 0000000000..98b6786c9b
--- /dev/null
+++ b/libs/assets/src/images/icon-switch-arrows.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/libs/assets/src/images/icon-unlocked.svg b/libs/assets/src/images/icon-unlocked.svg
new file mode 100644
index 0000000000..7ccd1d2701
--- /dev/null
+++ b/libs/assets/src/images/icon-unlocked.svg
@@ -0,0 +1 @@
+
\ No newline at end of file