Files
dydx-v4-web/src/views/forms/ClosePositionForm.tsx
T
Jared Vu 15bf8a1b34 Update Abacus v0.6.0 (#55)
* Abacus 6: New init, deprecate asset.symbol

* deprecate asset.symbol

* Update networks constants

* filesystem

* Update networks.ts

* 0.6.0 -> 0.6.3

* Remove Market filter
2023-10-03 13:10:55 -07:00

396 lines
12 KiB
TypeScript

import { type FormEvent, useCallback, useEffect, useState } from 'react';
import styled, { type AnyStyledComponent } from 'styled-components';
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
import {
ClosePositionInputField,
type HumanReadablePlaceOrderPayload,
type Nullable,
} from '@/constants/abacus';
import { AlertType } from '@/constants/alerts';
import { ButtonAction, ButtonShape, ButtonSize, ButtonType } from '@/constants/buttons';
import { TOKEN_DECIMALS } from '@/constants/numbers';
import { STRING_KEYS } from '@/constants/localization';
import { MobilePlaceOrderSteps } from '@/constants/trade';
import { useBreakpoints, useIsFirstRender, useStringGetter, useSubaccount } from '@/hooks';
import { useOnLastOrderIndexed } from '@/hooks/useOnLastOrderIndexed';
import { breakpoints } from '@/styles';
import { layoutMixins } from '@/styles/layoutMixins';
import { formMixins } from '@/styles/formMixins';
import { AlertMessage } from '@/components/AlertMessage';
import { Button } from '@/components/Button';
import { FormInput } from '@/components/FormInput';
import { InputType } from '@/components/Input';
import { Tag } from '@/components/Tag';
import { ToggleGroup } from '@/components/ToggleGroup';
import { PlaceOrderButtonAndReceipt } from './TradeForm/PlaceOrderButtonAndReceipt';
import { Orderbook, orderbookMixins, type OrderbookScrollBehavior } from '@/views/tables/Orderbook';
import { PositionPreview } from '@/views/forms/TradeForm/PositionPreview';
import { getCurrentMarketPositionData } from '@/state/accountSelectors';
import { getCurrentMarketAssetData } from '@/state/assetsSelectors';
import { getInputClosePositionData } from '@/state/inputsSelectors';
import { getCurrentMarketConfig, getCurrentMarketId } from '@/state/perpetualsSelectors';
import { closeDialog } from '@/state/dialogs';
import { getCurrentInput } from '@/state/inputsSelectors';
import abacusStateManager from '@/lib/abacus';
import { MustBigNumber } from '@/lib/numbers';
const MAX_KEY = 'MAX';
// Abacus only takes in these percent options
const SIZE_PERCENT_OPTIONS = {
'25%': 0.25,
'50%': 0.5,
'75%': 0.75,
[MAX_KEY]: 1,
};
type ElementProps = {
onClosePositionSuccess?: () => void;
currentStep?: MobilePlaceOrderSteps;
setCurrentStep?: (step: MobilePlaceOrderSteps) => void;
};
type StyledProps = {
className?: string;
};
export const ClosePositionForm = ({
onClosePositionSuccess,
currentStep,
setCurrentStep,
className,
}: ElementProps & StyledProps) => {
const stringGetter = useStringGetter();
const dispatch = useDispatch();
const { isTablet } = useBreakpoints();
const isFirstRender = useIsFirstRender();
const [closePositionError, setClosePositionError] = useState<string | undefined>(undefined);
const [isClosingPosition, setIsClosingPosition] = useState(false);
const { closePosition } = useSubaccount();
const market = useSelector(getCurrentMarketId);
const { id } = useSelector(getCurrentMarketAssetData, shallowEqual) || {};
const { stepSizeDecimals } = useSelector(getCurrentMarketConfig, shallowEqual) || {};
const { size: sizeData, summary } = useSelector(getInputClosePositionData, shallowEqual) || {};
const { size, percent } = sizeData || {};
const currentInput = useSelector(getCurrentInput);
const currentPositionData = useSelector(getCurrentMarketPositionData, shallowEqual);
const { size: currentPositionSize } = currentPositionData || {};
const { current: currentSize } = currentPositionSize || {};
const currentSizeBN = MustBigNumber(currentSize).abs();
useEffect(() => {
if (currentStep && currentStep !== MobilePlaceOrderSteps.EditOrder) return;
if (currentInput !== 'closePosition') {
abacusStateManager.setClosePositionValue({
value: market,
field: ClosePositionInputField.market,
});
abacusStateManager.setClosePositionValue({
value: SIZE_PERCENT_OPTIONS[MAX_KEY],
field: ClosePositionInputField.percent,
});
}
}, [currentInput, market, currentStep]);
const onLastOrderIndexed = useCallback(() => {
if (!isFirstRender) {
abacusStateManager.clearClosePositionInputValues({ shouldFocusOnTradeInput: true });
onClosePositionSuccess?.();
if (currentStep === MobilePlaceOrderSteps.PlacingOrder) {
setCurrentStep?.(MobilePlaceOrderSteps.Confirmation);
}
setIsClosingPosition(false);
}
}, [currentStep, isFirstRender]);
const { setUnIndexedClientId } = useOnLastOrderIndexed({
callback: onLastOrderIndexed,
});
const onAmountInput = ({ floatValue }: { floatValue?: number }) => {
if (currentSize == null) return;
const closeAmount = MustBigNumber(floatValue)
.abs()
.toFixed(stepSizeDecimals || TOKEN_DECIMALS);
abacusStateManager.setClosePositionValue({
value: floatValue ? closeAmount : null,
field: ClosePositionInputField.size,
});
};
const onSelectPercentage = (optionVal: string) => {
abacusStateManager.setClosePositionValue({
value: optionVal,
field: ClosePositionInputField.percent,
});
};
const onSubmit = async (e: FormEvent) => {
e.preventDefault();
switch (currentStep) {
case MobilePlaceOrderSteps.EditOrder: {
setCurrentStep?.(MobilePlaceOrderSteps.PreviewOrder);
break;
}
case MobilePlaceOrderSteps.PlacingOrder:
case MobilePlaceOrderSteps.Confirmation: {
dispatch(closeDialog());
break;
}
case MobilePlaceOrderSteps.PreviewOrder:
default: {
onClosePosition();
setCurrentStep?.(MobilePlaceOrderSteps.PlacingOrder);
break;
}
}
};
const onClosePosition = async () => {
setClosePositionError(undefined);
setIsClosingPosition(true);
await closePosition({
onError: (errorParams?: { errorStringKey?: Nullable<string> }) => {
setClosePositionError(
stringGetter({ key: errorParams?.errorStringKey || STRING_KEYS.SOMETHING_WENT_WRONG })
);
setIsClosingPosition(false);
},
onSuccess: (placeOrderPayload: Nullable<HumanReadablePlaceOrderPayload>) => {
setUnIndexedClientId(placeOrderPayload?.clientId);
},
});
};
const alertMessage = closePositionError && (
<AlertMessage type={AlertType.Error}>{closePositionError}</AlertMessage>
);
const inputs = (
<Styled.InputsColumn>
<Styled.FormInput
id="close-position-amount"
label={
<>
{stringGetter({ key: STRING_KEYS.AMOUNT })}
{id && <Tag>{id}</Tag>}
</>
}
decimals={stepSizeDecimals || TOKEN_DECIMALS}
onInput={onAmountInput}
type={InputType.Number}
value={size ?? ''}
max={currentSize !== null ? currentSizeBN.toNumber() : undefined}
/>
<Styled.ToggleGroup
items={Object.entries(SIZE_PERCENT_OPTIONS).map(([key, value]) => ({
label: key === MAX_KEY ? stringGetter({ key: STRING_KEYS.FULL_CLOSE }) : key,
value: value.toString(),
}))}
value={percent?.toString() ?? ''}
onValueChange={(value: any) => onSelectPercentage((value || percent?.toString()) ?? '')}
shape={ButtonShape.Rectangle}
/>
{alertMessage}
</Styled.InputsColumn>
);
return (
<Styled.ClosePositionForm onSubmit={onSubmit} className={className}>
{!isTablet ? (
inputs
) : currentStep && currentStep !== MobilePlaceOrderSteps.EditOrder ? (
<Styled.PreviewAndConfirmContent>
<PositionPreview />
{alertMessage}
</Styled.PreviewAndConfirmContent>
) : (
<Styled.MobileLayout>
<Styled.OrderbookScrollArea scrollBehavior="snapToCenterUnlessHovered">
<Styled.Orderbook hideHeader />
</Styled.OrderbookScrollArea>
<Styled.Right>
<PositionPreview showNarrowVariation />
{inputs}
</Styled.Right>
</Styled.MobileLayout>
)}
<Styled.Footer>
{size != null && (
<Styled.ButtonRow>
<Button
type={ButtonType.Reset}
action={ButtonAction.Reset}
shape={ButtonShape.Pill}
size={ButtonSize.XSmall}
onClick={() => {
abacusStateManager.setClosePositionValue({
value: null,
field: ClosePositionInputField.percent,
});
abacusStateManager.setClosePositionValue({
value: null,
field: ClosePositionInputField.size,
});
}}
>
{stringGetter({ key: STRING_KEYS.CLEAR })}
</Button>
</Styled.ButtonRow>
)}
<PlaceOrderButtonAndReceipt
isLoading={isClosingPosition}
hasValidationErrors={!size}
actionStringKey={!size ? STRING_KEYS.ENTER_AMOUNT : undefined}
summary={summary ?? undefined}
currentStep={currentStep}
isClosePosition
/>
</Styled.Footer>
</Styled.ClosePositionForm>
);
};
const Styled: Record<string, AnyStyledComponent> = {};
Styled.ClosePositionForm = styled.form`
--form-rowGap: 1.25rem;
${layoutMixins.expandingColumnWithFooter}
gap: var(--form-rowGap);
align-items: start;
${layoutMixins.stickyArea1}
--stickyArea1-background: var(--color-layer-2);
--stickyArea1-paddingBottom: var(--dialog-content-paddingBottom);
@media ${breakpoints.tablet} {
${layoutMixins.expandingColumnWithFooter}
--orderbox-column-width: 140px;
--orderbook-width: calc(var(--orderbox-column-width) + var(--dialog-content-paddingLeft));
&& * {
outline: none !important;
}
}
`;
Styled.PreviewAndConfirmContent = styled.div`
${layoutMixins.flexColumn}
gap: var(--form-input-gap);
`;
Styled.MobileLayout = styled.div`
height: 0;
// Apply dialog's top/left/right padding to inner scroll areas
min-height: calc(100% + var(--dialog-content-paddingTop) + var(--dialog-content-paddingBottom));
margin: calc(-1 * var(--dialog-content-paddingTop)) calc(-1 * var(--dialog-content-paddingLeft))
calc(-1 * var(--form-rowGap)) calc(-1 * var(--dialog-content-paddingRight));
display: grid;
grid-template-columns: 3fr 4fr;
gap: var(--form-input-gap);
`;
Styled.OrderbookScrollArea = styled.div<{
scrollBehavior: OrderbookScrollBehavior;
}>`
${layoutMixins.stickyLeft}
${layoutMixins.scrollArea}
overscroll-behavior: contain;
${layoutMixins.stickyArea0}
--stickyArea0-paddingTop: calc(-1 * var(--dialog-content-paddingTop));
--stickyArea0-paddingBottom: calc(-1 * var(--form-rowGap));
/* --stickyArea0-paddingBottom: 10rem;
height: calc(100% + 10rem);
margin-bottom: -10rem;
padding-bottom: 10rem; */
${layoutMixins.contentContainer}
${orderbookMixins.scrollArea}
display: block;
padding-top: var(--dialog-content-paddingTop);
padding-bottom: var(--form-rowGap);
`;
Styled.Orderbook = styled(Orderbook)`
min-height: 100%;
--tableCell-padding: 0.5em 1em;
`;
Styled.Right = styled.div`
height: 0;
min-height: 100%;
${layoutMixins.scrollArea}
${layoutMixins.flexColumn}
padding-right: var(--dialog-content-paddingRight);
padding-top: var(--dialog-content-paddingTop);
padding-bottom: var(--form-rowGap);
gap: 1rem;
`;
Styled.FormInput = styled(FormInput)`
width: 100%;
`;
Styled.ToggleGroup = styled(ToggleGroup)`
${formMixins.inputToggleGroup}
@media ${breakpoints.mobile} {
> :last-child {
flex-basis: 100%;
}
}
`;
Styled.Footer = styled.footer`
${layoutMixins.stickyFooter}
backdrop-filter: none;
${layoutMixins.column}
${layoutMixins.noPointerEvents}
margin-top: auto;
`;
Styled.ButtonRow = styled.div`
${layoutMixins.row}
justify-self: end;
padding: 0.5rem 0 0.5rem 0;
`;
Styled.InputsColumn = styled.div`
${formMixins.inputsColumn}
`;