Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/limit UI upgrade 5 #5285

Merged
merged 8 commits into from
Jan 16, 2025
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ export const Input = React.memo(function InnerInput({
placeholder,
prependSymbol,
onFocus,
pattern: _pattern,
...rest
}: {
value: string | number
Expand All @@ -73,10 +74,20 @@ export const Input = React.memo(function InnerInput({
fontSize?: string
align?: 'right' | 'left'
prependSymbol?: string | undefined
pattern?: string
} & Omit<React.HTMLProps<HTMLInputElement>, 'ref' | 'onChange' | 'as'>) {
// Keep the input strictly as a string
const stringValue = typeof value === 'string' ? value : String(value)

const titleRef = React.useCallback(
(node: HTMLInputElement | null) => {
if (node) {
node.title = node.scrollWidth > node.clientWidth ? stringValue : ''
}
},
[stringValue],
)

const enforcer = (nextUserInput: string) => {
// Always allow empty input
if (nextUserInput === '') {
Expand Down Expand Up @@ -118,6 +129,7 @@ export const Input = React.memo(function InnerInput({
)}
<StyledInput
{...rest}
ref={titleRef}
value={stringValue}
readOnly={readOnly}
onFocus={(event) => {
Expand All @@ -142,8 +154,6 @@ export const Input = React.memo(function InnerInput({
autoCorrect="off"
// Keep type="text" to preserve trailing decimals
type="text"
// Remove pattern to prevent browser validation interference
pattern=""
placeholder={placeholder || '0'}
// minLength to 0 so empty strings are always valid
minLength={0}
Expand Down
6 changes: 4 additions & 2 deletions apps/cowswap-frontend/src/legacy/components/Toggle/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export const ToggleElement = styled.span<{ isActive?: boolean; bgColor?: string;
${({ isActive, isInitialToggleLoad }) => (isInitialToggleLoad ? 'none' : isActive ? turnOnToggle : turnOffToggle)}
ease-in;
background: ${({ bgColor, isActive }) =>
isActive ? bgColor ?? `var(${UI.COLOR_PRIMARY})` : `var(${UI.COLOR_PAPER_DARKER})`};
isActive ? (bgColor ?? `var(${UI.COLOR_PRIMARY})`) : `var(${UI.COLOR_PAPER_DARKER})`};
border-radius: 50%;
height: 24px;
:hover {
Expand Down Expand Up @@ -101,7 +101,9 @@ export interface ToggleProps extends WithClassName {
export function Toggle({ id, bgColor, isActive, toggle, className, isDisabled }: ToggleProps) {
const [isInitialToggleLoad, setIsInitialToggleLoad] = useState(true)

const switchToggle = () => {
const switchToggle = (e: React.MouseEvent) => {
e.stopPropagation()
e.preventDefault()
toggle()
if (!isDisabled && isInitialToggleLoad) setIsInitialToggleLoad(false)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,14 @@ export function SettingsWidget() {
</SettingsButton>
<MenuPopover portal={false}>
<MenuItems>
<MenuItem onSelect={() => {}}>
<Settings state={settingsState} onStateChanged={updateSettingsState} />
<MenuItem onSelect={() => null}>
<div
onClick={(e) => e.stopPropagation()}
onMouseDown={(e) => e.stopPropagation()}
onMouseUp={(e) => e.stopPropagation()}
>
<Settings state={settingsState} onStateChanged={updateSettingsState} />
</div>
</MenuItem>
</MenuItems>
</MenuPopover>
Expand Down
137 changes: 73 additions & 64 deletions apps/cowswap-frontend/src/modules/limitOrders/pure/Settings/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -169,70 +169,79 @@ export function Settings({ state, onStateChanged }: SettingsProps) {
onStateChanged({ isUsdValuesMode: !isUsdValuesMode })
}, [isUsdValuesMode, onStateChanged])

const handleContainerClick = (e: React.MouseEvent) => {
e.stopPropagation()
}

return (
<SettingsContainer>
<SettingsTitle>Limit Order Settings</SettingsTitle>

<SettingsBox
title="Custom Recipient"
tooltip="Allows you to choose a destination address for the swap other than the connected one."
value={showRecipient}
toggle={handleRecipientToggle}
/>

<SettingsBox
title="Enable Partial Executions"
tooltip={
<>
Allow you to chose whether your limit orders will be <i>Partially fillable</i> or <i>Fill or kill</i>.
<br />
<br />
<i>Fill or kill</i> orders will either be filled fully or not at all.
<br />
<i>Partially fillable</i> orders may be filled partially if there isn't enough liquidity to fill the full
amount.
</>
}
value={partialFillsEnabled}
toggle={handlePartialFillsToggle}
/>

<SettingsBox
title="Lock Limit Price"
tooltip="When enabled, the limit price stays fixed when changing the BUY amount. When disabled, the limit price will update based on the BUY amount changes."
value={limitPriceLocked}
toggle={handleLimitPriceLockedToggle}
/>

<SettingsBox
title="Global USD Mode"
tooltip="When enabled, all prices will be displayed in USD by default."
value={isUsdValuesMode}
toggle={handleUsdValuesModeToggle}
/>

<SettingsBox
title={ORDERS_TABLE_SETTINGS.LEFT_ALIGNED.title}
tooltip={ORDERS_TABLE_SETTINGS.LEFT_ALIGNED.tooltip}
value={ordersTableOnLeft}
toggle={handleOrdersTablePositionToggle}
/>

<SettingsRow>
<SettingsLabel>
Limit price position <HelpTooltip text="Choose where to display the limit price input." />
</SettingsLabel>
<DropdownContainer>
<DropdownButton onClick={toggleDropdown}>{POSITION_LABELS[limitPricePosition]}</DropdownButton>
<DropdownList isOpen={isOpen}>
{Object.entries(POSITION_LABELS).map(([value, label]) => (
<DropdownItem key={value} onClick={handleSelect(value as LimitOrdersSettingsState['limitPricePosition'])}>
{label}
</DropdownItem>
))}
</DropdownList>
</DropdownContainer>
</SettingsRow>
</SettingsContainer>
<div onClick={handleContainerClick}>
<SettingsContainer>
<SettingsTitle>Limit Order Settings</SettingsTitle>

<SettingsBox
title="Custom Recipient"
tooltip="Allows you to choose a destination address for the swap other than the connected one."
value={showRecipient}
toggle={handleRecipientToggle}
/>

<SettingsBox
title="Enable Partial Executions"
tooltip={
<>
Allow you to chose whether your limit orders will be <i>Partially fillable</i> or <i>Fill or kill</i>.
<br />
<br />
<i>Fill or kill</i> orders will either be filled fully or not at all.
<br />
<i>Partially fillable</i> orders may be filled partially if there isn't enough liquidity to fill the full
amount.
</>
}
value={partialFillsEnabled}
toggle={handlePartialFillsToggle}
/>

<SettingsBox
title="Lock Limit Price"
tooltip="When enabled, the limit price stays fixed when changing the BUY amount. When disabled, the limit price will update based on the BUY amount changes."
value={limitPriceLocked}
toggle={handleLimitPriceLockedToggle}
/>

<SettingsBox
title="Global USD Mode"
tooltip="When enabled, all prices will be displayed in USD by default."
value={isUsdValuesMode}
toggle={handleUsdValuesModeToggle}
/>

<SettingsBox
title={ORDERS_TABLE_SETTINGS.LEFT_ALIGNED.title}
tooltip={ORDERS_TABLE_SETTINGS.LEFT_ALIGNED.tooltip}
value={ordersTableOnLeft}
toggle={handleOrdersTablePositionToggle}
/>

<SettingsRow>
<SettingsLabel>
Limit price position <HelpTooltip text="Choose where to display the limit price input." />
</SettingsLabel>
<DropdownContainer>
<DropdownButton onClick={toggleDropdown}>{POSITION_LABELS[limitPricePosition]}</DropdownButton>
<DropdownList isOpen={isOpen}>
{Object.entries(POSITION_LABELS).map(([value, label]) => (
<DropdownItem
key={value}
onClick={handleSelect(value as LimitOrdersSettingsState['limitPricePosition'])}
>
{label}
</DropdownItem>
))}
</DropdownList>
</DropdownContainer>
</SettingsRow>
</SettingsContainer>
</div>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,8 @@ export function getOrderStatusTitleAndColor(order: ParsedOrder): { title: string
if (order.isCancelling) {
return {
title: 'Cancelling...',
color: `var(${UI.COLOR_TEXT})`,
background: `var(${UI.COLOR_TEXT_OPACITY_10})`,
color: `var(${UI.COLOR_DANGER_TEXT})`,
background: `var(${UI.COLOR_DANGER_BG})`,
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,31 @@ import orderPresignaturePending from '@cowprotocol/assets/cow-swap/order-presign
import { Command } from '@cowprotocol/types'

import SVG from 'react-inlinesvg'
import styled from 'styled-components/macro'
import styled, { css, keyframes } from 'styled-components/macro'

import { OrderStatus } from 'legacy/state/orders/actions'

import { ParsedOrder } from 'utils/orderUtils/parseOrder'

import { getOrderStatusTitleAndColor } from './getOrderStatusTitleAndColor'

const shimmerAnimation = keyframes`
0% {
transform: translateX(-100%);
}
100% {
transform: translateX(100%);
}
`

const Wrapper = styled.div<{
color: string
background: string
withWarning?: boolean
widthAuto?: boolean
clickable?: boolean
isCancelling?: boolean
isSigning?: boolean
}>`
--height: 26px;
--statusColor: ${({ color }) => color};
Expand Down Expand Up @@ -46,6 +57,26 @@ const Wrapper = styled.div<{
z-index: 1;
border-radius: 16px;
}

${({ isCancelling, isSigning }) =>
(isCancelling || isSigning) &&
css`
overflow: hidden;
border-radius: 16px;

&::after {
content: '';
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
background: linear-gradient(90deg, transparent 0%, rgba(255, 255, 255, 0.3) 50%, transparent 100%);
animation: ${shimmerAnimation} 1.5s infinite;
z-index: 2;
border-radius: 16px;
}
`}
`

const StatusContent = styled.div`
Expand Down Expand Up @@ -95,6 +126,8 @@ export function OrderStatusBox({ order, widthAuto, withWarning, onClick, Warning
withWarning={withWarning}
clickable={!!onClick}
onClick={onClick}
isCancelling={order.isCancelling && !order.executionData.fullyFilled}
isSigning={order.status === OrderStatus.PRESIGNATURE_PENDING}
>
{content}
</Wrapper>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { getAddress, getEtherscanLink, formatDateWithTimezone } from '@cowprotoc
import { SupportedChainId } from '@cowprotocol/cow-sdk'
import { TokenLogo } from '@cowprotocol/tokens'
import { Command, UiOrderType } from '@cowprotocol/types'
import { Loader, TokenAmount, UI, HoverTooltip } from '@cowprotocol/ui'
import { UI, TokenAmount, Loader, HoverTooltip } from '@cowprotocol/ui'
import { PercentDisplay, percentIsAlmostHundred } from '@cowprotocol/ui'
import { useIsSafeWallet } from '@cowprotocol/wallet'
import { Currency, CurrencyAmount, Percent, Price } from '@uniswap/sdk-core'
Expand Down Expand Up @@ -435,12 +435,12 @@ export function OrderRow({
<styledEl.CellElement doubleRow>
<b
title={
expirationTime && !(getIsFinalizedOrder(order) && order.status !== OrderStatus.EXPIRED)
expirationTime && !shouldShowDashForExpiration(order, children)
? formatDateWithTimezone(expirationTime)
: undefined
}
>
{getIsFinalizedOrder(order) && order.status !== OrderStatus.EXPIRED ? '-' : expirationTimeAgo}
{shouldShowDashForExpiration(order, children) ? '-' : expirationTimeAgo}
</b>
<i title={creationTime && !isScheduledCreating ? formatDateWithTimezone(creationTime) : undefined}>
{isScheduledCreating ? 'Creating...' : creationTimeAgo}
Expand Down Expand Up @@ -585,3 +585,28 @@ function getActivityUrl(chainId: SupportedChainId, order: ParsedOrder): string |

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

function shouldShowDashForExpiration(order: ParsedOrder, _children: React.ReactNode | undefined): boolean {
alfetopito marked this conversation as resolved.
Show resolved Hide resolved
// Show dash for finalized orders that are not expired
if (getIsFinalizedOrder(order) && order.status !== OrderStatus.EXPIRED) {
return true
}

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

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

return false
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,7 @@ const Content = styled.div`
position: absolute;
top: 0;
left: 0;
background: var(${UI.COLOR_PRIMARY});
opacity: 0.16;
background: var(${UI.COLOR_PAPER_DARKER});
width: var(--size);
height: var(--size);
border-radius: var(--size);
Expand Down
Loading