From d491b166d1574ba010ac644dc39d6be677661a4d Mon Sep 17 00:00:00 2001 From: kate-deriv Date: Thu, 16 May 2024 13:31:32 +0300 Subject: [PATCH 01/54] chore: empty commit From ef49019dfa5845c4c0de8de1e7567a85eeba5f07 Mon Sep 17 00:00:00 2001 From: maryia-deriv Date: Fri, 17 May 2024 18:55:07 +0300 Subject: [PATCH 02/54] feat: tabs + background + initialized EmptyMessage component --- .../Components/BottomNav/bottom-nav.scss | 1 + .../AppV2/Components/BottomNav/bottom-nav.tsx | 7 ++- .../EmptyMessage/empty-message.scss | 26 +++++++++ .../Components/EmptyMessage/empty-message.tsx | 53 +++++++++++++++++++ .../AppV2/Components/EmptyMessage/index.ts | 4 ++ .../Positions/positions-content.tsx | 19 +++++++ .../AppV2/Containers/Positions/positions.scss | 36 +++++++++++++ .../AppV2/Containers/Positions/positions.tsx | 34 +++++++++++- 8 files changed, 174 insertions(+), 6 deletions(-) create mode 100644 packages/trader/src/AppV2/Components/EmptyMessage/empty-message.scss create mode 100644 packages/trader/src/AppV2/Components/EmptyMessage/empty-message.tsx create mode 100644 packages/trader/src/AppV2/Components/EmptyMessage/index.ts create mode 100644 packages/trader/src/AppV2/Containers/Positions/positions-content.tsx diff --git a/packages/trader/src/AppV2/Components/BottomNav/bottom-nav.scss b/packages/trader/src/AppV2/Components/BottomNav/bottom-nav.scss index 404f176c8c95..850c9cef9f3e 100644 --- a/packages/trader/src/AppV2/Components/BottomNav/bottom-nav.scss +++ b/packages/trader/src/AppV2/Components/BottomNav/bottom-nav.scss @@ -8,6 +8,7 @@ bottom: 0; width: 100%; border-top: 1px solid var(--core-color-opacity-black-100); // waiting on quill tokens in deriv-app + background-color: var(--core-color-solid-slate-50); } &-item { display: flex; diff --git a/packages/trader/src/AppV2/Components/BottomNav/bottom-nav.tsx b/packages/trader/src/AppV2/Components/BottomNav/bottom-nav.tsx index 34bb66871e42..1be56dbde027 100644 --- a/packages/trader/src/AppV2/Components/BottomNav/bottom-nav.tsx +++ b/packages/trader/src/AppV2/Components/BottomNav/bottom-nav.tsx @@ -10,7 +10,6 @@ import BottomNavItem from './bottom-nav-item'; import { Badge } from '@deriv-com/quill-ui'; type BottomNavProps = { - className?: string; children: React.ReactNode[]; }; @@ -47,11 +46,11 @@ const bottomNavItems = [ }, ]; -const BottomNav = ({ className, children }: BottomNavProps) => { +const BottomNav = ({ children }: BottomNavProps) => { const [selectedIndex, setSelectedIndex] = React.useState(0); return ( -
+
{bottomNavItems.map((item, index) => ( { ))}
{children[selectedIndex]} -
+ ); }; diff --git a/packages/trader/src/AppV2/Components/EmptyMessage/empty-message.scss b/packages/trader/src/AppV2/Components/EmptyMessage/empty-message.scss new file mode 100644 index 000000000000..0008befa20fb --- /dev/null +++ b/packages/trader/src/AppV2/Components/EmptyMessage/empty-message.scss @@ -0,0 +1,26 @@ +// TODO: Utilize tokens from "@deriv-com/quill-ui/quill.css"; +.empty-message { + &__open, + &__closed { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100%; + padding: 0 4.8rem; + + .icon { + margin-bottom: 0.8rem; + } + .message { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.4rem; + } + .button-container { + align-self: stretch; + margin-top: 1.6rem; + } + } +} diff --git a/packages/trader/src/AppV2/Components/EmptyMessage/empty-message.tsx b/packages/trader/src/AppV2/Components/EmptyMessage/empty-message.tsx new file mode 100644 index 000000000000..83ef07fef5bb --- /dev/null +++ b/packages/trader/src/AppV2/Components/EmptyMessage/empty-message.tsx @@ -0,0 +1,53 @@ +import React from 'react'; +import { Button, Text } from '@deriv-com/quill-ui'; +import { IllustrativeEtfIcon } from '@deriv/quill-icons'; +import { Localize } from '@deriv/translations'; + +export type TEmptyMessageProps = { + isClosedTab?: boolean; + noMatchesFound?: boolean; +}; + +const EmptyMessage = ({ isClosedTab, noMatchesFound }: TEmptyMessageProps) => ( +
+
+ {/* The icon is not final, adding the correct one to quill-icons is being discussed. */} + +
+
+ + {noMatchesFound && } + {!noMatchesFound && + (isClosedTab ? ( + + ) : ( + + ))} + + + {noMatchesFound && ( + + )} + {!noMatchesFound && + (isClosedTab ? ( + + ) : ( + + ))} + +
+ {!noMatchesFound && !isClosedTab && ( +
+
+ )} +
+); + +export default EmptyMessage; diff --git a/packages/trader/src/AppV2/Components/EmptyMessage/index.ts b/packages/trader/src/AppV2/Components/EmptyMessage/index.ts new file mode 100644 index 000000000000..d30c40d4fc01 --- /dev/null +++ b/packages/trader/src/AppV2/Components/EmptyMessage/index.ts @@ -0,0 +1,4 @@ +import EmptyMessage from './empty-message'; +import './empty-message.scss'; + +export default EmptyMessage; diff --git a/packages/trader/src/AppV2/Containers/Positions/positions-content.tsx b/packages/trader/src/AppV2/Containers/Positions/positions-content.tsx new file mode 100644 index 000000000000..2ad2e0a945ee --- /dev/null +++ b/packages/trader/src/AppV2/Containers/Positions/positions-content.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import { TPortfolioPosition } from '@deriv/stores/types'; +import EmptyMessage from 'AppV2/Components/EmptyMessage'; +import { TEmptyMessageProps } from 'AppV2/Components/EmptyMessage/empty-message'; + +type TPositionsContentProps = Omit & { + positions?: TPortfolioPosition[]; +}; + +const PositionsContent = ({ isClosedTab, positions = [] }: TPositionsContentProps) => { + const noMatchesFound = false; // TODO: Implement noMatchesFound state change based on filter results + return ( +
+ {positions.length ? <> : } +
+ ); +}; + +export default PositionsContent; diff --git a/packages/trader/src/AppV2/Containers/Positions/positions.scss b/packages/trader/src/AppV2/Containers/Positions/positions.scss index e69de29bb2d1..adc220e8c5b7 100644 --- a/packages/trader/src/AppV2/Containers/Positions/positions.scss +++ b/packages/trader/src/AppV2/Containers/Positions/positions.scss @@ -0,0 +1,36 @@ +// TODO: Utilize tokens from "@deriv-com/quill-ui/quill.css"; +$TABS_HEIGHT: 4.8rem; +$FOOTER_HEIGHT: 5.8rem; + +.positions-page { + height: calc(100% - $FOOTER_HEIGHT); + background-color: var(--core-color-solid-slate-75); + + .tab-list--container { + display: block; + justify-content: unset; + background-color: var(--core-color-solid-slate-50); + + button { + width: 50%; + } + } + &__tabs { + height: 100%; + + &-content { + height: calc(100% - $TABS_HEIGHT); + + & > div { + height: 100%; + + .positions-page { + &__open, + &__closed { + height: 100%; + } + } + } + } + } +} diff --git a/packages/trader/src/AppV2/Containers/Positions/positions.tsx b/packages/trader/src/AppV2/Containers/Positions/positions.tsx index 815cf0156296..88a6e2ef0000 100644 --- a/packages/trader/src/AppV2/Containers/Positions/positions.tsx +++ b/packages/trader/src/AppV2/Containers/Positions/positions.tsx @@ -1,8 +1,38 @@ import React from 'react'; -import { Text } from '@deriv-com/quill-ui'; +import { Localize } from '@deriv/translations'; +import { Tab } from '@deriv-com/quill-ui'; +import PositionsContent from './positions-content'; const Positions = () => { - return Positions; + const tabs = [ + { + id: 'open', + title: , + content: , + }, + { + id: 'closed', + title: , + content: , + }, + ]; + + return ( +
+ + + {tabs.map(({ id, title }) => ( + {title} + ))} + + + {tabs.map(({ id, content }) => ( + {content} + ))} + + +
+ ); }; export default Positions; From 163829f3f10f5c2520a5e1e01d77b1c4e992bf3c Mon Sep 17 00:00:00 2001 From: Maryia <103177211+maryia-deriv@users.noreply.github.com> Date: Sat, 18 May 2024 18:16:02 +0300 Subject: [PATCH 03/54] Maryia/Positions-redesign/improve EmptyMessage component + add tests (#50) * feat: redirect to trade upon button click on the empty page * test: EmptyMessage --- .../AppV2/Components/BottomNav/bottom-nav.tsx | 17 +++++-- .../__tests__/empty-message.spec.tsx | 45 +++++++++++++++++++ .../Components/EmptyMessage/empty-message.tsx | 12 +++-- .../Positions/positions-content.tsx | 12 ++++- .../AppV2/Containers/Positions/positions.tsx | 8 +++- packages/trader/src/AppV2/app.tsx | 6 ++- 6 files changed, 87 insertions(+), 13 deletions(-) create mode 100644 packages/trader/src/AppV2/Components/EmptyMessage/__tests__/empty-message.spec.tsx diff --git a/packages/trader/src/AppV2/Components/BottomNav/bottom-nav.tsx b/packages/trader/src/AppV2/Components/BottomNav/bottom-nav.tsx index 1be56dbde027..c3c2bcc9f376 100644 --- a/packages/trader/src/AppV2/Components/BottomNav/bottom-nav.tsx +++ b/packages/trader/src/AppV2/Components/BottomNav/bottom-nav.tsx @@ -11,6 +11,8 @@ import { Badge } from '@deriv-com/quill-ui'; type BottomNavProps = { children: React.ReactNode[]; + selectedItemIdx?: number; + setSelectedItemIdx?: React.Dispatch>; }; const bottomNavItems = [ @@ -46,8 +48,17 @@ const bottomNavItems = [ }, ]; -const BottomNav = ({ children }: BottomNavProps) => { - const [selectedIndex, setSelectedIndex] = React.useState(0); +const BottomNav = ({ children, selectedItemIdx = 0, setSelectedItemIdx }: BottomNavProps) => { + const [selectedIndex, setSelectedIndex] = React.useState(selectedItemIdx); + + const handleSelect = (index: number) => { + setSelectedIndex(index); + setSelectedItemIdx?.(index); + }; + + React.useEffect(() => { + setSelectedIndex(selectedItemIdx); + }, [selectedItemIdx]); return ( @@ -59,7 +70,7 @@ const BottomNav = ({ children }: BottomNavProps) => { icon={item.icon} selectedIndex={selectedIndex} label={item.label} - setSelectedIndex={setSelectedIndex} + setSelectedIndex={handleSelect} /> ))} diff --git a/packages/trader/src/AppV2/Components/EmptyMessage/__tests__/empty-message.spec.tsx b/packages/trader/src/AppV2/Components/EmptyMessage/__tests__/empty-message.spec.tsx new file mode 100644 index 000000000000..ae1d52b6cf91 --- /dev/null +++ b/packages/trader/src/AppV2/Components/EmptyMessage/__tests__/empty-message.spec.tsx @@ -0,0 +1,45 @@ +import React from 'react'; +import userEvent from '@testing-library/user-event'; +import { render, screen } from '@testing-library/react'; +import EmptyMessage from '../empty-message'; + +describe('EmptyMessage', () => { + const startTradingButtonLabel = 'Start trading'; + const iconId = 'dt_empty_state_icon'; + + it('should render "No open positions" content with button when isClosedTab prop is false', () => { + render(); + expect(screen.getByText('No open positions')).toBeInTheDocument(); + expect(screen.getByText('Ready to open a position?')).toBeInTheDocument(); + expect(screen.getByRole('button', { name: startTradingButtonLabel })).toBeInTheDocument(); + }); + + it('should render "No closed positions" content when isClosedTab prop is true', () => { + render(); + expect(screen.getByText('No closed positions')).toBeInTheDocument(); + expect(screen.getByText('Your completed trades will appear here.')).toBeInTheDocument(); + }); + + it('should render "No matches found" content when noMatchesFound prop is true', () => { + render(); + expect(screen.getByText('No matches found')).toBeInTheDocument(); + expect(screen.getByText(/Try changing or removing filters/i)).toBeInTheDocument(); + }); + + it('should render an empty state icon regardless of props', () => { + const { rerender } = render(); + expect(screen.getByTestId(iconId)).toBeInTheDocument(); + rerender(); + expect(screen.getByTestId(iconId)).toBeInTheDocument(); + rerender(); + expect(screen.getByTestId(iconId)).toBeInTheDocument(); + }); + + it('should call onRedirectToTrade when "Start trading" button is clicked', () => { + const mockOnRedirectToTrade = jest.fn(); + render(); + const startTradingButton = screen.getByRole('button', { name: startTradingButtonLabel }); + userEvent.click(startTradingButton); + expect(mockOnRedirectToTrade).toHaveBeenCalled(); + }); +}); diff --git a/packages/trader/src/AppV2/Components/EmptyMessage/empty-message.tsx b/packages/trader/src/AppV2/Components/EmptyMessage/empty-message.tsx index 83ef07fef5bb..586a31e5833d 100644 --- a/packages/trader/src/AppV2/Components/EmptyMessage/empty-message.tsx +++ b/packages/trader/src/AppV2/Components/EmptyMessage/empty-message.tsx @@ -6,15 +6,17 @@ import { Localize } from '@deriv/translations'; export type TEmptyMessageProps = { isClosedTab?: boolean; noMatchesFound?: boolean; + onRedirectToTrade?: () => void; }; -const EmptyMessage = ({ isClosedTab, noMatchesFound }: TEmptyMessageProps) => ( +const EmptyMessage = ({ isClosedTab, onRedirectToTrade, noMatchesFound }: TEmptyMessageProps) => (
-
+
{/* The icon is not final, adding the correct one to quill-icons is being discussed. */}
+ {/* There is an issue with tokens: the 'lg' size should give 18px but it's giving 20px, it's being discussed. */} {noMatchesFound && } {!noMatchesFound && @@ -38,12 +40,14 @@ const EmptyMessage = ({ isClosedTab, noMatchesFound }: TEmptyMessageProps) => (
{!noMatchesFound && !isClosedTab && (
+ {/* There is an issue with tokens: the Button has a border width of 1.5px. The tokens in quill-ui will be updated, and it will change to the correct 1px. */}
)} diff --git a/packages/trader/src/AppV2/Containers/Positions/positions-content.tsx b/packages/trader/src/AppV2/Containers/Positions/positions-content.tsx index 2ad2e0a945ee..d9c20e8ca9a0 100644 --- a/packages/trader/src/AppV2/Containers/Positions/positions-content.tsx +++ b/packages/trader/src/AppV2/Containers/Positions/positions-content.tsx @@ -7,11 +7,19 @@ type TPositionsContentProps = Omit & { positions?: TPortfolioPosition[]; }; -const PositionsContent = ({ isClosedTab, positions = [] }: TPositionsContentProps) => { +const PositionsContent = ({ isClosedTab, onRedirectToTrade, positions = [] }: TPositionsContentProps) => { const noMatchesFound = false; // TODO: Implement noMatchesFound state change based on filter results return (
- {positions.length ? <> : } + {positions.length ? ( + <> + ) : ( + + )}
); }; diff --git a/packages/trader/src/AppV2/Containers/Positions/positions.tsx b/packages/trader/src/AppV2/Containers/Positions/positions.tsx index 88a6e2ef0000..74cccd748b56 100644 --- a/packages/trader/src/AppV2/Containers/Positions/positions.tsx +++ b/packages/trader/src/AppV2/Containers/Positions/positions.tsx @@ -3,12 +3,16 @@ import { Localize } from '@deriv/translations'; import { Tab } from '@deriv-com/quill-ui'; import PositionsContent from './positions-content'; -const Positions = () => { +type TPositionsProps = { + onRedirectToTrade?: () => void; +}; + +const Positions = ({ onRedirectToTrade }: TPositionsProps) => { const tabs = [ { id: 'open', title: , - content: , + content: , }, { id: 'closed', diff --git a/packages/trader/src/AppV2/app.tsx b/packages/trader/src/AppV2/app.tsx index d9d796474e9d..4ea89b7648e6 100644 --- a/packages/trader/src/AppV2/app.tsx +++ b/packages/trader/src/AppV2/app.tsx @@ -19,16 +19,18 @@ type Apptypes = { const App = ({ passthrough }: Apptypes) => { const root_store = initStore(passthrough.root_store, passthrough.WS); + const [currentPageIdx, setCurrentPageIdx] = React.useState(0); + React.useEffect(() => { return () => root_store.ui.setPromptHandler(false); }, [root_store]); return ( - + - + setCurrentPageIdx(0)} /> From 51fe75653ea6d04908c6ce8a777a5155c10b0b28 Mon Sep 17 00:00:00 2001 From: kate-deriv Date: Mon, 20 May 2024 11:58:40 +0300 Subject: [PATCH 04/54] chore: empty commit From 4be88d0c6738b34224d86c8d26ef07344b220733 Mon Sep 17 00:00:00 2001 From: kate-deriv <121025168+kate-deriv@users.noreply.github.com> Date: Mon, 20 May 2024 18:15:41 +0300 Subject: [PATCH 05/54] DTRA-1279 / Kate / Filter [WIP] (#51) * feat: add dropdowm and action sheet * feat: add apply functionality * chore: remove unused functionality * refactor: separate apply logic * feat: add clear all functionality * feat: apply filtration logic * fix: filtration bug * chore: rename variables * refactor: extract filtration logic into a util function --- .../src/AppV2/Components/Chip/chip.scss | 124 ++++++++++++++++++ .../trader/src/AppV2/Components/Chip/chip.tsx | 55 ++++++++ .../trader/src/AppV2/Components/Chip/index.ts | 4 + .../src/AppV2/Components/Filter/filter.scss | 44 +++++++ .../src/AppV2/Components/Filter/filter.tsx | 96 ++++++++++++++ .../src/AppV2/Components/Filter/index.ts | 4 + .../Positions/positions-content.tsx | 48 ++++++- .../AppV2/Containers/Positions/positions.scss | 22 ++++ .../AppV2/Containers/Positions/positions.tsx | 78 ++++++++++- .../trader/src/AppV2/Utils/positions-utils.ts | 16 +++ 10 files changed, 486 insertions(+), 5 deletions(-) create mode 100644 packages/trader/src/AppV2/Components/Chip/chip.scss create mode 100644 packages/trader/src/AppV2/Components/Chip/chip.tsx create mode 100644 packages/trader/src/AppV2/Components/Chip/index.ts create mode 100644 packages/trader/src/AppV2/Components/Filter/filter.scss create mode 100644 packages/trader/src/AppV2/Components/Filter/filter.tsx create mode 100644 packages/trader/src/AppV2/Components/Filter/index.ts create mode 100644 packages/trader/src/AppV2/Utils/positions-utils.ts diff --git a/packages/trader/src/AppV2/Components/Chip/chip.scss b/packages/trader/src/AppV2/Components/Chip/chip.scss new file mode 100644 index 000000000000..c093730835eb --- /dev/null +++ b/packages/trader/src/AppV2/Components/Chip/chip.scss @@ -0,0 +1,124 @@ +.quill-chip { + display: flex; + align-items: center; + justify-content: center; + border-style: solid; + border-width: var(--component-chip-border-width); + border-color: var(--component-chip-border-color); + border-radius: var(--component-chip-border-radius); + background-color: var(--core-color-solid-slate-50); + + &:hover { + background-color: var(--component-chip-bg-hover); + } + + &:active { + background-color: var(--component-chip-bg-active); + } + + & > svg { + fill: var(--component-chip-icon-default); + } + + &[data-state='selected']:hover { + background-color: var(--component-chip-bg-selectedHover); + } + + &[data-state='selected']:active { + background-color: var(--component-chip-bg-selectedActive); + } + + &[data-state='selected'] { + background-color: var(--component-chip-bg-selected); + + & > p { + color: var(--component-chip-item-color-default); + } + + & > svg { + fill: var(--component-chip-icon-selected); + } + + &:has(.rotate[data-state='open']) { + background-color: var(--component-chip-bg-selectedExpand); + } + } + + &:disabled { + pointer-events: none; + user-select: none; + + & > p { + color: var(--component-chip-label-color-disabled); + } + + & > svg { + fill: var(--component-chip-icon-disabled); + } + + &[data-state='selected'] { + background-color: var(--component-chip-bg-selectedDisabled); + + & > p, + svg { + color: var(--component-chip-label-color-disabledWhite); + fill: var(--component-chip-icon-disabledWhite); + } + } + } + + &__disabled { + &--true { + & > svg { + fill: var(--component-chip-icon-disabled) !important; + } + + & > p { + color: var(--component-chip-label-color-disabled) !important; + } + } + } + + &__size { + &--sm { + padding-inline: var(--component-chip-spacing-padding-md); + height: var(--component-chip-height-sm); + gap: var(--component-chip-spacing-padding-xs); + } + &--md { + padding-inline: var(--component-chip-spacing-padding-lg); + height: var(--component-chip-height-md); + gap: var(--component-chip-spacing-padding-sm); + } + &--lg { + padding-inline: var(--component-chip-spacing-padding-xl); + height: var(--component-chip-height-lg); + gap: var(--component-chip-spacing-padding-md); + } + } + + &__custom-right-padding { + &__size { + &--sm { + padding-inline: var(--component-chip-spacing-padding-md) var(--component-chip-spacing-padding-xs); + } + &--md { + padding-inline: var(--component-chip-spacing-padding-lg) var(--component-chip-spacing-padding-sm); + } + &--lg { + padding-inline: var(--component-chip-spacing-padding-xl) var(--component-chip-spacing-padding-md); + } + } + } +} + +.rotate { + transition-property: transform; + transition-timing-function: var(--core-motion-ease-400); + transition-duration: var(--core-motion-duration-200); + transform: rotate(0); + + &[data-state='open'] { + transform: rotate(180deg); + } +} diff --git a/packages/trader/src/AppV2/Components/Chip/chip.tsx b/packages/trader/src/AppV2/Components/Chip/chip.tsx new file mode 100644 index 000000000000..2c174debf5ae --- /dev/null +++ b/packages/trader/src/AppV2/Components/Chip/chip.tsx @@ -0,0 +1,55 @@ +import React from 'react'; +import { StandaloneChevronDownRegularIcon } from '@deriv/quill-icons'; +import './chip.scss'; +import clsx from 'clsx'; +import { CaptionText, Text } from '@deriv-com/quill-ui'; +import { TRegularSizes } from '@deriv-com/quill-ui/dist/types'; + +export const LabelTextSizes: Record = { + sm: , + md: , + lg: , +}; + +type BaseChipProps = Omit, 'label'> & { + label?: React.ReactNode; + disabled?: boolean; + isDropdownOpen?: boolean; + dropdown?: boolean; + selected?: boolean; + size?: TRegularSizes; + onClick?: () => void; +}; + +const Chip = React.forwardRef( + ({ size = 'md', label, dropdown = false, className, selected, isDropdownOpen = false, onClick, ...rest }, ref) => ( + + ) +); + +Chip.displayName = 'Chip'; +export default Chip; diff --git a/packages/trader/src/AppV2/Components/Chip/index.ts b/packages/trader/src/AppV2/Components/Chip/index.ts new file mode 100644 index 000000000000..e755e0f4bf52 --- /dev/null +++ b/packages/trader/src/AppV2/Components/Chip/index.ts @@ -0,0 +1,4 @@ +import Chip from './chip'; +import './chip.scss'; + +export default Chip; diff --git a/packages/trader/src/AppV2/Components/Filter/filter.scss b/packages/trader/src/AppV2/Components/Filter/filter.scss new file mode 100644 index 000000000000..8f4865e84aba --- /dev/null +++ b/packages/trader/src/AppV2/Components/Filter/filter.scss @@ -0,0 +1,44 @@ +.filter { + &__item { + justify-content: space-between; + width: 100%; + height: var(--core-size-2000); + padding-block: var(--core-spacing-400); + + &__wrapper { + padding-block: var(--core-spacing-800); + } + // TODO: Temporary css (until quill fix) + .quill-checkbox__wrapper { + order: 3; + } + } +} + +// TODO: Temporary css (until quill fix) +.quill-action-sheet { + &--root { + width: 100%; + } + + &--handle-bar--line { + width: var(--core-size-2400); + } + + &--header > p, + &--title--icon { + display: none; + } + + &--header { + padding-block: var(--core-spacing-400); + } + + &--title { + height: var(--core-size-2400); + } + + &--footer { + padding-block: var(--core-spacing-800); + } +} diff --git a/packages/trader/src/AppV2/Components/Filter/filter.tsx b/packages/trader/src/AppV2/Components/Filter/filter.tsx new file mode 100644 index 000000000000..7722ebab7dc4 --- /dev/null +++ b/packages/trader/src/AppV2/Components/Filter/filter.tsx @@ -0,0 +1,96 @@ +import React from 'react'; +import Chip from 'AppV2/Components/Chip'; +import { ActionSheet, Checkbox } from '@deriv-com/quill-ui'; +import { Localize } from '@deriv/translations'; + +type TFilter = { + setContractTypeFilter: React.Dispatch>; +}; + +// TODO: Replace mockAvailableContractsList with real data when BE will be ready (send list of all available contracts based on account) +const mockAvailableContractsList = [ + { tradeType: , id: 'Accumulators' }, + { tradeType: , id: 'Vanillas' }, + { tradeType: , id: 'Turbos' }, + { tradeType: , id: 'Multipliers' }, + { tradeType: , id: 'Rise/Fall' }, + { tradeType: , id: 'Higher/Lower' }, + { tradeType: , id: 'Touch/No touch' }, + { tradeType: , id: 'Matches/Differs' }, + { tradeType: , id: 'Even/Odd' }, + { tradeType: , id: 'Over/Under' }, +]; + +const Filter = ({ setContractTypeFilter }: TFilter) => { + const [isDropdownOpen, setIsDropDownOpen] = React.useState(false); + const [changedOptions, setChangedOptions] = React.useState([]); + + const onDropdownClick = () => { + setIsDropDownOpen(!isDropdownOpen); + }; + + const onChange = (e: React.ChangeEvent | React.KeyboardEvent) => { + const newSelectedOption = (e.target as EventTarget & HTMLInputElement).id; + + if (changedOptions.includes(newSelectedOption)) { + setChangedOptions([...changedOptions.filter(item => item !== newSelectedOption)]); + } else { + setChangedOptions([...changedOptions, newSelectedOption]); + } + }; + + const onApply = () => { + setContractTypeFilter(changedOptions); + }; + const onClearAll = () => { + setContractTypeFilter([]); + setChangedOptions([]); + }; + + const chipLabelFormatting = () => { + const arrayLength = changedOptions.length; + if (!arrayLength) return ; + if (changedOptions.length === 1) + return mockAvailableContractsList.find(type => type.id === changedOptions[0])?.tradeType; + return ; + }; + + return ( + <> + + setIsDropDownOpen(false)} position='left'> + + {/* TODO: Add a PR to Quill with changing type of title (need ReactNode)*/} + + + {mockAvailableContractsList.map(({ tradeType, id }) => ( + + ))} + + {/* TODO: Add PR to Quill in order to switch off (make optional) ability to close action sheet by clicking on btns */} + + + + + ); +}; + +export default Filter; diff --git a/packages/trader/src/AppV2/Components/Filter/index.ts b/packages/trader/src/AppV2/Components/Filter/index.ts new file mode 100644 index 000000000000..ab75de2bac4b --- /dev/null +++ b/packages/trader/src/AppV2/Components/Filter/index.ts @@ -0,0 +1,4 @@ +import Filter from './filter'; +import './filter.scss'; + +export default Filter; diff --git a/packages/trader/src/AppV2/Containers/Positions/positions-content.tsx b/packages/trader/src/AppV2/Containers/Positions/positions-content.tsx index d9c20e8ca9a0..bfe1f5645bc3 100644 --- a/packages/trader/src/AppV2/Containers/Positions/positions-content.tsx +++ b/packages/trader/src/AppV2/Containers/Positions/positions-content.tsx @@ -2,17 +2,59 @@ import React from 'react'; import { TPortfolioPosition } from '@deriv/stores/types'; import EmptyMessage from 'AppV2/Components/EmptyMessage'; import { TEmptyMessageProps } from 'AppV2/Components/EmptyMessage/empty-message'; +import Filter from 'AppV2/Components/Filter'; +import { isHighLow } from '@deriv/shared'; type TPositionsContentProps = Omit & { + noMatchesFound?: boolean; positions?: TPortfolioPosition[]; + setContractTypeFilter: React.Dispatch>; }; -const PositionsContent = ({ isClosedTab, onRedirectToTrade, positions = [] }: TPositionsContentProps) => { - const noMatchesFound = false; // TODO: Implement noMatchesFound state change based on filter results +//TODO: Implement contract card +const ContractCard = ({ + contractType, + purchaseTime, + shortcode, +}: { + contractType?: string; + purchaseTime?: number; + shortcode?: string; +}) => ( +
+
{contractType}
+
{purchaseTime}
+
{`${isHighLow({ shortcode }) ? 'High/Low' : 'Rise/Fall'}`}
+
+); + +const PositionsContent = ({ + isClosedTab, + noMatchesFound, + onRedirectToTrade, + positions = [], + setContractTypeFilter, +}: TPositionsContentProps) => { return (
+
+
+ {(positions.length || (!positions.length && noMatchesFound)) && ( + + )} +
+
{positions.length ? ( - <> + + {positions.map(({ contract_info }) => ( + + ))} + ) : ( void; }; +//TODO: Remove after Bala's PR with hook for real data will be merged +const mockPositions = [ + { + contract_info: { + contract_type: 'MULTUP', + purchase_time: 1716198125, + shortcode: 'MULTUP_1HZ100V_10.00_10_1716014450_4869676799_0_0.00_N1', + }, + }, + { + contract_info: { + contract_type: 'VANILLALONGCALL', + purchase_time: 1716198390, + shortcode: 'VANILLALONGCALL_1HZ100V_10.00_1716205893_1716206073_S0P_12.66345_1716205893', + }, + }, + { + contract_info: { + contract_type: 'CALL', + purchase_time: 1716205999, + shortcode: 'CALL_1HZ100V_19.55_1716205999_1716206179_S0P_0', + }, + }, + { + contract_info: { + contract_type: 'PUT', + purchase_time: 1716206101, + shortcode: 'PUT_1HZ100V_19.51_1716206101_1716206281_S0P_0', + }, + }, + { + contract_info: { + contract_type: 'CALL', + purchase_time: 1716206154, + shortcode: 'CALL_1HZ100V_24.09_1716206154_1716206334_S40P_0', + }, + }, + { + contract_info: { + contract_type: 'PUT', + purchase_time: 1716206194, + shortcode: 'PUT_1HZ100V_16.42_1716206194_1716206374_S40P_0', + }, + }, +] as TPortfolioPosition[]; + const Positions = ({ onRedirectToTrade }: TPositionsProps) => { + const [contractTypeFilter, setContractTypeFilter] = React.useState([]); + const [filteredPositions, setFilteredPositions] = React.useState(mockPositions || []); + const [noMatchesFound, setNoMatchesFound] = React.useState(false); + const tabs = [ { id: 'open', title: , - content: , + content: ( + + ), }, { id: 'closed', title: , - content: , + content: ( + + ), }, ]; + React.useEffect(() => { + if (contractTypeFilter.length) { + const result = filterPositions(mockPositions, contractTypeFilter); + setNoMatchesFound(!result.length); + setFilteredPositions(result); + } else setFilteredPositions(mockPositions); + }, [contractTypeFilter]); + return (
diff --git a/packages/trader/src/AppV2/Utils/positions-utils.ts b/packages/trader/src/AppV2/Utils/positions-utils.ts new file mode 100644 index 000000000000..9d3d42d0b6cc --- /dev/null +++ b/packages/trader/src/AppV2/Utils/positions-utils.ts @@ -0,0 +1,16 @@ +import { TPortfolioPosition } from '@deriv/stores/types'; +import { getSupportedContracts, isHighLow } from '@deriv/shared'; + +export const filterPositions = (positions: TPortfolioPosition[], filter: string[]) => { + // Split contract type names with '/' (e.g. Rise/Fall) + const splittedFilter = filter.map(option => (option.includes('/') ? option.split('/') : option)).flat(); + + //TODO: Create own config instead of getSupportedContracts + return positions.filter(({ contract_info }) => { + const config = getSupportedContracts(isHighLow({ shortcode: contract_info.shortcode }))[ + contract_info.contract_type as keyof ReturnType + ]; + + return splittedFilter.includes('main_title' in config ? config.main_title : config.name); + }); +}; From 146a56fcbf4cc43545a950e82a0e1f9cd6968e81 Mon Sep 17 00:00:00 2001 From: kate-deriv Date: Mon, 20 May 2024 18:23:58 +0300 Subject: [PATCH 06/54] chore: empty commit From 185a4bc4a78fde827d84f77de6116c55a05f9230 Mon Sep 17 00:00:00 2001 From: kate-deriv Date: Mon, 20 May 2024 18:31:31 +0300 Subject: [PATCH 07/54] fix: sonarcloud issues --- packages/trader/src/AppV2/Components/Filter/filter.tsx | 6 +++--- .../src/AppV2/Containers/Positions/positions-content.tsx | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/trader/src/AppV2/Components/Filter/filter.tsx b/packages/trader/src/AppV2/Components/Filter/filter.tsx index 7722ebab7dc4..26cc5d8de512 100644 --- a/packages/trader/src/AppV2/Components/Filter/filter.tsx +++ b/packages/trader/src/AppV2/Components/Filter/filter.tsx @@ -22,11 +22,11 @@ const mockAvailableContractsList = [ ]; const Filter = ({ setContractTypeFilter }: TFilter) => { - const [isDropdownOpen, setIsDropDownOpen] = React.useState(false); + const [isDropdownOpen, setIsDropdownOpen] = React.useState(false); const [changedOptions, setChangedOptions] = React.useState([]); const onDropdownClick = () => { - setIsDropDownOpen(!isDropdownOpen); + setIsDropdownOpen(!isDropdownOpen); }; const onChange = (e: React.ChangeEvent | React.KeyboardEvent) => { @@ -64,7 +64,7 @@ const Filter = ({ setContractTypeFilter }: TFilter) => { onClick={onDropdownClick} selected={!!changedOptions.length} /> - setIsDropDownOpen(false)} position='left'> + setIsDropdownOpen(false)} position='left'> {/* TODO: Add a PR to Quill with changing type of title (need ReactNode)*/} diff --git a/packages/trader/src/AppV2/Containers/Positions/positions-content.tsx b/packages/trader/src/AppV2/Containers/Positions/positions-content.tsx index bfe1f5645bc3..87c3d9970ed3 100644 --- a/packages/trader/src/AppV2/Containers/Positions/positions-content.tsx +++ b/packages/trader/src/AppV2/Containers/Positions/positions-content.tsx @@ -39,7 +39,7 @@ const PositionsContent = ({
- {(positions.length || (!positions.length && noMatchesFound)) && ( + {(!!positions.length || (!positions.length && noMatchesFound)) && ( )}
From 32e60f1630833fbbf22378a608f74115d8ea4328 Mon Sep 17 00:00:00 2001 From: kate-deriv Date: Tue, 21 May 2024 09:16:31 +0300 Subject: [PATCH 08/54] chore: empty commit From 59e9b174a7b6010b5b2159b7168af41755dd3abe Mon Sep 17 00:00:00 2001 From: balakrishna-deriv <56330681+balakrishna-deriv@users.noreply.github.com> Date: Tue, 21 May 2024 15:14:58 +0800 Subject: [PATCH 09/54] chore: add modules store and useclosedposition hook (#52) --- packages/trader/src/App/app.tsx | 15 ++++---- .../AppV2/Containers/Positions/positions.tsx | 16 +++++++-- .../src/AppV2/Hooks/useClosedPositions.ts | 34 +++++++++++++++++++ packages/trader/src/AppV2/app.tsx | 15 ++++---- .../Modules/Positions/positions-store.ts | 22 ++++++++++++ .../Trading/__tests__/trade-store.spec.ts | 3 +- .../src/Stores/Modules/Trading/trade-store.ts | 5 ++- packages/trader/src/Stores/Modules/index.js | 8 ----- packages/trader/src/Stores/Modules/index.ts | 16 +++++++++ .../Stores/Providers/modules-providers.tsx | 14 ++++++++ packages/trader/src/Stores/base-store.ts | 6 ++-- .../trader/src/Stores/useModulesStores.tsx | 20 +++++++++++ packages/trader/src/Types/common-prop.type.ts | 15 ++++++++ 13 files changed, 160 insertions(+), 29 deletions(-) create mode 100644 packages/trader/src/AppV2/Hooks/useClosedPositions.ts create mode 100644 packages/trader/src/Stores/Modules/Positions/positions-store.ts delete mode 100644 packages/trader/src/Stores/Modules/index.js create mode 100644 packages/trader/src/Stores/Modules/index.ts create mode 100644 packages/trader/src/Stores/Providers/modules-providers.tsx create mode 100644 packages/trader/src/Stores/useModulesStores.tsx diff --git a/packages/trader/src/App/app.tsx b/packages/trader/src/App/app.tsx index 87ce015eddc5..79be0e1500fc 100644 --- a/packages/trader/src/App/app.tsx +++ b/packages/trader/src/App/app.tsx @@ -10,6 +10,7 @@ import initStore from './init-store'; import 'Sass/app.scss'; import type { TCoreStores } from '@deriv/stores/types'; import TraderProviders from '../trader-providers'; +import ModulesProvider from 'Stores/Providers/modules-providers'; type Apptypes = { passthrough: { @@ -31,12 +32,14 @@ const App = ({ passthrough }: Apptypes) => { return ( - - - - - - + + + + + + + + ); }; diff --git a/packages/trader/src/AppV2/Containers/Positions/positions.tsx b/packages/trader/src/AppV2/Containers/Positions/positions.tsx index 41a82df196ae..b4c18cd68e87 100644 --- a/packages/trader/src/AppV2/Containers/Positions/positions.tsx +++ b/packages/trader/src/AppV2/Containers/Positions/positions.tsx @@ -2,8 +2,11 @@ import React from 'react'; import { Localize } from '@deriv/translations'; import { Tab } from '@deriv-com/quill-ui'; import { TPortfolioPosition } from '@deriv/stores/types'; +import useClosedPositions from 'AppV2/Hooks/useClosedPositions'; +import { useModulesStore } from 'Stores/useModulesStores'; import { filterPositions } from '../../Utils/positions-utils'; import PositionsContent from './positions-content'; +import { observer } from '@deriv/stores'; type TPositionsProps = { onRedirectToTrade?: () => void; @@ -55,11 +58,20 @@ const mockPositions = [ }, ] as TPortfolioPosition[]; -const Positions = ({ onRedirectToTrade }: TPositionsProps) => { +const Positions = observer(({ onRedirectToTrade }: TPositionsProps) => { const [contractTypeFilter, setContractTypeFilter] = React.useState([]); const [filteredPositions, setFilteredPositions] = React.useState(mockPositions || []); const [noMatchesFound, setNoMatchesFound] = React.useState(false); + const { positions: positionsStore } = useModulesStore(); + const { closedContractTypeFilter } = positionsStore; + + const { closedPositions } = useClosedPositions({ contractTypes: closedContractTypeFilter }); + + // TODO: remove this line + // eslint-disable-next-line no-console + console.log('closedPositions', closedPositions); + const tabs = [ { id: 'open', @@ -111,6 +123,6 @@ const Positions = ({ onRedirectToTrade }: TPositionsProps) => {
); -}; +}); export default Positions; diff --git a/packages/trader/src/AppV2/Hooks/useClosedPositions.ts b/packages/trader/src/AppV2/Hooks/useClosedPositions.ts new file mode 100644 index 000000000000..e91b3793f862 --- /dev/null +++ b/packages/trader/src/AppV2/Hooks/useClosedPositions.ts @@ -0,0 +1,34 @@ +import { WS } from '@deriv/shared'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; +import { ProfitTable, ProfitTableResponse } from '@deriv/api-types'; + +type TPros = { + contractTypes: string[]; +}; + +const useClosedPositions = ({ contractTypes }: TPros) => { + const [positions, setPositions] = useState>([]); + const [isLoading, setLoading] = React.useState(false); + const positionsRef = useRef>([]); + + const fetch = useCallback(async () => { + setLoading(true); + const data: ProfitTableResponse = await WS.profitTable(50, positionsRef.current.length, { + contract_type: contractTypes.length > 0 ? contractTypes : undefined, + }); + + setLoading(false); + // TODO: handle errors + setPositions(prevPositions => [...prevPositions, ...(data?.profit_table?.transactions ?? [])]); + }, [contractTypes]); + + useEffect(() => { + setPositions([]); + positionsRef.current = []; + fetch(); + }, [fetch, contractTypes]); + + return { closedPositions: positions, isLoading, fetchMore: fetch }; +}; + +export default useClosedPositions; diff --git a/packages/trader/src/AppV2/app.tsx b/packages/trader/src/AppV2/app.tsx index ba2a90e113f8..f6fadef0dd8e 100644 --- a/packages/trader/src/AppV2/app.tsx +++ b/packages/trader/src/AppV2/app.tsx @@ -2,6 +2,7 @@ import React from 'react'; import type { TWebSocket } from 'Types'; import initStore from 'App/init-store'; import type { TCoreStores } from '@deriv/stores/types'; +import ModulesProvider from 'Stores/Providers/modules-providers'; import TraderProviders from '../trader-providers'; import BottomNav from './Components/BottomNav'; import Trade from './Containers/Trade'; @@ -28,12 +29,14 @@ const App = ({ passthrough }: Apptypes) => { return ( - - - - setCurrentPageIdx(0)} /> - - + + + + + setCurrentPageIdx(0)} /> + + + ); }; diff --git a/packages/trader/src/Stores/Modules/Positions/positions-store.ts b/packages/trader/src/Stores/Modules/Positions/positions-store.ts new file mode 100644 index 000000000000..3786fc238f3b --- /dev/null +++ b/packages/trader/src/Stores/Modules/Positions/positions-store.ts @@ -0,0 +1,22 @@ +import { makeObservable, observable, action } from 'mobx'; +import { TRootStore } from 'Types'; +import BaseStore from 'Stores/base-store'; + +export default class PositionsStore extends BaseStore { + openContractTypeFilter: string[] = []; + closedContractTypeFilter: string[] = []; + + constructor({ root_store }: { root_store: TRootStore }) { + super({ root_store }); + + makeObservable(this, { + openContractTypeFilter: observable, + closedContractTypeFilter: observable, + addToClosedContractTypeFilter: action.bound, + }); + } + + addToClosedContractTypeFilter(contractType: string) { + this.closedContractTypeFilter = [...this.closedContractTypeFilter, contractType]; + } +} diff --git a/packages/trader/src/Stores/Modules/Trading/__tests__/trade-store.spec.ts b/packages/trader/src/Stores/Modules/Trading/__tests__/trade-store.spec.ts index cabfca201c7d..9b0b04621e59 100644 --- a/packages/trader/src/Stores/Modules/Trading/__tests__/trade-store.spec.ts +++ b/packages/trader/src/Stores/Modules/Trading/__tests__/trade-store.spec.ts @@ -5,6 +5,7 @@ import { mockStore } from '@deriv/stores'; import TradeStore from '../trade-store'; import { configure } from 'mobx'; import { ContractType } from '../Helpers/contract-type'; +import { TRootStore } from 'Types'; configure({ safeDescriptors: false }); @@ -248,7 +249,7 @@ beforeAll(async () => { common: { server_time: moment('2024-02-26T11:59:59.488Z'), }, - }), + }) as unknown as TRootStore, }); await ContractType.buildContractTypesConfig(symbol); mockedTradeStore.onMount(); diff --git a/packages/trader/src/Stores/Modules/Trading/trade-store.ts b/packages/trader/src/Stores/Modules/Trading/trade-store.ts index dc793ea00544..17a5c5a9a3e8 100644 --- a/packages/trader/src/Stores/Modules/Trading/trade-store.ts +++ b/packages/trader/src/Stores/Modules/Trading/trade-store.ts @@ -50,11 +50,10 @@ import { action, computed, makeObservable, observable, override, reaction, runIn import { createProposalRequests, getProposalErrorField, getProposalInfo } from './Helpers/proposal'; import { getHoveredColor } from './Helpers/barrier-utils'; import BaseStore from '../../base-store'; -import { TTextValueNumber, TTextValueStrings } from 'Types'; +import { TRootStore, TTextValueNumber, TTextValueStrings } from 'Types'; import { ChartBarrierStore } from '../SmartChart/chart-barrier-store'; import debounce from 'lodash.debounce'; import { setLimitOrderBarriers } from './Helpers/limit-orders'; -import type { TCoreStores } from '@deriv/stores/types'; import { ActiveSymbols, ActiveSymbolsRequest, @@ -319,7 +318,7 @@ export default class TradeStore extends BaseStore { is_initial_barrier_applied = false; is_digits_widget_active = false; should_skip_prepost_lifecycle = false; - constructor({ root_store }: { root_store: TCoreStores }) { + constructor({ root_store }: { root_store: TRootStore }) { const local_storage_properties = [ 'amount', 'currency', diff --git a/packages/trader/src/Stores/Modules/index.js b/packages/trader/src/Stores/Modules/index.js deleted file mode 100644 index 27463b9f10eb..000000000000 --- a/packages/trader/src/Stores/Modules/index.js +++ /dev/null @@ -1,8 +0,0 @@ -import TradeStore from './Trading/trade-store'; - -export default class ModulesStore { - constructor(root_store, core_store) { - this.cashier = core_store.modules.cashier; - this.trade = new TradeStore({ root_store }); - } -} diff --git a/packages/trader/src/Stores/Modules/index.ts b/packages/trader/src/Stores/Modules/index.ts new file mode 100644 index 000000000000..2504e498067c --- /dev/null +++ b/packages/trader/src/Stores/Modules/index.ts @@ -0,0 +1,16 @@ +import TradeStore from './Trading/trade-store'; +import PositionsStore from './Positions/positions-store'; +import { TCoreStores } from '@deriv/stores/types'; +import { TRootStore } from 'Types'; + +export default class ModulesStore { + positions: PositionsStore; + trade: TradeStore; + cashier: any; + + constructor(root_store: TRootStore, core_store: TCoreStores) { + this.cashier = core_store.modules.cashier; + this.trade = new TradeStore({ root_store }); + this.positions = new PositionsStore({ root_store }); + } +} diff --git a/packages/trader/src/Stores/Providers/modules-providers.tsx b/packages/trader/src/Stores/Providers/modules-providers.tsx new file mode 100644 index 000000000000..fee7631e0fff --- /dev/null +++ b/packages/trader/src/Stores/Providers/modules-providers.tsx @@ -0,0 +1,14 @@ +import React from 'react'; +import { StoreProvider } from '@deriv/stores'; +import { ModulesStoreProvider } from 'Stores/useModulesStores'; +import type { TCoreStores } from '@deriv/stores/types'; + +export const ModulesProvider = ({ children, store }: React.PropsWithChildren<{ store: TCoreStores }>) => { + return ( + + {children} + + ); +}; + +export default ModulesProvider; diff --git a/packages/trader/src/Stores/base-store.ts b/packages/trader/src/Stores/base-store.ts index 3d666736ba27..1037acd53796 100644 --- a/packages/trader/src/Stores/base-store.ts +++ b/packages/trader/src/Stores/base-store.ts @@ -1,12 +1,12 @@ import { action, intercept, observable, reaction, toJS, when, makeObservable } from 'mobx'; import { isProduction, isEmptyObject, Validator } from '@deriv/shared'; -import { TCoreStores } from '@deriv/stores/types'; import { getValidationRules } from './Modules/Trading/Constants/validation-rules'; +import { TRootStore } from 'Types'; type TValidationRules = ReturnType | Record; type TBaseStoreOptions = { - root_store: TCoreStores; + root_store: TRootStore; local_storage_properties?: string[]; session_storage_properties?: string[]; validation_rules?: TValidationRules; @@ -38,7 +38,7 @@ export default class BaseStore { pre_switch_account_listener: null | (() => Promise) = null; realAccountSignupEndedDisposer: null | (() => void) = null; real_account_signup_ended_listener: null | (() => Promise) = null; - root_store: TCoreStores; + root_store: TRootStore; session_storage_properties: string[]; store_name = ''; switchAccountDisposer: null | (() => void) = null; diff --git a/packages/trader/src/Stores/useModulesStores.tsx b/packages/trader/src/Stores/useModulesStores.tsx new file mode 100644 index 000000000000..3ac894968fa0 --- /dev/null +++ b/packages/trader/src/Stores/useModulesStores.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import { useStore } from '@deriv/stores'; +import ModulesStore from './Modules'; + +const ModulesStoreContext = React.createContext(null); + +export const ModulesStoreProvider = ({ children }: React.PropsWithChildren) => { + const { modules } = useStore(); + return {children}; +}; + +export const useModulesStore = () => { + const store = React.useContext(ModulesStoreContext); + + if (!store) { + throw new Error('useModulesStore must be used within ModulesStoreProvider'); + } + + return store; +}; diff --git a/packages/trader/src/Types/common-prop.type.ts b/packages/trader/src/Types/common-prop.type.ts index 1ef702385b3d..0ff83bdbd3cc 100644 --- a/packages/trader/src/Types/common-prop.type.ts +++ b/packages/trader/src/Types/common-prop.type.ts @@ -19,10 +19,25 @@ import { UpdateContractRequest, } from '@deriv/api-types'; import { TCoreStores } from '@deriv/stores/types'; +import ModulesStore from 'Stores/Modules'; import { useTraderStore } from 'Stores/useTraderStores'; import { Redirect, RouteComponentProps } from 'react-router-dom'; import { TSocketEndpointNames, TSocketResponse } from '../../../api/types'; +export type TRootStore = { + client: TCoreStores['client']; + common: TCoreStores['common']; + modules: ModulesStore; + ui: TCoreStores['ui']; + gtm: TCoreStores['gtm']; + notifications: TCoreStores['notifications']; + contract_replay: TCoreStores['contract_replay']; + contract_trade: TCoreStores['contract_trade']; + portfolio: TCoreStores['portfolio']; + chart_barrier_store: TCoreStores['chart_barrier_store']; + active_symbols: TCoreStores['active_symbols']; +}; + export type TBinaryRoutesProps = { is_logged_in: boolean; is_logging_in: boolean; From 2e7c763efcdcd0c7a718ed49941e4709b7fe9297 Mon Sep 17 00:00:00 2001 From: kate-deriv <121025168+kate-deriv@users.noreply.github.com> Date: Tue, 21 May 2024 10:17:08 +0300 Subject: [PATCH 10/54] refactor: remove todo and change some prop based on quill updates (#54) --- .../src/AppV2/Components/Filter/filter.scss | 32 ----------------- .../src/AppV2/Components/Filter/filter.tsx | 36 ++++++++----------- .../Positions/positions-content.tsx | 4 ++- .../AppV2/Containers/Positions/positions.tsx | 2 ++ 4 files changed, 20 insertions(+), 54 deletions(-) diff --git a/packages/trader/src/AppV2/Components/Filter/filter.scss b/packages/trader/src/AppV2/Components/Filter/filter.scss index 8f4865e84aba..9c8444e21dfb 100644 --- a/packages/trader/src/AppV2/Components/Filter/filter.scss +++ b/packages/trader/src/AppV2/Components/Filter/filter.scss @@ -8,37 +8,5 @@ &__wrapper { padding-block: var(--core-spacing-800); } - // TODO: Temporary css (until quill fix) - .quill-checkbox__wrapper { - order: 3; - } - } -} - -// TODO: Temporary css (until quill fix) -.quill-action-sheet { - &--root { - width: 100%; - } - - &--handle-bar--line { - width: var(--core-size-2400); - } - - &--header > p, - &--title--icon { - display: none; - } - - &--header { - padding-block: var(--core-spacing-400); - } - - &--title { - height: var(--core-size-2400); - } - - &--footer { - padding-block: var(--core-spacing-800); } } diff --git a/packages/trader/src/AppV2/Components/Filter/filter.tsx b/packages/trader/src/AppV2/Components/Filter/filter.tsx index 26cc5d8de512..fa83ad2b843f 100644 --- a/packages/trader/src/AppV2/Components/Filter/filter.tsx +++ b/packages/trader/src/AppV2/Components/Filter/filter.tsx @@ -5,6 +5,7 @@ import { Localize } from '@deriv/translations'; type TFilter = { setContractTypeFilter: React.Dispatch>; + contractTypeFilter: string[] | []; }; // TODO: Replace mockAvailableContractsList with real data when BE will be ready (send list of all available contracts based on account) @@ -21,12 +22,13 @@ const mockAvailableContractsList = [ { tradeType: , id: 'Over/Under' }, ]; -const Filter = ({ setContractTypeFilter }: TFilter) => { +const Filter = ({ setContractTypeFilter, contractTypeFilter }: TFilter) => { const [isDropdownOpen, setIsDropdownOpen] = React.useState(false); - const [changedOptions, setChangedOptions] = React.useState([]); + const [changedOptions, setChangedOptions] = React.useState(contractTypeFilter); - const onDropdownClick = () => { - setIsDropdownOpen(!isDropdownOpen); + const onActionSheetClose = () => { + setIsDropdownOpen(false); + setChangedOptions(contractTypeFilter); }; const onChange = (e: React.ChangeEvent | React.KeyboardEvent) => { @@ -39,14 +41,6 @@ const Filter = ({ setContractTypeFilter }: TFilter) => { } }; - const onApply = () => { - setContractTypeFilter(changedOptions); - }; - const onClearAll = () => { - setContractTypeFilter([]); - setChangedOptions([]); - }; - const chipLabelFormatting = () => { const arrayLength = changedOptions.length; if (!arrayLength) return ; @@ -56,18 +50,17 @@ const Filter = ({ setContractTypeFilter }: TFilter) => { }; return ( - <> + setIsDropdownOpen(!isDropdownOpen)} selected={!!changedOptions.length} /> - setIsDropdownOpen(false)} position='left'> + - {/* TODO: Add a PR to Quill with changing type of title (need ReactNode)*/} - + } /> {mockAvailableContractsList.map(({ tradeType, id }) => ( { id={id} checked={changedOptions.includes(id)} size='md' + checkboxPosition='right' /> ))} - {/* TODO: Add PR to Quill in order to switch off (make optional) ability to close action sheet by clicking on btns */} setContractTypeFilter(changedOptions) }} + secondaryAction={{ content: 'Clear All', onAction: () => setChangedOptions([]) }} alignment='vertical' + shouldCloseOnSecondaryButtonClick={false} /> - + ); }; diff --git a/packages/trader/src/AppV2/Containers/Positions/positions-content.tsx b/packages/trader/src/AppV2/Containers/Positions/positions-content.tsx index 87c3d9970ed3..fc835d96c3e5 100644 --- a/packages/trader/src/AppV2/Containers/Positions/positions-content.tsx +++ b/packages/trader/src/AppV2/Containers/Positions/positions-content.tsx @@ -9,6 +9,7 @@ type TPositionsContentProps = Omit & { noMatchesFound?: boolean; positions?: TPortfolioPosition[]; setContractTypeFilter: React.Dispatch>; + contractTypeFilter: string[] | []; }; //TODO: Implement contract card @@ -34,13 +35,14 @@ const PositionsContent = ({ onRedirectToTrade, positions = [], setContractTypeFilter, + contractTypeFilter, }: TPositionsContentProps) => { return (
{(!!positions.length || (!positions.length && noMatchesFound)) && ( - + )}
diff --git a/packages/trader/src/AppV2/Containers/Positions/positions.tsx b/packages/trader/src/AppV2/Containers/Positions/positions.tsx index b4c18cd68e87..7adbc5e2ca20 100644 --- a/packages/trader/src/AppV2/Containers/Positions/positions.tsx +++ b/packages/trader/src/AppV2/Containers/Positions/positions.tsx @@ -82,6 +82,7 @@ const Positions = observer(({ onRedirectToTrade }: TPositionsProps) => { onRedirectToTrade={onRedirectToTrade} positions={filteredPositions} setContractTypeFilter={setContractTypeFilter} + contractTypeFilter={contractTypeFilter} /> ), }, @@ -94,6 +95,7 @@ const Positions = observer(({ onRedirectToTrade }: TPositionsProps) => { noMatchesFound={noMatchesFound} positions={filteredPositions} setContractTypeFilter={setContractTypeFilter} + contractTypeFilter={contractTypeFilter} /> ), }, From 559182d28c7f99eb4daa0245db82f0fb45f6a5be Mon Sep 17 00:00:00 2001 From: kate-deriv <121025168+kate-deriv@users.noreply.github.com> Date: Tue, 21 May 2024 15:31:29 +0300 Subject: [PATCH 11/54] DTRA-1279 / Kate / Use hook for real data (#55) * feat: use hook for real data * refactor: apply suggestions --- .../src/AppV2/Components/Filter/filter.tsx | 8 +- .../Positions/positions-content.tsx | 14 ++-- .../AppV2/Containers/Positions/positions.scss | 9 +- .../AppV2/Containers/Positions/positions.tsx | 82 ++++--------------- .../src/AppV2/Hooks/useClosedPositions.ts | 14 ++-- .../trader/src/AppV2/Utils/positions-utils.ts | 10 +-- 6 files changed, 52 insertions(+), 85 deletions(-) diff --git a/packages/trader/src/AppV2/Components/Filter/filter.tsx b/packages/trader/src/AppV2/Components/Filter/filter.tsx index fa83ad2b843f..e2fdb71d4b8a 100644 --- a/packages/trader/src/AppV2/Components/Filter/filter.tsx +++ b/packages/trader/src/AppV2/Components/Filter/filter.tsx @@ -42,10 +42,10 @@ const Filter = ({ setContractTypeFilter, contractTypeFilter }: TFilter) => { }; const chipLabelFormatting = () => { - const arrayLength = changedOptions.length; + const arrayLength = contractTypeFilter.length; if (!arrayLength) return ; - if (changedOptions.length === 1) - return mockAvailableContractsList.find(type => type.id === changedOptions[0])?.tradeType; + if (arrayLength === 1) + return mockAvailableContractsList.find(type => type.id === contractTypeFilter[0])?.tradeType; return ; }; @@ -80,6 +80,8 @@ const Filter = ({ setContractTypeFilter, contractTypeFilter }: TFilter) => { secondaryAction={{ content: 'Clear All', onAction: () => setChangedOptions([]) }} alignment='vertical' shouldCloseOnSecondaryButtonClick={false} + // TODO: replace className with disabling props after Quill library updates + className={`${changedOptions.length ? '' : 'disabled'}`} /> diff --git a/packages/trader/src/AppV2/Containers/Positions/positions-content.tsx b/packages/trader/src/AppV2/Containers/Positions/positions-content.tsx index fc835d96c3e5..4f3331325424 100644 --- a/packages/trader/src/AppV2/Containers/Positions/positions-content.tsx +++ b/packages/trader/src/AppV2/Containers/Positions/positions-content.tsx @@ -1,13 +1,13 @@ import React from 'react'; -import { TPortfolioPosition } from '@deriv/stores/types'; import EmptyMessage from 'AppV2/Components/EmptyMessage'; import { TEmptyMessageProps } from 'AppV2/Components/EmptyMessage/empty-message'; import Filter from 'AppV2/Components/Filter'; import { isHighLow } from '@deriv/shared'; +import { TClosedPositions } from './positions'; type TPositionsContentProps = Omit & { noMatchesFound?: boolean; - positions?: TPortfolioPosition[]; + positions?: TClosedPositions; setContractTypeFilter: React.Dispatch>; contractTypeFilter: string[] | []; }; @@ -48,12 +48,12 @@ const PositionsContent = ({
{positions.length ? ( - {positions.map(({ contract_info }) => ( + {positions.map(({ contract_type, purchase_time, shortcode }) => ( ))} diff --git a/packages/trader/src/AppV2/Containers/Positions/positions.scss b/packages/trader/src/AppV2/Containers/Positions/positions.scss index 85c0b0281d87..ecd0401fd6a0 100644 --- a/packages/trader/src/AppV2/Containers/Positions/positions.scss +++ b/packages/trader/src/AppV2/Containers/Positions/positions.scss @@ -28,7 +28,6 @@ $FOOTER_HEIGHT: 5.8rem; &__open, &__closed { height: 100%; - // TODO: Temporary css overflow-y: scroll; } } @@ -56,3 +55,11 @@ $FOOTER_HEIGHT: 5.8rem; flex-direction: column; } } + +// TODO: Temporary style, waiting for new Quill version +.disabled { + .quill__color--secondary-black { + pointer-events: none; + opacity: 0.1; + } +} diff --git a/packages/trader/src/AppV2/Containers/Positions/positions.tsx b/packages/trader/src/AppV2/Containers/Positions/positions.tsx index 7adbc5e2ca20..e35244f5c953 100644 --- a/packages/trader/src/AppV2/Containers/Positions/positions.tsx +++ b/packages/trader/src/AppV2/Containers/Positions/positions.tsx @@ -1,77 +1,28 @@ import React from 'react'; import { Localize } from '@deriv/translations'; import { Tab } from '@deriv-com/quill-ui'; -import { TPortfolioPosition } from '@deriv/stores/types'; +import { observer } from '@deriv/stores'; import useClosedPositions from 'AppV2/Hooks/useClosedPositions'; -import { useModulesStore } from 'Stores/useModulesStores'; import { filterPositions } from '../../Utils/positions-utils'; import PositionsContent from './positions-content'; -import { observer } from '@deriv/stores'; type TPositionsProps = { onRedirectToTrade?: () => void; }; -//TODO: Remove after Bala's PR with hook for real data will be merged -const mockPositions = [ - { - contract_info: { - contract_type: 'MULTUP', - purchase_time: 1716198125, - shortcode: 'MULTUP_1HZ100V_10.00_10_1716014450_4869676799_0_0.00_N1', - }, - }, - { - contract_info: { - contract_type: 'VANILLALONGCALL', - purchase_time: 1716198390, - shortcode: 'VANILLALONGCALL_1HZ100V_10.00_1716205893_1716206073_S0P_12.66345_1716205893', - }, - }, - { - contract_info: { - contract_type: 'CALL', - purchase_time: 1716205999, - shortcode: 'CALL_1HZ100V_19.55_1716205999_1716206179_S0P_0', - }, - }, - { - contract_info: { - contract_type: 'PUT', - purchase_time: 1716206101, - shortcode: 'PUT_1HZ100V_19.51_1716206101_1716206281_S0P_0', - }, - }, - { - contract_info: { - contract_type: 'CALL', - purchase_time: 1716206154, - shortcode: 'CALL_1HZ100V_24.09_1716206154_1716206334_S40P_0', - }, - }, - { - contract_info: { - contract_type: 'PUT', - purchase_time: 1716206194, - shortcode: 'PUT_1HZ100V_16.42_1716206194_1716206374_S40P_0', - }, - }, -] as TPortfolioPosition[]; +export type TClosedPositions = ReturnType['closedPositions']; + +// TODO: Temporary loader, will be replaced +const Loader = () =>
Loader
; const Positions = observer(({ onRedirectToTrade }: TPositionsProps) => { + // TODO: refactor this hook for date filtration. e.g. date_from and date_to + const { closedPositions, isLoading } = useClosedPositions(); + const [contractTypeFilter, setContractTypeFilter] = React.useState([]); - const [filteredPositions, setFilteredPositions] = React.useState(mockPositions || []); + const [filteredPositions, setFilteredPositions] = React.useState(closedPositions); const [noMatchesFound, setNoMatchesFound] = React.useState(false); - const { positions: positionsStore } = useModulesStore(); - const { closedContractTypeFilter } = positionsStore; - - const { closedPositions } = useClosedPositions({ contractTypes: closedContractTypeFilter }); - - // TODO: remove this line - // eslint-disable-next-line no-console - console.log('closedPositions', closedPositions); - const tabs = [ { id: 'open', @@ -80,7 +31,8 @@ const Positions = observer(({ onRedirectToTrade }: TPositionsProps) => { @@ -103,12 +55,16 @@ const Positions = observer(({ onRedirectToTrade }: TPositionsProps) => { React.useEffect(() => { if (contractTypeFilter.length) { - const result = filterPositions(mockPositions, contractTypeFilter); + const result = filterPositions(closedPositions, contractTypeFilter); setNoMatchesFound(!result.length); setFilteredPositions(result); - } else setFilteredPositions(mockPositions); + } else setFilteredPositions(closedPositions); }, [contractTypeFilter]); + React.useEffect(() => { + if (!isLoading) setFilteredPositions(closedPositions); + }, [closedPositions]); + return (
@@ -118,9 +74,7 @@ const Positions = observer(({ onRedirectToTrade }: TPositionsProps) => { ))} - {tabs.map(({ id, content }) => ( - {content} - ))} + {isLoading ? : tabs.map(({ id, content }) => {content})}
diff --git a/packages/trader/src/AppV2/Hooks/useClosedPositions.ts b/packages/trader/src/AppV2/Hooks/useClosedPositions.ts index e91b3793f862..6c936a7f9b3c 100644 --- a/packages/trader/src/AppV2/Hooks/useClosedPositions.ts +++ b/packages/trader/src/AppV2/Hooks/useClosedPositions.ts @@ -3,10 +3,13 @@ import React, { useCallback, useEffect, useRef, useState } from 'react'; import { ProfitTable, ProfitTableResponse } from '@deriv/api-types'; type TPros = { - contractTypes: string[]; + date_from?: string; + date_to?: string; }; +// TODO: we can't filtrate by contractTypes here as for BE High/low and Rise/Fall is ONE contract. So for contractTypes filtration is implemented in position.tsx +// TODO: refactor this hook for date filtration. e.g. date_from and date_to -const useClosedPositions = ({ contractTypes }: TPros) => { +const useClosedPositions = ({ date_from, date_to }: TPros = {}) => { const [positions, setPositions] = useState>([]); const [isLoading, setLoading] = React.useState(false); const positionsRef = useRef>([]); @@ -14,19 +17,20 @@ const useClosedPositions = ({ contractTypes }: TPros) => { const fetch = useCallback(async () => { setLoading(true); const data: ProfitTableResponse = await WS.profitTable(50, positionsRef.current.length, { - contract_type: contractTypes.length > 0 ? contractTypes : undefined, + date_from, + date_to, }); setLoading(false); // TODO: handle errors setPositions(prevPositions => [...prevPositions, ...(data?.profit_table?.transactions ?? [])]); - }, [contractTypes]); + }, [date_from, date_to]); useEffect(() => { setPositions([]); positionsRef.current = []; fetch(); - }, [fetch, contractTypes]); + }, [fetch, date_from, date_to]); return { closedPositions: positions, isLoading, fetchMore: fetch }; }; diff --git a/packages/trader/src/AppV2/Utils/positions-utils.ts b/packages/trader/src/AppV2/Utils/positions-utils.ts index 9d3d42d0b6cc..340e73fe5394 100644 --- a/packages/trader/src/AppV2/Utils/positions-utils.ts +++ b/packages/trader/src/AppV2/Utils/positions-utils.ts @@ -1,14 +1,14 @@ -import { TPortfolioPosition } from '@deriv/stores/types'; import { getSupportedContracts, isHighLow } from '@deriv/shared'; +import { TClosedPositions } from '../Containers/Positions/positions'; -export const filterPositions = (positions: TPortfolioPosition[], filter: string[]) => { +export const filterPositions = (positions: TClosedPositions, filter: string[]) => { // Split contract type names with '/' (e.g. Rise/Fall) const splittedFilter = filter.map(option => (option.includes('/') ? option.split('/') : option)).flat(); //TODO: Create own config instead of getSupportedContracts - return positions.filter(({ contract_info }) => { - const config = getSupportedContracts(isHighLow({ shortcode: contract_info.shortcode }))[ - contract_info.contract_type as keyof ReturnType + return positions.filter(({ contract_type, shortcode }) => { + const config = getSupportedContracts(isHighLow({ shortcode }))[ + contract_type as keyof ReturnType ]; return splittedFilter.includes('main_title' in config ? config.main_title : config.name); From 4c4ced58af38cdd9fef6f582b7b24f704fa19044 Mon Sep 17 00:00:00 2001 From: Maryia <103177211+maryia-deriv@users.noreply.github.com> Date: Tue, 21 May 2024 17:57:46 +0300 Subject: [PATCH 12/54] Maryia/positions-redesign/Contract cards [WIP] (#53) * feat: init ContractCard * feat: Contract cards * feat: use ReportsStoreProvider * style: remove unnecessary comment * fix: key * fix: remove old card styles * fix: types * fix: use contract_info in filterPositions * fix: do not show buttons for sold contracts --- packages/stores/types.ts | 4 + packages/trader/package.json | 1 + .../src/App/Components/Routes/binary-link.tsx | 60 ++-- .../ContractCard/contract-card-duration.tsx | 35 ++ .../ContractCard/contract-card-list.tsx | 81 +++++ .../ContractCard/contract-card.scss | 148 ++++++++ .../Components/ContractCard/contract-card.tsx | 146 ++++++++ .../AppV2/Components/ContractCard/index.ts | 4 + .../EmptyMessage/empty-message.scss | 2 +- .../Positions/positions-content.tsx | 53 +-- .../AppV2/Containers/Positions/positions.scss | 14 - .../AppV2/Containers/Positions/positions.tsx | 316 ++++++++++++++++-- .../trader/src/AppV2/Utils/positions-utils.ts | 10 +- packages/trader/src/AppV2/app.tsx | 19 +- 14 files changed, 782 insertions(+), 111 deletions(-) create mode 100644 packages/trader/src/AppV2/Components/ContractCard/contract-card-duration.tsx create mode 100644 packages/trader/src/AppV2/Components/ContractCard/contract-card-list.tsx create mode 100644 packages/trader/src/AppV2/Components/ContractCard/contract-card.scss create mode 100644 packages/trader/src/AppV2/Components/ContractCard/contract-card.tsx create mode 100644 packages/trader/src/AppV2/Components/ContractCard/index.ts diff --git a/packages/stores/types.ts b/packages/stores/types.ts index 6063cae32edd..e610b510ef87 100644 --- a/packages/stores/types.ts +++ b/packages/stores/types.ts @@ -144,12 +144,14 @@ type BrandConfig = { }; export type TPortfolioPosition = { + barrier?: number; contract_info: ProposalOpenContract & Portfolio1 & { contract_update?: ContractUpdate; }; details?: string; display_name: string; + entry_spot?: number; id?: number; indicative: number; payout?: number; @@ -159,7 +161,9 @@ export type TPortfolioPosition = { is_unsupported: boolean; contract_update: ProposalOpenContract['limit_order']; is_sell_requested: boolean; + is_valid_to_sell?: boolean; profit_loss: number; + status?: null | string; }; type TAppRoutingHistory = { diff --git a/packages/trader/package.json b/packages/trader/package.json index 6a3f9cd30123..15ee44ee9492 100644 --- a/packages/trader/package.json +++ b/packages/trader/package.json @@ -121,6 +121,7 @@ "react-loadable": "^5.5.0", "react-router": "^5.2.0", "react-router-dom": "^5.2.0", + "react-swipeable": "^6.2.1", "react-transition-group": "4.4.2" } } diff --git a/packages/trader/src/App/Components/Routes/binary-link.tsx b/packages/trader/src/App/Components/Routes/binary-link.tsx index 71e310ec7dc9..5ecb0a4d3c6e 100644 --- a/packages/trader/src/App/Components/Routes/binary-link.tsx +++ b/packages/trader/src/App/Components/Routes/binary-link.tsx @@ -3,39 +3,45 @@ import { NavLink } from 'react-router-dom'; import { findRouteByPath, normalizePath } from './helpers'; import getRoutesConfig from '../../Constants/routes-config'; -type TBinaryLinkProps = React.PropsWithChildren<{ - active_class?: string; - className?: string; - to?: string; - onClick?: () => void; -}>; +type TBinaryLinkProps = Omit, 'title' | 'ref'> & + React.PropsWithChildren<{ + active_class?: string; + className?: string; + to?: string; + onClick?: () => void; + }>; // TODO: solve circular dependency problem // when binary link is imported into components present in routes config // or into their descendants -const BinaryLink = ({ active_class = '', to, children, ...props }: TBinaryLinkProps) => { - const path = normalizePath(to); - const route = findRouteByPath(path, getRoutesConfig()); +const BinaryLink = React.forwardRef( + ({ active_class = '', to, children, ...props }, ref) => { + const path = normalizePath(to); + const route = findRouteByPath(path, getRoutesConfig()); - if (!route) { - throw new Error(`Route not found: ${to}`); + if (!route) { + throw new Error(`Route not found: ${to}`); + } + + return to ? ( + + {children} + + ) : ( + + {children} + + ); } +); - return to ? ( - - {children} - - ) : ( - - {children} - - ); -}; +BinaryLink.displayName = 'BinaryLink'; export default BinaryLink; diff --git a/packages/trader/src/AppV2/Components/ContractCard/contract-card-duration.tsx b/packages/trader/src/AppV2/Components/ContractCard/contract-card-duration.tsx new file mode 100644 index 000000000000..8c4fbd41ce0b --- /dev/null +++ b/packages/trader/src/AppV2/Components/ContractCard/contract-card-duration.tsx @@ -0,0 +1,35 @@ +import React from 'react'; +import { TPortfolioPosition } from '@deriv/stores/types'; +import { Localize } from '@deriv/translations'; +import { CaptionText } from '@deriv-com/quill-ui'; +import { LabelPairedStopwatchCaptionRegularIcon } from '@deriv/quill-icons'; +import { useStore } from '@deriv/stores'; +import { observer } from 'mobx-react'; + +export type TContractCardDurationProps = Pick< + TPortfolioPosition['contract_info'], + 'expiry_time' | 'purchase_time' | 'tick_count' +> & { + currentTick: number | null; + isMultiplier?: boolean; +}; + +export const ContractCardDuration = observer( + ({ currentTick, expiry_time, isMultiplier, purchase_time, tick_count }: TContractCardDurationProps) => { + const { server_time } = useStore().common; + return ( + // TODO: when Tag is exported from quill-ui, use } + // label={isMultiplier ? : 'Duration'} + // /> +
+ + + {/* in progress */} + {isMultiplier ? : 'Duration'} + +
+ ); + } +); diff --git a/packages/trader/src/AppV2/Components/ContractCard/contract-card-list.tsx b/packages/trader/src/AppV2/Components/ContractCard/contract-card-list.tsx new file mode 100644 index 000000000000..cf736dc4f49a --- /dev/null +++ b/packages/trader/src/AppV2/Components/ContractCard/contract-card-list.tsx @@ -0,0 +1,81 @@ +import { + getContractPath, + getCurrentTick, + getTotalProfit, + getTradeTypeName, + isHighLow, + isMultiplierContract, + isValidToCancel, + isValidToSell, +} from '@deriv/shared'; +import { TPortfolioPosition } from '@deriv/stores/types'; +import React from 'react'; +import ContractCard from './contract-card'; + +export type TContractCardListProps = { + currency?: string; + onClickCancel?: (contractId: number) => void; + onClickSell?: (contractId: number) => void; + positions?: TPortfolioPosition[]; +}; + +const ContractCardList = ({ onClickCancel, onClickSell, positions = [], ...rest }: TContractCardListProps) => { + // TODO: make it work not only with an open position data but also with a profit_table transaction data + const timeoutIds = React.useRef>>([]); + + React.useEffect(() => { + const timers = timeoutIds.current; + return () => { + if (timers.length) { + timers.forEach(id => clearTimeout(id)); + } + }; + }, []); + + const handleClose = (id: number, shouldCancel?: boolean) => { + const timeoutId = setTimeout(() => { + shouldCancel ? onClickCancel?.(id) : onClickSell?.(id); + }, 160); + timeoutIds.current.push(timeoutId); + }; + + if (!positions.length) return null; + return ( +
+ {positions.map(({ id, is_sell_requested, contract_info }) => { + const { contract_type, display_name, profit, shortcode } = contract_info; + const contract_main_title = getTradeTypeName(contract_type ?? '', { + isHighLow: isHighLow({ shortcode }), + showMainTitle: true, + }); + const currentTick = contract_info.tick_count ? getCurrentTick(contract_info) : null; + const tradeTypeName = `${contract_main_title} ${getTradeTypeName(contract_type ?? '', { + isHighLow: isHighLow({ shortcode }), + })}`.trim(); + const isMultiplier = isMultiplierContract(contract_type); + const validToCancel = isValidToCancel(contract_info); + const validToSell = isValidToSell(contract_info) && !is_sell_requested; + const totalProfit = isMultiplierContract(contract_type) ? getTotalProfit(contract_info) : profit; + return ( + id && handleClose?.(id, true)} + onClose={() => id && handleClose?.(id)} + redirectTo={getContractPath(id)} + symbolName={display_name} + totalProfit={totalProfit} + tradeTypeName={tradeTypeName} + /> + ); + })} +
+ ); +}; + +export default ContractCardList; diff --git a/packages/trader/src/AppV2/Components/ContractCard/contract-card.scss b/packages/trader/src/AppV2/Components/ContractCard/contract-card.scss new file mode 100644 index 000000000000..fba9210e30e5 --- /dev/null +++ b/packages/trader/src/AppV2/Components/ContractCard/contract-card.scss @@ -0,0 +1,148 @@ +// TODO: Utilize tokens from quill and logical css properties; +.contract-card { + position: relative; + display: flex; + width: 100%; + cursor: pointer; + background-color: var(--component-modal-bg); + padding: var(--semantic-spacing-general-md); + justify-content: space-between; + height: 10.4rem; + transform: translateX(0); + transition: transform var(--motion-duration-snappy) var(--motion-easing-inandout); + + .icon, + .dc-icon { + position: relative; + display: flex; + flex-shrink: 0; + justify-content: center; + align-items: center; + width: var(--core-size-1600); + height: var(--core-size-1600); + cursor: pointer; + } + .body { + display: flex; + flex-direction: column; + gap: var(--semantic-spacing-gap-md); + } + .body, + .title { + min-width: 0; + flex-grow: 1; + } + .details, + .status-and-profit, + &-duration { + display: flex; + gap: 0.8rem; + align-items: center; + } + &-duration { + background-color: color-mix(in sRGB, var(--component-textIcon-normal-default) 4%, transparent); + height: 2.4rem; + padding: 0 0.8rem; + border-radius: var(--semantic-borderRadius-sm); + } + .status-and-profit { + justify-content: space-between; + } + .title { + display: flex; + flex-direction: column; + } + .trade-type, + .symbol { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + + &__icon { + padding: 0.4rem; + } + } + .symbol, + .stake { + color: var(--component-textIcon-normal-subtle); + } + .stake { + align-self: flex-end; + } + .buttons { + display: flex; + align-self: center; + justify-content: flex-end; + flex-shrink: 0; + width: fit-content; + height: 100%; + position: absolute; + top: 0; + right: 0; + bottom: 0; + transform: translateX(100%); + + button { + width: var(--core-size-3600); + height: 100%; + + p { + color: var(--core-color-solid-slate-50); + } + + &:disabled { + background-color: var(--core-color-opacity-black-200); + + p { + color: var(--component-textIcon-normal-disabled); + } + } + } + } + &.lost { + .total-profit { + color: var(--core-color-solid-red-600); + } + button:not(:disabled) { + background-color: var(--core-color-solid-cherry-700); + } + } + &.won { + .total-profit { + color: var(--core-color-solid-green-600); + } + button:not(:disabled) { + background-color: var(--core-color-solid-emerald-700); + } + } + &.show-buttons { + transform: translateX(calc(var(--core-size-3600) * -1)); + + &--has-cancel-button { + transform: translateX(calc(var(--core-size-3600) * -2)); + } + } + &-wrapper { + position: relative; + width: inherit; + overflow: hidden; + max-height: 10.4rem; // equal to __item's height of 104px, must be an exact value for for item deletion transition to work + box-shadow: var(--core-elevation-shadow-130); + border-radius: var(--semantic-borderRadius-md); + + &.deleted { + opacity: var(--core-opacity-50); + max-height: 0; + transition: max-height var(--core-motion-duration-200), opacity var(--core-motion-duration-200); + transition-timing-function: var(--motion-easing-inandout); + } + } + &-list { + display: flex; + flex-direction: column; + gap: 0.8rem; + width: inherit; + padding: 0.8rem; + overflow: hidden; + } +} diff --git a/packages/trader/src/AppV2/Components/ContractCard/contract-card.tsx b/packages/trader/src/AppV2/Components/ContractCard/contract-card.tsx new file mode 100644 index 000000000000..02e8652d9d73 --- /dev/null +++ b/packages/trader/src/AppV2/Components/ContractCard/contract-card.tsx @@ -0,0 +1,146 @@ +import React from 'react'; +import classNames from 'classnames'; +import { CaptionText, Text } from '@deriv-com/quill-ui'; +import { useSwipeable } from 'react-swipeable'; +import { IconTradeTypes, Money } from '@deriv/components'; +import { getCardLabels } from '@deriv/shared'; +import { TPortfolioPosition } from '@deriv/stores/types'; +import { ContractCardDuration, TContractCardDurationProps } from './contract-card-duration'; +import { BinaryLink } from 'App/Components/Routes'; + +type TContractCardProps = Pick< + TPortfolioPosition['contract_info'], + 'buy_price' | 'contract_type' | 'sell_time' | 'profit' +> & + TContractCardDurationProps & { + className?: string; + currency?: string; + isValidToCancel?: boolean; + isValidToSell?: boolean; + onClick?: (e?: React.MouseEvent) => void; + onCancel?: (e?: React.MouseEvent) => void; + onClose?: (e?: React.MouseEvent) => void; + redirectTo?: string; + symbolName?: string; + totalProfit?: string | number; + tradeTypeName?: string; + }; + +const DIRECTION = { + LEFT: 'left', + RIGHT: 'right', +}; + +const swipeConfig = { + trackMouse: true, + preventScrollOnSwipe: true, +}; + +const ContractCard = ({ + className, + contract_type, + currency, + buy_price, + isValidToCancel, + isValidToSell, + onCancel, + onClick, + onClose, + profit, + redirectTo, + sell_time, + symbolName, + totalProfit, + tradeTypeName, + ...rest +}: TContractCardProps) => { + const [isDeleted, setIsDeleted] = React.useState(false); + const [shouldShowButtons, setShouldShowButtons] = React.useState(false); + + const handleSwipe = (direction: string) => { + const isLeft = direction === DIRECTION.LEFT; + setShouldShowButtons(isLeft); + }; + + const swipeHandlers = useSwipeable({ + onSwipedLeft: () => handleSwipe(DIRECTION.LEFT), + onSwipedRight: () => handleSwipe(DIRECTION.RIGHT), + ...swipeConfig, + }); + + const handleClose = (e: React.MouseEvent, shouldCancel?: boolean) => { + e.preventDefault(); + e.stopPropagation(); + setIsDeleted(true); + shouldCancel ? onCancel?.(e) : onClose?.(e); + }; + + return ( +
+ 0, + })} + to={redirectTo} + onDragStart={e => e.preventDefault()} + > +
+
+ +
+ + {tradeTypeName} + + + {symbolName} + +
+ + + +
+
+ {sell_time ? ( + + {getCardLabels().CLOSED} + + ) : ( + + )} + + + +
+
+ {!sell_time && ( +
+ {isValidToCancel && ( + + )} + +
+ )} +
+
+ ); +}; + +export default ContractCard; diff --git a/packages/trader/src/AppV2/Components/ContractCard/index.ts b/packages/trader/src/AppV2/Components/ContractCard/index.ts new file mode 100644 index 000000000000..55e8cedd5260 --- /dev/null +++ b/packages/trader/src/AppV2/Components/ContractCard/index.ts @@ -0,0 +1,4 @@ +import './contract-card.scss'; + +export { default as ContractCard } from './contract-card'; +export { default as ContractCardList } from './contract-card-list'; diff --git a/packages/trader/src/AppV2/Components/EmptyMessage/empty-message.scss b/packages/trader/src/AppV2/Components/EmptyMessage/empty-message.scss index 0008befa20fb..061f0a5cec1f 100644 --- a/packages/trader/src/AppV2/Components/EmptyMessage/empty-message.scss +++ b/packages/trader/src/AppV2/Components/EmptyMessage/empty-message.scss @@ -1,4 +1,4 @@ -// TODO: Utilize tokens from "@deriv-com/quill-ui/quill.css"; +// TODO: Utilize tokens from quill and logical css properties; .empty-message { &__open, &__closed { diff --git a/packages/trader/src/AppV2/Containers/Positions/positions-content.tsx b/packages/trader/src/AppV2/Containers/Positions/positions-content.tsx index 86799d49780d..ac2ba5c05931 100644 --- a/packages/trader/src/AppV2/Containers/Positions/positions-content.tsx +++ b/packages/trader/src/AppV2/Containers/Positions/positions-content.tsx @@ -1,42 +1,32 @@ import React from 'react'; import EmptyMessage from 'AppV2/Components/EmptyMessage'; import { TEmptyMessageProps } from 'AppV2/Components/EmptyMessage/empty-message'; +import { TPortfolioPosition } from '@deriv/stores/types'; +import { ContractCardList } from 'AppV2/Components/ContractCard'; import Filter from 'AppV2/Components/Filter'; -import { isHighLow } from '@deriv/shared'; -import { TClosedPositions } from './positions'; +import type { TContractCardListProps } from 'AppV2/Components/ContractCard/contract-card-list'; +import { Loading } from '@deriv/components'; -type TPositionsContentProps = Omit & { - noMatchesFound?: boolean; - positions?: TClosedPositions; - setContractTypeFilter: React.Dispatch>; - contractTypeFilter: string[] | []; -}; - -//TODO: Implement contract card -const ContractCard = ({ - contractType, - purchaseTime, - shortcode, -}: { - contractType?: string; - purchaseTime?: number; - shortcode?: string; -}) => ( -
-
{contractType}
-
{purchaseTime}
-
{`${isHighLow({ shortcode }) ? 'High/Low' : 'Rise/Fall'}`}
-
-); +type TPositionsContentProps = Omit & + Pick & { + contractTypeFilter: string[] | []; + isLoading?: boolean; + noMatchesFound?: boolean; + positions?: TPortfolioPosition[]; + setContractTypeFilter: React.Dispatch>; + }; const PositionsContent = ({ + contractTypeFilter, isClosedTab, + isLoading, noMatchesFound, onRedirectToTrade, positions = [], setContractTypeFilter, - contractTypeFilter, + ...rest }: TPositionsContentProps) => { + if (isLoading) return ; return (
@@ -47,16 +37,7 @@ const PositionsContent = ({ )}
{positions.length ? ( - - {positions.map(({ contract_type, purchase_time, shortcode }) => ( - - ))} - + ) : ( void; }; -export type TClosedPositions = ReturnType['closedPositions']; - -// TODO: Temporary loader, will be replaced -const Loader = () =>
Loader
; +// TODO: Remove after real data is available +const mockedActivePositions = [ + { + contract_info: { + account_id: 112905368, + barrier: '682.60', + barrier_count: 1, + bid_price: 6.38, + buy_price: 9, + contract_id: 242807007748, + contract_type: 'CALL', + currency: 'USD', + current_spot: 681.76, + current_spot_display_value: '681.76', + current_spot_time: 1716220628, + date_expiry: 1716221100, + date_settlement: 1716221100, + date_start: 1716220562, + display_name: 'Volatility 100 (1s) Index', + entry_spot: 682.6, + entry_spot_display_value: '682.60', + entry_tick: 682.6, + entry_tick_display_value: '682.60', + entry_tick_time: 1716220563, + expiry_time: 1716221100, + id: '917d1b48-305b-a2f4-5b9c-7fb1f2c6c145', + is_expired: 0, + is_forward_starting: 0, + is_intraday: 1, + is_path_dependent: 0, + is_settleable: 0, + is_sold: 0, + is_valid_to_cancel: 0, + is_valid_to_sell: 1, + longcode: + 'Win payout if Volatility 100 (1s) Index is strictly higher than entry spot at 2024-05-20 16:05:00 GMT.', + payout: 17.61, + profit: -2.62, + profit_percentage: -29.11, + purchase_time: 1716220562, + shortcode: 'CALL_1HZ100V_17.61_1716220562_1716221100F_S0P_0', + status: 'open', + transaction_ids: { + buy: 484286139408, + }, + underlying: '1HZ100V', + }, + details: + 'Win payout if Volatility 100 (1s) Index is strictly higher than entry spot at 2024-05-20 16:05:00 GMT.', + display_name: '', + id: 242807007748, + indicative: 6.38, + payout: 17.61, + purchase: 9, + reference: 484286139408, + type: 'CALL', + profit_loss: -2.62, + is_valid_to_sell: true, + status: 'profit', + barrier: 682.6, + entry_spot: 682.6, + }, + { + contract_info: { + account_id: 112905368, + barrier_count: 1, + bid_price: 8.9, + buy_price: 9.39, + cancellation: { + ask_price: 0.39, + date_expiry: 1716224183, + }, + commission: 0.03, + contract_id: 242807045608, + contract_type: 'MULTUP', + currency: 'USD', + current_spot: 681.71, + current_spot_display_value: '681.71', + current_spot_time: 1716220672, + date_expiry: 4869849599, + date_settlement: 4869849600, + date_start: 1716220583, + display_name: 'Volatility 100 (1s) Index', + entry_spot: 682.23, + entry_spot_display_value: '682.23', + entry_tick: 682.23, + entry_tick_display_value: '682.23', + entry_tick_time: 1716220584, + expiry_time: 4869849599, + id: '917d1b48-305b-a2f4-5b9c-7fb1f2c6c145', + is_expired: 0, + is_forward_starting: 0, + is_intraday: 0, + is_path_dependent: 1, + is_settleable: 0, + is_sold: 0, + is_valid_to_cancel: 1, + is_valid_to_sell: 0, + limit_order: { + stop_out: { + display_name: 'Stop out', + order_amount: -9, + order_date: 1716220583, + value: '614.26', + }, + }, + longcode: + "If you select 'Up', your total profit/loss will be the percentage increase in Volatility 100 (1s) Index, multiplied by 90, minus commissions.", + multiplier: 10, + profit: -0.1, + profit_percentage: -1.11, + purchase_time: 1716220583, + shortcode: 'MULTUP_1HZ100V_9.00_10_1716220583_4869849599_60m_0.00_N1', + status: 'open', + transaction_ids: { + buy: 484286215128, + }, + underlying: '1HZ100V', + validation_error: + 'The spot price has moved. We have not closed this contract because your profit is negative and deal cancellation is active. Cancel your contract to get your full stake back.', + validation_error_code: 'General', + }, + details: + "If you select 'Up', your total profit/loss will be the percentage increase in Volatility 100 (1s) Index, multiplied by 90, minus commissions.", + display_name: '', + id: 242807045608, + indicative: 8.9, + purchase: 9.39, + reference: 484286215128, + type: 'MULTUP', + contract_update: { + stop_out: { + display_name: 'Stop out', + order_amount: -9, + order_date: 1716220583, + value: '614.26', + }, + }, + profit_loss: -0.1, + is_valid_to_sell: false, + status: 'profit', + entry_spot: 682.23, + }, + { + contract_info: { + account_id: 112905368, + barrier_count: 2, + barrier_spot_distance: '0.296', + bid_price: 9.84, + buy_price: 9, + contract_id: 242807268688, + contract_type: 'ACCU', + currency: 'USD', + current_spot: 682.72, + current_spot_display_value: '682.72', + current_spot_high_barrier: '683.016', + current_spot_low_barrier: '682.424', + current_spot_time: 1716220720, + date_expiry: 1747785599, + date_settlement: 1747785600, + date_start: 1716220710, + display_name: 'Volatility 100 (1s) Index', + entry_spot: 682.58, + entry_spot_display_value: '682.58', + entry_tick: 682.58, + entry_tick_display_value: '682.58', + entry_tick_time: 1716220711, + expiry_time: 1747785599, + growth_rate: 0.01, + high_barrier: '683.046', + id: '917d1b48-305b-a2f4-5b9c-7fb1f2c6c145', + is_expired: 0, + is_forward_starting: 0, + is_intraday: 0, + is_path_dependent: 1, + is_settleable: 0, + is_sold: 0, + is_valid_to_cancel: 0, + is_valid_to_sell: 1, + longcode: + 'After the entry spot tick, your stake will grow continuously by 1% for every tick that the spot price remains within the ± 0.04331% from the previous spot price.', + low_barrier: '682.454', + profit: 0.84, + profit_percentage: 9.33, + purchase_time: 1716220710, + shortcode: 'ACCU_1HZ100V_9.00_0_0.01_1_0.000433139675_1716220710', + status: 'open', + tick_count: 230, + tick_passed: 9, + tick_stream: [ + { + epoch: 1716220711, + tick: 682.58, + tick_display_value: '682.58', + }, + { + epoch: 1716220712, + tick: 682.71, + tick_display_value: '682.71', + }, + { + epoch: 1716220713, + tick: 682.5, + tick_display_value: '682.50', + }, + { + epoch: 1716220714, + tick: 682.57, + tick_display_value: '682.57', + }, + { + epoch: 1716220715, + tick: 682.57, + tick_display_value: '682.57', + }, + { + epoch: 1716220716, + tick: 682.75, + tick_display_value: '682.75', + }, + { + epoch: 1716220717, + tick: 682.87, + tick_display_value: '682.87', + }, + { + epoch: 1716220718, + tick: 682.74, + tick_display_value: '682.74', + }, + { + epoch: 1716220719, + tick: 682.75, + tick_display_value: '682.75', + }, + { + epoch: 1716220720, + tick: 682.72, + tick_display_value: '682.72', + }, + ], + transaction_ids: { + buy: 484286658868, + }, + underlying: '1HZ100V', + }, + details: + 'After the entry spot tick, your stake will grow continuously by 1% for every tick that the spot price remains within the ± 0.04331% from the previous spot price.', + display_name: '', + id: 242807268688, + indicative: 9.84, + purchase: 9, + reference: 484286658868, + type: 'ACCU', + profit_loss: 0.84, + is_valid_to_sell: true, + current_tick: 9, + status: 'profit', + entry_spot: 682.58, + high_barrier: 683.046, + low_barrier: 682.454, + }, +] as TPortfolioPosition[]; const Positions = observer(({ onRedirectToTrade }: TPositionsProps) => { - // TODO: refactor this hook for date filtration. e.g. date_from and date_to - const { closedPositions, isLoading } = useClosedPositions(); - const [contractTypeFilter, setContractTypeFilter] = React.useState([]); - const [filteredPositions, setFilteredPositions] = React.useState(closedPositions); + const [filteredPositions, setFilteredPositions] = React.useState>>( + [] + ); const [noMatchesFound, setNoMatchesFound] = React.useState(false); + const { client, portfolio } = useStore(); + const { currency } = client; + const { onClickCancel, onClickSell } = portfolio; + const { data, is_loading, onMount } = useReportsStore().profit_table; + const closedPositions = React.useMemo(() => data.map(d => ({ contract_info: d })), [data]); const tabs = [ { @@ -29,10 +293,13 @@ const Positions = observer(({ onRedirectToTrade }: TPositionsProps) => { title: , content: ( @@ -43,9 +310,11 @@ const Positions = observer(({ onRedirectToTrade }: TPositionsProps) => { title: , content: ( @@ -53,17 +322,22 @@ const Positions = observer(({ onRedirectToTrade }: TPositionsProps) => { }, ]; + React.useEffect(() => { + onMount(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + React.useEffect(() => { if (contractTypeFilter.length) { - const result = filterPositions(closedPositions, contractTypeFilter); + const result = filterPositions(closedPositions as unknown as TPortfolioPosition[], contractTypeFilter); setNoMatchesFound(!result.length); - setFilteredPositions(result); - } else setFilteredPositions(closedPositions); - }, [contractTypeFilter]); + setFilteredPositions(result as unknown as TPortfolioPosition[]); + } else setFilteredPositions(closedPositions as unknown as TPortfolioPosition[]); + }, [contractTypeFilter, closedPositions]); React.useEffect(() => { - if (!isLoading) setFilteredPositions(closedPositions); - }, [closedPositions]); + if (!is_loading) setFilteredPositions(closedPositions as unknown as TPortfolioPosition[]); + }, [is_loading, closedPositions]); return (
@@ -74,7 +348,9 @@ const Positions = observer(({ onRedirectToTrade }: TPositionsProps) => { ))} - {isLoading ? : tabs.map(({ id, content }) => {content})} + {tabs.map(({ id, content }) => ( + {content} + ))}
diff --git a/packages/trader/src/AppV2/Utils/positions-utils.ts b/packages/trader/src/AppV2/Utils/positions-utils.ts index 340e73fe5394..90e8ecec81a3 100644 --- a/packages/trader/src/AppV2/Utils/positions-utils.ts +++ b/packages/trader/src/AppV2/Utils/positions-utils.ts @@ -1,14 +1,14 @@ import { getSupportedContracts, isHighLow } from '@deriv/shared'; -import { TClosedPositions } from '../Containers/Positions/positions'; +import { TPortfolioPosition } from '@deriv/stores/types'; -export const filterPositions = (positions: TClosedPositions, filter: string[]) => { +export const filterPositions = (positions: TPortfolioPosition[], filter: string[]) => { // Split contract type names with '/' (e.g. Rise/Fall) const splittedFilter = filter.map(option => (option.includes('/') ? option.split('/') : option)).flat(); //TODO: Create own config instead of getSupportedContracts - return positions.filter(({ contract_type, shortcode }) => { - const config = getSupportedContracts(isHighLow({ shortcode }))[ - contract_type as keyof ReturnType + return positions.filter(({ contract_info }) => { + const config = getSupportedContracts(isHighLow({ shortcode: contract_info.shortcode }))[ + contract_info.contract_type as keyof ReturnType ]; return splittedFilter.includes('main_title' in config ? config.main_title : config.name); diff --git a/packages/trader/src/AppV2/app.tsx b/packages/trader/src/AppV2/app.tsx index f6fadef0dd8e..eda39cf823d5 100644 --- a/packages/trader/src/AppV2/app.tsx +++ b/packages/trader/src/AppV2/app.tsx @@ -9,6 +9,7 @@ import Trade from './Containers/Trade'; import Markets from './Containers/Markets'; import Positions from './Containers/Positions'; import Menu from './Containers/Menu'; +import { ReportsStoreProvider } from '../../../reports/src/Stores/useReportsStores'; import 'Sass/app.scss'; import '@deriv-com/quill-tokens/dist/quill.css'; @@ -29,14 +30,16 @@ const App = ({ passthrough }: Apptypes) => { return ( - - - - - setCurrentPageIdx(0)} /> - - - + + + + + + setCurrentPageIdx(0)} /> + + + + ); }; From 1437e77d18dc1c6762768a8a89f25fe94a20a032 Mon Sep 17 00:00:00 2001 From: kate-deriv <121025168+kate-deriv@users.noreply.github.com> Date: Wed, 22 May 2024 10:28:34 +0300 Subject: [PATCH 13/54] DTRA-1279 / Kate / Refactor handling open and closed positions and their filtration (#56) * refactor: move stor values to poitions content file * chore: remove code smell --- .../Positions/positions-content.tsx | 333 ++++++++++++++++-- .../AppV2/Containers/Positions/positions.tsx | 326 +---------------- .../src/AppV2/Hooks/useClosedPositions.ts | 6 +- 3 files changed, 316 insertions(+), 349 deletions(-) diff --git a/packages/trader/src/AppV2/Containers/Positions/positions-content.tsx b/packages/trader/src/AppV2/Containers/Positions/positions-content.tsx index ac2ba5c05931..e4000c61caf0 100644 --- a/packages/trader/src/AppV2/Containers/Positions/positions-content.tsx +++ b/packages/trader/src/AppV2/Containers/Positions/positions-content.tsx @@ -4,40 +4,325 @@ import { TEmptyMessageProps } from 'AppV2/Components/EmptyMessage/empty-message' import { TPortfolioPosition } from '@deriv/stores/types'; import { ContractCardList } from 'AppV2/Components/ContractCard'; import Filter from 'AppV2/Components/Filter'; -import type { TContractCardListProps } from 'AppV2/Components/ContractCard/contract-card-list'; import { Loading } from '@deriv/components'; +import { observer, useStore } from '@deriv/stores'; +import { filterPositions } from '../../Utils/positions-utils'; +import { useReportsStore } from '../../../../../reports/src/Stores/useReportsStores'; + +type TPositionsContentProps = Omit; + +// TODO: Remove after real data is available +const mockedActivePositions = [ + { + contract_info: { + account_id: 112905368, + barrier: '682.60', + barrier_count: 1, + bid_price: 6.38, + buy_price: 9, + contract_id: 242807007748, + contract_type: 'CALL', + currency: 'USD', + current_spot: 681.76, + current_spot_display_value: '681.76', + current_spot_time: 1716220628, + date_expiry: 1716221100, + date_settlement: 1716221100, + date_start: 1716220562, + display_name: 'Volatility 100 (1s) Index', + entry_spot: 682.6, + entry_spot_display_value: '682.60', + entry_tick: 682.6, + entry_tick_display_value: '682.60', + entry_tick_time: 1716220563, + expiry_time: 1716221100, + id: '917d1b48-305b-a2f4-5b9c-7fb1f2c6c145', + is_expired: 0, + is_forward_starting: 0, + is_intraday: 1, + is_path_dependent: 0, + is_settleable: 0, + is_sold: 0, + is_valid_to_cancel: 0, + is_valid_to_sell: 1, + longcode: + 'Win payout if Volatility 100 (1s) Index is strictly higher than entry spot at 2024-05-20 16:05:00 GMT.', + payout: 17.61, + profit: -2.62, + profit_percentage: -29.11, + purchase_time: 1716220562, + shortcode: 'CALL_1HZ100V_17.61_1716220562_1716221100F_S0P_0', + status: 'open', + transaction_ids: { + buy: 484286139408, + }, + underlying: '1HZ100V', + }, + details: + 'Win payout if Volatility 100 (1s) Index is strictly higher than entry spot at 2024-05-20 16:05:00 GMT.', + display_name: '', + id: 242807007748, + indicative: 6.38, + payout: 17.61, + purchase: 9, + reference: 484286139408, + type: 'CALL', + profit_loss: -2.62, + is_valid_to_sell: true, + status: 'profit', + barrier: 682.6, + entry_spot: 682.6, + }, + { + contract_info: { + account_id: 112905368, + barrier_count: 1, + bid_price: 8.9, + buy_price: 9.39, + cancellation: { + ask_price: 0.39, + date_expiry: 1716224183, + }, + commission: 0.03, + contract_id: 242807045608, + contract_type: 'MULTUP', + currency: 'USD', + current_spot: 681.71, + current_spot_display_value: '681.71', + current_spot_time: 1716220672, + date_expiry: 4869849599, + date_settlement: 4869849600, + date_start: 1716220583, + display_name: 'Volatility 100 (1s) Index', + entry_spot: 682.23, + entry_spot_display_value: '682.23', + entry_tick: 682.23, + entry_tick_display_value: '682.23', + entry_tick_time: 1716220584, + expiry_time: 4869849599, + id: '917d1b48-305b-a2f4-5b9c-7fb1f2c6c145', + is_expired: 0, + is_forward_starting: 0, + is_intraday: 0, + is_path_dependent: 1, + is_settleable: 0, + is_sold: 0, + is_valid_to_cancel: 1, + is_valid_to_sell: 0, + limit_order: { + stop_out: { + display_name: 'Stop out', + order_amount: -9, + order_date: 1716220583, + value: '614.26', + }, + }, + longcode: + "If you select 'Up', your total profit/loss will be the percentage increase in Volatility 100 (1s) Index, multiplied by 90, minus commissions.", + multiplier: 10, + profit: -0.1, + profit_percentage: -1.11, + purchase_time: 1716220583, + shortcode: 'MULTUP_1HZ100V_9.00_10_1716220583_4869849599_60m_0.00_N1', + status: 'open', + transaction_ids: { + buy: 484286215128, + }, + underlying: '1HZ100V', + validation_error: + 'The spot price has moved. We have not closed this contract because your profit is negative and deal cancellation is active. Cancel your contract to get your full stake back.', + validation_error_code: 'General', + }, + details: + "If you select 'Up', your total profit/loss will be the percentage increase in Volatility 100 (1s) Index, multiplied by 90, minus commissions.", + display_name: '', + id: 242807045608, + indicative: 8.9, + purchase: 9.39, + reference: 484286215128, + type: 'MULTUP', + contract_update: { + stop_out: { + display_name: 'Stop out', + order_amount: -9, + order_date: 1716220583, + value: '614.26', + }, + }, + profit_loss: -0.1, + is_valid_to_sell: false, + status: 'profit', + entry_spot: 682.23, + }, + { + contract_info: { + account_id: 112905368, + barrier_count: 2, + barrier_spot_distance: '0.296', + bid_price: 9.84, + buy_price: 9, + contract_id: 242807268688, + contract_type: 'ACCU', + currency: 'USD', + current_spot: 682.72, + current_spot_display_value: '682.72', + current_spot_high_barrier: '683.016', + current_spot_low_barrier: '682.424', + current_spot_time: 1716220720, + date_expiry: 1747785599, + date_settlement: 1747785600, + date_start: 1716220710, + display_name: 'Volatility 100 (1s) Index', + entry_spot: 682.58, + entry_spot_display_value: '682.58', + entry_tick: 682.58, + entry_tick_display_value: '682.58', + entry_tick_time: 1716220711, + expiry_time: 1747785599, + growth_rate: 0.01, + high_barrier: '683.046', + id: '917d1b48-305b-a2f4-5b9c-7fb1f2c6c145', + is_expired: 0, + is_forward_starting: 0, + is_intraday: 0, + is_path_dependent: 1, + is_settleable: 0, + is_sold: 0, + is_valid_to_cancel: 0, + is_valid_to_sell: 1, + longcode: + 'After the entry spot tick, your stake will grow continuously by 1% for every tick that the spot price remains within the ± 0.04331% from the previous spot price.', + low_barrier: '682.454', + profit: 0.84, + profit_percentage: 9.33, + purchase_time: 1716220710, + shortcode: 'ACCU_1HZ100V_9.00_0_0.01_1_0.000433139675_1716220710', + status: 'open', + tick_count: 230, + tick_passed: 9, + tick_stream: [ + { + epoch: 1716220711, + tick: 682.58, + tick_display_value: '682.58', + }, + { + epoch: 1716220712, + tick: 682.71, + tick_display_value: '682.71', + }, + { + epoch: 1716220713, + tick: 682.5, + tick_display_value: '682.50', + }, + { + epoch: 1716220714, + tick: 682.57, + tick_display_value: '682.57', + }, + { + epoch: 1716220715, + tick: 682.57, + tick_display_value: '682.57', + }, + { + epoch: 1716220716, + tick: 682.75, + tick_display_value: '682.75', + }, + { + epoch: 1716220717, + tick: 682.87, + tick_display_value: '682.87', + }, + { + epoch: 1716220718, + tick: 682.74, + tick_display_value: '682.74', + }, + { + epoch: 1716220719, + tick: 682.75, + tick_display_value: '682.75', + }, + { + epoch: 1716220720, + tick: 682.72, + tick_display_value: '682.72', + }, + ], + transaction_ids: { + buy: 484286658868, + }, + underlying: '1HZ100V', + }, + details: + 'After the entry spot tick, your stake will grow continuously by 1% for every tick that the spot price remains within the ± 0.04331% from the previous spot price.', + display_name: '', + id: 242807268688, + indicative: 9.84, + purchase: 9, + reference: 484286658868, + type: 'ACCU', + profit_loss: 0.84, + is_valid_to_sell: true, + current_tick: 9, + status: 'profit', + entry_spot: 682.58, + high_barrier: 683.046, + low_barrier: 682.454, + }, +] as TPortfolioPosition[]; + +const PositionsContent = observer(({ isClosedTab, onRedirectToTrade }: TPositionsContentProps) => { + const [contractTypeFilter, setContractTypeFilter] = React.useState([]); + const [filteredPositions, setFilteredPositions] = React.useState([]); + const [positions, setPositions] = React.useState([]); + const [noMatchesFound, setNoMatchesFound] = React.useState(false); + + const { client, portfolio } = useStore(); + const { currency } = client; + const { onClickCancel, onClickSell } = portfolio; + const { data, is_loading: isLoading, onMount } = useReportsStore().profit_table; + const closedPositions = React.useMemo(() => data.map(d => ({ contract_info: d })), [data]); + + React.useEffect(() => { + onMount(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + React.useEffect(() => { + if (!isLoading) + setPositions(isClosedTab ? (closedPositions as unknown as TPortfolioPosition[]) : mockedActivePositions); + }, [isLoading, isClosedTab, closedPositions, mockedActivePositions]); + + React.useEffect(() => { + if (contractTypeFilter.length) { + const result = filterPositions(positions, contractTypeFilter); + setNoMatchesFound(!result.length); + setFilteredPositions(result); + } else setFilteredPositions(positions); + }, [contractTypeFilter, positions]); -type TPositionsContentProps = Omit & - Pick & { - contractTypeFilter: string[] | []; - isLoading?: boolean; - noMatchesFound?: boolean; - positions?: TPortfolioPosition[]; - setContractTypeFilter: React.Dispatch>; - }; - -const PositionsContent = ({ - contractTypeFilter, - isClosedTab, - isLoading, - noMatchesFound, - onRedirectToTrade, - positions = [], - setContractTypeFilter, - ...rest -}: TPositionsContentProps) => { if (isLoading) return ; + return (
- {(!!positions.length || (!positions.length && noMatchesFound)) && ( + {(!!filteredPositions.length || (!filteredPositions.length && noMatchesFound)) && (
)}
- {positions.length ? ( - + {filteredPositions.length ? ( + ) : ( ); -}; +}); export default PositionsContent; diff --git a/packages/trader/src/AppV2/Containers/Positions/positions.tsx b/packages/trader/src/AppV2/Containers/Positions/positions.tsx index 848b39c41137..74cccd748b56 100644 --- a/packages/trader/src/AppV2/Containers/Positions/positions.tsx +++ b/packages/trader/src/AppV2/Containers/Positions/positions.tsx @@ -1,344 +1,26 @@ import React from 'react'; import { Localize } from '@deriv/translations'; import { Tab } from '@deriv-com/quill-ui'; -import { TPortfolioPosition } from '@deriv/stores/types'; -import { observer, useStore } from '@deriv/stores'; -import { filterPositions } from '../../Utils/positions-utils'; import PositionsContent from './positions-content'; -import { useReportsStore } from '../../../../../reports/src/Stores/useReportsStores'; type TPositionsProps = { onRedirectToTrade?: () => void; }; -// TODO: Remove after real data is available -const mockedActivePositions = [ - { - contract_info: { - account_id: 112905368, - barrier: '682.60', - barrier_count: 1, - bid_price: 6.38, - buy_price: 9, - contract_id: 242807007748, - contract_type: 'CALL', - currency: 'USD', - current_spot: 681.76, - current_spot_display_value: '681.76', - current_spot_time: 1716220628, - date_expiry: 1716221100, - date_settlement: 1716221100, - date_start: 1716220562, - display_name: 'Volatility 100 (1s) Index', - entry_spot: 682.6, - entry_spot_display_value: '682.60', - entry_tick: 682.6, - entry_tick_display_value: '682.60', - entry_tick_time: 1716220563, - expiry_time: 1716221100, - id: '917d1b48-305b-a2f4-5b9c-7fb1f2c6c145', - is_expired: 0, - is_forward_starting: 0, - is_intraday: 1, - is_path_dependent: 0, - is_settleable: 0, - is_sold: 0, - is_valid_to_cancel: 0, - is_valid_to_sell: 1, - longcode: - 'Win payout if Volatility 100 (1s) Index is strictly higher than entry spot at 2024-05-20 16:05:00 GMT.', - payout: 17.61, - profit: -2.62, - profit_percentage: -29.11, - purchase_time: 1716220562, - shortcode: 'CALL_1HZ100V_17.61_1716220562_1716221100F_S0P_0', - status: 'open', - transaction_ids: { - buy: 484286139408, - }, - underlying: '1HZ100V', - }, - details: - 'Win payout if Volatility 100 (1s) Index is strictly higher than entry spot at 2024-05-20 16:05:00 GMT.', - display_name: '', - id: 242807007748, - indicative: 6.38, - payout: 17.61, - purchase: 9, - reference: 484286139408, - type: 'CALL', - profit_loss: -2.62, - is_valid_to_sell: true, - status: 'profit', - barrier: 682.6, - entry_spot: 682.6, - }, - { - contract_info: { - account_id: 112905368, - barrier_count: 1, - bid_price: 8.9, - buy_price: 9.39, - cancellation: { - ask_price: 0.39, - date_expiry: 1716224183, - }, - commission: 0.03, - contract_id: 242807045608, - contract_type: 'MULTUP', - currency: 'USD', - current_spot: 681.71, - current_spot_display_value: '681.71', - current_spot_time: 1716220672, - date_expiry: 4869849599, - date_settlement: 4869849600, - date_start: 1716220583, - display_name: 'Volatility 100 (1s) Index', - entry_spot: 682.23, - entry_spot_display_value: '682.23', - entry_tick: 682.23, - entry_tick_display_value: '682.23', - entry_tick_time: 1716220584, - expiry_time: 4869849599, - id: '917d1b48-305b-a2f4-5b9c-7fb1f2c6c145', - is_expired: 0, - is_forward_starting: 0, - is_intraday: 0, - is_path_dependent: 1, - is_settleable: 0, - is_sold: 0, - is_valid_to_cancel: 1, - is_valid_to_sell: 0, - limit_order: { - stop_out: { - display_name: 'Stop out', - order_amount: -9, - order_date: 1716220583, - value: '614.26', - }, - }, - longcode: - "If you select 'Up', your total profit/loss will be the percentage increase in Volatility 100 (1s) Index, multiplied by 90, minus commissions.", - multiplier: 10, - profit: -0.1, - profit_percentage: -1.11, - purchase_time: 1716220583, - shortcode: 'MULTUP_1HZ100V_9.00_10_1716220583_4869849599_60m_0.00_N1', - status: 'open', - transaction_ids: { - buy: 484286215128, - }, - underlying: '1HZ100V', - validation_error: - 'The spot price has moved. We have not closed this contract because your profit is negative and deal cancellation is active. Cancel your contract to get your full stake back.', - validation_error_code: 'General', - }, - details: - "If you select 'Up', your total profit/loss will be the percentage increase in Volatility 100 (1s) Index, multiplied by 90, minus commissions.", - display_name: '', - id: 242807045608, - indicative: 8.9, - purchase: 9.39, - reference: 484286215128, - type: 'MULTUP', - contract_update: { - stop_out: { - display_name: 'Stop out', - order_amount: -9, - order_date: 1716220583, - value: '614.26', - }, - }, - profit_loss: -0.1, - is_valid_to_sell: false, - status: 'profit', - entry_spot: 682.23, - }, - { - contract_info: { - account_id: 112905368, - barrier_count: 2, - barrier_spot_distance: '0.296', - bid_price: 9.84, - buy_price: 9, - contract_id: 242807268688, - contract_type: 'ACCU', - currency: 'USD', - current_spot: 682.72, - current_spot_display_value: '682.72', - current_spot_high_barrier: '683.016', - current_spot_low_barrier: '682.424', - current_spot_time: 1716220720, - date_expiry: 1747785599, - date_settlement: 1747785600, - date_start: 1716220710, - display_name: 'Volatility 100 (1s) Index', - entry_spot: 682.58, - entry_spot_display_value: '682.58', - entry_tick: 682.58, - entry_tick_display_value: '682.58', - entry_tick_time: 1716220711, - expiry_time: 1747785599, - growth_rate: 0.01, - high_barrier: '683.046', - id: '917d1b48-305b-a2f4-5b9c-7fb1f2c6c145', - is_expired: 0, - is_forward_starting: 0, - is_intraday: 0, - is_path_dependent: 1, - is_settleable: 0, - is_sold: 0, - is_valid_to_cancel: 0, - is_valid_to_sell: 1, - longcode: - 'After the entry spot tick, your stake will grow continuously by 1% for every tick that the spot price remains within the ± 0.04331% from the previous spot price.', - low_barrier: '682.454', - profit: 0.84, - profit_percentage: 9.33, - purchase_time: 1716220710, - shortcode: 'ACCU_1HZ100V_9.00_0_0.01_1_0.000433139675_1716220710', - status: 'open', - tick_count: 230, - tick_passed: 9, - tick_stream: [ - { - epoch: 1716220711, - tick: 682.58, - tick_display_value: '682.58', - }, - { - epoch: 1716220712, - tick: 682.71, - tick_display_value: '682.71', - }, - { - epoch: 1716220713, - tick: 682.5, - tick_display_value: '682.50', - }, - { - epoch: 1716220714, - tick: 682.57, - tick_display_value: '682.57', - }, - { - epoch: 1716220715, - tick: 682.57, - tick_display_value: '682.57', - }, - { - epoch: 1716220716, - tick: 682.75, - tick_display_value: '682.75', - }, - { - epoch: 1716220717, - tick: 682.87, - tick_display_value: '682.87', - }, - { - epoch: 1716220718, - tick: 682.74, - tick_display_value: '682.74', - }, - { - epoch: 1716220719, - tick: 682.75, - tick_display_value: '682.75', - }, - { - epoch: 1716220720, - tick: 682.72, - tick_display_value: '682.72', - }, - ], - transaction_ids: { - buy: 484286658868, - }, - underlying: '1HZ100V', - }, - details: - 'After the entry spot tick, your stake will grow continuously by 1% for every tick that the spot price remains within the ± 0.04331% from the previous spot price.', - display_name: '', - id: 242807268688, - indicative: 9.84, - purchase: 9, - reference: 484286658868, - type: 'ACCU', - profit_loss: 0.84, - is_valid_to_sell: true, - current_tick: 9, - status: 'profit', - entry_spot: 682.58, - high_barrier: 683.046, - low_barrier: 682.454, - }, -] as TPortfolioPosition[]; - -const Positions = observer(({ onRedirectToTrade }: TPositionsProps) => { - const [contractTypeFilter, setContractTypeFilter] = React.useState([]); - const [filteredPositions, setFilteredPositions] = React.useState>>( - [] - ); - const [noMatchesFound, setNoMatchesFound] = React.useState(false); - const { client, portfolio } = useStore(); - const { currency } = client; - const { onClickCancel, onClickSell } = portfolio; - const { data, is_loading, onMount } = useReportsStore().profit_table; - const closedPositions = React.useMemo(() => data.map(d => ({ contract_info: d })), [data]); - +const Positions = ({ onRedirectToTrade }: TPositionsProps) => { const tabs = [ { id: 'open', title: , - content: ( - - ), + content: , }, { id: 'closed', title: , - content: ( - - ), + content: , }, ]; - React.useEffect(() => { - onMount(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - React.useEffect(() => { - if (contractTypeFilter.length) { - const result = filterPositions(closedPositions as unknown as TPortfolioPosition[], contractTypeFilter); - setNoMatchesFound(!result.length); - setFilteredPositions(result as unknown as TPortfolioPosition[]); - } else setFilteredPositions(closedPositions as unknown as TPortfolioPosition[]); - }, [contractTypeFilter, closedPositions]); - - React.useEffect(() => { - if (!is_loading) setFilteredPositions(closedPositions as unknown as TPortfolioPosition[]); - }, [is_loading, closedPositions]); - return (
@@ -355,6 +37,6 @@ const Positions = observer(({ onRedirectToTrade }: TPositionsProps) => {
); -}); +}; export default Positions; diff --git a/packages/trader/src/AppV2/Hooks/useClosedPositions.ts b/packages/trader/src/AppV2/Hooks/useClosedPositions.ts index 6c936a7f9b3c..a6d4b1e8da9b 100644 --- a/packages/trader/src/AppV2/Hooks/useClosedPositions.ts +++ b/packages/trader/src/AppV2/Hooks/useClosedPositions.ts @@ -11,17 +11,17 @@ type TPros = { const useClosedPositions = ({ date_from, date_to }: TPros = {}) => { const [positions, setPositions] = useState>([]); - const [isLoading, setLoading] = React.useState(false); + const [isLoading, setIsLoading] = React.useState(false); const positionsRef = useRef>([]); const fetch = useCallback(async () => { - setLoading(true); + setIsLoading(true); const data: ProfitTableResponse = await WS.profitTable(50, positionsRef.current.length, { date_from, date_to, }); - setLoading(false); + setIsLoading(false); // TODO: handle errors setPositions(prevPositions => [...prevPositions, ...(data?.profit_table?.transactions ?? [])]); }, [date_from, date_to]); From 6c5106bc1b62193024268aa97d4f8afe59c27a77 Mon Sep 17 00:00:00 2001 From: Maryia <103177211+maryia-deriv@users.noreply.github.com> Date: Thu, 23 May 2024 09:19:02 +0300 Subject: [PATCH 14/54] Maryia/positions-redesign/Contract cards improvements + fetching Open positions + formatProfitTableTransactions TS migration (#58) * refactor: contract-card-list and card * feat: buttons demo + animation improvements * feat: finilize Duration component for the card * chore: ts migration for closed positions * fix: console error with remaining time & showing empty message only when empty * feat: connect real open postions + style and filter fixes --- .../Modules/Profit/Helpers/format-response.js | 27 -- .../Modules/Profit/Helpers/format-response.ts | 35 ++ .../reports/src/Stores/useReportsStores.tsx | 5 +- packages/stores/src/mockStore.ts | 1 + packages/stores/types.ts | 1 + .../ContractCard/contract-card-duration.tsx | 34 +- .../ContractCard/contract-card-list.tsx | 75 ++-- .../ContractCard/contract-card.scss | 57 ++- .../Components/ContractCard/contract-card.tsx | 99 +++-- .../EmptyMessage/empty-message.scss | 2 +- .../Positions/positions-content.tsx | 386 ++++-------------- .../AppV2/Containers/Positions/positions.scss | 2 + .../AppV2/Containers/Positions/positions.tsx | 10 +- .../trader/src/AppV2/Utils/positions-utils.ts | 3 +- 14 files changed, 287 insertions(+), 450 deletions(-) delete mode 100644 packages/reports/src/Stores/Modules/Profit/Helpers/format-response.js create mode 100644 packages/reports/src/Stores/Modules/Profit/Helpers/format-response.ts diff --git a/packages/reports/src/Stores/Modules/Profit/Helpers/format-response.js b/packages/reports/src/Stores/Modules/Profit/Helpers/format-response.js deleted file mode 100644 index f691a0a7b596..000000000000 --- a/packages/reports/src/Stores/Modules/Profit/Helpers/format-response.js +++ /dev/null @@ -1,27 +0,0 @@ -import { formatMoney, toMoment, getSymbolDisplayName, getMarketInformation } from '@deriv/shared'; - -export const formatProfitTableTransactions = (transaction, currency, active_symbols = []) => { - const format_string = 'DD MMM YYYY HH:mm:ss'; - const purchase_time = `${toMoment(+transaction.purchase_time).format(format_string)}`; - const purchase_time_unix = transaction.purchase_time; - const sell_time = `${toMoment(+transaction.sell_time).format(format_string)}`; - const payout = parseFloat(transaction.payout); - const sell_price = parseFloat(transaction.sell_price); - const buy_price = parseFloat(transaction.buy_price); - const profit_loss = formatMoney(currency, Number(sell_price - buy_price), true); - const display_name = getSymbolDisplayName(active_symbols, getMarketInformation(transaction.shortcode).underlying); - - return { - ...transaction, - ...{ - payout, - sell_price, - buy_price, - profit_loss, - sell_time, - purchase_time, - display_name, - purchase_time_unix, - }, - }; -}; diff --git a/packages/reports/src/Stores/Modules/Profit/Helpers/format-response.ts b/packages/reports/src/Stores/Modules/Profit/Helpers/format-response.ts new file mode 100644 index 000000000000..957d9d28231f --- /dev/null +++ b/packages/reports/src/Stores/Modules/Profit/Helpers/format-response.ts @@ -0,0 +1,35 @@ +import { formatMoney, toMoment, getSymbolDisplayName, getMarketInformation } from '@deriv/shared'; +import { ActiveSymbols, ProfitTable } from '@deriv/api-types'; + +export const formatProfitTableTransactions = ( + transaction: NonNullable[number], + currency: string, + active_symbols: ActiveSymbols = [] +) => { + const format_string = 'DD MMM YYYY HH:mm:ss'; + const purchase_time = `${toMoment(Number(transaction.purchase_time)).format(format_string)}`; + const purchase_time_unix = transaction.purchase_time; + const sell_time = `${toMoment(Number(transaction.sell_time)).format(format_string)}`; + const payout = transaction.payout ?? NaN; + const sell_price = transaction.sell_price ?? NaN; + const buy_price = transaction.buy_price ?? NaN; + const profit_loss = formatMoney(currency, Number(sell_price - buy_price), true); + const display_name = getSymbolDisplayName( + active_symbols, + getMarketInformation(transaction.shortcode ?? '').underlying + ); + + return { + ...transaction, + ...{ + payout, + sell_price, + buy_price, + profit_loss, + sell_time, + purchase_time, + display_name, + purchase_time_unix, + }, + }; +}; diff --git a/packages/reports/src/Stores/useReportsStores.tsx b/packages/reports/src/Stores/useReportsStores.tsx index 1e4e6bf76950..dd3b45c1a315 100644 --- a/packages/reports/src/Stores/useReportsStores.tsx +++ b/packages/reports/src/Stores/useReportsStores.tsx @@ -2,10 +2,11 @@ import React from 'react'; import { useStore } from '@deriv/stores'; import ProfitStores from './Modules/Profit/profit-store'; import StatementStores from './Modules/Statement/statement-store'; +import { formatProfitTableTransactions } from './Modules/Profit/Helpers/format-response'; type TOverrideProfitStore = Omit & { date_from: number; - data: { [key: string]: string }[]; + data: ReturnType[]; totals: { [key: string]: unknown }; }; @@ -39,7 +40,7 @@ type TOverrideStatementStore = Omit< suffix_icon: string; }; -type TReportsStore = { +export type TReportsStore = { profit_table: TOverrideProfitStore; statement: TOverrideStatementStore; }; diff --git a/packages/stores/src/mockStore.ts b/packages/stores/src/mockStore.ts index 21fd87a0b7ee..a7e54b80a804 100644 --- a/packages/stores/src/mockStore.ts +++ b/packages/stores/src/mockStore.ts @@ -580,6 +580,7 @@ const mock = (): TStores & { is_mock: boolean } => { barriers: [], error: '', getPositionById: jest.fn(), + is_active_empty: false, is_loading: false, is_accumulator: false, is_multiplier: false, diff --git a/packages/stores/types.ts b/packages/stores/types.ts index e610b510ef87..f5660fc85167 100644 --- a/packages/stores/types.ts +++ b/packages/stores/types.ts @@ -802,6 +802,7 @@ type TPortfolioStore = { barriers: TBarriers; error: string; getPositionById: (id: number) => TPortfolioPosition; + is_active_empty: boolean; is_loading: boolean; is_multiplier: boolean; is_accumulator: boolean; diff --git a/packages/trader/src/AppV2/Components/ContractCard/contract-card-duration.tsx b/packages/trader/src/AppV2/Components/ContractCard/contract-card-duration.tsx index 8c4fbd41ce0b..701db2212746 100644 --- a/packages/trader/src/AppV2/Components/ContractCard/contract-card-duration.tsx +++ b/packages/trader/src/AppV2/Components/ContractCard/contract-card-duration.tsx @@ -5,29 +5,33 @@ import { CaptionText } from '@deriv-com/quill-ui'; import { LabelPairedStopwatchCaptionRegularIcon } from '@deriv/quill-icons'; import { useStore } from '@deriv/stores'; import { observer } from 'mobx-react'; +import { getCardLabels } from '@deriv/shared'; +import { RemainingTime } from '@deriv/components'; -export type TContractCardDurationProps = Pick< - TPortfolioPosition['contract_info'], - 'expiry_time' | 'purchase_time' | 'tick_count' -> & { - currentTick: number | null; - isMultiplier?: boolean; +export type TContractCardDurationProps = Pick & { + currentTick?: number | null; + hasNoAutoExpiry?: boolean; }; export const ContractCardDuration = observer( - ({ currentTick, expiry_time, isMultiplier, purchase_time, tick_count }: TContractCardDurationProps) => { + ({ currentTick, expiry_time, hasNoAutoExpiry, tick_count }: TContractCardDurationProps) => { const { server_time } = useStore().common; + + const getDisplayedDuration = () => { + if (hasNoAutoExpiry) return ; + if (tick_count && currentTick) { + return `${currentTick}/${tick_count} ${getCardLabels().TICKS.toLowerCase()}`; + } + return ; + }; + + if (!expiry_time) return null; return ( - // TODO: when Tag is exported from quill-ui, use } - // label={isMultiplier ? : 'Duration'} - // /> + // TODO: when is exported from quill-ui, use it instead
- - {/* in progress */} - {isMultiplier ? : 'Duration'} + + {getDisplayedDuration()}
); diff --git a/packages/trader/src/AppV2/Components/ContractCard/contract-card-list.tsx b/packages/trader/src/AppV2/Components/ContractCard/contract-card-list.tsx index cf736dc4f49a..5e030ff31623 100644 --- a/packages/trader/src/AppV2/Components/ContractCard/contract-card-list.tsx +++ b/packages/trader/src/AppV2/Components/ContractCard/contract-card-list.tsx @@ -1,76 +1,67 @@ -import { - getContractPath, - getCurrentTick, - getTotalProfit, - getTradeTypeName, - isHighLow, - isMultiplierContract, - isValidToCancel, - isValidToSell, -} from '@deriv/shared'; +import { getContractPath } from '@deriv/shared'; import { TPortfolioPosition } from '@deriv/stores/types'; import React from 'react'; import ContractCard from './contract-card'; +import classNames from 'classnames'; +import { TClosedPosition } from 'AppV2/Containers/Positions/positions-content'; export type TContractCardListProps = { currency?: string; + hasButtonsDemo?: boolean; onClickCancel?: (contractId: number) => void; onClickSell?: (contractId: number) => void; - positions?: TPortfolioPosition[]; + positions?: (TPortfolioPosition | TClosedPosition)[]; + setHasButtonsDemo?: React.Dispatch>; }; -const ContractCardList = ({ onClickCancel, onClickSell, positions = [], ...rest }: TContractCardListProps) => { - // TODO: make it work not only with an open position data but also with a profit_table transaction data - const timeoutIds = React.useRef>>([]); +const ContractCardList = ({ + currency, + hasButtonsDemo, + onClickCancel, + onClickSell, + positions = [], + setHasButtonsDemo, +}: TContractCardListProps) => { + const closedCardsTimeouts = React.useRef>>([]); React.useEffect(() => { - const timers = timeoutIds.current; + const timers = closedCardsTimeouts.current; + const demoTimeout = setTimeout(() => setHasButtonsDemo?.(false), 720); return () => { if (timers.length) { timers.forEach(id => clearTimeout(id)); } + if (demoTimeout) clearTimeout(demoTimeout); }; + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const handleClose = (id: number, shouldCancel?: boolean) => { const timeoutId = setTimeout(() => { shouldCancel ? onClickCancel?.(id) : onClickSell?.(id); }, 160); - timeoutIds.current.push(timeoutId); + closedCardsTimeouts.current.push(timeoutId); }; if (!positions.length) return null; return ( -
- {positions.map(({ id, is_sell_requested, contract_info }) => { - const { contract_type, display_name, profit, shortcode } = contract_info; - const contract_main_title = getTradeTypeName(contract_type ?? '', { - isHighLow: isHighLow({ shortcode }), - showMainTitle: true, - }); - const currentTick = contract_info.tick_count ? getCurrentTick(contract_info) : null; - const tradeTypeName = `${contract_main_title} ${getTradeTypeName(contract_type ?? '', { - isHighLow: isHighLow({ shortcode }), - })}`.trim(); - const isMultiplier = isMultiplierContract(contract_type); - const validToCancel = isValidToCancel(contract_info); - const validToSell = isValidToSell(contract_info) && !is_sell_requested; - const totalProfit = isMultiplierContract(contract_type) ? getTotalProfit(contract_info) : profit; +
+ {positions.map(position => { + const { contract_id: id } = position.contract_info; return ( id && handleClose?.(id, true)} onClose={() => id && handleClose?.(id)} - redirectTo={getContractPath(id)} - symbolName={display_name} - totalProfit={totalProfit} - tradeTypeName={tradeTypeName} + redirectTo={id ? getContractPath(id) : undefined} /> ); })} diff --git a/packages/trader/src/AppV2/Components/ContractCard/contract-card.scss b/packages/trader/src/AppV2/Components/ContractCard/contract-card.scss index fba9210e30e5..63719c3b0c7f 100644 --- a/packages/trader/src/AppV2/Components/ContractCard/contract-card.scss +++ b/packages/trader/src/AppV2/Components/ContractCard/contract-card.scss @@ -9,7 +9,7 @@ justify-content: space-between; height: 10.4rem; transform: translateX(0); - transition: transform var(--motion-duration-snappy) var(--motion-easing-inandout); + transition: transform var(--motion-duration-snappy) var(--motion-easing-linear); .icon, .dc-icon { @@ -34,16 +34,22 @@ } .details, .status-and-profit, + .status, &-duration { display: flex; gap: 0.8rem; align-items: center; } + .status, &-duration { - background-color: color-mix(in sRGB, var(--component-textIcon-normal-default) 4%, transparent); + background-color: var(--core-color-opacity-black-75); height: 2.4rem; padding: 0 0.8rem; border-radius: var(--semantic-borderRadius-sm); + + .dc-remaining-time { + font-size: unset; + } } .status-and-profit { justify-content: space-between; @@ -106,6 +112,10 @@ button:not(:disabled) { background-color: var(--core-color-solid-cherry-700); } + .status { + color: var(--core-color-solid-red-900); + background-color: var(--core-color-opacity-red-100); + } } &.won { .total-profit { @@ -114,11 +124,15 @@ button:not(:disabled) { background-color: var(--core-color-solid-emerald-700); } + .status { + color: var(--core-color-solid-green-900); + background-color: var(--core-color-opacity-green-100); + } } &.show-buttons { transform: translateX(calc(var(--core-size-3600) * -1)); - &--has-cancel-button { + &.has-cancel-button { transform: translateX(calc(var(--core-size-3600) * -2)); } } @@ -133,16 +147,51 @@ &.deleted { opacity: var(--core-opacity-50); max-height: 0; - transition: max-height var(--core-motion-duration-200), opacity var(--core-motion-duration-200); + transition: max-height var(--motion-duration-snappy), opacity var(--motion-duration-snappy); transition-timing-function: var(--motion-easing-inandout); } } &-list { display: flex; + flex-grow: 1; flex-direction: column; gap: 0.8rem; width: inherit; padding: 0.8rem; overflow: hidden; + + &--has-buttons-demo { + .contract-card-wrapper:first-child { + .contract-card { + animation: var(--motion-duration-relax) var(--motion-easing-inandout) bounce-one-button; + + &.has-cancel-button { + animation: var(--motion-duration-relax) var(--motion-easing-inandout) bounce-two-buttons; + } + } + } + } + } +} + +@keyframes bounce-one-button { + 0%, + 100% { + transform: translateX(0); + } + 30%, + 70% { + transform: translateX(calc(var(--core-size-3600) * -1)); + } +} + +@keyframes bounce-two-buttons { + 0%, + 100% { + transform: translateX(0); + } + 30%, + 70% { + transform: translateX(calc(var(--core-size-3600) * -2)); } } diff --git a/packages/trader/src/AppV2/Components/ContractCard/contract-card.tsx b/packages/trader/src/AppV2/Components/ContractCard/contract-card.tsx index 02e8652d9d73..9a1ac60f08f0 100644 --- a/packages/trader/src/AppV2/Components/ContractCard/contract-card.tsx +++ b/packages/trader/src/AppV2/Components/ContractCard/contract-card.tsx @@ -3,28 +3,34 @@ import classNames from 'classnames'; import { CaptionText, Text } from '@deriv-com/quill-ui'; import { useSwipeable } from 'react-swipeable'; import { IconTradeTypes, Money } from '@deriv/components'; -import { getCardLabels } from '@deriv/shared'; -import { TPortfolioPosition } from '@deriv/stores/types'; +import { + TContractInfo, + getCardLabels, + getCurrentTick, + getMarketName, + getTotalProfit, + getTradeTypeName, + isHighLow, + isMultiplierContract, + isValidToCancel, + isValidToSell, +} from '@deriv/shared'; import { ContractCardDuration, TContractCardDurationProps } from './contract-card-duration'; import { BinaryLink } from 'App/Components/Routes'; +import { TClosedPosition } from 'AppV2/Containers/Positions/positions-content'; -type TContractCardProps = Pick< - TPortfolioPosition['contract_info'], - 'buy_price' | 'contract_type' | 'sell_time' | 'profit' -> & - TContractCardDurationProps & { - className?: string; - currency?: string; - isValidToCancel?: boolean; - isValidToSell?: boolean; - onClick?: (e?: React.MouseEvent) => void; - onCancel?: (e?: React.MouseEvent) => void; - onClose?: (e?: React.MouseEvent) => void; - redirectTo?: string; - symbolName?: string; - totalProfit?: string | number; - tradeTypeName?: string; - }; +type TContractCardProps = TContractCardDurationProps & { + className?: string; + contractInfo: TContractInfo | TClosedPosition['contract_info']; + currency?: string; + hasActionButtons?: boolean; + id?: number | null; + isSellRequested?: boolean; + onClick?: (e?: React.MouseEvent) => void; + onCancel?: (e?: React.MouseEvent) => void; + onClose?: (e?: React.MouseEvent) => void; + redirectTo?: string; +}; const DIRECTION = { LEFT: 'left', @@ -38,24 +44,34 @@ const swipeConfig = { const ContractCard = ({ className, - contract_type, + contractInfo, currency, - buy_price, - isValidToCancel, - isValidToSell, + hasActionButtons = true, + isSellRequested, onCancel, onClick, onClose, - profit, redirectTo, - sell_time, - symbolName, - totalProfit, - tradeTypeName, - ...rest }: TContractCardProps) => { const [isDeleted, setIsDeleted] = React.useState(false); const [shouldShowButtons, setShouldShowButtons] = React.useState(false); + const { buy_price, contract_type, display_name, sell_time, shortcode } = contractInfo; + const contract_main_title = getTradeTypeName(contract_type ?? '', { + isHighLow: isHighLow({ shortcode }), + showMainTitle: true, + }); + const currentTick = 'tick_count' in contractInfo && contractInfo.tick_count ? getCurrentTick(contractInfo) : null; + const tradeTypeName = `${contract_main_title} ${getTradeTypeName(contract_type ?? '', { + isHighLow: isHighLow({ shortcode }), + })}`.trim(); + const symbolName = + 'underlying_symbol' in contractInfo ? getMarketName(contractInfo.underlying_symbol ?? '') : display_name; + const isMultiplier = isMultiplierContract(contract_type); + const totalProfit = isMultiplierContract(contract_type) + ? getTotalProfit(contractInfo as TContractInfo) + : (contractInfo as TContractInfo).profit ?? (contractInfo as TClosedPosition['contract_info']).profit_loss; + const validToCancel = isValidToCancel(contractInfo as TContractInfo); + const validToSell = isValidToSell(contractInfo as TContractInfo) && !isSellRequested; const handleSwipe = (direction: string) => { const isLeft = direction === DIRECTION.LEFT; @@ -79,14 +95,15 @@ const ContractCard = ({
0, })} - to={redirectTo} + onClick={onClick} onDragStart={e => e.preventDefault()} + to={redirectTo} >
@@ -105,24 +122,26 @@ const ContractCard = ({
{sell_time ? ( - - {getCardLabels().CLOSED} - + {getCardLabels().CLOSED} ) : ( - + )}
- {!sell_time && ( + {!sell_time && hasActionButtons && (
- {isValidToCancel && ( + {validToCancel && (
+ ); +}; export default EmptyMessage; diff --git a/packages/trader/src/AppV2/Containers/Positions/positions-content.tsx b/packages/trader/src/AppV2/Containers/Positions/positions-content.tsx index 3f57caaf7bbe..025089f710e9 100644 --- a/packages/trader/src/AppV2/Containers/Positions/positions-content.tsx +++ b/packages/trader/src/AppV2/Containers/Positions/positions-content.tsx @@ -18,86 +18,80 @@ export type TClosedPosition = { contract_info: TReportsStore['profit_table']['data'][number]; }; -const PositionsContent = observer( - ({ hasButtonsDemo, isClosedTab, onRedirectToTrade, setHasButtonsDemo }: TPositionsContentProps) => { - const [contractTypeFilter, setContractTypeFilter] = React.useState([]); - const [chosenTimeFilter, setChosenTimeFilter] = React.useState(''); - const [filteredPositions, setFilteredPositions] = React.useState<(TPortfolioPosition | TClosedPosition)[]>([]); - const [noMatchesFound, setNoMatchesFound] = React.useState(false); +const PositionsContent = observer(({ hasButtonsDemo, isClosedTab, setHasButtonsDemo }: TPositionsContentProps) => { + const [contractTypeFilter, setContractTypeFilter] = React.useState([]); + const [chosenTimeFilter, setChosenTimeFilter] = React.useState(''); + const [filteredPositions, setFilteredPositions] = React.useState<(TPortfolioPosition | TClosedPosition)[]>([]); + const [noMatchesFound, setNoMatchesFound] = React.useState(false); - const { client, portfolio } = useStore(); - const { currency } = client; - const { active_positions, is_active_empty, onClickCancel, onClickSell, onMount: onOpenTabMount } = portfolio; - const { - data, - is_empty, - is_loading: isLoading, - onMount: onClosedTabMount, - handleDateChange, - } = useReportsStore().profit_table; - const closedPositions = React.useMemo(() => data.map(d => ({ contract_info: d })), [data]); - const positions = React.useMemo( - () => (isClosedTab ? closedPositions : active_positions), - [active_positions, isClosedTab, closedPositions] - ); - const emptyPositions = isClosedTab ? is_empty : is_active_empty; - const shouldShowEmptyMessage = emptyPositions || noMatchesFound; + const { client, portfolio } = useStore(); + const { currency } = client; + const { active_positions, is_active_empty, onClickCancel, onClickSell, onMount: onOpenTabMount } = portfolio; + const { + data, + is_empty, + is_loading: isLoading, + onMount: onClosedTabMount, + handleDateChange, + } = useReportsStore().profit_table; + const closedPositions = React.useMemo(() => data.map(d => ({ contract_info: d })), [data]); + const positions = React.useMemo( + () => (isClosedTab ? closedPositions : active_positions), + [active_positions, isClosedTab, closedPositions] + ); + const emptyPositions = isClosedTab ? is_empty : is_active_empty; + const shouldShowEmptyMessage = emptyPositions || noMatchesFound; - React.useEffect(() => { - isClosedTab ? onClosedTabMount() : onOpenTabMount(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + React.useEffect(() => { + isClosedTab ? onClosedTabMount() : onOpenTabMount(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); - React.useEffect(() => { - if (contractTypeFilter.length) { - const result = filterPositions(positions, contractTypeFilter); - setNoMatchesFound(!result.length); - setFilteredPositions(result); - } else { - setNoMatchesFound(false); - setFilteredPositions(positions); - } - }, [contractTypeFilter, positions]); + React.useEffect(() => { + if (contractTypeFilter.length) { + const result = filterPositions(positions, contractTypeFilter); + setNoMatchesFound(!result.length); + setFilteredPositions(result); + } else { + setNoMatchesFound(false); + setFilteredPositions(positions); + } + }, [contractTypeFilter, positions]); - if (isLoading) return ; - return ( -
-
- {!emptyPositions && ( -
- {isClosedTab && ( - - )} - ; + return ( +
+
+ {!emptyPositions && ( +
+ {isClosedTab && ( + -
- )} -
- {shouldShowEmptyMessage ? ( - - ) : ( - + )} + +
)}
- ); - } -); + {shouldShowEmptyMessage ? ( + + ) : ( + + )} +
+ ); +}); export default PositionsContent; diff --git a/packages/trader/src/AppV2/Containers/Positions/positions.tsx b/packages/trader/src/AppV2/Containers/Positions/positions.tsx index 0442ef8a9441..3fa9b81c5ff9 100644 --- a/packages/trader/src/AppV2/Containers/Positions/positions.tsx +++ b/packages/trader/src/AppV2/Containers/Positions/positions.tsx @@ -3,24 +3,14 @@ import { Localize } from '@deriv/translations'; import { Tab } from '@deriv-com/quill-ui'; import PositionsContent from './positions-content'; -type TPositionsProps = { - onRedirectToTrade?: () => void; -}; - -const Positions = ({ onRedirectToTrade }: TPositionsProps) => { +const Positions = () => { const [hasButtonsDemo, setHasButtonsDemo] = React.useState(true); const tabs = [ { id: 'open', title: , - content: ( - - ), + content: , }, { id: 'closed', diff --git a/packages/trader/src/AppV2/app.tsx b/packages/trader/src/AppV2/app.tsx index eda39cf823d5..8170e2891c40 100644 --- a/packages/trader/src/AppV2/app.tsx +++ b/packages/trader/src/AppV2/app.tsx @@ -35,7 +35,7 @@ const App = ({ passthrough }: Apptypes) => { - setCurrentPageIdx(0)} /> + From 42dc7afaa6eeef2a2093adadccbbe32a76c94d28 Mon Sep 17 00:00:00 2001 From: Maryia <103177211+maryia-deriv@users.noreply.github.com> Date: Thu, 23 May 2024 17:27:44 +0300 Subject: [PATCH 18/54] Maryia/positions-redesign/Contract cards data update fix (#61) * fix: Accumulators tick passed count * fix: contract cards update * fix: show loading only when should not show empty message or cards --- .../shared/src/utils/contract/contract.tsx | 2 +- .../ContractCard/contract-card-duration.tsx | 52 ++++++++++--------- .../ContractCard/contract-card-list.tsx | 9 ++-- .../Components/ContractCard/contract-card.tsx | 8 ++- .../Filter/contract-type-filter.tsx | 2 +- .../Positions/positions-content.tsx | 51 +++++++++++------- .../AppV2/Containers/Positions/positions.scss | 4 +- 7 files changed, 75 insertions(+), 53 deletions(-) diff --git a/packages/shared/src/utils/contract/contract.tsx b/packages/shared/src/utils/contract/contract.tsx index 0d100840fbfd..4801a2fdf00a 100644 --- a/packages/shared/src/utils/contract/contract.tsx +++ b/packages/shared/src/utils/contract/contract.tsx @@ -203,7 +203,7 @@ export const getCurrentTick = (contract_info: TContractInfo) => { const current_tick = isDigitContract(contract_info.contract_type) || isAsiansContract(contract_info.contract_type) ? tick_stream.length - : tick_stream.length - 1; + : contract_info.tick_passed ?? tick_stream.length - 1; return !current_tick || current_tick < 0 ? 0 : current_tick; }; diff --git a/packages/trader/src/AppV2/Components/ContractCard/contract-card-duration.tsx b/packages/trader/src/AppV2/Components/ContractCard/contract-card-duration.tsx index 701db2212746..1b0e8f7662c2 100644 --- a/packages/trader/src/AppV2/Components/ContractCard/contract-card-duration.tsx +++ b/packages/trader/src/AppV2/Components/ContractCard/contract-card-duration.tsx @@ -3,37 +3,39 @@ import { TPortfolioPosition } from '@deriv/stores/types'; import { Localize } from '@deriv/translations'; import { CaptionText } from '@deriv-com/quill-ui'; import { LabelPairedStopwatchCaptionRegularIcon } from '@deriv/quill-icons'; -import { useStore } from '@deriv/stores'; -import { observer } from 'mobx-react'; import { getCardLabels } from '@deriv/shared'; import { RemainingTime } from '@deriv/components'; +import { TRootStore } from 'Types'; export type TContractCardDurationProps = Pick & { currentTick?: number | null; hasNoAutoExpiry?: boolean; + serverTime: TRootStore['common']['server_time']; }; -export const ContractCardDuration = observer( - ({ currentTick, expiry_time, hasNoAutoExpiry, tick_count }: TContractCardDurationProps) => { - const { server_time } = useStore().common; +export const ContractCardDuration = ({ + currentTick, + expiry_time, + hasNoAutoExpiry, + serverTime, + tick_count, +}: TContractCardDurationProps) => { + const getDisplayedDuration = () => { + if (hasNoAutoExpiry) return ; + if (tick_count) { + return `${currentTick ?? 0}/${tick_count} ${getCardLabels().TICKS.toLowerCase()}`; + } + return ; + }; - const getDisplayedDuration = () => { - if (hasNoAutoExpiry) return ; - if (tick_count && currentTick) { - return `${currentTick}/${tick_count} ${getCardLabels().TICKS.toLowerCase()}`; - } - return ; - }; - - if (!expiry_time) return null; - return ( - // TODO: when is exported from quill-ui, use it instead -
- - - {getDisplayedDuration()} - -
- ); - } -); + if (!expiry_time) return null; + return ( + // TODO: when is exported from quill-ui, use it instead +
+ + + {getDisplayedDuration()} + +
+ ); +}; diff --git a/packages/trader/src/AppV2/Components/ContractCard/contract-card-list.tsx b/packages/trader/src/AppV2/Components/ContractCard/contract-card-list.tsx index 5e030ff31623..09e6f46b73c4 100644 --- a/packages/trader/src/AppV2/Components/ContractCard/contract-card-list.tsx +++ b/packages/trader/src/AppV2/Components/ContractCard/contract-card-list.tsx @@ -1,9 +1,10 @@ +import React from 'react'; import { getContractPath } from '@deriv/shared'; import { TPortfolioPosition } from '@deriv/stores/types'; -import React from 'react'; -import ContractCard from './contract-card'; import classNames from 'classnames'; import { TClosedPosition } from 'AppV2/Containers/Positions/positions-content'; +import { TRootStore } from 'Types'; +import ContractCard from './contract-card'; export type TContractCardListProps = { currency?: string; @@ -12,6 +13,7 @@ export type TContractCardListProps = { onClickSell?: (contractId: number) => void; positions?: (TPortfolioPosition | TClosedPosition)[]; setHasButtonsDemo?: React.Dispatch>; + serverTime: TRootStore['common']['server_time']; }; const ContractCardList = ({ @@ -20,6 +22,7 @@ const ContractCardList = ({ onClickCancel, onClickSell, positions = [], + serverTime, setHasButtonsDemo, }: TContractCardListProps) => { const closedCardsTimeouts = React.useRef>>([]); @@ -57,11 +60,11 @@ const ContractCardList = ({ key={id} contractInfo={position.contract_info} currency={currency} - id={id} isSellRequested={(position as TPortfolioPosition).is_sell_requested} onCancel={() => id && handleClose?.(id, true)} onClose={() => id && handleClose?.(id)} redirectTo={id ? getContractPath(id) : undefined} + serverTime={serverTime} /> ); })} diff --git a/packages/trader/src/AppV2/Components/ContractCard/contract-card.tsx b/packages/trader/src/AppV2/Components/ContractCard/contract-card.tsx index 9a1ac60f08f0..b28b44ec7a87 100644 --- a/packages/trader/src/AppV2/Components/ContractCard/contract-card.tsx +++ b/packages/trader/src/AppV2/Components/ContractCard/contract-card.tsx @@ -18,18 +18,19 @@ import { import { ContractCardDuration, TContractCardDurationProps } from './contract-card-duration'; import { BinaryLink } from 'App/Components/Routes'; import { TClosedPosition } from 'AppV2/Containers/Positions/positions-content'; +import { TRootStore } from 'Types'; type TContractCardProps = TContractCardDurationProps & { className?: string; contractInfo: TContractInfo | TClosedPosition['contract_info']; currency?: string; hasActionButtons?: boolean; - id?: number | null; isSellRequested?: boolean; onClick?: (e?: React.MouseEvent) => void; onCancel?: (e?: React.MouseEvent) => void; onClose?: (e?: React.MouseEvent) => void; redirectTo?: string; + serverTime: TRootStore['common']['server_time']; }; const DIRECTION = { @@ -52,6 +53,7 @@ const ContractCard = ({ onClick, onClose, redirectTo, + serverTime, }: TContractCardProps) => { const [isDeleted, setIsDeleted] = React.useState(false); const [shouldShowButtons, setShouldShowButtons] = React.useState(false); @@ -91,6 +93,7 @@ const ContractCard = ({ shouldCancel ? onCancel?.(e) : onClose?.(e); }; + if (!contract_type) return null; return (
)} @@ -162,4 +166,4 @@ const ContractCard = ({ ); }; -export default ContractCard; +export default React.memo(ContractCard); diff --git a/packages/trader/src/AppV2/Components/Filter/contract-type-filter.tsx b/packages/trader/src/AppV2/Components/Filter/contract-type-filter.tsx index e2300c04a82a..18fb30728203 100644 --- a/packages/trader/src/AppV2/Components/Filter/contract-type-filter.tsx +++ b/packages/trader/src/AppV2/Components/Filter/contract-type-filter.tsx @@ -4,7 +4,7 @@ import { ActionSheet, Checkbox } from '@deriv-com/quill-ui'; import { Localize } from '@deriv/translations'; type TContractTypeFilter = { - setContractTypeFilter: React.Dispatch>; + setContractTypeFilter: (filterValues: string[]) => void; contractTypeFilter: string[] | []; }; diff --git a/packages/trader/src/AppV2/Containers/Positions/positions-content.tsx b/packages/trader/src/AppV2/Containers/Positions/positions-content.tsx index 025089f710e9..9e0d0f9336ca 100644 --- a/packages/trader/src/AppV2/Containers/Positions/positions-content.tsx +++ b/packages/trader/src/AppV2/Containers/Positions/positions-content.tsx @@ -1,11 +1,13 @@ import React from 'react'; +import moment from 'moment'; +import { TContractInfo } from '@deriv/shared'; +import { Loading } from '@deriv/components'; +import { observer, useStore } from '@deriv/stores'; import EmptyMessage from 'AppV2/Components/EmptyMessage'; import { TEmptyMessageProps } from 'AppV2/Components/EmptyMessage/empty-message'; import { TPortfolioPosition } from '@deriv/stores/types'; import { ContractCardList } from 'AppV2/Components/ContractCard'; import { ContractTypeFilter, TimeFilter } from 'AppV2/Components/Filter'; -import { Loading } from '@deriv/components'; -import { observer, useStore } from '@deriv/stores'; import { filterPositions } from '../../Utils/positions-utils'; import { TReportsStore, useReportsStore } from '../../../../../reports/src/Stores/useReportsStores'; @@ -24,7 +26,8 @@ const PositionsContent = observer(({ hasButtonsDemo, isClosedTab, setHasButtonsD const [filteredPositions, setFilteredPositions] = React.useState<(TPortfolioPosition | TClosedPosition)[]>([]); const [noMatchesFound, setNoMatchesFound] = React.useState(false); - const { client, portfolio } = useStore(); + const { common, client, portfolio } = useStore(); + const { server_time = moment() } = isClosedTab ? {} : common; const { currency } = client; const { active_positions, is_active_empty, onClickCancel, onClickSell, onMount: onOpenTabMount } = portfolio; const { @@ -39,30 +42,35 @@ const PositionsContent = observer(({ hasButtonsDemo, isClosedTab, setHasButtonsD () => (isClosedTab ? closedPositions : active_positions), [active_positions, isClosedTab, closedPositions] ); - const emptyPositions = isClosedTab ? is_empty : is_active_empty; - const shouldShowEmptyMessage = emptyPositions || noMatchesFound; + const hasNoPositions = isClosedTab ? is_empty : is_active_empty; + const shouldShowEmptyMessage = hasNoPositions || noMatchesFound; + const shouldShowContractCards = + isClosedTab || (filteredPositions.length && (filteredPositions[0]?.contract_info as TContractInfo)?.status); React.useEffect(() => { isClosedTab ? onClosedTabMount() : onOpenTabMount(); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - React.useEffect(() => { - if (contractTypeFilter.length) { - const result = filterPositions(positions, contractTypeFilter); + React.useEffect(() => setFilteredPositions(positions), [positions]); + + const handleTradeTypeFilterChange = (filterValues: string[]) => { + setContractTypeFilter(filterValues); + if (filterValues.length) { + const result = filterPositions(positions, filterValues); setNoMatchesFound(!result.length); setFilteredPositions(result); } else { setNoMatchesFound(false); setFilteredPositions(positions); } - }, [contractTypeFilter, positions]); + }; - if (isLoading) return ; + if (isLoading || (!shouldShowContractCards && !shouldShowEmptyMessage)) return ; return (
- {!emptyPositions && ( + {!hasNoPositions && (
{isClosedTab && ( )} handleTradeTypeFilterChange(filterValues)} contractTypeFilter={contractTypeFilter} />
@@ -81,14 +89,17 @@ const PositionsContent = observer(({ hasButtonsDemo, isClosedTab, setHasButtonsD {shouldShowEmptyMessage ? ( ) : ( - + shouldShowContractCards && ( + + ) )}
); diff --git a/packages/trader/src/AppV2/Containers/Positions/positions.scss b/packages/trader/src/AppV2/Containers/Positions/positions.scss index 659b6cef9c17..928ab10cdd8f 100644 --- a/packages/trader/src/AppV2/Containers/Positions/positions.scss +++ b/packages/trader/src/AppV2/Containers/Positions/positions.scss @@ -35,7 +35,6 @@ $TABS_HEIGHT: 4.8rem; } } } - &__filter { &__wrapper { padding: var(--core-spacing-400); @@ -43,6 +42,9 @@ $TABS_HEIGHT: 4.8rem; gap: var(--core-spacing-400); } } + .initial-loader { + height: 100%; + } } // TODO: Temporary style, waiting for new Quill version From 3a5dc295147001ffbcfdf785fa8b76758fe4de9c Mon Sep 17 00:00:00 2001 From: kate-deriv <121025168+kate-deriv@users.noreply.github.com> Date: Fri, 24 May 2024 11:33:37 +0300 Subject: [PATCH 19/54] chore: update quill version (#63) --- packages/core/package.json | 2 +- packages/trader/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/package.json b/packages/core/package.json index 827abaa9a6b3..722a73f4e802 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -97,7 +97,7 @@ "@datadog/browser-rum": "^5.11.0", "@deriv-com/analytics": "1.4.13", "@deriv-com/quill-tokens": "^2.0.2", - "@deriv-com/quill-ui": "^1.10.2", + "@deriv-com/quill-ui": "^1.10.8", "@deriv-com/utils": "^0.0.20", "@deriv/account": "^1.0.0", "@deriv/account-v2": "^1.0.0", diff --git a/packages/trader/package.json b/packages/trader/package.json index 15ee44ee9492..27144620c36d 100644 --- a/packages/trader/package.json +++ b/packages/trader/package.json @@ -90,7 +90,7 @@ "@cloudflare/stream-react": "^1.9.1", "@deriv-com/analytics": "1.4.13", "@deriv-com/quill-tokens": "^2.0.2", - "@deriv-com/quill-ui": "^1.10.2", + "@deriv-com/quill-ui": "^1.10.8", "@deriv-com/utils": "^0.0.20", "@deriv/api-types": "^1.0.172", "@deriv/components": "^1.0.0", From 13254e61d1d42766a17484398411e621fd6bba80 Mon Sep 17 00:00:00 2001 From: Maryia <103177211+maryia-deriv@users.noreply.github.com> Date: Fri, 24 May 2024 14:47:24 +0300 Subject: [PATCH 20/54] Maryia/positions-redesign/Contract card loading state and status timer updates + EmptyPositions update (#64) * feat: loading functionality + fix for status timer * chore: update copy for empty-positions * revert: use hasActionButtons prop instead of impicit onClose --- .../ContractCard/contract-card-list.tsx | 20 +-- ...ion.tsx => contract-card-status-timer.tsx} | 23 ++-- .../ContractCard/contract-card.scss | 114 +++++++++++------- .../Components/ContractCard/contract-card.tsx | 60 +++++---- .../__tests__/empty-message.spec.tsx | 34 ------ .../AppV2/Components/EmptyMessage/index.ts | 4 - .../__tests__/empty-positions.spec.tsx | 34 ++++++ .../empty-positions.scss} | 2 +- .../empty-positions.tsx} | 14 +-- .../AppV2/Components/EmptyPositions/index.ts | 4 + .../Positions/positions-content.tsx | 7 +- 11 files changed, 172 insertions(+), 144 deletions(-) rename packages/trader/src/AppV2/Components/ContractCard/{contract-card-duration.tsx => contract-card-status-timer.tsx} (64%) delete mode 100644 packages/trader/src/AppV2/Components/EmptyMessage/__tests__/empty-message.spec.tsx delete mode 100644 packages/trader/src/AppV2/Components/EmptyMessage/index.ts create mode 100644 packages/trader/src/AppV2/Components/EmptyPositions/__tests__/empty-positions.spec.tsx rename packages/trader/src/AppV2/Components/{EmptyMessage/empty-message.scss => EmptyPositions/empty-positions.scss} (95%) rename packages/trader/src/AppV2/Components/{EmptyMessage/empty-message.tsx => EmptyPositions/empty-positions.tsx} (82%) create mode 100644 packages/trader/src/AppV2/Components/EmptyPositions/index.ts diff --git a/packages/trader/src/AppV2/Components/ContractCard/contract-card-list.tsx b/packages/trader/src/AppV2/Components/ContractCard/contract-card-list.tsx index 09e6f46b73c4..15a50d600aa9 100644 --- a/packages/trader/src/AppV2/Components/ContractCard/contract-card-list.tsx +++ b/packages/trader/src/AppV2/Components/ContractCard/contract-card-list.tsx @@ -25,27 +25,14 @@ const ContractCardList = ({ serverTime, setHasButtonsDemo, }: TContractCardListProps) => { - const closedCardsTimeouts = React.useRef>>([]); - React.useEffect(() => { - const timers = closedCardsTimeouts.current; - const demoTimeout = setTimeout(() => setHasButtonsDemo?.(false), 720); + const demoTimeout = setTimeout(() => setHasButtonsDemo?.(false), 720); // 720 is the length of demo animation return () => { - if (timers.length) { - timers.forEach(id => clearTimeout(id)); - } if (demoTimeout) clearTimeout(demoTimeout); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - const handleClose = (id: number, shouldCancel?: boolean) => { - const timeoutId = setTimeout(() => { - shouldCancel ? onClickCancel?.(id) : onClickSell?.(id); - }, 160); - closedCardsTimeouts.current.push(timeoutId); - }; - if (!positions.length) return null; return (
id && handleClose?.(id, true)} - onClose={() => id && handleClose?.(id)} + onCancel={() => id && onClickCancel?.(id)} + onClose={() => id && onClickSell?.(id)} redirectTo={id ? getContractPath(id) : undefined} serverTime={serverTime} /> diff --git a/packages/trader/src/AppV2/Components/ContractCard/contract-card-duration.tsx b/packages/trader/src/AppV2/Components/ContractCard/contract-card-status-timer.tsx similarity index 64% rename from packages/trader/src/AppV2/Components/ContractCard/contract-card-duration.tsx rename to packages/trader/src/AppV2/Components/ContractCard/contract-card-status-timer.tsx index 1b0e8f7662c2..e309adf877fe 100644 --- a/packages/trader/src/AppV2/Components/ContractCard/contract-card-duration.tsx +++ b/packages/trader/src/AppV2/Components/ContractCard/contract-card-status-timer.tsx @@ -7,35 +7,36 @@ import { getCardLabels } from '@deriv/shared'; import { RemainingTime } from '@deriv/components'; import { TRootStore } from 'Types'; -export type TContractCardDurationProps = Pick & { +export type TContractCardStatusTimerProps = Pick & { currentTick?: number | null; hasNoAutoExpiry?: boolean; + isSold?: boolean; serverTime: TRootStore['common']['server_time']; }; -export const ContractCardDuration = ({ +export const ContractCardStatusTimer = ({ currentTick, - expiry_time, + date_expiry, hasNoAutoExpiry, + isSold, serverTime, tick_count, -}: TContractCardDurationProps) => { +}: TContractCardStatusTimerProps) => { const getDisplayedDuration = () => { if (hasNoAutoExpiry) return ; if (tick_count) { return `${currentTick ?? 0}/${tick_count} ${getCardLabels().TICKS.toLowerCase()}`; } - return ; + return ; }; - - if (!expiry_time) return null; + if (!date_expiry || serverTime.unix() > +date_expiry || isSold) { + return {getCardLabels().CLOSED}; + } return ( // TODO: when is exported from quill-ui, use it instead -
+
- - {getDisplayedDuration()} - + {getDisplayedDuration()}
); }; diff --git a/packages/trader/src/AppV2/Components/ContractCard/contract-card.scss b/packages/trader/src/AppV2/Components/ContractCard/contract-card.scss index 22ff851e25f1..7fc1cba56025 100644 --- a/packages/trader/src/AppV2/Components/ContractCard/contract-card.scss +++ b/packages/trader/src/AppV2/Components/ContractCard/contract-card.scss @@ -9,7 +9,7 @@ justify-content: space-between; height: 10.4rem; transform: translateX(0); - transition: transform var(--motion-duration-snappy) var(--motion-easing-linear); + transition: transform var(--core-motion-duration-200) var(--motion-easing-inandout); .icon, .dc-icon { @@ -35,16 +35,16 @@ .details, .status-and-profit, .status, - &-duration { + .timer { display: flex; - gap: 0.8rem; + gap: var(--core-spacing-400); align-items: center; } .status, - &-duration { + .timer { background-color: var(--core-color-opacity-black-75); - height: 2.4rem; - padding: 0 0.8rem; + height: var(--core-size-1200); + padding: 0 var(--core-spacing-400); border-radius: var(--semantic-borderRadius-sm); .dc-remaining-time { @@ -65,7 +65,7 @@ text-overflow: ellipsis; &__icon { - padding: 0.4rem; + padding: var(--core-size-200); } } .symbol, @@ -83,33 +83,64 @@ width: fit-content; height: 100%; position: absolute; - top: 0; - right: 0; - bottom: 0; + inset-block-start: 0; + inset-inline-end: 0; + inset-block-end: 0; transform: translateX(100%); button { width: var(--core-size-3600); height: 100%; - p { + div { color: var(--core-color-solid-slate-50); } - - &:disabled { + &:disabled:not(.loading) { background-color: var(--core-color-opacity-black-200); - p { + div { color: var(--component-textIcon-normal-disabled); } } + .circle-loader { + width: var(--core-size-600); + height: var(--core-size-600); + display: grid; + border-radius: 50%; + mask: radial-gradient(farthest-side, #0000 50%, var(--core-color-solid-slate-1400) 51%); + background: linear-gradient(0deg, rgba(255, 255, 255, 0.5) 50%, var(--core-color-solid-slate-50) 0) + center/1px 100%, + linear-gradient(90deg, rgba(255, 255, 255, 0.25) 50%, rgba(255, 255, 255, 0.75) 0) center/100% 1px; + background-repeat: no-repeat; + animation: rotate-loader 1s infinite steps(8); + + &:before, + &:after { + content: ''; + grid-area: 1/1; + border-radius: 50%; + background: inherit; + opacity: 0.915; + transform: rotate(45deg); + } + &:after { + opacity: 0.83; + transform: rotate(90deg); + } + @keyframes rotate-loader { + 100% { + transform: rotate(360deg); + } + } + } } } &.lost { .total-profit { color: var(--core-color-solid-red-600); } - button:not(:disabled) { + button:not(:disabled), + button.loading { background-color: var(--core-color-solid-cherry-700); } .status { @@ -121,7 +152,8 @@ .total-profit { color: var(--core-color-solid-green-600); } - button:not(:disabled) { + button:not(:disabled), + button.loading { background-color: var(--core-color-solid-emerald-700); } .status { @@ -147,17 +179,17 @@ &.deleted { opacity: var(--core-opacity-50); max-height: 0; - transition: max-height var(--motion-duration-snappy), opacity var(--motion-duration-snappy); - transition-timing-function: var(--motion-easing-inandout); + transition: max-height 0.3s, opacity 0.3s; + transition-timing-function: var(--motion-easing-out); } } &-list { display: flex; flex-grow: 1; flex-direction: column; - gap: 0.8rem; + gap: var(--core-spacing-400); width: inherit; - padding: 0.8rem; + padding: var(--core-spacing-400); &--has-buttons-demo { .contract-card-wrapper:first-child { @@ -167,30 +199,28 @@ &.has-cancel-button { animation: var(--motion-duration-relax) var(--motion-easing-inandout) bounce-two-buttons; } + @keyframes bounce-one-button { + 0%, + 100% { + transform: translateX(0); + } + 30%, + 70% { + transform: translateX(calc(var(--core-size-3600) * -1)); + } + } + @keyframes bounce-two-buttons { + 0%, + 100% { + transform: translateX(0); + } + 30%, + 70% { + transform: translateX(calc(var(--core-size-3600) * -2)); + } + } } } } } } - -@keyframes bounce-one-button { - 0%, - 100% { - transform: translateX(0); - } - 30%, - 70% { - transform: translateX(calc(var(--core-size-3600) * -1)); - } -} - -@keyframes bounce-two-buttons { - 0%, - 100% { - transform: translateX(0); - } - 30%, - 70% { - transform: translateX(calc(var(--core-size-3600) * -2)); - } -} diff --git a/packages/trader/src/AppV2/Components/ContractCard/contract-card.tsx b/packages/trader/src/AppV2/Components/ContractCard/contract-card.tsx index b28b44ec7a87..6ddf98ab8e24 100644 --- a/packages/trader/src/AppV2/Components/ContractCard/contract-card.tsx +++ b/packages/trader/src/AppV2/Components/ContractCard/contract-card.tsx @@ -10,17 +10,18 @@ import { getMarketName, getTotalProfit, getTradeTypeName, + isEnded, isHighLow, isMultiplierContract, isValidToCancel, isValidToSell, } from '@deriv/shared'; -import { ContractCardDuration, TContractCardDurationProps } from './contract-card-duration'; +import { ContractCardStatusTimer, TContractCardStatusTimerProps } from './contract-card-status-timer'; import { BinaryLink } from 'App/Components/Routes'; import { TClosedPosition } from 'AppV2/Containers/Positions/positions-content'; import { TRootStore } from 'Types'; -type TContractCardProps = TContractCardDurationProps & { +type TContractCardProps = TContractCardStatusTimerProps & { className?: string; contractInfo: TContractInfo | TClosedPosition['contract_info']; currency?: string; @@ -47,7 +48,7 @@ const ContractCard = ({ className, contractInfo, currency, - hasActionButtons = true, + hasActionButtons, isSellRequested, onCancel, onClick, @@ -69,9 +70,12 @@ const ContractCard = ({ const symbolName = 'underlying_symbol' in contractInfo ? getMarketName(contractInfo.underlying_symbol ?? '') : display_name; const isMultiplier = isMultiplierContract(contract_type); - const totalProfit = isMultiplierContract(contract_type) - ? getTotalProfit(contractInfo as TContractInfo) - : (contractInfo as TContractInfo).profit ?? (contractInfo as TClosedPosition['contract_info']).profit_loss; + const isSold = isEnded(contractInfo as TContractInfo); + const totalProfit = + (contractInfo as TClosedPosition['contract_info']).profit_loss ?? + (isMultiplierContract(contract_type) + ? getTotalProfit(contractInfo as TContractInfo) + : (contractInfo as TContractInfo).profit); const validToCancel = isValidToCancel(contractInfo as TContractInfo); const validToSell = isValidToSell(contractInfo as TContractInfo) && !isSellRequested; @@ -89,20 +93,25 @@ const ContractCard = ({ const handleClose = (e: React.MouseEvent, shouldCancel?: boolean) => { e.preventDefault(); e.stopPropagation(); - setIsDeleted(true); shouldCancel ? onCancel?.(e) : onClose?.(e); }; + React.useEffect(() => { + if (isSold && hasActionButtons) { + setIsDeleted(true); + } + }, [isSold, hasActionButtons]); + if (!contract_type) return null; return (
0, + won: Number(totalProfit) >= 0, })} onClick={onClick} onDragStart={e => e.preventDefault()} @@ -124,40 +133,41 @@ const ContractCard = ({
- {sell_time ? ( - {getCardLabels().CLOSED} - ) : ( - - )} +
- {!sell_time && hasActionButtons && ( + {hasActionButtons && (
{validToCancel && ( )}
)} @@ -166,4 +176,4 @@ const ContractCard = ({ ); }; -export default React.memo(ContractCard); +export default ContractCard; diff --git a/packages/trader/src/AppV2/Components/EmptyMessage/__tests__/empty-message.spec.tsx b/packages/trader/src/AppV2/Components/EmptyMessage/__tests__/empty-message.spec.tsx deleted file mode 100644 index 9f8423ca472a..000000000000 --- a/packages/trader/src/AppV2/Components/EmptyMessage/__tests__/empty-message.spec.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import React from 'react'; -import { render, screen } from '@testing-library/react'; -import EmptyMessage from '../empty-message'; - -describe('EmptyMessage', () => { - const iconId = 'dt_empty_state_icon'; - - it('should render "No open positions" content when isClosedTab prop is false', () => { - render(); - expect(screen.getByText('No open positions')).toBeInTheDocument(); - expect(screen.getByText('Your open trades will appear here.')).toBeInTheDocument(); - }); - - it('should render "No closed positions" content when isClosedTab prop is true', () => { - render(); - expect(screen.getByText('No closed positions')).toBeInTheDocument(); - expect(screen.getByText('Your completed trades will appear here.')).toBeInTheDocument(); - }); - - it('should render "No matches found" content when noMatchesFound prop is true', () => { - render(); - expect(screen.getByText('No matches found')).toBeInTheDocument(); - expect(screen.getByText(/Try changing or removing filters/i)).toBeInTheDocument(); - }); - - it('should render an empty state icon regardless of props', () => { - const { rerender } = render(); - expect(screen.getByTestId(iconId)).toBeInTheDocument(); - rerender(); - expect(screen.getByTestId(iconId)).toBeInTheDocument(); - rerender(); - expect(screen.getByTestId(iconId)).toBeInTheDocument(); - }); -}); diff --git a/packages/trader/src/AppV2/Components/EmptyMessage/index.ts b/packages/trader/src/AppV2/Components/EmptyMessage/index.ts deleted file mode 100644 index d30c40d4fc01..000000000000 --- a/packages/trader/src/AppV2/Components/EmptyMessage/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -import EmptyMessage from './empty-message'; -import './empty-message.scss'; - -export default EmptyMessage; diff --git a/packages/trader/src/AppV2/Components/EmptyPositions/__tests__/empty-positions.spec.tsx b/packages/trader/src/AppV2/Components/EmptyPositions/__tests__/empty-positions.spec.tsx new file mode 100644 index 000000000000..c5fc07361abe --- /dev/null +++ b/packages/trader/src/AppV2/Components/EmptyPositions/__tests__/empty-positions.spec.tsx @@ -0,0 +1,34 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import EmptyPositions from '../empty-positions'; + +describe('EmptyPositions', () => { + const iconId = 'dt_empty_state_icon'; + + it('should render "No open trades" content when isClosedTab prop is false', () => { + render(); + expect(screen.getByText('No open trades')).toBeInTheDocument(); + expect(screen.getByText('Your open trades will appear here.')).toBeInTheDocument(); + }); + + it('should render "No closed trades" content when isClosedTab prop is true', () => { + render(); + expect(screen.getByText('No closed trades')).toBeInTheDocument(); + expect(screen.getByText('Your closed trades will be shown here.')).toBeInTheDocument(); + }); + + it('should render "No matches found" content when noMatchesFound prop is true', () => { + render(); + expect(screen.getByText('No matches found')).toBeInTheDocument(); + expect(screen.getByText(/Try changing or removing filters/i)).toBeInTheDocument(); + }); + + it('should render an empty state icon regardless of props', () => { + const { rerender } = render(); + expect(screen.getByTestId(iconId)).toBeInTheDocument(); + rerender(); + expect(screen.getByTestId(iconId)).toBeInTheDocument(); + rerender(); + expect(screen.getByTestId(iconId)).toBeInTheDocument(); + }); +}); diff --git a/packages/trader/src/AppV2/Components/EmptyMessage/empty-message.scss b/packages/trader/src/AppV2/Components/EmptyPositions/empty-positions.scss similarity index 95% rename from packages/trader/src/AppV2/Components/EmptyMessage/empty-message.scss rename to packages/trader/src/AppV2/Components/EmptyPositions/empty-positions.scss index ded6dc0851ad..07ed9f3c1926 100644 --- a/packages/trader/src/AppV2/Components/EmptyMessage/empty-message.scss +++ b/packages/trader/src/AppV2/Components/EmptyPositions/empty-positions.scss @@ -1,4 +1,4 @@ -.empty-message { +.empty-positions { &__open, &__closed { display: flex; diff --git a/packages/trader/src/AppV2/Components/EmptyMessage/empty-message.tsx b/packages/trader/src/AppV2/Components/EmptyPositions/empty-positions.tsx similarity index 82% rename from packages/trader/src/AppV2/Components/EmptyMessage/empty-message.tsx rename to packages/trader/src/AppV2/Components/EmptyPositions/empty-positions.tsx index e3950c55e0a7..2abcaaf7b621 100644 --- a/packages/trader/src/AppV2/Components/EmptyMessage/empty-message.tsx +++ b/packages/trader/src/AppV2/Components/EmptyPositions/empty-positions.tsx @@ -3,16 +3,16 @@ import { Text } from '@deriv-com/quill-ui'; import { StandaloneBriefcaseFillIcon, StandaloneSearchFillIcon } from '@deriv/quill-icons'; import { Localize } from '@deriv/translations'; -export type TEmptyMessageProps = { +export type TEmptyPositionsProps = { isClosedTab?: boolean; noMatchesFound?: boolean; }; -const EmptyMessage = ({ isClosedTab, noMatchesFound }: TEmptyMessageProps) => { +const EmptyPositions = ({ isClosedTab, noMatchesFound }: TEmptyPositionsProps) => { const Icon = noMatchesFound ? StandaloneSearchFillIcon : StandaloneBriefcaseFillIcon; return ( -
+
@@ -22,9 +22,9 @@ const EmptyMessage = ({ isClosedTab, noMatchesFound }: TEmptyMessageProps) => { {noMatchesFound && } {!noMatchesFound && (isClosedTab ? ( - + ) : ( - + ))} @@ -33,7 +33,7 @@ const EmptyMessage = ({ isClosedTab, noMatchesFound }: TEmptyMessageProps) => { )} {!noMatchesFound && (isClosedTab ? ( - + ) : ( ))} @@ -43,4 +43,4 @@ const EmptyMessage = ({ isClosedTab, noMatchesFound }: TEmptyMessageProps) => { ); }; -export default EmptyMessage; +export default EmptyPositions; diff --git a/packages/trader/src/AppV2/Components/EmptyPositions/index.ts b/packages/trader/src/AppV2/Components/EmptyPositions/index.ts new file mode 100644 index 000000000000..f3ead86e9adf --- /dev/null +++ b/packages/trader/src/AppV2/Components/EmptyPositions/index.ts @@ -0,0 +1,4 @@ +import './empty-positions.scss'; + +export { default as EmptyPositions } from './empty-positions'; +export type { TEmptyPositionsProps } from './empty-positions'; diff --git a/packages/trader/src/AppV2/Containers/Positions/positions-content.tsx b/packages/trader/src/AppV2/Containers/Positions/positions-content.tsx index 9e0d0f9336ca..8cfe9b4b9e7e 100644 --- a/packages/trader/src/AppV2/Containers/Positions/positions-content.tsx +++ b/packages/trader/src/AppV2/Containers/Positions/positions-content.tsx @@ -3,15 +3,14 @@ import moment from 'moment'; import { TContractInfo } from '@deriv/shared'; import { Loading } from '@deriv/components'; import { observer, useStore } from '@deriv/stores'; -import EmptyMessage from 'AppV2/Components/EmptyMessage'; -import { TEmptyMessageProps } from 'AppV2/Components/EmptyMessage/empty-message'; +import { EmptyPositions, TEmptyPositionsProps } from 'AppV2/Components/EmptyPositions'; import { TPortfolioPosition } from '@deriv/stores/types'; import { ContractCardList } from 'AppV2/Components/ContractCard'; import { ContractTypeFilter, TimeFilter } from 'AppV2/Components/Filter'; import { filterPositions } from '../../Utils/positions-utils'; import { TReportsStore, useReportsStore } from '../../../../../reports/src/Stores/useReportsStores'; -type TPositionsContentProps = Omit & { +type TPositionsContentProps = Omit & { hasButtonsDemo?: boolean; setHasButtonsDemo?: React.Dispatch>; }; @@ -87,7 +86,7 @@ const PositionsContent = observer(({ hasButtonsDemo, isClosedTab, setHasButtonsD )}
{shouldShowEmptyMessage ? ( - + ) : ( shouldShowContractCards && ( Date: Fri, 24 May 2024 16:23:34 +0300 Subject: [PATCH 21/54] DTRA-1279/ Kate/ Feat: add Date picker (#62) * feat: add second action sheet * feat: add date range formatting and refactored existing code * feat: add range selection filtration * refactor: chip and time filter * fix: empty posituions after filtration * refactor: do clean up * chore: rename variables --- .../src/AppV2/Components/Chip/chip.scss | 45 ++------------ .../trader/src/AppV2/Components/Chip/chip.tsx | 26 ++------ .../Components/DatePicker/date-picker.scss | 15 +++++ .../Components/DatePicker/date-picker.tsx | 53 ++++++++++++++++ .../src/AppV2/Components/DatePicker/index.ts | 4 ++ .../EmptyPositions/empty-positions.scss | 4 ++ .../EmptyPositions/empty-positions.tsx | 2 +- .../Filter/contract-type-filter.tsx | 4 +- .../Filter/custom-time-filter-button.tsx | 25 ++++++++ .../src/AppV2/Components/Filter/filter.scss | 33 ++++++++++ .../AppV2/Components/Filter/time-filter.tsx | 62 ++++++++++++++----- .../Positions/positions-content.tsx | 16 ++++- .../AppV2/Containers/Positions/positions.scss | 12 +--- 13 files changed, 209 insertions(+), 92 deletions(-) create mode 100644 packages/trader/src/AppV2/Components/DatePicker/date-picker.scss create mode 100644 packages/trader/src/AppV2/Components/DatePicker/date-picker.tsx create mode 100644 packages/trader/src/AppV2/Components/DatePicker/index.ts create mode 100644 packages/trader/src/AppV2/Components/Filter/custom-time-filter-button.tsx diff --git a/packages/trader/src/AppV2/Components/Chip/chip.scss b/packages/trader/src/AppV2/Components/Chip/chip.scss index c093730835eb..74be670a031a 100644 --- a/packages/trader/src/AppV2/Components/Chip/chip.scss +++ b/packages/trader/src/AppV2/Components/Chip/chip.scss @@ -7,6 +7,9 @@ border-color: var(--component-chip-border-color); border-radius: var(--component-chip-border-radius); background-color: var(--core-color-solid-slate-50); + padding-inline: var(--component-chip-spacing-padding-lg); + height: var(--component-chip-height-md); + gap: var(--component-chip-spacing-padding-sm); &:hover { background-color: var(--component-chip-bg-hover); @@ -67,48 +70,8 @@ } } - &__disabled { - &--true { - & > svg { - fill: var(--component-chip-icon-disabled) !important; - } - - & > p { - color: var(--component-chip-label-color-disabled) !important; - } - } - } - - &__size { - &--sm { - padding-inline: var(--component-chip-spacing-padding-md); - height: var(--component-chip-height-sm); - gap: var(--component-chip-spacing-padding-xs); - } - &--md { - padding-inline: var(--component-chip-spacing-padding-lg); - height: var(--component-chip-height-md); - gap: var(--component-chip-spacing-padding-sm); - } - &--lg { - padding-inline: var(--component-chip-spacing-padding-xl); - height: var(--component-chip-height-lg); - gap: var(--component-chip-spacing-padding-md); - } - } - &__custom-right-padding { - &__size { - &--sm { - padding-inline: var(--component-chip-spacing-padding-md) var(--component-chip-spacing-padding-xs); - } - &--md { - padding-inline: var(--component-chip-spacing-padding-lg) var(--component-chip-spacing-padding-sm); - } - &--lg { - padding-inline: var(--component-chip-spacing-padding-xl) var(--component-chip-spacing-padding-md); - } - } + padding-inline: var(--component-chip-spacing-padding-lg) var(--component-chip-spacing-padding-sm); } } diff --git a/packages/trader/src/AppV2/Components/Chip/chip.tsx b/packages/trader/src/AppV2/Components/Chip/chip.tsx index 2c174debf5ae..01bb389074e9 100644 --- a/packages/trader/src/AppV2/Components/Chip/chip.tsx +++ b/packages/trader/src/AppV2/Components/Chip/chip.tsx @@ -1,16 +1,10 @@ import React from 'react'; -import { StandaloneChevronDownRegularIcon } from '@deriv/quill-icons'; +import { LabelPairedChevronDownSmRegularIcon } from '@deriv/quill-icons'; import './chip.scss'; import clsx from 'clsx'; -import { CaptionText, Text } from '@deriv-com/quill-ui'; +import { Text } from '@deriv-com/quill-ui'; import { TRegularSizes } from '@deriv-com/quill-ui/dist/types'; -export const LabelTextSizes: Record = { - sm: , - md: , - lg: , -}; - type BaseChipProps = Omit, 'label'> & { label?: React.ReactNode; disabled?: boolean; @@ -25,24 +19,14 @@ const Chip = React.forwardRef( ({ size = 'md', label, dropdown = false, className, selected, isDropdownOpen = false, onClick, ...rest }, ref) => ( +); + +export default CustomDateFilterButton; diff --git a/packages/trader/src/AppV2/Components/Filter/filter.scss b/packages/trader/src/AppV2/Components/Filter/filter.scss index 9c8444e21dfb..50487a926c16 100644 --- a/packages/trader/src/AppV2/Components/Filter/filter.scss +++ b/packages/trader/src/AppV2/Components/Filter/filter.scss @@ -5,8 +5,41 @@ height: var(--core-size-2000); padding-block: var(--core-spacing-400); + label { + flex-grow: 1; + } + &__wrapper { padding-block: var(--core-spacing-800); + + .quill-radio-button { + padding-block: var(--core-spacing-400); + width: 100%; + + &__label { + font-size: var(--core-fontSize-100); + flex-grow: 1; + margin: 0; + } + } } } } + +.custom-time-filter { + &__wrapper { + display: flex; + gap: var(--core-spacing-400); + margin-block: var(--core-spacing-400); + width: 100%; + } + + &__label { + flex-grow: 1; + } + + &__icon { + width: var(--core-size-1200); + height: var(--core-size-1200); + } +} diff --git a/packages/trader/src/AppV2/Components/Filter/time-filter.tsx b/packages/trader/src/AppV2/Components/Filter/time-filter.tsx index 37f021bd1ad0..25aa16b6ce02 100644 --- a/packages/trader/src/AppV2/Components/Filter/time-filter.tsx +++ b/packages/trader/src/AppV2/Components/Filter/time-filter.tsx @@ -3,19 +3,16 @@ import Chip from 'AppV2/Components/Chip'; import { toMoment } from '@deriv/shared'; import { ActionSheet, RadioGroup } from '@deriv-com/quill-ui'; import { Localize } from '@deriv/translations'; +import CustomDateFilterButton from './custom-time-filter-button'; +import DateRangePicker from 'AppV2/Components/DatePicker'; type TTimeFilter = { + handleDateChange: (values: { to?: moment.Moment; from?: moment.Moment; is_batch?: boolean }) => void; chosenTimeFilter?: string; - setChosenTimeFilter: React.Dispatch>; - handleDateChange: ( - date_values: { from?: moment.Moment; to: moment.Moment; is_batch: boolean }, - { - date_range, - }?: { - // TODO: refactor type - date_range: any; - } - ) => void; + setChosenTimeFilter: React.Dispatch>; + selectedRangeDateString?: string; + setSelectedDateRangeString: React.Dispatch>; + setNoMatchesFound: React.Dispatch>; }; // TODO: replace strings with numbers when types in Quill be changed @@ -24,6 +21,7 @@ const timeFilterList = [ value: '0', label: , }, + // TODO: Fix today and yesterday { value: '1', label: , @@ -50,8 +48,16 @@ const timeFilterList = [ }, ]; -const TimeFilter = ({ chosenTimeFilter, setChosenTimeFilter, handleDateChange }: TTimeFilter) => { +const TimeFilter = ({ + handleDateChange, + chosenTimeFilter, + setChosenTimeFilter, + selectedRangeDateString, + setSelectedDateRangeString, + setNoMatchesFound, +}: TTimeFilter) => { const [isDropdownOpen, setIsDropdownOpen] = React.useState(false); + const [showDatePicker, setShowDatePicker] = React.useState(false); const defaultCheckedTime = '0'; @@ -72,6 +78,7 @@ const TimeFilter = ({ chosenTimeFilter, setChosenTimeFilter, handleDateChange }: const onReset = () => { setChosenTimeFilter(''); + setSelectedDateRangeString(''); setIsDropdownOpen(false); handleDateChange({ from: Number(defaultCheckedTime) @@ -80,9 +87,11 @@ const TimeFilter = ({ chosenTimeFilter, setChosenTimeFilter, handleDateChange }: to: toMoment().endOf('day'), is_batch: true, }); + setNoMatchesFound(false); }; const chipLabelFormatting = () => + selectedRangeDateString || timeFilterList.find(item => item.value === (chosenTimeFilter || defaultCheckedTime))?.label; return ( @@ -92,19 +101,32 @@ const TimeFilter = ({ chosenTimeFilter, setChosenTimeFilter, handleDateChange }: dropdown isDropdownOpen={isDropdownOpen} onClick={() => setIsDropdownOpen(!isDropdownOpen)} - selected={!!chosenTimeFilter} + selected={!!(selectedRangeDateString || chosenTimeFilter)} + size='sm' /> setIsDropdownOpen(false)} position='left'> } /> - + {timeFilterList.map(({ value, label }) => ( - + ))} - {/* TODO: Replace with real component*/} -
Custom
+
+ {showDatePicker && ( + setShowDatePicker(false)} + setSelectedDateRangeString={setSelectedDateRangeString} + handleDateChange={handleDateChange} + /> + )} ); }; diff --git a/packages/trader/src/AppV2/Containers/Positions/positions-content.tsx b/packages/trader/src/AppV2/Containers/Positions/positions-content.tsx index 8cfe9b4b9e7e..62ad9a955f0b 100644 --- a/packages/trader/src/AppV2/Containers/Positions/positions-content.tsx +++ b/packages/trader/src/AppV2/Containers/Positions/positions-content.tsx @@ -21,8 +21,9 @@ export type TClosedPosition = { const PositionsContent = observer(({ hasButtonsDemo, isClosedTab, setHasButtonsDemo }: TPositionsContentProps) => { const [contractTypeFilter, setContractTypeFilter] = React.useState([]); - const [chosenTimeFilter, setChosenTimeFilter] = React.useState(''); + const [chosenTimeFilter, setChosenTimeFilter] = React.useState(); const [filteredPositions, setFilteredPositions] = React.useState<(TPortfolioPosition | TClosedPosition)[]>([]); + const [selectedRangeDateString, setSelectedDateRangeString] = React.useState(); const [noMatchesFound, setNoMatchesFound] = React.useState(false); const { common, client, portfolio } = useStore(); @@ -41,7 +42,7 @@ const PositionsContent = observer(({ hasButtonsDemo, isClosedTab, setHasButtonsD () => (isClosedTab ? closedPositions : active_positions), [active_positions, isClosedTab, closedPositions] ); - const hasNoPositions = isClosedTab ? is_empty : is_active_empty; + const hasNoPositions = isClosedTab ? is_empty && !chosenTimeFilter && !selectedRangeDateString : is_active_empty; const shouldShowEmptyMessage = hasNoPositions || noMatchesFound; const shouldShowContractCards = isClosedTab || (filteredPositions.length && (filteredPositions[0]?.contract_info as TContractInfo)?.status); @@ -65,6 +66,14 @@ const PositionsContent = observer(({ hasButtonsDemo, isClosedTab, setHasButtonsD } }; + React.useEffect(() => { + if (!positions.length && isClosedTab && (selectedRangeDateString || chosenTimeFilter)) { + setNoMatchesFound(true); + } else { + setNoMatchesFound(false); + } + }, [selectedRangeDateString, chosenTimeFilter, positions, isClosedTab]); + if (isLoading || (!shouldShowContractCards && !shouldShowEmptyMessage)) return ; return (
@@ -76,6 +85,9 @@ const PositionsContent = observer(({ hasButtonsDemo, isClosedTab, setHasButtonsD chosenTimeFilter={chosenTimeFilter} setChosenTimeFilter={setChosenTimeFilter} handleDateChange={handleDateChange} + selectedRangeDateString={selectedRangeDateString} + setSelectedDateRangeString={setSelectedDateRangeString} + setNoMatchesFound={setNoMatchesFound} /> )} Date: Fri, 24 May 2024 16:35:03 +0300 Subject: [PATCH 22/54] chore: localization --- .../trader/src/AppV2/Components/Filter/time-filter.tsx | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/packages/trader/src/AppV2/Components/Filter/time-filter.tsx b/packages/trader/src/AppV2/Components/Filter/time-filter.tsx index 25aa16b6ce02..b225f12b3482 100644 --- a/packages/trader/src/AppV2/Components/Filter/time-filter.tsx +++ b/packages/trader/src/AppV2/Components/Filter/time-filter.tsx @@ -115,12 +115,7 @@ const TimeFilter = ({ className='filter__item--radio' > {timeFilterList.map(({ value, label }) => ( - + ))} Date: Sat, 25 May 2024 19:15:32 +0300 Subject: [PATCH 23/54] DTRA-1279 / Kate/ Add filtration hooks (#65) * feat: create hooks * refactor: rename methods --- .../Components/DatePicker/date-picker.tsx | 6 ++-- .../Filter/custom-time-filter-button.tsx | 8 ++--- .../AppV2/Components/Filter/time-filter.tsx | 35 ++++++++++--------- .../Positions/positions-content.tsx | 22 ++++++------ .../trader/src/AppV2/Hooks/useTimeFilter.ts | 10 ++++++ .../src/AppV2/Hooks/useTradeTypeFilter.ts | 16 +++++++++ .../Modules/Positions/positions-store.ts | 25 +++++++++++-- 7 files changed, 85 insertions(+), 37 deletions(-) create mode 100644 packages/trader/src/AppV2/Hooks/useTimeFilter.ts create mode 100644 packages/trader/src/AppV2/Hooks/useTradeTypeFilter.ts diff --git a/packages/trader/src/AppV2/Components/DatePicker/date-picker.tsx b/packages/trader/src/AppV2/Components/DatePicker/date-picker.tsx index 70d246f5e194..0545265ffa3c 100644 --- a/packages/trader/src/AppV2/Components/DatePicker/date-picker.tsx +++ b/packages/trader/src/AppV2/Components/DatePicker/date-picker.tsx @@ -6,15 +6,15 @@ import { Localize } from '@deriv/translations'; type TDateRangePicker = { isOpen?: boolean; onClose: () => void; - setSelectedDateRangeString: React.Dispatch>; + setCustomTimeRangeFilter: (newCustomTimeFilter?: string | undefined) => void; handleDateChange: (values: { to?: moment.Moment; from?: moment.Moment; is_batch?: boolean }) => void; }; -const DateRangePicker = ({ isOpen, onClose, setSelectedDateRangeString, handleDateChange }: TDateRangePicker) => { +const DateRangePicker = ({ isOpen, onClose, setCustomTimeRangeFilter, handleDateChange }: TDateRangePicker) => { const [chosenRangeString, setChosenRangeString] = React.useState(); const [chosenRange, setChosenRange] = React.useState<(string | null | Date)[] | null | Date>([]); const onApply = () => { - setSelectedDateRangeString(chosenRangeString); + setCustomTimeRangeFilter(chosenRangeString); if (Array.isArray(chosenRange) && chosenRange.length) handleDateChange({ from: toMoment(chosenRange[0]), to: toMoment(chosenRange[1]) }); onClose(); diff --git a/packages/trader/src/AppV2/Components/Filter/custom-time-filter-button.tsx b/packages/trader/src/AppV2/Components/Filter/custom-time-filter-button.tsx index 5946b09c3ca6..20bd4042c589 100644 --- a/packages/trader/src/AppV2/Components/Filter/custom-time-filter-button.tsx +++ b/packages/trader/src/AppV2/Components/Filter/custom-time-filter-button.tsx @@ -5,17 +5,17 @@ import { Localize } from '@deriv/translations'; type TCustomDateFilterButton = { setShowDatePicker: React.Dispatch>; - selectedRangeDateString?: string; + customTimeRangeFilter?: string; }; -const CustomDateFilterButton = ({ setShowDatePicker, selectedRangeDateString }: TCustomDateFilterButton) => ( +const CustomDateFilterButton = ({ setShowDatePicker, customTimeRangeFilter }: TCustomDateFilterButton) => ( diff --git a/packages/trader/src/AppV2/Components/DatePicker/__tests__/date-picker.spec.tsx b/packages/trader/src/AppV2/Components/DatePicker/__tests__/date-picker.spec.tsx new file mode 100644 index 000000000000..07d8df80cd61 --- /dev/null +++ b/packages/trader/src/AppV2/Components/DatePicker/__tests__/date-picker.spec.tsx @@ -0,0 +1,44 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import DateRangePicker from '../date-picker'; + +const header = 'Choose a date range'; +const footer = 'Apply'; +const mockProps = { + isOpen: true, + onClose: jest.fn(), + setCustomTimeRangeFilter: jest.fn(), + handleDateChange: jest.fn(), +}; +const mediaQueryList = { + matches: true, + addEventListener: jest.fn(), + removeEventListener: jest.fn(), +}; + +window.matchMedia = jest.fn().mockImplementation(() => mediaQueryList); + +describe('DateRangePicker', () => { + it('should render Action Sheet with Date Picker', () => { + render(); + + expect(screen.getByText(header)).toBeInTheDocument(); + expect(screen.getByText(footer)).toBeInTheDocument(); + }); + + it('should call setCustomTimeRangeFilter, handleDateChange and onClose if user choses some range date and clicks on Apply button', () => { + render(); + + const fromDate = screen.getByText('1'); + const toDate = screen.getByText('2'); + const applyButton = screen.getByText(footer); + userEvent.click(fromDate); + userEvent.click(toDate); + userEvent.click(applyButton); + + expect(mockProps.setCustomTimeRangeFilter).toBeCalled(); + expect(mockProps.handleDateChange).toBeCalled(); + expect(mockProps.onClose).toBeCalled(); + }); +}); diff --git a/packages/trader/src/AppV2/Components/Filter/__tests__/contract-type-filter.spec.tsx b/packages/trader/src/AppV2/Components/Filter/__tests__/contract-type-filter.spec.tsx new file mode 100644 index 000000000000..f41e66e4541e --- /dev/null +++ b/packages/trader/src/AppV2/Components/Filter/__tests__/contract-type-filter.spec.tsx @@ -0,0 +1,89 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import ContractTypeFilter from '../contract-type-filter'; + +const defaultFilterName = 'All trade types'; +const mockProps = { + setContractTypeFilter: jest.fn(), + contractTypeFilter: [], +}; +const mediaQueryList = { + matches: true, + addEventListener: jest.fn(), + removeEventListener: jest.fn(), +}; + +window.matchMedia = jest.fn().mockImplementation(() => mediaQueryList); + +describe('ContractTypeFilter', () => { + it('should change data-state of the dropdown if user clicks on the filter', () => { + render(); + + const dropdownChevron = screen.getByTestId('dt_chevron'); + expect(dropdownChevron).toHaveAttribute('data-state', 'close'); + + userEvent.click(screen.getByText(defaultFilterName)); + expect(dropdownChevron).toHaveAttribute('data-state', 'open'); + }); + + it('should render correct chip name if contractTypeFilter is with single item', () => { + const mockContractTypeFilter = ['Multipliers']; + + render(); + + expect(screen.queryByText(defaultFilterName)).not.toBeInTheDocument(); + expect(screen.getAllByText(mockContractTypeFilter[0])).toHaveLength(2); + }); + + it('should render correct chip name is contractTypeFilter is with multiple items', () => { + const mockContractTypeFilter = ['Vanillas', 'Turbos']; + render(); + + expect(screen.queryByText(defaultFilterName)).not.toBeInTheDocument(); + expect(screen.getByText(`${mockContractTypeFilter.length} trade types`)).toBeInTheDocument(); + }); + + it('should call setContractTypeFilter and setter (spied on) with array with chosen option after user clicks on contract type and clicks on "Apply" button', async () => { + const mockSetChangedOptions = jest.fn(); + jest.spyOn(React, 'useState') + .mockImplementationOnce(() => [false, jest.fn()]) + .mockImplementationOnce(() => [[], mockSetChangedOptions]); + + render(); + + userEvent.click(screen.getByText('Accumulators')); + userEvent.click(screen.getByText('Apply')); + + expect(mockSetChangedOptions).toHaveBeenCalledWith(['Accumulators']); + expect(mockProps.setContractTypeFilter).toBeCalled(); + }); + + it('should call setter (spied on) with array without chosen option if user clicks on it, but it was already in contractTypeFilter', async () => { + const mockContractTypeFilter = ['Rise/Fall', 'Higher/Lower']; + const mockSetChangedOptions = jest.fn(); + jest.spyOn(React, 'useState') + .mockImplementationOnce(() => [false, jest.fn()]) + .mockImplementationOnce(() => [mockContractTypeFilter, mockSetChangedOptions]); + + render(); + + userEvent.click(screen.getByText('Rise/Fall')); + + expect(mockSetChangedOptions).toHaveBeenCalledWith(['Higher/Lower']); + }); + + it('should call setter (spied on) with empty array if user clicks on "Clear All" button', async () => { + const mockContractTypeFilter = ['Touch/No Touch']; + const mockSetChangedOptions = jest.fn(); + jest.spyOn(React, 'useState') + .mockImplementationOnce(() => [false, jest.fn()]) + .mockImplementationOnce(() => [mockContractTypeFilter, mockSetChangedOptions]); + + render(); + + userEvent.click(screen.getByText('Clear All')); + + expect(mockSetChangedOptions).toHaveBeenCalledWith([]); + }); +}); diff --git a/packages/trader/src/AppV2/Components/Filter/__tests__/custom-time-filter-button.spec.tsx b/packages/trader/src/AppV2/Components/Filter/__tests__/custom-time-filter-button.spec.tsx new file mode 100644 index 000000000000..2ecc49d5a3df --- /dev/null +++ b/packages/trader/src/AppV2/Components/Filter/__tests__/custom-time-filter-button.spec.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import CustomDateFilterButton from '../custom-time-filter-button'; + +const customTimeRangeFilter = '25 May 2024'; +const mockProps = { + setShowDatePicker: jest.fn(), +}; + +describe('CustomDateFilterButton', () => { + it('should render component with default props', () => { + render(); + + expect(screen.getByText('Custom')).toBeInTheDocument(); + }); + + it('should render component with customTimeRangeFilter if it was passed', () => { + render(); + + expect(screen.getByText(customTimeRangeFilter)).toBeInTheDocument(); + }); + + it('should call setShowDatePicker if user clicks on the component', () => { + render(); + + userEvent.click(screen.getByText('Custom')); + expect(mockProps.setShowDatePicker).toBeCalled(); + }); +}); diff --git a/packages/trader/src/AppV2/Utils/__tests__/positions-utils.spec.ts b/packages/trader/src/AppV2/Utils/__tests__/positions-utils.spec.ts new file mode 100644 index 000000000000..1acabcff2d60 --- /dev/null +++ b/packages/trader/src/AppV2/Utils/__tests__/positions-utils.spec.ts @@ -0,0 +1,278 @@ +import { TPortfolioPosition } from '@deriv/stores/types'; +import { filterPositions } from '../positions-utils'; + +const mockedActivePositions = [ + { + contract_info: { + account_id: 112905368, + barrier: '682.60', + barrier_count: 1, + bid_price: 6.38, + buy_price: 9, + contract_id: 242807007748, + contract_type: 'CALL', + currency: 'USD', + current_spot: 681.76, + current_spot_display_value: '681.76', + current_spot_time: 1716220628, + date_expiry: 1716221100, + date_settlement: 1716221100, + date_start: 1716220562, + display_name: 'Volatility 100 (1s) Index', + entry_spot: 682.6, + entry_spot_display_value: '682.60', + entry_tick: 682.6, + entry_tick_display_value: '682.60', + entry_tick_time: 1716220563, + expiry_time: 1716221100, + id: '917d1b48-305b-a2f4-5b9c-7fb1f2c6c145', + is_expired: 0, + is_forward_starting: 0, + is_intraday: 1, + is_path_dependent: 0, + is_settleable: 0, + is_sold: 0, + is_valid_to_cancel: 0, + is_valid_to_sell: 1, + longcode: + 'Win payout if Volatility 100 (1s) Index is strictly higher than entry spot at 2024-05-20 16:05:00 GMT.', + payout: 17.61, + profit: -2.62, + profit_percentage: -29.11, + purchase_time: 1716220562, + shortcode: 'CALL_1HZ100V_17.61_1716220562_1716221100F_S0P_0', + status: 'open', + transaction_ids: { + buy: 484286139408, + }, + underlying: '1HZ100V', + }, + details: + 'Win payout if Volatility 100 (1s) Index is strictly higher than entry spot at 2024-05-20 16:05:00 GMT.', + display_name: '', + id: 242807007748, + indicative: 6.38, + payout: 17.61, + purchase: 9, + reference: 484286139408, + type: 'CALL', + profit_loss: -2.62, + is_valid_to_sell: true, + status: 'profit', + barrier: 682.6, + entry_spot: 682.6, + }, + { + contract_info: { + account_id: 112905368, + barrier_count: 1, + bid_price: 8.9, + buy_price: 9.39, + cancellation: { + ask_price: 0.39, + date_expiry: 1716224183, + }, + commission: 0.03, + contract_id: 242807045608, + contract_type: 'MULTUP', + currency: 'USD', + current_spot: 681.71, + current_spot_display_value: '681.71', + current_spot_time: 1716220672, + date_expiry: 4869849599, + date_settlement: 4869849600, + date_start: 1716220583, + display_name: 'Volatility 100 (1s) Index', + entry_spot: 682.23, + entry_spot_display_value: '682.23', + entry_tick: 682.23, + entry_tick_display_value: '682.23', + entry_tick_time: 1716220584, + expiry_time: 4869849599, + id: '917d1b48-305b-a2f4-5b9c-7fb1f2c6c145', + is_expired: 0, + is_forward_starting: 0, + is_intraday: 0, + is_path_dependent: 1, + is_settleable: 0, + is_sold: 0, + is_valid_to_cancel: 1, + is_valid_to_sell: 0, + limit_order: { + stop_out: { + display_name: 'Stop out', + order_amount: -9, + order_date: 1716220583, + value: '614.26', + }, + }, + longcode: + "If you select 'Up', your total profit/loss will be the percentage increase in Volatility 100 (1s) Index, multiplied by 90, minus commissions.", + multiplier: 10, + profit: -0.1, + profit_percentage: -1.11, + purchase_time: 1716220583, + shortcode: 'MULTUP_1HZ100V_9.00_10_1716220583_4869849599_60m_0.00_N1', + status: 'open', + transaction_ids: { + buy: 484286215128, + }, + underlying: '1HZ100V', + validation_error: + 'The spot price has moved. We have not closed this contract because your profit is negative and deal cancellation is active. Cancel your contract to get your full stake back.', + validation_error_code: 'General', + }, + details: + "If you select 'Up', your total profit/loss will be the percentage increase in Volatility 100 (1s) Index, multiplied by 90, minus commissions.", + display_name: '', + id: 242807045608, + indicative: 8.9, + purchase: 9.39, + reference: 484286215128, + type: 'MULTUP', + contract_update: { + stop_out: { + display_name: 'Stop out', + order_amount: -9, + order_date: 1716220583, + value: '614.26', + }, + }, + profit_loss: -0.1, + is_valid_to_sell: false, + status: 'profit', + entry_spot: 682.23, + }, + { + contract_info: { + account_id: 112905368, + barrier_count: 2, + barrier_spot_distance: '0.296', + bid_price: 9.84, + buy_price: 9, + contract_id: 242807268688, + contract_type: 'ACCU', + currency: 'USD', + current_spot: 682.72, + current_spot_display_value: '682.72', + current_spot_high_barrier: '683.016', + current_spot_low_barrier: '682.424', + current_spot_time: 1716220720, + date_expiry: 1747785599, + date_settlement: 1747785600, + date_start: 1716220710, + display_name: 'Volatility 100 (1s) Index', + entry_spot: 682.58, + entry_spot_display_value: '682.58', + entry_tick: 682.58, + entry_tick_display_value: '682.58', + entry_tick_time: 1716220711, + expiry_time: 1747785599, + growth_rate: 0.01, + high_barrier: '683.046', + id: '917d1b48-305b-a2f4-5b9c-7fb1f2c6c145', + is_expired: 0, + is_forward_starting: 0, + is_intraday: 0, + is_path_dependent: 1, + is_settleable: 0, + is_sold: 0, + is_valid_to_cancel: 0, + is_valid_to_sell: 1, + longcode: + 'After the entry spot tick, your stake will grow continuously by 1% for every tick that the spot price remains within the ± 0.04331% from the previous spot price.', + low_barrier: '682.454', + profit: 0.84, + profit_percentage: 9.33, + purchase_time: 1716220710, + shortcode: 'ACCU_1HZ100V_9.00_0_0.01_1_0.000433139675_1716220710', + status: 'open', + tick_count: 230, + tick_passed: 9, + tick_stream: [ + { + epoch: 1716220711, + tick: 682.58, + tick_display_value: '682.58', + }, + { + epoch: 1716220712, + tick: 682.71, + tick_display_value: '682.71', + }, + { + epoch: 1716220713, + tick: 682.5, + tick_display_value: '682.50', + }, + { + epoch: 1716220714, + tick: 682.57, + tick_display_value: '682.57', + }, + { + epoch: 1716220715, + tick: 682.57, + tick_display_value: '682.57', + }, + { + epoch: 1716220716, + tick: 682.75, + tick_display_value: '682.75', + }, + { + epoch: 1716220717, + tick: 682.87, + tick_display_value: '682.87', + }, + { + epoch: 1716220718, + tick: 682.74, + tick_display_value: '682.74', + }, + { + epoch: 1716220719, + tick: 682.75, + tick_display_value: '682.75', + }, + { + epoch: 1716220720, + tick: 682.72, + tick_display_value: '682.72', + }, + ], + transaction_ids: { + buy: 484286658868, + }, + underlying: '1HZ100V', + }, + details: + 'After the entry spot tick, your stake will grow continuously by 1% for every tick that the spot price remains within the ± 0.04331% from the previous spot price.', + display_name: '', + id: 242807268688, + indicative: 9.84, + purchase: 9, + reference: 484286658868, + type: 'ACCU', + profit_loss: 0.84, + is_valid_to_sell: true, + current_tick: 9, + status: 'profit', + entry_spot: 682.58, + high_barrier: 683.046, + low_barrier: 682.454, + }, +] as TPortfolioPosition[]; + +describe('filterPositions', () => { + it('should filter positions based on passed filter array', () => { + expect(filterPositions(mockedActivePositions, ['Multipliers'])).toEqual([mockedActivePositions[1]]); + + expect(filterPositions(mockedActivePositions, ['Multipliers', 'Rise/Fall'])).toEqual([ + mockedActivePositions[0], + mockedActivePositions[1], + ]); + + expect(filterPositions(mockedActivePositions, ['Turbos'])).toEqual([]); + }); +}); From 29053e53990bd857cce3197634b75ab52b027c98 Mon Sep 17 00:00:00 2001 From: kate-deriv <121025168+kate-deriv@users.noreply.github.com> Date: Mon, 27 May 2024 09:39:12 +0300 Subject: [PATCH 25/54] DTRA-1279 / Kate / Double filtration and extra filter options (#67) * fix: filtration for today and yersterday * fix: double filter * refactor: change style after design confirmation and sort props * refactor: start adding tets for time filter * chore: apply suggestions --- .../Components/DatePicker/date-picker.scss | 30 ++++++++-- .../Components/DatePicker/date-picker.tsx | 11 ++-- .../Filter/__tests__/time-filter.spec.tsx | 37 +++++++++++++ .../Filter/contract-type-filter.tsx | 19 ++++--- .../Filter/custom-time-filter-button.tsx | 4 +- .../AppV2/Components/Filter/time-filter.tsx | 55 +++++++++++-------- .../Positions/positions-content.tsx | 16 ++++-- 7 files changed, 122 insertions(+), 50 deletions(-) create mode 100644 packages/trader/src/AppV2/Components/Filter/__tests__/time-filter.spec.tsx diff --git a/packages/trader/src/AppV2/Components/DatePicker/date-picker.scss b/packages/trader/src/AppV2/Components/DatePicker/date-picker.scss index 11c74f7fdb77..a463d3216040 100644 --- a/packages/trader/src/AppV2/Components/DatePicker/date-picker.scss +++ b/packages/trader/src/AppV2/Components/DatePicker/date-picker.scss @@ -1,12 +1,30 @@ .date-picker__action-sheet { padding: 0; - .react-calendar__month-view__days { - font-size: var(--core-fontSize-100); - } - //TODO: temporary CSS - .react-calendar__tile--now:after { - display: none; + .react-calendar { + &__tile { + font-size: var(--core-fontSize-100); + + &--now:after { + display: none; + } + + &--range { + background-color: var(--semantic-color-monochrome-surface-normal-low); + color: var(--component-dropdownItem-label-color-default); + } + + &--rangeStart, + &--rangeEnd { + background-color: var(--component-dropdownItem-bg-selected); + color: var(--component-dropdownItem-label-color-selectedWhite); + } + + &:disabled { + background-color: unset; + color: var(--component-textIcon-normal-disabled); + } + } } } diff --git a/packages/trader/src/AppV2/Components/DatePicker/date-picker.tsx b/packages/trader/src/AppV2/Components/DatePicker/date-picker.tsx index 0545265ffa3c..0415b458dd45 100644 --- a/packages/trader/src/AppV2/Components/DatePicker/date-picker.tsx +++ b/packages/trader/src/AppV2/Components/DatePicker/date-picker.tsx @@ -4,12 +4,12 @@ import { toMoment } from '@deriv/shared'; import { Localize } from '@deriv/translations'; type TDateRangePicker = { + handleDateChange: (values: { to?: moment.Moment; from?: moment.Moment; is_batch?: boolean }) => void; isOpen?: boolean; onClose: () => void; setCustomTimeRangeFilter: (newCustomTimeFilter?: string | undefined) => void; - handleDateChange: (values: { to?: moment.Moment; from?: moment.Moment; is_batch?: boolean }) => void; }; -const DateRangePicker = ({ isOpen, onClose, setCustomTimeRangeFilter, handleDateChange }: TDateRangePicker) => { +const DateRangePicker = ({ handleDateChange, isOpen, onClose, setCustomTimeRangeFilter }: TDateRangePicker) => { const [chosenRangeString, setChosenRangeString] = React.useState(); const [chosenRange, setChosenRange] = React.useState<(string | null | Date)[] | null | Date>([]); @@ -26,24 +26,25 @@ const DateRangePicker = ({ isOpen, onClose, setCustomTimeRangeFilter, handleDate } /> setChosenRangeString(value)} - className='date-picker__action-sheet' onChange={value => setChosenRange(value)} optionsConfig={{ day: '2-digit', month: 'short', year: 'numeric', }} + tileDisabled={({ date }) => Date.parse(date.toDateString()) > Date.parse(toMoment().toString())} /> diff --git a/packages/trader/src/AppV2/Components/Filter/__tests__/time-filter.spec.tsx b/packages/trader/src/AppV2/Components/Filter/__tests__/time-filter.spec.tsx new file mode 100644 index 000000000000..ca21707a6cf2 --- /dev/null +++ b/packages/trader/src/AppV2/Components/Filter/__tests__/time-filter.spec.tsx @@ -0,0 +1,37 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import TimeFilter from '../time-filter'; + +const defaultFilterName = 'All time'; +const mockProps = { + handleDateChange: jest.fn(), + setTimeFilter: jest.fn(), + setCustomTimeRangeFilter: jest.fn(), + setNoMatchesFound: jest.fn(), +}; +const mediaQueryList = { + matches: true, + addEventListener: jest.fn(), + removeEventListener: jest.fn(), +}; + +window.matchMedia = jest.fn().mockImplementation(() => mediaQueryList); + +describe('TimeFilter', () => { + it('should change data-state of the dropdown if user clicks on the filter', () => { + render(); + + const dropdownChevron = screen.getByTestId('dt_chevron'); + expect(dropdownChevron).toHaveAttribute('data-state', 'close'); + + userEvent.click(dropdownChevron); + expect(dropdownChevron).toHaveAttribute('data-state', 'open'); + }); + + it('should render correct chip name if user have not chosen anything else', () => { + render(); + + expect(screen.getAllByText(defaultFilterName)).toHaveLength(2); + }); +}); diff --git a/packages/trader/src/AppV2/Components/Filter/contract-type-filter.tsx b/packages/trader/src/AppV2/Components/Filter/contract-type-filter.tsx index 19027772fb73..ac8a7c0cc409 100644 --- a/packages/trader/src/AppV2/Components/Filter/contract-type-filter.tsx +++ b/packages/trader/src/AppV2/Components/Filter/contract-type-filter.tsx @@ -4,8 +4,8 @@ import { ActionSheet, Checkbox } from '@deriv-com/quill-ui'; import { Localize } from '@deriv/translations'; type TContractTypeFilter = { - setContractTypeFilter: (filterValues: string[]) => void; contractTypeFilter: string[] | []; + setContractTypeFilter: (filterValues: string[]) => void; }; // TODO: Replace mockAvailableContractsList with real data when BE will be ready (send list of all available contracts based on account) @@ -22,7 +22,7 @@ const mockAvailableContractsList = [ { tradeType: , id: 'Over/Under' }, ]; -const ContractTypeFilter = ({ setContractTypeFilter, contractTypeFilter }: TContractTypeFilter) => { +const ContractTypeFilter = ({ contractTypeFilter, setContractTypeFilter }: TContractTypeFilter) => { const [isDropdownOpen, setIsDropdownOpen] = React.useState(false); const [changedOptions, setChangedOptions] = React.useState(contractTypeFilter); @@ -52,9 +52,9 @@ const ContractTypeFilter = ({ setContractTypeFilter, contractTypeFilter }: TCont return ( setIsDropdownOpen(!isDropdownOpen)} selected={!!changedOptions.length} size='sm' @@ -65,23 +65,24 @@ const ContractTypeFilter = ({ setContractTypeFilter, contractTypeFilter }: TCont {mockAvailableContractsList.map(({ tradeType, id }) => ( ))} setContractTypeFilter(changedOptions) }} secondaryAction={{ content: 'Clear All', onAction: () => setChangedOptions([]) }} - alignment='vertical' shouldCloseOnSecondaryButtonClick={false} - isSecondaryButtonDisabled={!changedOptions.length} /> diff --git a/packages/trader/src/AppV2/Components/Filter/custom-time-filter-button.tsx b/packages/trader/src/AppV2/Components/Filter/custom-time-filter-button.tsx index 20bd4042c589..b69116ced4ef 100644 --- a/packages/trader/src/AppV2/Components/Filter/custom-time-filter-button.tsx +++ b/packages/trader/src/AppV2/Components/Filter/custom-time-filter-button.tsx @@ -4,11 +4,11 @@ import { LabelPairedChevronRightSmBoldIcon } from '@deriv/quill-icons'; import { Localize } from '@deriv/translations'; type TCustomDateFilterButton = { - setShowDatePicker: React.Dispatch>; customTimeRangeFilter?: string; + setShowDatePicker: React.Dispatch>; }; -const CustomDateFilterButton = ({ setShowDatePicker, customTimeRangeFilter }: TCustomDateFilterButton) => ( +const CustomDateFilterButton = ({ customTimeRangeFilter, setShowDatePicker }: TCustomDateFilterButton) => ( )}
)} diff --git a/packages/trader/src/AppV2/Components/ContractCard/contract-cards-sections.tsx b/packages/trader/src/AppV2/Components/ContractCard/contract-cards-sections.tsx index 9f0d994c598b..1079caf95012 100644 --- a/packages/trader/src/AppV2/Components/ContractCard/contract-cards-sections.tsx +++ b/packages/trader/src/AppV2/Components/ContractCard/contract-cards-sections.tsx @@ -6,10 +6,11 @@ import { formatDate } from 'AppV2/Utils/positions-utils'; import ContractCardList from './contract-card-list'; type TContractCardsSections = { + onScroll?: (e: React.UIEvent) => void; positions?: (TClosedPosition | TPortfolioPosition)[]; }; -const ContractCardsSections = ({ positions }: TContractCardsSections) => { +const ContractCardsSections = ({ onScroll, positions }: TContractCardsSections) => { const dates = positions?.map(element => { const sellTime = element.contract_info.sell_time; return sellTime && formatDate({ time: sellTime }); @@ -19,7 +20,7 @@ const ContractCardsSections = ({ positions }: TContractCardsSections) => { if (!positions?.length) return null; return ( -
+
{uniqueDates.map(date => (
diff --git a/packages/trader/src/AppV2/Containers/Positions/positions-content.tsx b/packages/trader/src/AppV2/Containers/Positions/positions-content.tsx index 7ef79d03a751..854f87536fe0 100644 --- a/packages/trader/src/AppV2/Containers/Positions/positions-content.tsx +++ b/packages/trader/src/AppV2/Containers/Positions/positions-content.tsx @@ -7,10 +7,11 @@ import { EmptyPositions, TEmptyPositionsProps } from 'AppV2/Components/EmptyPosi import { TPortfolioPosition } from '@deriv/stores/types'; import { ContractCardList, ContractCardsSections } from 'AppV2/Components/ContractCard'; import { ContractTypeFilter, TimeFilter } from 'AppV2/Components/Filter'; -import { filterPositions } from '../../Utils/positions-utils'; +import { filterPositions, getTotalPositionsProfit } from '../../Utils/positions-utils'; import { TReportsStore, useReportsStore } from '../../../../../reports/src/Stores/useReportsStores'; import useTradeTypeFilter from 'AppV2/Hooks/useTradeTypeFilter'; import useTimeFilter from 'AppV2/Hooks/useTimeFilter'; +import TotalProfitLoss from './total-profit-loss'; type TPositionsContentProps = Omit & { hasButtonsDemo?: boolean; @@ -28,14 +29,16 @@ const PositionsContent = observer(({ hasButtonsDemo, isClosedTab, setHasButtonsD const [noMatchesFound, setNoMatchesFound] = React.useState(false); const { common, client, portfolio } = useStore(); - const { server_time = moment() } = isClosedTab ? {} : common; + const { server_time = moment() } = isClosedTab ? {} : common; // Server time is required for cards update in Open positions const { currency } = client; const { active_positions, is_active_empty, onClickCancel, onClickSell, onMount: onOpenTabMount } = portfolio; const { data, + handleScroll, is_empty, is_loading: isLoading, onMount: onClosedTabMount, + onUnmount: onClosedTabUnmount, handleDateChange, } = useReportsStore().profit_table; const closedPositions = React.useMemo(() => data.map(d => ({ contract_info: d })), [data]); @@ -61,7 +64,7 @@ const PositionsContent = observer(({ hasButtonsDemo, isClosedTab, setHasButtonsD }; const contractCards = isClosedTab ? ( - + ) : ( { isClosedTab ? onClosedTabMount() : onOpenTabMount(); + + return () => { + isClosedTab && onClosedTabUnmount(); + }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); @@ -113,6 +120,13 @@ const PositionsContent = observer(({ hasButtonsDemo, isClosedTab, setHasButtonsD setContractTypeFilter={filterValues => handleTradeTypeFilterChange(filterValues)} contractTypeFilter={contractTypeFilter} /> + {shouldShowContractCards && ( + + )}
)} {shouldShowEmptyMessage ? ( diff --git a/packages/trader/src/AppV2/Containers/Positions/positions.scss b/packages/trader/src/AppV2/Containers/Positions/positions.scss index 249129b0af41..3eef01aeb44c 100644 --- a/packages/trader/src/AppV2/Containers/Positions/positions.scss +++ b/packages/trader/src/AppV2/Containers/Positions/positions.scss @@ -4,6 +4,7 @@ $TABS_HEIGHT: 4.8rem; .positions-page { height: 100%; background-color: var(--core-color-solid-slate-75); + position: relative; .tab-list--container { display: block; @@ -47,4 +48,28 @@ $TABS_HEIGHT: 4.8rem; .initial-loader { height: 100%; } + .total-profit-loss { + display: flex; + justify-content: space-between; + width: 100%; + padding: var(--core-spacing-400) var(--core-spacing-800) 0 var(--core-spacing-800); + + &.bottom { + position: absolute; + inset-inline-start: 0; + inset-block-end: 0; + z-index: 1; + background-color: var(--component-modal-bg); + box-shadow: var(--core-elevation-shadow-210); + padding: var(--core-spacing-400) var(--core-spacing-800); + } + &__amount { + &.positive { + color: var(--core-color-solid-green-600); + } + &.negative { + color: var(--core-color-solid-red-600); + } + } + } } diff --git a/packages/trader/src/AppV2/Containers/Positions/total-profit-loss.tsx b/packages/trader/src/AppV2/Containers/Positions/total-profit-loss.tsx new file mode 100644 index 000000000000..2f81765120e7 --- /dev/null +++ b/packages/trader/src/AppV2/Containers/Positions/total-profit-loss.tsx @@ -0,0 +1,31 @@ +import React from 'react'; +import { Text } from '@deriv-com/quill-ui'; +import { getCardLabels } from '@deriv/shared'; +import { Money } from '@deriv/components'; +import classNames from 'classnames'; + +type TTotalProfitLossProps = { + currency?: string; + hasBottomAlignment?: boolean; + totalProfitLoss: number; +}; + +const TotalProfitLoss = ({ currency, hasBottomAlignment, totalProfitLoss }: TTotalProfitLossProps) => ( +
+ + {getCardLabels().TOTAL_PROFIT_LOSS} + + 0, + negative: totalProfitLoss < 0, + })} + bold + size='sm' + > + + +
+); + +export default TotalProfitLoss; diff --git a/packages/trader/src/AppV2/Utils/positions-utils.ts b/packages/trader/src/AppV2/Utils/positions-utils.ts index b15a04438855..19a7e3f2ee2d 100644 --- a/packages/trader/src/AppV2/Utils/positions-utils.ts +++ b/packages/trader/src/AppV2/Utils/positions-utils.ts @@ -1,4 +1,4 @@ -import { getSupportedContracts, isHighLow } from '@deriv/shared'; +import { getSupportedContracts, getTotalProfit, isHighLow, isMultiplierContract } from '@deriv/shared'; import { TPortfolioPosition } from '@deriv/stores/types'; import { TClosedPosition } from 'AppV2/Containers/Positions/positions-content'; @@ -36,3 +36,16 @@ export const formatDate: TFormatDate = ({ locale = 'en-GB', dateFormattingConfig = DEFAULT_DATE_FORMATTING_CONFIG, }) => new Date(time).toLocaleDateString(locale, dateFormattingConfig); + +export const getProfit = (contract_info: TPortfolioPosition['contract_info'] | TClosedPosition['contract_info']) => { + return ( + (contract_info as TClosedPosition['contract_info']).profit_loss ?? + (isMultiplierContract(contract_info.contract_type) + ? getTotalProfit(contract_info as TPortfolioPosition['contract_info']) + : (contract_info as TPortfolioPosition['contract_info']).profit) + ); +}; + +export const getTotalPositionsProfit = (positions: (TPortfolioPosition | TClosedPosition)[]) => { + return positions.reduce((sum, { contract_info }) => sum + Number(getProfit(contract_info)), 0); +}; From c36e3ae371cc149a16c24cb797db0954048224f0 Mon Sep 17 00:00:00 2001 From: Maryia <103177211+maryia-deriv@users.noreply.github.com> Date: Tue, 28 May 2024 08:45:03 +0300 Subject: [PATCH 30/54] fix: loading state and loading more on infinite scroll in Closed tab (#71) --- .../contract-cards-sections.spec.tsx | 7 +++++++ .../ContractCard/contract-cards-sections.tsx | 7 +++++-- .../Positions/positions-content.tsx | 21 +++++++++++++++---- 3 files changed, 29 insertions(+), 6 deletions(-) diff --git a/packages/trader/src/AppV2/Components/ContractCard/__tests__/contract-cards-sections.spec.tsx b/packages/trader/src/AppV2/Components/ContractCard/__tests__/contract-cards-sections.spec.tsx index 3cdde3b29d65..059412d5bb0f 100644 --- a/packages/trader/src/AppV2/Components/ContractCard/__tests__/contract-cards-sections.spec.tsx +++ b/packages/trader/src/AppV2/Components/ContractCard/__tests__/contract-cards-sections.spec.tsx @@ -69,4 +69,11 @@ describe('ContractCardsSections', () => { expect(dateSeparator).toBeInTheDocument(); expect(screen.getAllByText(ContractCard)).toHaveLength(2); }); + + it('should render Loading when isLoading true', () => { + render(); + + const loader = screen.getByTestId('dt_initial_loader'); + expect(loader).toBeInTheDocument(); + }); }); diff --git a/packages/trader/src/AppV2/Components/ContractCard/contract-cards-sections.tsx b/packages/trader/src/AppV2/Components/ContractCard/contract-cards-sections.tsx index 1079caf95012..c5319d49976c 100644 --- a/packages/trader/src/AppV2/Components/ContractCard/contract-cards-sections.tsx +++ b/packages/trader/src/AppV2/Components/ContractCard/contract-cards-sections.tsx @@ -2,15 +2,17 @@ import React from 'react'; import { TClosedPosition } from 'AppV2/Containers/Positions/positions-content'; import { TPortfolioPosition } from '@deriv/stores/types'; import { Text } from '@deriv-com/quill-ui'; +import { Loading } from '@deriv/components'; import { formatDate } from 'AppV2/Utils/positions-utils'; import ContractCardList from './contract-card-list'; type TContractCardsSections = { + isLoadingMore?: boolean; onScroll?: (e: React.UIEvent) => void; positions?: (TClosedPosition | TPortfolioPosition)[]; }; -const ContractCardsSections = ({ onScroll, positions }: TContractCardsSections) => { +const ContractCardsSections = ({ isLoadingMore, onScroll, positions }: TContractCardsSections) => { const dates = positions?.map(element => { const sellTime = element.contract_info.sell_time; return sellTime && formatDate({ time: sellTime }); @@ -32,10 +34,11 @@ const ContractCardsSections = ({ onScroll, positions }: TContractCardsSections) return sellTime && formatDate({ time: sellTime }) === date; })} /> + {isLoadingMore && }
))}
); }; -export default ContractCardsSections; +export default React.memo(ContractCardsSections); diff --git a/packages/trader/src/AppV2/Containers/Positions/positions-content.tsx b/packages/trader/src/AppV2/Containers/Positions/positions-content.tsx index 854f87536fe0..4ff1b6c404e5 100644 --- a/packages/trader/src/AppV2/Containers/Positions/positions-content.tsx +++ b/packages/trader/src/AppV2/Containers/Positions/positions-content.tsx @@ -36,7 +36,7 @@ const PositionsContent = observer(({ hasButtonsDemo, isClosedTab, setHasButtonsD data, handleScroll, is_empty, - is_loading: isLoading, + is_loading: isFetchingClosedPositions, onMount: onClosedTabMount, onUnmount: onClosedTabUnmount, handleDateChange, @@ -49,7 +49,7 @@ const PositionsContent = observer(({ hasButtonsDemo, isClosedTab, setHasButtonsD const hasNoPositions = isClosedTab ? is_empty && !timeFilter && !customTimeRangeFilter : is_active_empty; const shouldShowEmptyMessage = hasNoPositions || noMatchesFound; const shouldShowContractCards = - isClosedTab || (filteredPositions.length && (filteredPositions[0]?.contract_info as TContractInfo)?.status); + filteredPositions.length && (isClosedTab || (filteredPositions[0]?.contract_info as TContractInfo)?.status); const handleTradeTypeFilterChange = (filterValues: string[]) => { setContractTypeFilter(filterValues); @@ -63,8 +63,21 @@ const PositionsContent = observer(({ hasButtonsDemo, isClosedTab, setHasButtonsD } }; + const onScroll = React.useCallback( + (e: React.UIEvent) => { + if (isClosedTab) { + handleScroll(e); + } + }, + [handleScroll, isClosedTab] + ); + const contractCards = isClosedTab ? ( - + ) : ( setFilteredPositions(positions), [positions]); - if (isLoading || (!shouldShowContractCards && !shouldShowEmptyMessage)) return ; + if (!shouldShowContractCards && !shouldShowEmptyMessage) return ; return (
{!hasNoPositions && ( From 132fe7b72104e0d6b0c824a3eea17d90ddaeaa73 Mon Sep 17 00:00:00 2001 From: kate-deriv <121025168+kate-deriv@users.noreply.github.com> Date: Tue, 28 May 2024 14:36:22 +0300 Subject: [PATCH 31/54] DTRA-1279 / Kate / Tech Debt part 2 [WIP] (#72) * refactor: add tests for utils functions + removed unused hook * refactor: move total profit loss to a separate folder and add tests * refactor: add tests for positions * refactor: add tests for position content file --- .../__tests__/total-profit-loss.spec.tsx | 31 ++ .../AppV2/Components/TotalProfitLoss/index.ts | 4 + .../TotalProfitLoss/total-profit-loss.scss | 24 ++ .../TotalProfitLoss}/total-profit-loss.tsx | 2 +- .../__tests__/positions-content.spec.tsx | 342 ++++++++++++++++++ .../Positions/__tests__/positions.spec.tsx | 34 ++ .../Positions/positions-content.tsx | 5 +- .../AppV2/Containers/Positions/positions.scss | 24 -- .../src/AppV2/Hooks/useClosedPositions.ts | 38 -- .../Utils/__tests__/positions-utils.spec.ts | 41 ++- .../trader/src/AppV2/Utils/positions-utils.ts | 4 +- 11 files changed, 480 insertions(+), 69 deletions(-) create mode 100644 packages/trader/src/AppV2/Components/TotalProfitLoss/__tests__/total-profit-loss.spec.tsx create mode 100644 packages/trader/src/AppV2/Components/TotalProfitLoss/index.ts create mode 100644 packages/trader/src/AppV2/Components/TotalProfitLoss/total-profit-loss.scss rename packages/trader/src/AppV2/{Containers/Positions => Components/TotalProfitLoss}/total-profit-loss.tsx (94%) create mode 100644 packages/trader/src/AppV2/Containers/Positions/__tests__/positions-content.spec.tsx create mode 100644 packages/trader/src/AppV2/Containers/Positions/__tests__/positions.spec.tsx delete mode 100644 packages/trader/src/AppV2/Hooks/useClosedPositions.ts diff --git a/packages/trader/src/AppV2/Components/TotalProfitLoss/__tests__/total-profit-loss.spec.tsx b/packages/trader/src/AppV2/Components/TotalProfitLoss/__tests__/total-profit-loss.spec.tsx new file mode 100644 index 000000000000..422ac43e2e97 --- /dev/null +++ b/packages/trader/src/AppV2/Components/TotalProfitLoss/__tests__/total-profit-loss.spec.tsx @@ -0,0 +1,31 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import TotalProfitLoss from '../total-profit-loss'; + +const totalProfitLoss = 'Total profit/loss:'; +const mockProps = { + totalProfitLoss: 230.56, +}; + +describe('TotalProfitLoss', () => { + it('should render component with default props', () => { + render(); + + expect(screen.getByText(totalProfitLoss)).toBeInTheDocument(); + expect(screen.getByText(/230.56/)).toBeInTheDocument(); + expect(screen.getByText(/USD/i)).toBeInTheDocument(); + }); + + it('should render component with passed currency', () => { + render(); + + expect(screen.getByText(totalProfitLoss)).toBeInTheDocument(); + expect(screen.getByText(/BYN/i)).toBeInTheDocument(); + }); + + it('should render component with specific className if hasBottomAlignment is true', () => { + render(); + + expect(screen.getByTestId('dt_total_profit_loss')).toHaveClass('total-profit-loss bottom'); + }); +}); diff --git a/packages/trader/src/AppV2/Components/TotalProfitLoss/index.ts b/packages/trader/src/AppV2/Components/TotalProfitLoss/index.ts new file mode 100644 index 000000000000..d58576aaae45 --- /dev/null +++ b/packages/trader/src/AppV2/Components/TotalProfitLoss/index.ts @@ -0,0 +1,4 @@ +import TotalProfitLoss from './total-profit-loss'; +import './total-profit-loss.scss'; + +export default TotalProfitLoss; diff --git a/packages/trader/src/AppV2/Components/TotalProfitLoss/total-profit-loss.scss b/packages/trader/src/AppV2/Components/TotalProfitLoss/total-profit-loss.scss new file mode 100644 index 000000000000..ad8872f37508 --- /dev/null +++ b/packages/trader/src/AppV2/Components/TotalProfitLoss/total-profit-loss.scss @@ -0,0 +1,24 @@ +.total-profit-loss { + display: flex; + justify-content: space-between; + width: 100%; + padding: var(--core-spacing-400) var(--core-spacing-800) 0 var(--core-spacing-800); + + &.bottom { + position: absolute; + inset-inline-start: 0; + inset-block-end: 0; + z-index: 1; + background-color: var(--component-modal-bg); + box-shadow: var(--core-elevation-shadow-210); + padding: var(--core-spacing-400) var(--core-spacing-800); + } + &__amount { + &.positive { + color: var(--core-color-solid-green-600); + } + &.negative { + color: var(--core-color-solid-red-600); + } + } +} diff --git a/packages/trader/src/AppV2/Containers/Positions/total-profit-loss.tsx b/packages/trader/src/AppV2/Components/TotalProfitLoss/total-profit-loss.tsx similarity index 94% rename from packages/trader/src/AppV2/Containers/Positions/total-profit-loss.tsx rename to packages/trader/src/AppV2/Components/TotalProfitLoss/total-profit-loss.tsx index 2f81765120e7..a6b545243d56 100644 --- a/packages/trader/src/AppV2/Containers/Positions/total-profit-loss.tsx +++ b/packages/trader/src/AppV2/Components/TotalProfitLoss/total-profit-loss.tsx @@ -11,7 +11,7 @@ type TTotalProfitLossProps = { }; const TotalProfitLoss = ({ currency, hasBottomAlignment, totalProfitLoss }: TTotalProfitLossProps) => ( -
+
{getCardLabels().TOTAL_PROFIT_LOSS} diff --git a/packages/trader/src/AppV2/Containers/Positions/__tests__/positions-content.spec.tsx b/packages/trader/src/AppV2/Containers/Positions/__tests__/positions-content.spec.tsx new file mode 100644 index 000000000000..f65b4b950ef7 --- /dev/null +++ b/packages/trader/src/AppV2/Containers/Positions/__tests__/positions-content.spec.tsx @@ -0,0 +1,342 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { mockStore } from '@deriv/stores'; +import { ReportsStoreProvider } from '../../../../../../reports/src/Stores/useReportsStores'; +import TraderProviders from '../../../../trader-providers'; +import ModulesProvider from 'Stores/Providers/modules-providers'; +import PositionsContent, { TClosedPosition } from '../positions-content'; +import { TPortfolioPosition } from '@deriv/stores/types'; + +const contractTypeFilter = 'Filter by trade types'; +const contractCardList = 'ContractCardList'; +const emptyPositions = 'EmptyPositions'; +const totalProfitLoss = 'Total profit/loss:'; + +const mediaQueryList = { + matches: true, + addEventListener: jest.fn(), + removeEventListener: jest.fn(), +}; + +window.matchMedia = jest.fn().mockImplementation(() => mediaQueryList); + +jest.mock('@deriv/shared', () => ({ + ...jest.requireActual('@deriv/shared'), + WS: { + activeSymbols: jest.fn(), + authorized: { + activeSymbols: jest.fn(), + subscribeProposalOpenContract: jest.fn(), + send: jest.fn(), + }, + buy: jest.fn(), + storage: { + contractsFor: jest.fn(), + send: jest.fn(), + }, + contractUpdate: jest.fn(), + contractUpdateHistory: jest.fn(), + subscribeTicksHistory: jest.fn(), + forgetStream: jest.fn(), + forget: jest.fn(), + forgetAll: jest.fn(), + send: jest.fn(), + subscribeProposal: jest.fn(), + subscribeTicks: jest.fn(), + time: jest.fn(), + tradingTimes: jest.fn(), + wait: jest.fn(), + profitTable: jest.fn().mockReturnValue({ + profit_table: { + transactions: [ + { + contract_info: { + app_id: 16929, + buy_price: 10, + contract_id: 243705193508, + contract_type: 'TURBOSLONG', + duration_type: 'minutes', + longcode: + 'You will receive a payout at expiry if the spot price never breaches the barrier. The payout is equal to the payout per point multiplied by the distance between the final price and the barrier.', + payout: 0, + purchase_time: '28 May 2024 10:18:24', + sell_price: 0, + sell_time: '28 May 2024 10:21:08', + shortcode: 'TURBOSLONG_1HZ100V_10.00_1716891504_1716891900_S-255P_3.692058_1716891504', + transaction_id: 486048790368, + underlying_symbol: '1HZ100V', + profit_loss: '-10.00', + display_name: '', + purchase_time_unix: 1716891504, + }, + }, + ], + }, + }), + }, +})); + +jest.mock('AppV2/Components/ContractCard', () => ({ + ...jest.requireActual('AppV2/Components/ContractCard'), + ContractCardList: jest.fn(({ positions }: { positions: (TPortfolioPosition | TClosedPosition)[] }) => ( +
+

{contractCardList}

+
    + {positions.map(({ contract_info }) => ( +
  • {contract_info.contract_type}
  • + ))} +
+
+ )), + ContractCardsSections: jest.fn(() =>
ContractCardsSections
), +})); + +jest.mock('AppV2/Components/EmptyPositions', () => ({ + ...jest.requireActual('AppV2/Components/EmptyPositions'), + EmptyPositions: jest.fn(() =>
{emptyPositions}
), +})); + +jest.mock('AppV2/Components/Filter', () => ({ + ...jest.requireActual('AppV2/Components/Filter'), + ContractTypeFilter: jest.fn(({ setContractTypeFilter }) => ( +
+ {contractTypeFilter} + + + +
+ )), + TimeFilter: jest.fn(() =>
TimeFilter
), +})); + +describe('PositionsContent', () => { + let defaultMockStore: ReturnType; + + const mockProps = { + hasButtonsDemo: false, + setHasButtonsDemo: jest.fn(), + }; + + beforeEach(() => { + defaultMockStore = mockStore({ + portfolio: { + active_positions: [ + { + contract_info: { + account_id: 147849428, + barrier_count: 1, + bid_price: 41.4, + buy_price: 10, + commission: 0.36, + contract_id: 243687440268, + contract_type: 'MULTUP', + currency: 'USD', + current_spot: 807.2, + current_spot_display_value: '807.20', + current_spot_time: 1716882618, + date_expiry: 4870540799, + date_settlement: 4870540800, + date_start: 1716877413, + display_name: 'Volatility 100 (1s) Index', + entry_spot: 782.35, + entry_spot_display_value: '782.35', + entry_tick: 782.35, + entry_tick_display_value: '782.35', + entry_tick_time: 1716877414, + expiry_time: 4870540799, + id: '3f168dfb-c3c3-5cb2-e636-e7b6e25a7c56', + is_expired: 0, + is_forward_starting: 0, + is_intraday: 0, + is_path_dependent: 1, + is_settleable: 0, + is_sold: 0, + is_valid_to_cancel: 0, + is_valid_to_sell: 1, + limit_order: { + stop_out: { + display_name: 'Stop out', + order_amount: -10, + order_date: 1716877413, + value: '774.81', + }, + }, + longcode: + "If you select 'Up', your total profit/loss will be the percentage increase in Volatility 100 (1s) Index, multiplied by 1000, minus commissions.", + multiplier: 100, + profit: 31.4, + profit_percentage: 314, + purchase_time: 1716877413, + shortcode: 'MULTUP_1HZ100V_10.00_100_1716877413_4870540799_0_0.00_N1', + status: 'open', + transaction_ids: { + buy: 486015531488, + }, + underlying: '1HZ100V', + }, + details: + "If you select 'Up', your total profit/loss will be the percentage increase in Volatility 100 (1s) Index, multiplied by 1000, minus commissions.", + display_name: '', + id: 243687440268, + indicative: 41.4, + purchase: 10, + reference: 486015531488, + type: 'MULTUP', + contract_update: { + stop_out: { + display_name: 'Stop out', + order_amount: -10, + order_date: 1716877413, + value: '774.81', + }, + }, + entry_spot: 782.35, + profit_loss: 31.4, + is_valid_to_sell: true, + status: 'profit', + }, + { + contract_info: { + account_id: 147849428, + barrier: '821.69', + barrier_count: 1, + bid_price: 4.4, + buy_price: 10, + contract_id: 243705193508, + contract_type: 'TURBOSLONG', + currency: 'USD', + current_spot: 823.04, + current_spot_display_value: '823.04', + current_spot_time: 1716891600, + date_expiry: 1716891900, + date_settlement: 1716891900, + date_start: 1716891504, + display_name: 'Volatility 100 (1s) Index', + display_number_of_contracts: '3.692058', + entry_spot: 824.24, + entry_spot_display_value: '824.24', + entry_tick: 824.24, + entry_tick_display_value: '824.24', + entry_tick_time: 1716891504, + expiry_time: 1716891900, + id: '631c07ee-ff93-a6e0-3e14-7917581b8b1b', + is_expired: 0, + is_forward_starting: 0, + is_intraday: 1, + is_path_dependent: 1, + is_settleable: 0, + is_sold: 0, + is_valid_to_cancel: 0, + is_valid_to_sell: 1, + longcode: + 'You will receive a payout at expiry if the spot price never breaches the barrier. The payout is equal to the payout per point multiplied by the distance between the final price and the barrier.', + profit: -5.6, + profit_percentage: -56, + purchase_time: 1716891504, + shortcode: 'TURBOSLONG_1HZ100V_10.00_1716891504_1716891900_S-255P_3.692058_1716891504', + status: 'open', + transaction_ids: { + buy: 486048790368, + }, + underlying: '1HZ100V', + }, + details: + 'You will receive a payout at expiry if the spot price never breaches the barrier. The payout is equal to the payout per point multiplied by the distance between the final price and the barrier.', + display_name: '', + id: 243705193508, + indicative: 4.4, + purchase: 10, + reference: 486048790368, + type: 'TURBOSLONG', + barrier: 821.69, + entry_spot: 824.24, + profit_loss: -5.6, + is_valid_to_sell: true, + status: 'profit', + }, + ], + }, + modules: { + profit_table: { + data: [], + handleScroll: jest.fn(), + is_empty: true, + is_loading: false, + onMount: jest.fn(), + onUnmount: jest.fn(), + handleDateChange: jest.fn(), + }, + }, + }); + }); + + const mockPositionsContent = (isClosedTab = false) => { + return ( + + + + + + + + ); + }; + + it('should render loader if there is no data yet', () => { + defaultMockStore = mockStore({}); + render(mockPositionsContent()); + + expect(screen.getByTestId('dt_initial_loader')).toBeInTheDocument(); + }); + + it('should render EmptyPositions if data was loaded but user have no open positions', () => { + defaultMockStore = mockStore({ portfolio: { active_positions: [], is_active_empty: true } }); + render(mockPositionsContent()); + + expect(screen.getByText(emptyPositions)).toBeInTheDocument(); + }); + + it('should render EmptyPositions if data was loaded but user have no close positions', () => { + render(mockPositionsContent(true)); + + expect(screen.getByText(emptyPositions)).toBeInTheDocument(); + }); + + it('should render contract type filter, total profit/loss and contract card list for open positions if they exist', () => { + render(mockPositionsContent()); + + expect(screen.queryByText(emptyPositions)).not.toBeInTheDocument(); + expect(screen.getByText(contractTypeFilter)).toBeInTheDocument(); + expect(screen.getByText(totalProfitLoss)).toBeInTheDocument(); + expect(screen.getByText(contractCardList)).toBeInTheDocument(); + }); + + it('should show EmptyPositions component if user chose contract type filter and such contracts are absent in positions', () => { + render(mockPositionsContent()); + + userEvent.click(screen.getByText('Accumulator')); + + expect(screen.getByText(emptyPositions)).toBeInTheDocument(); + }); + + it('should show filtered cards if user chose contract type filter and such contracts are present in positions', () => { + render(mockPositionsContent()); + + userEvent.click(screen.getByText('Multipliers')); + + expect(screen.queryByText(emptyPositions)).not.toBeInTheDocument(); + expect(screen.queryByText('TURBOSLONG')).not.toBeInTheDocument(); + expect(screen.getByText('MULTUP')).toBeInTheDocument(); + }); + + it('should show all cards if user reset filter', () => { + render(mockPositionsContent()); + + userEvent.click(screen.getByText('Reset')); + + expect(screen.queryByText(emptyPositions)).not.toBeInTheDocument(); + expect(screen.getByText('MULTUP')).toBeInTheDocument(); + expect(screen.getByText('TURBOSLONG')).toBeInTheDocument(); + }); +}); diff --git a/packages/trader/src/AppV2/Containers/Positions/__tests__/positions.spec.tsx b/packages/trader/src/AppV2/Containers/Positions/__tests__/positions.spec.tsx new file mode 100644 index 000000000000..3289518e43ee --- /dev/null +++ b/packages/trader/src/AppV2/Containers/Positions/__tests__/positions.spec.tsx @@ -0,0 +1,34 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import { mockStore } from '@deriv/stores'; +import { ReportsStoreProvider } from '../../../../../../reports/src/Stores/useReportsStores'; +import TraderProviders from '../../../../trader-providers'; +import ModulesProvider from 'Stores/Providers/modules-providers'; +import Positions from '../positions'; + +const defaultMockStore = mockStore({}); + +describe('Positions', () => { + it('should render component', () => { + render( + + + + + + + + ); + + const tabs = screen.getAllByRole('tab'); + expect(tabs).toHaveLength(2); + + const openTab = tabs[0]; + const closedTab = tabs[1]; + expect(openTab).toHaveAttribute('aria-selected', 'true'); + expect(closedTab).toHaveAttribute('aria-selected', 'false'); + + expect(screen.getByText('Open')).toBeInTheDocument(); + expect(screen.getByText('Closed')).toBeInTheDocument(); + }); +}); diff --git a/packages/trader/src/AppV2/Containers/Positions/positions-content.tsx b/packages/trader/src/AppV2/Containers/Positions/positions-content.tsx index 4ff1b6c404e5..76396e86cddf 100644 --- a/packages/trader/src/AppV2/Containers/Positions/positions-content.tsx +++ b/packages/trader/src/AppV2/Containers/Positions/positions-content.tsx @@ -1,5 +1,4 @@ import React from 'react'; -import moment from 'moment'; import { TContractInfo } from '@deriv/shared'; import { Loading } from '@deriv/components'; import { observer, useStore } from '@deriv/stores'; @@ -7,11 +6,11 @@ import { EmptyPositions, TEmptyPositionsProps } from 'AppV2/Components/EmptyPosi import { TPortfolioPosition } from '@deriv/stores/types'; import { ContractCardList, ContractCardsSections } from 'AppV2/Components/ContractCard'; import { ContractTypeFilter, TimeFilter } from 'AppV2/Components/Filter'; +import TotalProfitLoss from 'AppV2/Components/TotalProfitLoss'; import { filterPositions, getTotalPositionsProfit } from '../../Utils/positions-utils'; import { TReportsStore, useReportsStore } from '../../../../../reports/src/Stores/useReportsStores'; import useTradeTypeFilter from 'AppV2/Hooks/useTradeTypeFilter'; import useTimeFilter from 'AppV2/Hooks/useTimeFilter'; -import TotalProfitLoss from './total-profit-loss'; type TPositionsContentProps = Omit & { hasButtonsDemo?: boolean; @@ -29,7 +28,7 @@ const PositionsContent = observer(({ hasButtonsDemo, isClosedTab, setHasButtonsD const [noMatchesFound, setNoMatchesFound] = React.useState(false); const { common, client, portfolio } = useStore(); - const { server_time = moment() } = isClosedTab ? {} : common; // Server time is required for cards update in Open positions + const { server_time = undefined } = isClosedTab ? {} : common; // Server time is required only to update cards timers in Open positions const { currency } = client; const { active_positions, is_active_empty, onClickCancel, onClickSell, onMount: onOpenTabMount } = portfolio; const { diff --git a/packages/trader/src/AppV2/Containers/Positions/positions.scss b/packages/trader/src/AppV2/Containers/Positions/positions.scss index 3eef01aeb44c..705caa78fbeb 100644 --- a/packages/trader/src/AppV2/Containers/Positions/positions.scss +++ b/packages/trader/src/AppV2/Containers/Positions/positions.scss @@ -48,28 +48,4 @@ $TABS_HEIGHT: 4.8rem; .initial-loader { height: 100%; } - .total-profit-loss { - display: flex; - justify-content: space-between; - width: 100%; - padding: var(--core-spacing-400) var(--core-spacing-800) 0 var(--core-spacing-800); - - &.bottom { - position: absolute; - inset-inline-start: 0; - inset-block-end: 0; - z-index: 1; - background-color: var(--component-modal-bg); - box-shadow: var(--core-elevation-shadow-210); - padding: var(--core-spacing-400) var(--core-spacing-800); - } - &__amount { - &.positive { - color: var(--core-color-solid-green-600); - } - &.negative { - color: var(--core-color-solid-red-600); - } - } - } } diff --git a/packages/trader/src/AppV2/Hooks/useClosedPositions.ts b/packages/trader/src/AppV2/Hooks/useClosedPositions.ts deleted file mode 100644 index a6d4b1e8da9b..000000000000 --- a/packages/trader/src/AppV2/Hooks/useClosedPositions.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { WS } from '@deriv/shared'; -import React, { useCallback, useEffect, useRef, useState } from 'react'; -import { ProfitTable, ProfitTableResponse } from '@deriv/api-types'; - -type TPros = { - date_from?: string; - date_to?: string; -}; -// TODO: we can't filtrate by contractTypes here as for BE High/low and Rise/Fall is ONE contract. So for contractTypes filtration is implemented in position.tsx -// TODO: refactor this hook for date filtration. e.g. date_from and date_to - -const useClosedPositions = ({ date_from, date_to }: TPros = {}) => { - const [positions, setPositions] = useState>([]); - const [isLoading, setIsLoading] = React.useState(false); - const positionsRef = useRef>([]); - - const fetch = useCallback(async () => { - setIsLoading(true); - const data: ProfitTableResponse = await WS.profitTable(50, positionsRef.current.length, { - date_from, - date_to, - }); - - setIsLoading(false); - // TODO: handle errors - setPositions(prevPositions => [...prevPositions, ...(data?.profit_table?.transactions ?? [])]); - }, [date_from, date_to]); - - useEffect(() => { - setPositions([]); - positionsRef.current = []; - fetch(); - }, [fetch, date_from, date_to]); - - return { closedPositions: positions, isLoading, fetchMore: fetch }; -}; - -export default useClosedPositions; diff --git a/packages/trader/src/AppV2/Utils/__tests__/positions-utils.spec.ts b/packages/trader/src/AppV2/Utils/__tests__/positions-utils.spec.ts index 5bff609538bf..4bb66e69b8a8 100644 --- a/packages/trader/src/AppV2/Utils/__tests__/positions-utils.spec.ts +++ b/packages/trader/src/AppV2/Utils/__tests__/positions-utils.spec.ts @@ -1,5 +1,5 @@ import { TPortfolioPosition } from '@deriv/stores/types'; -import { filterPositions, formatDate } from '../positions-utils'; +import { filterPositions, formatDate, getProfit, getTotalPositionsProfit } from '../positions-utils'; const mockedActivePositions = [ { @@ -262,6 +262,27 @@ const mockedActivePositions = [ high_barrier: 683.046, low_barrier: 682.454, }, + { + contract_info: { + app_id: 16929, + buy_price: 10, + contract_id: 243585717228, + contract_type: 'TURBOSLONG', + duration_type: 'minutes', + longcode: + 'You will receive a payout at expiry if the spot price never breaches the barrier. The payout is equal to the payout per point multiplied by the distance between the final price and the barrier.', + payout: 0, + purchase_time: '27 May 2024 09:41:00', + sell_price: 0, + sell_time: '27 May 2024 09:43:36', + shortcode: 'TURBOSLONG_1HZ100V_10.00_1716802860_1716804660_S-237P_3.971435_1716802860', + transaction_id: 485824148848, + underlying_symbol: '1HZ100V', + profit_loss: '-10.00', + display_name: '', + purchase_time_unix: 1716802860, + }, + }, ] as TPortfolioPosition[]; describe('filterPositions', () => { @@ -273,7 +294,8 @@ describe('filterPositions', () => { mockedActivePositions[1], ]); - expect(filterPositions(mockedActivePositions, ['Turbos'])).toEqual([]); + expect(filterPositions(mockedActivePositions, ['Turbos'])).toEqual([mockedActivePositions[3]]); + expect(filterPositions(mockedActivePositions, ['Even/Odd'])).toEqual([]); }); }); @@ -302,3 +324,18 @@ describe('formatDate', () => { expect(formatDate({ time: 'May 1, 2014 12:13:00', dateFormattingConfig })).toEqual('01/05/2014'); }); }); + +describe('getProfit', () => { + it('should return correct profit, based on contract_info', () => { + expect(getProfit(mockedActivePositions[0].contract_info)).toEqual(-2.62); + expect(getProfit(mockedActivePositions[1].contract_info)).toEqual(-0.4900000000000002); + expect(getProfit(mockedActivePositions[2].contract_info)).toEqual(0.84); + expect(getProfit(mockedActivePositions[3].contract_info)).toEqual('-10.00'); + }); +}); + +describe('getTotalPositionsProfit', () => { + it('should return correct total profit, based on all positions', () => { + expect(getTotalPositionsProfit(mockedActivePositions)).toEqual(-12.27); + }); +}); diff --git a/packages/trader/src/AppV2/Utils/positions-utils.ts b/packages/trader/src/AppV2/Utils/positions-utils.ts index 19a7e3f2ee2d..5a2334145979 100644 --- a/packages/trader/src/AppV2/Utils/positions-utils.ts +++ b/packages/trader/src/AppV2/Utils/positions-utils.ts @@ -37,7 +37,9 @@ export const formatDate: TFormatDate = ({ dateFormattingConfig = DEFAULT_DATE_FORMATTING_CONFIG, }) => new Date(time).toLocaleDateString(locale, dateFormattingConfig); -export const getProfit = (contract_info: TPortfolioPosition['contract_info'] | TClosedPosition['contract_info']) => { +export const getProfit = ( + contract_info: TPortfolioPosition['contract_info'] | TClosedPosition['contract_info'] +): string | number => { return ( (contract_info as TClosedPosition['contract_info']).profit_loss ?? (isMultiplierContract(contract_info.contract_type) From b6e45fba345d0b3650994d20a5a53ac158f7f507 Mon Sep 17 00:00:00 2001 From: Maryia <103177211+maryia-deriv@users.noreply.github.com> Date: Tue, 28 May 2024 14:49:04 +0300 Subject: [PATCH 32/54] Maryia/positions-redesign/test contract card + fix scroll behavior, dates formatting, and filtering Closed positions (#73) * test: contract-card * fix: hide filters on scroll + utilize moment for formatting date in closed tab --- .../Modules/Profit/Helpers/format-response.ts | 9 +- .../__tests__/contract-card.spec.tsx | 436 ++++++++++++++++++ .../ContractCard/contract-card.scss | 5 - .../Components/ContractCard/contract-card.tsx | 6 +- .../ContractCard/contract-cards-sections.tsx | 18 +- .../TotalProfitLoss/total-profit-loss.scss | 8 +- .../Positions/positions-content.tsx | 22 +- .../AppV2/Containers/Positions/positions.scss | 4 +- .../Utils/__tests__/positions-utils.spec.ts | 28 +- .../trader/src/AppV2/Utils/positions-utils.ts | 16 - 10 files changed, 474 insertions(+), 78 deletions(-) create mode 100644 packages/trader/src/AppV2/Components/ContractCard/__tests__/contract-card.spec.tsx diff --git a/packages/reports/src/Stores/Modules/Profit/Helpers/format-response.ts b/packages/reports/src/Stores/Modules/Profit/Helpers/format-response.ts index 957d9d28231f..4f13f1b96dae 100644 --- a/packages/reports/src/Stores/Modules/Profit/Helpers/format-response.ts +++ b/packages/reports/src/Stores/Modules/Profit/Helpers/format-response.ts @@ -1,15 +1,18 @@ import { formatMoney, toMoment, getSymbolDisplayName, getMarketInformation } from '@deriv/shared'; import { ActiveSymbols, ProfitTable } from '@deriv/api-types'; +export type TTransaction = NonNullable['transactions']>[number]; + export const formatProfitTableTransactions = ( - transaction: NonNullable[number], + transaction: TTransaction, currency: string, active_symbols: ActiveSymbols = [] ) => { const format_string = 'DD MMM YYYY HH:mm:ss'; - const purchase_time = `${toMoment(Number(transaction.purchase_time)).format(format_string)}`; + const purchase_time = + transaction.purchase_time && `${toMoment(Number(transaction.purchase_time)).format(format_string)}`; const purchase_time_unix = transaction.purchase_time; - const sell_time = `${toMoment(Number(transaction.sell_time)).format(format_string)}`; + const sell_time = transaction.sell_time && `${toMoment(Number(transaction.sell_time)).format(format_string)}`; const payout = transaction.payout ?? NaN; const sell_price = transaction.sell_price ?? NaN; const buy_price = transaction.buy_price ?? NaN; diff --git a/packages/trader/src/AppV2/Components/ContractCard/__tests__/contract-card.spec.tsx b/packages/trader/src/AppV2/Components/ContractCard/__tests__/contract-card.spec.tsx new file mode 100644 index 000000000000..7f1326df0029 --- /dev/null +++ b/packages/trader/src/AppV2/Components/ContractCard/__tests__/contract-card.spec.tsx @@ -0,0 +1,436 @@ +import React from 'react'; +import { Router } from 'react-router-dom'; +import { createBrowserHistory } from 'history'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { getCardLabels, getContractPath, toMoment } from '@deriv/shared'; +import { TPortfolioPosition } from '@deriv/stores/types'; +import { TClosedPosition } from 'AppV2/Containers/Positions/positions-content'; +import ContractCard from '../contract-card'; + +const closedPositions = [ + { + contract_info: { + app_id: 16929, + buy_price: 10, + contract_id: 243585717228, + contract_type: 'TURBOSLONG', + duration_type: 'minutes', + longcode: + 'You will receive a payout at expiry if the spot price never breaches the barrier. The payout is equal to the payout per point multiplied by the distance between the final price and the barrier.', + payout: 0, + purchase_time: '27 May 2024 09:41:00', + sell_price: 0, + sell_time: '27 May 2024 09:43:36', + shortcode: 'TURBOSLONG_1HZ100V_10.00_1716802860_1716804660_S-237P_3.971435_1716802860', + transaction_id: 485824148848, + underlying_symbol: '1HZ100V', + profit_loss: '-10.00', + display_name: '', + purchase_time_unix: 1716802860, + }, + }, +] as TClosedPosition[]; + +const openPositions = [ + { + contract_info: { + account_id: 112905368, + barrier: '682.60', + barrier_count: 1, + bid_price: 6.38, + buy_price: 9, + contract_id: 242807007748, + contract_type: 'CALL', + currency: 'USD', + current_spot: 681.76, + current_spot_display_value: '681.76', + current_spot_time: 1716220628, + date_expiry: Date.now() / 1000 + 1000, + date_settlement: Date.now() / 1000 + 1000, + date_start: 1716220562, + display_name: 'Volatility 100 (1s) Index', + entry_spot: 682.6, + entry_spot_display_value: '682.60', + entry_tick: 682.6, + entry_tick_display_value: '682.60', + entry_tick_time: 1716220563, + expiry_time: Date.now() / 1000 + 1000, + id: '917d1b48-305b-a2f4-5b9c-7fb1f2c6c145', + is_expired: 0, + is_forward_starting: 0, + is_intraday: 1, + is_path_dependent: 0, + is_settleable: 0, + is_sold: 0, + is_valid_to_cancel: 0, + is_valid_to_sell: 1, + longcode: + 'Win payout if Volatility 100 (1s) Index is strictly higher than entry spot at 2024-05-20 16:05:00 GMT.', + payout: 17.61, + profit: -2.62, + profit_percentage: -29.11, + purchase_time: 1716220562, + shortcode: `CALL_1HZ100V_17.61_1716220562_${Date.now() / 1000 + 1000}F_S0P_0`, + status: 'open', + transaction_ids: { + buy: 484286139408, + }, + underlying: '1HZ100V', + }, + details: + 'Win payout if Volatility 100 (1s) Index is strictly higher than entry spot at 2024-05-20 16:05:00 GMT.', + display_name: '', + id: 242807007748, + indicative: 6.38, + payout: 17.61, + purchase: 9, + reference: 484286139408, + type: 'CALL', + profit_loss: -2.62, + is_valid_to_sell: true, + status: 'profit', + barrier: 682.6, + entry_spot: 682.6, + }, + { + contract_info: { + account_id: 112905368, + barrier_count: 1, + bid_price: 8.9, + buy_price: 9.39, + cancellation: { + ask_price: 0.39, + date_expiry: 1716224183, + }, + commission: 0.03, + contract_id: 242807045608, + contract_type: 'MULTUP', + currency: 'USD', + current_spot: 681.71, + current_spot_display_value: '681.71', + current_spot_time: 1716220672, + date_expiry: Date.now() / 1000 + 1000, + date_settlement: Date.now() / 1000 + 1000, + date_start: 1716220583, + display_name: 'Volatility 100 (1s) Index', + entry_spot: 682.23, + entry_spot_display_value: '682.23', + entry_tick: 682.23, + entry_tick_display_value: '682.23', + entry_tick_time: 1716220584, + expiry_time: Date.now() / 1000 + 1000, + id: '917d1b48-305b-a2f4-5b9c-7fb1f2c6c145', + is_expired: 0, + is_forward_starting: 0, + is_intraday: 0, + is_path_dependent: 1, + is_settleable: 0, + is_sold: 0, + is_valid_to_cancel: 1, + is_valid_to_sell: 0, + limit_order: { + stop_out: { + display_name: 'Stop out', + order_amount: -9, + order_date: 1716220583, + value: '614.26', + }, + }, + longcode: + "If you select 'Up', your total profit/loss will be the percentage increase in Volatility 100 (1s) Index, multiplied by 90, minus commissions.", + multiplier: 10, + profit: -0.1, + profit_percentage: -1.11, + purchase_time: 1716220583, + shortcode: `MULTUP_1HZ100V_9.00_10_1716220583_${Date.now() / 1000 + 1000}_60m_0.00_N1`, + status: 'open', + transaction_ids: { + buy: 484286215128, + }, + underlying: '1HZ100V', + validation_error: + 'The spot price has moved. We have not closed this contract because your profit is negative and deal cancellation is active. Cancel your contract to get your full stake back.', + validation_error_code: 'General', + }, + details: + "If you select 'Up', your total profit/loss will be the percentage increase in Volatility 100 (1s) Index, multiplied by 90, minus commissions.", + display_name: '', + id: 242807045608, + indicative: 8.9, + purchase: 9.39, + reference: 484286215128, + type: 'MULTUP', + contract_update: { + stop_out: { + display_name: 'Stop out', + order_amount: -9, + order_date: 1716220583, + value: '614.26', + }, + }, + profit_loss: -0.1, + is_valid_to_sell: false, + status: 'profit', + entry_spot: 682.23, + }, + { + contract_info: { + account_id: 112905368, + barrier_count: 2, + barrier_spot_distance: '0.296', + bid_price: 9.84, + buy_price: 9, + contract_id: 242807268688, + contract_type: 'ACCU', + currency: 'USD', + current_spot: 682.72, + current_spot_display_value: '682.72', + current_spot_high_barrier: '683.016', + current_spot_low_barrier: '682.424', + current_spot_time: 1716220720, + date_expiry: Date.now() / 1000 + 1000, + date_settlement: Date.now() / 1000 + 1000, + date_start: 1716220710, + display_name: 'Volatility 100 (1s) Index', + entry_spot: 682.58, + entry_spot_display_value: '682.58', + entry_tick: 682.58, + entry_tick_display_value: '682.58', + entry_tick_time: 1716220711, + expiry_time: Date.now() / 1000 + 1000, + growth_rate: 0.01, + high_barrier: '683.046', + id: '917d1b48-305b-a2f4-5b9c-7fb1f2c6c145', + is_expired: 0, + is_forward_starting: 0, + is_intraday: 0, + is_path_dependent: 1, + is_settleable: 0, + is_sold: 0, + is_valid_to_cancel: 0, + is_valid_to_sell: 1, + longcode: + 'After the entry spot tick, your stake will grow continuously by 1% for every tick that the spot price remains within the ± 0.04331% from the previous spot price.', + low_barrier: '682.454', + profit: 0.84, + profit_percentage: 9.33, + purchase_time: 1716220710, + shortcode: 'ACCU_1HZ100V_9.00_0_0.01_1_0.000433139675_1716220710', + status: 'open', + tick_count: 230, + tick_passed: 9, + tick_stream: [ + { + epoch: 1716220711, + tick: 682.58, + tick_display_value: '682.58', + }, + { + epoch: 1716220712, + tick: 682.71, + tick_display_value: '682.71', + }, + { + epoch: 1716220713, + tick: 682.5, + tick_display_value: '682.50', + }, + { + epoch: 1716220714, + tick: 682.57, + tick_display_value: '682.57', + }, + { + epoch: 1716220715, + tick: 682.57, + tick_display_value: '682.57', + }, + { + epoch: 1716220716, + tick: 682.75, + tick_display_value: '682.75', + }, + { + epoch: 1716220717, + tick: 682.87, + tick_display_value: '682.87', + }, + { + epoch: 1716220718, + tick: 682.74, + tick_display_value: '682.74', + }, + { + epoch: 1716220719, + tick: 682.75, + tick_display_value: '682.75', + }, + { + epoch: 1716220720, + tick: 682.72, + tick_display_value: '682.72', + }, + ], + transaction_ids: { + buy: 484286658868, + }, + underlying: '1HZ100V', + }, + details: + 'After the entry spot tick, your stake will grow continuously by 1% for every tick that the spot price remains within the ± 0.04331% from the previous spot price.', + display_name: '', + id: 242807268688, + indicative: 9.84, + purchase: 9, + reference: 484286658868, + type: 'ACCU', + profit_loss: 0.84, + is_valid_to_sell: true, + current_tick: 9, + status: 'profit', + entry_spot: 682.58, + high_barrier: 683.046, + low_barrier: 682.454, + }, +] as TPortfolioPosition[]; + +const buttonLoaderId = 'dt_button_loader'; +const symbolName = 'Volatility 100 (1s) Index'; + +describe('ContractCard', () => { + const { CANCEL, CLOSE, CLOSED } = getCardLabels(); + const history = createBrowserHistory(); + const mockProps: React.ComponentProps = { + contractInfo: openPositions[0].contract_info, + currency: 'USD', + hasActionButtons: true, + isSellRequested: false, + serverTime: toMoment(Date.now() / 1000), + }; + const mockedContractCard = (props = mockProps) => ( + + + + ); + + it('should not render component if contractInfo prop is empty/missing contract_type', () => { + const { container } = render(mockedContractCard({ contractInfo: {} })); + + expect(container).toBeEmptyDOMElement(); + }); + it('should render a card for an open Rise position with a Close button only and with remaining time', () => { + render(mockedContractCard()); + expect(screen.getByText('Rise')).toBeInTheDocument(); + expect(screen.getByText(symbolName)).toBeInTheDocument(); + expect(screen.getByText('9.00 USD')).toBeInTheDocument(); + expect(screen.getByText('00:16:40')).toBeInTheDocument(); + expect(screen.getByText('2.62 USD')).toBeInTheDocument(); + expect(screen.getByRole('button', { name: CLOSE })).toBeEnabled(); + expect(screen.queryByRole('button', { name: CANCEL })).not.toBeInTheDocument(); + }); + it('should render a card for an open Multiplier position with Cancel & Close buttons and with Ongoing status instead of remaining time', () => { + render( + mockedContractCard({ + ...mockProps, + contractInfo: openPositions[1].contract_info, + }) + ); + expect(screen.getByText('Multipliers Up')).toBeInTheDocument(); + expect(screen.getByText(symbolName)).toBeInTheDocument(); + expect(screen.getByText('9.39 USD')).toBeInTheDocument(); + expect(screen.getByText('Ongoing')).toBeInTheDocument(); + expect(screen.getByText('0.49 USD')).toBeInTheDocument(); + expect(screen.getByRole('button', { name: CANCEL })).toBeEnabled(); + expect(screen.getByRole('button', { name: CLOSE })).toBeDisabled(); + }); + it('should render a card for an open Accumulators position with a Close button only and remaining number of ticks', () => { + render( + mockedContractCard({ + ...mockProps, + contractInfo: openPositions[2].contract_info, + }) + ); + expect(screen.getByText('Accumulators')).toBeInTheDocument(); + expect(screen.getByText(symbolName)).toBeInTheDocument(); + expect(screen.getByText('9.00 USD')).toBeInTheDocument(); + expect(screen.getByText('9/230 ticks')).toBeInTheDocument(); + expect(screen.getByText('0.84 USD')).toBeInTheDocument(); + expect(screen.getByRole('button', { name: CLOSE })).toBeEnabled(); + expect(screen.queryByRole('button', { name: CANCEL })).not.toBeInTheDocument(); + }); + it('should show loader when a Close button is clicked and isSellRequested === true', () => { + const mockedOnClose = jest.fn(); + const { rerender } = render( + mockedContractCard({ + ...mockProps, + contractInfo: openPositions[2].contract_info, + onClose: mockedOnClose, + }) + ); + const closeButton = screen.getByRole('button', { name: getCardLabels().CLOSE }); + userEvent.click(closeButton); + expect(mockedOnClose).toHaveBeenCalledTimes(1); + + rerender( + mockedContractCard({ + ...mockProps, + contractInfo: openPositions[2].contract_info, + isSellRequested: true, + }) + ); + expect(screen.getByTestId(buttonLoaderId)).toBeInTheDocument(); + expect(closeButton).toBeDisabled(); + }); + it('should show loader when a Cancel button is clicked and isSellRequested === true', () => { + const mockedOnCancel = jest.fn(); + const { rerender } = render( + mockedContractCard({ + ...mockProps, + contractInfo: openPositions[1].contract_info, + onCancel: mockedOnCancel, + }) + ); + const cancelButton = screen.getByRole('button', { name: CANCEL }); + userEvent.click(cancelButton); + expect(mockedOnCancel).toHaveBeenCalledTimes(1); + + rerender( + mockedContractCard({ + ...mockProps, + contractInfo: openPositions[1].contract_info, + isSellRequested: true, + }) + ); + expect(screen.getByTestId(buttonLoaderId)).toBeInTheDocument(); + expect(cancelButton).toBeDisabled(); + }); + it('should render a card for a closed position with Closed status and with no buttons if hasActionButtons === false', () => { + render( + mockedContractCard({ + ...mockProps, + contractInfo: closedPositions[0].contract_info, + hasActionButtons: false, + }) + ); + expect(screen.getByText(CLOSED)).toBeInTheDocument(); + expect(screen.queryByRole('button', { name: CLOSE })).not.toBeInTheDocument(); + expect(screen.queryByRole('button', { name: CANCEL })).not.toBeInTheDocument(); + }); + it('should call onClick when a card is clicked, and should redirect to a correct route if redirectTo is passed', () => { + const mockedOnClick = jest.fn(); + const redirectTo = getContractPath(Number(closedPositions[0].contract_info.contract_id)); + render( + mockedContractCard({ + ...mockProps, + contractInfo: closedPositions[0].contract_info, + onClick: mockedOnClick, + redirectTo, + }) + ); + const card = screen.getByText('Turbos Up'); + userEvent.click(card); + expect(mockedOnClick).toHaveBeenCalledTimes(1); + expect(history.location.pathname).toBe(redirectTo); + }); +}); diff --git a/packages/trader/src/AppV2/Components/ContractCard/contract-card.scss b/packages/trader/src/AppV2/Components/ContractCard/contract-card.scss index 272a0152c178..41ed080da649 100644 --- a/packages/trader/src/AppV2/Components/ContractCard/contract-card.scss +++ b/packages/trader/src/AppV2/Components/ContractCard/contract-card.scss @@ -188,7 +188,6 @@ gap: var(--core-spacing-400); width: inherit; padding-inline: var(--core-spacing-400); - overflow-y: scroll; &--has-buttons-demo { .contract-card-wrapper:first-child { @@ -231,10 +230,6 @@ } .contract-cards { - &-sections { - overflow-y: scroll; - } - &-section { &__title { padding: var(--core-spacing-400) var(--core-spacing-800); diff --git a/packages/trader/src/AppV2/Components/ContractCard/contract-card.tsx b/packages/trader/src/AppV2/Components/ContractCard/contract-card.tsx index 8ac293c12bf0..6ea798c313ed 100644 --- a/packages/trader/src/AppV2/Components/ContractCard/contract-card.tsx +++ b/packages/trader/src/AppV2/Components/ContractCard/contract-card.tsx @@ -154,11 +154,11 @@ const ContractCard = ({ {validToCancel && (
)} - {shouldShowContractCards && ( - - )} {shouldShowEmptyMessage ? ( ) : ( - shouldShowContractCards && contractCards + shouldShowContractCards && ( + + + {contractCards} + + ) )}
); From b38274e2b97fe1d7f2f38e2191c5e252e098db6e Mon Sep 17 00:00:00 2001 From: Maryia <103177211+maryia-deriv@users.noreply.github.com> Date: Tue, 28 May 2024 21:06:26 +0300 Subject: [PATCH 36/54] Maryia/positions-redesign/test: ContractCardList, ContractCardStatusTimer, PositionsStore, getCurrentTick() + refactoring (#75) * test: contract-card-list * test: ContractCardStatusTimer * test: getCurrentTick() in contract.tsx in shared * test: PositionsStore * test: add more tests to ContractCardList * refactor: desctructure props in mocked component --- .../utils/contract/__tests__/contract.spec.ts | 73 ++++++++++++++++++ .../__tests__/contract-card-list.spec.tsx | 75 +++++++++++++++++++ .../contract-card-status-timer.spec.tsx | 38 ++++++++++ .../__tests__/contract-card.spec.tsx | 5 +- .../contract-cards-sections.spec.tsx | 1 - .../ContractCard/contract-card-list.tsx | 13 ++-- .../contract-card-status-timer.tsx | 29 ++++--- .../Components/ContractCard/contract-card.tsx | 2 +- .../Filter/contract-type-filter.tsx | 7 +- .../__tests__/positions-store.spec.ts | 49 ++++++++++++ 10 files changed, 266 insertions(+), 26 deletions(-) create mode 100644 packages/trader/src/AppV2/Components/ContractCard/__tests__/contract-card-list.spec.tsx create mode 100644 packages/trader/src/AppV2/Components/ContractCard/__tests__/contract-card-status-timer.spec.tsx create mode 100644 packages/trader/src/Stores/Modules/Positions/__tests__/positions-store.spec.ts diff --git a/packages/shared/src/utils/contract/__tests__/contract.spec.ts b/packages/shared/src/utils/contract/__tests__/contract.spec.ts index e064e24c94f9..1ce02663c06b 100644 --- a/packages/shared/src/utils/contract/__tests__/contract.spec.ts +++ b/packages/shared/src/utils/contract/__tests__/contract.spec.ts @@ -786,3 +786,76 @@ describe('isForwardStartingBuyTransaction', () => { ).toBe(false); }); }); + +describe('getCurrentTick', () => { + const tickStreamWithFourTicks = [ + { + epoch: 1716910930, + tick: 1338.44, + tick_display_value: '1338.44', + }, + { + epoch: 1716910932, + tick: 1338.71, + tick_display_value: '1338.71', + }, + { + epoch: 1716910934, + tick: 1338.69, + tick_display_value: '1338.69', + }, + { + epoch: 1716910936, + tick: 1338.94, + tick_display_value: '1338.94', + }, + ]; + it('should return tick_passed if tick_passed is available for a non-digit/non-asian contract', () => { + const mockedAccuContractInfo = mockContractInfo({ + contract_type: CONTRACT_TYPES.ACCUMULATOR, + tick_passed: 2, + }); + expect(ContractUtils.getCurrentTick(mockedAccuContractInfo)).toEqual(2); + }); + it('should return tick_stream.length - 1 if tick_passed is missing for a non-digit/non-asian contract', () => { + const mockedRiseContractInfo = mockContractInfo({ + contract_type: CONTRACT_TYPES.RISE, + tick_stream: tickStreamWithFourTicks, + }); + expect(ContractUtils.getCurrentTick(mockedRiseContractInfo)).toEqual(3); + }); + it('should return tick_stream.length for a digit/asian contract', () => { + const mockedDigitContractInfo = mockContractInfo({ + contract_type: CONTRACT_TYPES.MATCH_DIFF.MATCH, + tick_stream: tickStreamWithFourTicks, + }); + const mockedAsianContractInfo = mockContractInfo({ + contract_type: CONTRACT_TYPES.ASIAN.UP, + tick_stream: tickStreamWithFourTicks, + }); + expect(ContractUtils.getCurrentTick(mockedDigitContractInfo)).toEqual(4); + expect(ContractUtils.getCurrentTick(mockedAsianContractInfo)).toEqual(4); + }); + it('should return 0 if tick_stream is empty/missing for a digit/asian contract', () => { + const mockedDigitContractInfo = mockContractInfo({ + contract_type: CONTRACT_TYPES.MATCH_DIFF.MATCH, + }); + const mockedAsianContractInfo = mockContractInfo({ + contract_type: CONTRACT_TYPES.ASIAN.UP, + tick_stream: [], + }); + expect(ContractUtils.getCurrentTick(mockedDigitContractInfo)).toEqual(0); + expect(ContractUtils.getCurrentTick(mockedAsianContractInfo)).toEqual(0); + }); + it('should return 0 if both tick_stream and tick_passed are empty/missing for a non-digit/non-asian contract', () => { + const mockedAccuContractInfo = mockContractInfo({ + contract_type: CONTRACT_TYPES.ACCUMULATOR, + }); + const mockedRiseContractInfo = mockContractInfo({ + contract_type: CONTRACT_TYPES.RISE, + tick_stream: [], + }); + expect(ContractUtils.getCurrentTick(mockedAccuContractInfo)).toEqual(0); + expect(ContractUtils.getCurrentTick(mockedRiseContractInfo)).toEqual(0); + }); +}); diff --git a/packages/trader/src/AppV2/Components/ContractCard/__tests__/contract-card-list.spec.tsx b/packages/trader/src/AppV2/Components/ContractCard/__tests__/contract-card-list.spec.tsx new file mode 100644 index 000000000000..1ec836699879 --- /dev/null +++ b/packages/trader/src/AppV2/Components/ContractCard/__tests__/contract-card-list.spec.tsx @@ -0,0 +1,75 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { TPortfolioPosition } from '@deriv/stores/types'; +import { mockContractInfo } from '@deriv/shared'; +import ContractCardList from '../contract-card-list'; +import ContractCard from '../contract-card'; + +const contractCard = 'Contract Card'; +const cancelButton = 'Cancel'; +const closeButton = 'Close'; + +jest.mock('../contract-card', () => + jest.fn(({ onCancel, onClose }: React.ComponentProps) => ( +
+ {contractCard} + <> + + + +
+ )) +); + +const mockProps: React.ComponentProps = { + positions: [ + { + contract_info: mockContractInfo({ + contract_id: 243585717228, + }), + }, + { + contract_info: mockContractInfo({ + contract_id: 243578583348, + }), + }, + ] as TPortfolioPosition[], +}; + +describe('ContractCardList', () => { + it('should not render component if positions are empty/not passed', () => { + const { container } = render(); + + expect(container).toBeEmptyDOMElement(); + }); + it('should render component if positions are not empty', () => { + render(); + + expect(screen.getAllByText(contractCard)).toHaveLength(2); + }); + it('should call setHasButtonsDemo with false after 720ms if hasButtonsDemo === true', () => { + const mockedSetHasButtonsDemo = jest.fn(); + jest.useFakeTimers(); + render(); + + jest.advanceTimersByTime(720); + expect(mockedSetHasButtonsDemo).toHaveBeenCalledWith(false); + }); + it('should call onClickCancel with contract_id when a Cancel button is clicked on a contract card', () => { + const mockedOnClickCancel = jest.fn(); + render(); + + const firstCardCancelButton = screen.getAllByText(cancelButton)[0]; + userEvent.click(firstCardCancelButton); + expect(mockedOnClickCancel).toHaveBeenCalledWith(243585717228); + }); + it('should call onClickSell with contract_id when a Close button is clicked on a contract card', () => { + const mockedOnClickSell = jest.fn(); + render(); + + const secondCardCloseButton = screen.getAllByText(closeButton)[1]; + userEvent.click(secondCardCloseButton); + expect(mockedOnClickSell).toHaveBeenCalledWith(243578583348); + }); +}); diff --git a/packages/trader/src/AppV2/Components/ContractCard/__tests__/contract-card-status-timer.spec.tsx b/packages/trader/src/AppV2/Components/ContractCard/__tests__/contract-card-status-timer.spec.tsx new file mode 100644 index 000000000000..2f503b79b38b --- /dev/null +++ b/packages/trader/src/AppV2/Components/ContractCard/__tests__/contract-card-status-timer.spec.tsx @@ -0,0 +1,38 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import { getCardLabels, toMoment } from '@deriv/shared'; +import { ContractCardStatusTimer } from '../contract-card-status-timer'; + +describe('ContractCardStatusTimer', () => { + const mockProps = { date_expiry: Date.now() / 1000 + 1000 }; + it('should render Closed status if date_expiry is not passed', () => { + render(); + + expect(screen.getByText(getCardLabels().CLOSED)).toBeInTheDocument(); + }); + it('should not render the component if date_expiry is passed without serverTime and no other props are passed', () => { + const { container } = render(); + + expect(container).toBeEmptyDOMElement(); + }); + it('should render remaining time if date_expiry and serverTime are passed', () => { + render(); + + expect(screen.getByText('00:16:40')).toBeInTheDocument(); + }); + it('should render ticks progress if currentTick and tick_count are passed', () => { + render(); + + expect(screen.getByText('2/10 ticks')).toBeInTheDocument(); + }); + it('should render Ongoing status if hasNoAutoExpiry === true', () => { + render(); + + expect(screen.getByText('Ongoing')).toBeInTheDocument(); + }); + it('should render Closed status if isSold === true', () => { + render(); + + expect(screen.getByText(getCardLabels().CLOSED)).toBeInTheDocument(); + }); +}); diff --git a/packages/trader/src/AppV2/Components/ContractCard/__tests__/contract-card.spec.tsx b/packages/trader/src/AppV2/Components/ContractCard/__tests__/contract-card.spec.tsx index 7f1326df0029..e55be656b7bd 100644 --- a/packages/trader/src/AppV2/Components/ContractCard/__tests__/contract-card.spec.tsx +++ b/packages/trader/src/AppV2/Components/ContractCard/__tests__/contract-card.spec.tsx @@ -306,6 +306,7 @@ describe('ContractCard', () => { currency: 'USD', hasActionButtons: true, isSellRequested: false, + redirectTo: getContractPath(openPositions[0].contract_info.contract_id), serverTime: toMoment(Date.now() / 1000), }; const mockedContractCard = (props = mockProps) => ( @@ -315,7 +316,7 @@ describe('ContractCard', () => { ); it('should not render component if contractInfo prop is empty/missing contract_type', () => { - const { container } = render(mockedContractCard({ contractInfo: {} })); + const { container } = render(mockedContractCard({ ...mockProps, contractInfo: {} })); expect(container).toBeEmptyDOMElement(); }); @@ -344,7 +345,7 @@ describe('ContractCard', () => { expect(screen.getByRole('button', { name: CANCEL })).toBeEnabled(); expect(screen.getByRole('button', { name: CLOSE })).toBeDisabled(); }); - it('should render a card for an open Accumulators position with a Close button only and remaining number of ticks', () => { + it('should render a card for an open Accumulators position with a Close button only and ticks progress', () => { render( mockedContractCard({ ...mockProps, diff --git a/packages/trader/src/AppV2/Components/ContractCard/__tests__/contract-cards-sections.spec.tsx b/packages/trader/src/AppV2/Components/ContractCard/__tests__/contract-cards-sections.spec.tsx index 059412d5bb0f..013f95decb26 100644 --- a/packages/trader/src/AppV2/Components/ContractCard/__tests__/contract-cards-sections.spec.tsx +++ b/packages/trader/src/AppV2/Components/ContractCard/__tests__/contract-cards-sections.spec.tsx @@ -7,7 +7,6 @@ const ContractCard = 'Contract Card'; jest.mock('../contract-card', () => jest.fn(() =>
{ContractCard}
)); const mockProps = { - onScroll: jest.fn(), positions: [ { contract_info: { diff --git a/packages/trader/src/AppV2/Components/ContractCard/contract-card-list.tsx b/packages/trader/src/AppV2/Components/ContractCard/contract-card-list.tsx index 0952811a0635..55febc5df072 100644 --- a/packages/trader/src/AppV2/Components/ContractCard/contract-card-list.tsx +++ b/packages/trader/src/AppV2/Components/ContractCard/contract-card-list.tsx @@ -17,16 +17,18 @@ export type TContractCardListProps = { }; const ContractCardList = ({ - currency, hasButtonsDemo, onClickCancel, onClickSell, positions = [], - serverTime, setHasButtonsDemo, + ...rest }: TContractCardListProps) => { React.useEffect(() => { - const demoTimeout = setTimeout(() => setHasButtonsDemo?.(false), 720); // 720 is the length of demo animation + let demoTimeout: ReturnType; + if (hasButtonsDemo && setHasButtonsDemo) { + demoTimeout = setTimeout(() => setHasButtonsDemo(false), 720); // 720 is the length of demo animation + } return () => { if (demoTimeout) clearTimeout(demoTimeout); }; @@ -46,13 +48,12 @@ const ContractCardList = ({ id && onClickCancel?.(id)} onClose={() => id && onClickSell?.(id)} - redirectTo={id ? getContractPath(id) : undefined} - serverTime={serverTime} + redirectTo={getContractPath(Number(id))} + {...rest} /> ); })} diff --git a/packages/trader/src/AppV2/Components/ContractCard/contract-card-status-timer.tsx b/packages/trader/src/AppV2/Components/ContractCard/contract-card-status-timer.tsx index b972fa8d81b8..29460d56cae7 100644 --- a/packages/trader/src/AppV2/Components/ContractCard/contract-card-status-timer.tsx +++ b/packages/trader/src/AppV2/Components/ContractCard/contract-card-status-timer.tsx @@ -27,25 +27,30 @@ export const ContractCardStatusTimer = ({ if (tick_count) { return `${currentTick ?? 0}/${tick_count} ${getCardLabels().TICKS.toLowerCase()}`; } - return ( - - ); + if (date_expiry && serverTime) { + return ( + + ); + } + return null; }; - if (!date_expiry || (serverTime as moment.Moment).unix() > +date_expiry || isSold) { + const displayedDuration = getDisplayedDuration(); + + if (!date_expiry || (serverTime as moment.Moment)?.unix() > +date_expiry || isSold) { return ; } - return ( + return displayedDuration ? ( } - label={getDisplayedDuration()} + label={displayedDuration} variant='custom' size='sm' /> - ); + ) : null; }; diff --git a/packages/trader/src/AppV2/Components/ContractCard/contract-card.tsx b/packages/trader/src/AppV2/Components/ContractCard/contract-card.tsx index 6ea798c313ed..c18d0e49cb3b 100644 --- a/packages/trader/src/AppV2/Components/ContractCard/contract-card.tsx +++ b/packages/trader/src/AppV2/Components/ContractCard/contract-card.tsx @@ -30,7 +30,7 @@ type TContractCardProps = TContractCardStatusTimerProps & { onClick?: (e?: React.MouseEvent) => void; onCancel?: (e?: React.MouseEvent) => void; onClose?: (e?: React.MouseEvent) => void; - redirectTo?: string; + redirectTo: string; serverTime?: TRootStore['common']['server_time']; }; diff --git a/packages/trader/src/AppV2/Components/Filter/contract-type-filter.tsx b/packages/trader/src/AppV2/Components/Filter/contract-type-filter.tsx index 71c7e1f64d57..bbabc1287af0 100644 --- a/packages/trader/src/AppV2/Components/Filter/contract-type-filter.tsx +++ b/packages/trader/src/AppV2/Components/Filter/contract-type-filter.tsx @@ -8,7 +8,7 @@ type TContractTypeFilter = { setContractTypeFilter: (filterValues: string[]) => void; }; -const mockAvailableContractsList = [ +const availableContracts = [ { tradeType: , id: 'Accumulators' }, { tradeType: , id: 'Vanillas' }, { tradeType: , id: 'Turbos' }, @@ -43,8 +43,7 @@ const ContractTypeFilter = ({ contractTypeFilter, setContractTypeFilter }: TCont const chipLabelFormatting = () => { const arrayLength = contractTypeFilter.length; if (!arrayLength) return ; - if (arrayLength === 1) - return mockAvailableContractsList.find(type => type.id === contractTypeFilter[0])?.tradeType; + if (arrayLength === 1) return availableContracts.find(type => type.id === contractTypeFilter[0])?.tradeType; return ; }; @@ -62,7 +61,7 @@ const ContractTypeFilter = ({ contractTypeFilter, setContractTypeFilter }: TCont } /> - {mockAvailableContractsList.map(({ tradeType, id }) => ( + {availableContracts.map(({ tradeType, id }) => ( { + mockedPositionsStore = new PositionsStore({ + root_store: mockStore({}) as unknown as TRootStore, + }); +}); + +describe('PositionsStore', () => { + describe('setClosedContractTypeFilter', () => { + it('should set closedContractTypeFilter', () => { + mockedPositionsStore.setClosedContractTypeFilter(['Accumulators']); + expect(mockedPositionsStore.closedContractTypeFilter).toEqual(['Accumulators']); + mockedPositionsStore.setClosedContractTypeFilter([]); + expect(mockedPositionsStore.closedContractTypeFilter).toEqual([]); + }); + }); + describe('setOpenContractTypeFilter', () => { + it('should set openContractTypeFilter', () => { + mockedPositionsStore.setOpenContractTypeFilter(['Rise/Fall']); + expect(mockedPositionsStore.openContractTypeFilter).toEqual(['Rise/Fall']); + mockedPositionsStore.setOpenContractTypeFilter([]); + expect(mockedPositionsStore.openContractTypeFilter).toEqual([]); + }); + }); + describe('setTimeFilter', () => { + it('should set timeFilter', () => { + mockedPositionsStore.setTimeFilter('All time'); + expect(mockedPositionsStore.timeFilter).toEqual('All time'); + mockedPositionsStore.setTimeFilter(); + expect(mockedPositionsStore.timeFilter).toEqual(''); + }); + }); + describe('setCustomTimeRangeFilter', () => { + it('should set customTimeRangeFilter', () => { + mockedPositionsStore.setCustomTimeRangeFilter('25 May 2024'); + expect(mockedPositionsStore.customTimeRangeFilter).toEqual('25 May 2024'); + mockedPositionsStore.setCustomTimeRangeFilter(); + expect(mockedPositionsStore.customTimeRangeFilter).toEqual(''); + }); + }); +}); From 2f79d0a7264d490a7345e565ab2151dd039a0805 Mon Sep 17 00:00:00 2001 From: Maryia <103177211+maryia-deriv@users.noreply.github.com> Date: Tue, 28 May 2024 22:13:28 +0300 Subject: [PATCH 37/54] Maryia/positions-redesign/fix: tests + address sonarcloud + use clsx (#76) * fix: tests + address sonarcloud * refactor: use clsx instead of classnames --- packages/trader/package.json | 1 + .../__tests__/contract-card-list.spec.tsx | 6 ++---- .../ContractCard/contract-card-list.tsx | 4 ++-- .../Components/ContractCard/contract-card.tsx | 16 +++++++++------- .../ContractCard/contract-cards-sections.tsx | 4 ++-- .../__tests__/contract-type-filter.spec.tsx | 4 ++-- .../Filter/__tests__/time-filter.spec.tsx | 4 ++-- .../TotalProfitLoss/total-profit-loss.tsx | 6 +++--- 8 files changed, 23 insertions(+), 22 deletions(-) diff --git a/packages/trader/package.json b/packages/trader/package.json index c8ef49214471..99673346120c 100644 --- a/packages/trader/package.json +++ b/packages/trader/package.json @@ -104,6 +104,7 @@ "@deriv/quill-icons": "^1.22.8", "@types/react-loadable": "^5.5.6", "classnames": "^2.2.6", + "clsx": "^2.0.0", "extend": "^3.0.2", "formik": "^2.1.4", "framer-motion": "^6.5.1", diff --git a/packages/trader/src/AppV2/Components/ContractCard/__tests__/contract-card-list.spec.tsx b/packages/trader/src/AppV2/Components/ContractCard/__tests__/contract-card-list.spec.tsx index 1ec836699879..defa49f446e9 100644 --- a/packages/trader/src/AppV2/Components/ContractCard/__tests__/contract-card-list.spec.tsx +++ b/packages/trader/src/AppV2/Components/ContractCard/__tests__/contract-card-list.spec.tsx @@ -14,10 +14,8 @@ jest.mock('../contract-card', () => jest.fn(({ onCancel, onClose }: React.ComponentProps) => (
{contractCard} - <> - - - + +
)) ); diff --git a/packages/trader/src/AppV2/Components/ContractCard/contract-card-list.tsx b/packages/trader/src/AppV2/Components/ContractCard/contract-card-list.tsx index 55febc5df072..047c09e7bb5b 100644 --- a/packages/trader/src/AppV2/Components/ContractCard/contract-card-list.tsx +++ b/packages/trader/src/AppV2/Components/ContractCard/contract-card-list.tsx @@ -1,7 +1,7 @@ import React from 'react'; +import clsx from 'clsx'; import { getContractPath } from '@deriv/shared'; import { TPortfolioPosition } from '@deriv/stores/types'; -import classNames from 'classnames'; import { TClosedPosition } from 'AppV2/Containers/Positions/positions-content'; import { TRootStore } from 'Types'; import ContractCard from './contract-card'; @@ -38,7 +38,7 @@ const ContractCardList = ({ if (!positions.length) return null; return (
diff --git a/packages/trader/src/AppV2/Components/ContractCard/contract-card.tsx b/packages/trader/src/AppV2/Components/ContractCard/contract-card.tsx index c18d0e49cb3b..426b5455177a 100644 --- a/packages/trader/src/AppV2/Components/ContractCard/contract-card.tsx +++ b/packages/trader/src/AppV2/Components/ContractCard/contract-card.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import classNames from 'classnames'; +import clsx from 'clsx'; import { CaptionText, Text } from '@deriv-com/quill-ui'; import { useSwipeable } from 'react-swipeable'; import { IconTradeTypes, Money } from '@deriv/components'; @@ -76,6 +76,8 @@ const ContractCard = ({ const totalProfit = getProfit(contractInfo); const validToCancel = isValidToCancel(contractInfo as TContractInfo); const validToSell = isValidToSell(contractInfo as TContractInfo) && !isSellRequested; + const isCancelButtonPressed = isSellRequested && isCanceling; + const isCloseButtonPressed = isSellRequested && isClosing; const handleSwipe = (direction: string) => { const isLeft = direction === DIRECTION.LEFT; @@ -108,10 +110,10 @@ const ContractCard = ({ if (!contract_type) return null; return ( -
+
{validToCancel && ( )} )} - - -
- )), + ContractTypeFilter: jest.fn(() =>
{contractTypeFilter}
), TimeFilter: jest.fn(() =>
TimeFilter
), })); @@ -267,6 +259,16 @@ describe('PositionsContent', () => { onUnmount: jest.fn(), handleDateChange: jest.fn(), }, + positions: { + openContractTypeFilter: [], + closedContractTypeFilter: [], + timeFilter: '', + customTimeRangeFilter: '', + setClosedContractTypeFilter: jest.fn(), + setOpenContractTypeFilter: jest.fn(), + setTimeFilter: jest.fn(), + setCustomTimeRangeFilter: jest.fn(), + }, }, }); }); @@ -290,14 +292,14 @@ describe('PositionsContent', () => { expect(screen.getByTestId('dt_initial_loader')).toBeInTheDocument(); }); - it('should render EmptyPositions if data was loaded but user have no open positions', () => { + it('should render EmptyPositions if data has loaded but user has no open positions', () => { defaultMockStore = mockStore({ portfolio: { active_positions: [], is_active_empty: true } }); render(mockPositionsContent()); expect(screen.getByText(emptyPositions)).toBeInTheDocument(); }); - it('should render EmptyPositions if data was loaded but user have no close positions', () => { + it('should render EmptyPositions if data has loaded but user has no closed positions', () => { render(mockPositionsContent(true)); expect(screen.getByText(emptyPositions)).toBeInTheDocument(); @@ -312,29 +314,53 @@ describe('PositionsContent', () => { expect(screen.getByText(contractCardList)).toBeInTheDocument(); }); - it('should show EmptyPositions component if user chose contract type filter and such contracts are absent in positions', () => { + it('should show EmptyPositions component if user chose contract type filter and such contracts are absent from positions', () => { + defaultMockStore = mockStore({ + modules: { + ...defaultMockStore.modules, + positions: { + ...defaultMockStore.modules.positions, + openContractTypeFilter: ['Accumulators'], + }, + }, + portfolio: defaultMockStore.portfolio, + }); render(mockPositionsContent()); - userEvent.click(screen.getByText('Accumulator')); - expect(screen.getByText(emptyPositions)).toBeInTheDocument(); }); it('should show filtered cards if user chose contract type filter and such contracts are present in positions', () => { + defaultMockStore = mockStore({ + modules: { + ...defaultMockStore.modules, + positions: { + ...defaultMockStore.modules.positions, + openContractTypeFilter: ['Multipliers'], + }, + }, + portfolio: defaultMockStore.portfolio, + }); render(mockPositionsContent()); - userEvent.click(screen.getByText('Multipliers')); - expect(screen.queryByText(emptyPositions)).not.toBeInTheDocument(); expect(screen.queryByText('TURBOSLONG')).not.toBeInTheDocument(); expect(screen.getByText('MULTUP')).toBeInTheDocument(); }); - it('should show all cards if user reset filter', () => { + it('should show all cards if a user has reset the filter', () => { + defaultMockStore = mockStore({ + modules: { + ...defaultMockStore.modules, + positions: { + ...defaultMockStore.modules.positions, + openContractTypeFilter: [], + }, + }, + portfolio: defaultMockStore.portfolio, + }); render(mockPositionsContent()); - userEvent.click(screen.getByText('Reset')); - expect(screen.queryByText(emptyPositions)).not.toBeInTheDocument(); expect(screen.getByText('MULTUP')).toBeInTheDocument(); expect(screen.getByText('TURBOSLONG')).toBeInTheDocument(); diff --git a/packages/trader/src/AppV2/Containers/Positions/positions-content.tsx b/packages/trader/src/AppV2/Containers/Positions/positions-content.tsx index b4a7062ef746..2fd5fba99af4 100644 --- a/packages/trader/src/AppV2/Containers/Positions/positions-content.tsx +++ b/packages/trader/src/AppV2/Containers/Positions/positions-content.tsx @@ -33,6 +33,7 @@ const PositionsContent = observer(({ hasButtonsDemo, isClosedTab, setHasButtonsD const { active_positions, is_active_empty, onClickCancel, onClickSell, onMount: onOpenTabMount } = portfolio; const { data, + fetchNextBatch: fetchMoreClosedPositions, handleScroll, is_empty, is_loading: isFetchingClosedPositions, @@ -45,23 +46,12 @@ const PositionsContent = observer(({ hasButtonsDemo, isClosedTab, setHasButtonsD () => (isClosedTab ? closedPositions : active_positions), [active_positions, isClosedTab, closedPositions] ); - const hasNoPositions = isClosedTab ? is_empty && !timeFilter && !customTimeRangeFilter : is_active_empty; + const hasNoActiveFilters = !timeFilter && !customTimeRangeFilter && !contractTypeFilter.length; + const hasNoPositions = hasNoActiveFilters && (isClosedTab ? is_empty : is_active_empty); const shouldShowEmptyMessage = hasNoPositions || noMatchesFound; const shouldShowContractCards = !!filteredPositions.length && (isClosedTab || (filteredPositions[0]?.contract_info as TContractInfo)?.status); - const handleTradeTypeFilterChange = (filterValues: string[]) => { - setContractTypeFilter(filterValues); - if (filterValues.length) { - const result = filterPositions(positions, filterValues); - setNoMatchesFound(!result.length); - setFilteredPositions(result); - } else { - setNoMatchesFound(false); - setFilteredPositions(positions); - } - }; - const onScroll = React.useCallback( (e: React.UIEvent) => { if (isClosedTab) { @@ -89,17 +79,19 @@ const PositionsContent = observer(({ hasButtonsDemo, isClosedTab, setHasButtonsD ); React.useEffect(() => { - if (isClosedTab) { - setNoMatchesFound(!positions.length && !!(timeFilter || customTimeRangeFilter)); - - // For cases with 2 filters: when time filter was reset and we received new positions, we need to filter them by contract type - if (contractTypeFilter.length && positions.length && !timeFilter && !customTimeRangeFilter) { - const result = filterPositions(positions, contractTypeFilter); - setNoMatchesFound(!result.length); - setFilteredPositions(result); + if (contractTypeFilter.length) { + const result = filterPositions(positions, contractTypeFilter); + setNoMatchesFound(!result.length); + setFilteredPositions(result); + if (result.length < 5 && isClosedTab) { + fetchMoreClosedPositions(); } + } else { + setNoMatchesFound(false); + setFilteredPositions(positions); } - }, [customTimeRangeFilter, timeFilter, isClosedTab, positions, contractTypeFilter]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isClosedTab, positions, contractTypeFilter]); React.useEffect(() => { isClosedTab ? onClosedTabMount() : onOpenTabMount(); @@ -110,8 +102,6 @@ const PositionsContent = observer(({ hasButtonsDemo, isClosedTab, setHasButtonsD // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - React.useEffect(() => setFilteredPositions(positions), [positions]); - if (!shouldShowContractCards && !shouldShowEmptyMessage) return ; return (
)} handleTradeTypeFilterChange(filterValues)} + setContractTypeFilter={setContractTypeFilter} contractTypeFilter={contractTypeFilter} />
diff --git a/packages/trader/src/AppV2/Utils/positions-utils.ts b/packages/trader/src/AppV2/Utils/positions-utils.ts index 32cf9febd717..2140fa5f1e8c 100644 --- a/packages/trader/src/AppV2/Utils/positions-utils.ts +++ b/packages/trader/src/AppV2/Utils/positions-utils.ts @@ -16,7 +16,7 @@ export const filterPositions = (positions: (TPortfolioPosition | TClosedPosition const config = getSupportedContracts(isHighLow({ shortcode: contract_info.shortcode }))[ contract_info.contract_type as keyof ReturnType ]; - + if (!config) return false; return splittedFilter.includes('main_title' in config ? config.main_title : config.name); }); }; From 823d8966db99d803511d48d27291f4fad46047f1 Mon Sep 17 00:00:00 2001 From: maryia-deriv Date: Wed, 29 May 2024 08:42:14 +0300 Subject: [PATCH 40/54] build: trigger checks From e6401429fd61e7e843697223c1d008c28350bbc2 Mon Sep 17 00:00:00 2001 From: maryia-deriv Date: Wed, 29 May 2024 08:49:53 +0300 Subject: [PATCH 41/54] fix: hasNoActiveFilters condition --- .../src/AppV2/Containers/Positions/positions-content.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/trader/src/AppV2/Containers/Positions/positions-content.tsx b/packages/trader/src/AppV2/Containers/Positions/positions-content.tsx index 2fd5fba99af4..18b48f6f4be5 100644 --- a/packages/trader/src/AppV2/Containers/Positions/positions-content.tsx +++ b/packages/trader/src/AppV2/Containers/Positions/positions-content.tsx @@ -46,7 +46,9 @@ const PositionsContent = observer(({ hasButtonsDemo, isClosedTab, setHasButtonsD () => (isClosedTab ? closedPositions : active_positions), [active_positions, isClosedTab, closedPositions] ); - const hasNoActiveFilters = !timeFilter && !customTimeRangeFilter && !contractTypeFilter.length; + const hasNoActiveFilters = isClosedTab + ? !timeFilter && !customTimeRangeFilter && !contractTypeFilter.length + : !contractTypeFilter.length; const hasNoPositions = hasNoActiveFilters && (isClosedTab ? is_empty : is_active_empty); const shouldShowEmptyMessage = hasNoPositions || noMatchesFound; const shouldShowContractCards = From 096946b3a52824839120b7e8f75d924ddb6d5026 Mon Sep 17 00:00:00 2001 From: kate-deriv Date: Wed, 29 May 2024 09:13:38 +0300 Subject: [PATCH 42/54] fix: update package version and remove prop from action sheet --- packages/trader/package.json | 2 +- packages/trader/src/AppV2/Components/DatePicker/date-picker.tsx | 2 +- .../trader/src/AppV2/Components/Filter/contract-type-filter.tsx | 2 +- packages/trader/src/AppV2/Components/Filter/time-filter.tsx | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/trader/package.json b/packages/trader/package.json index 99673346120c..f807c5ca2dfb 100644 --- a/packages/trader/package.json +++ b/packages/trader/package.json @@ -104,7 +104,7 @@ "@deriv/quill-icons": "^1.22.8", "@types/react-loadable": "^5.5.6", "classnames": "^2.2.6", - "clsx": "^2.0.0", + "clsx": "^2.1.1", "extend": "^3.0.2", "formik": "^2.1.4", "framer-motion": "^6.5.1", diff --git a/packages/trader/src/AppV2/Components/DatePicker/date-picker.tsx b/packages/trader/src/AppV2/Components/DatePicker/date-picker.tsx index 26ac9cd19c3e..40d3309e419c 100644 --- a/packages/trader/src/AppV2/Components/DatePicker/date-picker.tsx +++ b/packages/trader/src/AppV2/Components/DatePicker/date-picker.tsx @@ -23,7 +23,7 @@ const DateRangePicker = ({ handleDateChange, isOpen, onClose, setCustomTimeRange return ( - + } /> - + } /> {availableContracts.map(({ tradeType, id }) => ( diff --git a/packages/trader/src/AppV2/Components/Filter/time-filter.tsx b/packages/trader/src/AppV2/Components/Filter/time-filter.tsx index 279ec86e5450..6d678929ec37 100644 --- a/packages/trader/src/AppV2/Components/Filter/time-filter.tsx +++ b/packages/trader/src/AppV2/Components/Filter/time-filter.tsx @@ -116,7 +116,7 @@ const TimeFilter = ({ size='sm' /> setIsDropdownOpen(false)} position='left'> - + } /> Date: Wed, 29 May 2024 09:17:04 +0300 Subject: [PATCH 43/54] chore: rename function --- .../src/AppV2/Components/Filter/contract-type-filter.tsx | 4 ++-- packages/trader/src/AppV2/Components/Filter/time-filter.tsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/trader/src/AppV2/Components/Filter/contract-type-filter.tsx b/packages/trader/src/AppV2/Components/Filter/contract-type-filter.tsx index 333bcf403560..c3153d55f852 100644 --- a/packages/trader/src/AppV2/Components/Filter/contract-type-filter.tsx +++ b/packages/trader/src/AppV2/Components/Filter/contract-type-filter.tsx @@ -40,7 +40,7 @@ const ContractTypeFilter = ({ contractTypeFilter, setContractTypeFilter }: TCont } }; - const chipLabelFormatting = () => { + const getChipLabel = () => { const arrayLength = contractTypeFilter.length; if (!arrayLength) return ; if (arrayLength === 1) return availableContracts.find(type => type.id === contractTypeFilter[0])?.tradeType; @@ -52,7 +52,7 @@ const ContractTypeFilter = ({ contractTypeFilter, setContractTypeFilter }: TCont setIsDropdownOpen(!isDropdownOpen)} selected={!!changedOptions.length} size='sm' diff --git a/packages/trader/src/AppV2/Components/Filter/time-filter.tsx b/packages/trader/src/AppV2/Components/Filter/time-filter.tsx index 6d678929ec37..aaef8aac9edb 100644 --- a/packages/trader/src/AppV2/Components/Filter/time-filter.tsx +++ b/packages/trader/src/AppV2/Components/Filter/time-filter.tsx @@ -102,7 +102,7 @@ const TimeFilter = ({ setNoMatchesFound(false); }; - const chipLabelFormatting = () => + const getChipLabel = () => customTimeRangeFilter || timeFilterList.find(item => item.value === (timeFilter || defaultCheckedTime))?.label; return ( @@ -110,7 +110,7 @@ const TimeFilter = ({ setIsDropdownOpen(!isDropdownOpen)} selected={isChipSelected} size='sm' From 8581ea258eabaf5decb5e498af47f7e0c8d5b844 Mon Sep 17 00:00:00 2001 From: Maryia <103177211+maryia-deriv@users.noreply.github.com> Date: Wed, 29 May 2024 10:59:42 +0300 Subject: [PATCH 44/54] Maryia/positions-redesign/feat: display correct active positions count (#77) * feat: display correct active positions count * fix: BottomNav tests --- .../BottomNav/__tests__/bottom-nav.spec.tsx | 15 ++-- .../AppV2/Components/BottomNav/bottom-nav.tsx | 83 +++++++++++-------- 2 files changed, 58 insertions(+), 40 deletions(-) diff --git a/packages/trader/src/AppV2/Components/BottomNav/__tests__/bottom-nav.spec.tsx b/packages/trader/src/AppV2/Components/BottomNav/__tests__/bottom-nav.spec.tsx index 74be614d911d..86c5ee8cd57d 100644 --- a/packages/trader/src/AppV2/Components/BottomNav/__tests__/bottom-nav.spec.tsx +++ b/packages/trader/src/AppV2/Components/BottomNav/__tests__/bottom-nav.spec.tsx @@ -1,7 +1,8 @@ import React from 'react'; import { render, screen } from '@testing-library/react'; -import BottomNav from '../bottom-nav'; import userEvent from '@testing-library/user-event'; +import { StoreProvider, mockStore } from '@deriv/stores'; +import BottomNav from '../bottom-nav'; jest.mock('../bottom-nav-item', () => { return jest.fn(({ index, setSelectedIndex }) => ( @@ -19,11 +20,13 @@ describe('BottomNav', () => { const mockedMarketsContainer =
MockedMarkets
; const mockedPositionsContainer =
MockedPositions
; const renderedBottomNav = ( - -
{mockedTradeContainer}
-
{mockedMarketsContainer}
-
{mockedPositionsContainer}
-
+ + +
{mockedTradeContainer}
+
{mockedMarketsContainer}
+
{mockedPositionsContainer}
+
+
); it('should render correctly', () => { const { container } = render(renderedBottomNav); diff --git a/packages/trader/src/AppV2/Components/BottomNav/bottom-nav.tsx b/packages/trader/src/AppV2/Components/BottomNav/bottom-nav.tsx index f805885f2079..51de0e9df296 100644 --- a/packages/trader/src/AppV2/Components/BottomNav/bottom-nav.tsx +++ b/packages/trader/src/AppV2/Components/BottomNav/bottom-nav.tsx @@ -7,8 +7,10 @@ import { StandaloneChartCandlestickRegularIcon, StandaloneClockThreeRegularIcon, } from '@deriv/quill-icons'; -import BottomNavItem from './bottom-nav-item'; import { Badge } from '@deriv-com/quill-ui'; +import { observer } from 'mobx-react'; +import { useStore } from '@deriv/stores'; +import BottomNavItem from './bottom-nav-item'; type BottomNavProps = { children: React.ReactNode[]; @@ -17,41 +19,54 @@ type BottomNavProps = { setSelectedItemIdx?: React.Dispatch>; }; -const bottomNavItems = [ - { - icon: ( - - ), - label: , - }, - { - icon: ( - - ), - label: , - }, - { - icon: ( - - { + const [selectedIndex, setSelectedIndex] = React.useState(selectedItemIdx); + const { active_positions_count } = useStore().portfolio; + + const bottomNavItems = [ + { + icon: ( + - - ), - label: , - }, - { - icon: , - label: , - }, -]; - -const BottomNav = ({ children, className, selectedItemIdx = 0, setSelectedItemIdx }: BottomNavProps) => { - const [selectedIndex, setSelectedIndex] = React.useState(selectedItemIdx); + ), + label: , + }, + { + icon: ( + + ), + label: , + }, + { + icon: ( + + + + ), + label: , + }, + { + icon: ( + + ), + label: , + }, + ]; const handleSelect = (index: number) => { setSelectedIndex(index); @@ -79,6 +94,6 @@ const BottomNav = ({ children, className, selectedItemIdx = 0, setSelectedItemId
); -}; +}); export default BottomNav; From 0826f6c95fc1e5de023f5b53e84ea6a42975ce06 Mon Sep 17 00:00:00 2001 From: kate-deriv Date: Wed, 29 May 2024 11:02:19 +0300 Subject: [PATCH 45/54] refactor: filter behaviour --- .../src/AppV2/Containers/Positions/positions.scss | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/packages/trader/src/AppV2/Containers/Positions/positions.scss b/packages/trader/src/AppV2/Containers/Positions/positions.scss index 4cf0260c57da..105fb6244c33 100644 --- a/packages/trader/src/AppV2/Containers/Positions/positions.scss +++ b/packages/trader/src/AppV2/Containers/Positions/positions.scss @@ -42,7 +42,20 @@ $TABS_HEIGHT: var(--core-size-2400); padding-inline-start: var(--core-spacing-400); display: flex; gap: var(--core-spacing-400); - flex-wrap: wrap; + overflow-x: auto; + min-height: var(--core-spacing-2400); + white-space: nowrap; + + -ms-overflow-style: none; /* Internet Explorer 10+ */ + scrollbar-width: none; /* Firefox */ + + &::-webkit-scrollbar { + display: none; /* Safari and Chrome */ + } + + .quill-chip { + display: inline-flex; + } } } .initial-loader { From 70b205879d2b286db6248e9ea9122a3f3f5a5940 Mon Sep 17 00:00:00 2001 From: kate-deriv Date: Wed, 29 May 2024 11:12:24 +0300 Subject: [PATCH 46/54] chore: add padding --- packages/trader/src/AppV2/Containers/Positions/positions.scss | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/trader/src/AppV2/Containers/Positions/positions.scss b/packages/trader/src/AppV2/Containers/Positions/positions.scss index 105fb6244c33..43f460db88bf 100644 --- a/packages/trader/src/AppV2/Containers/Positions/positions.scss +++ b/packages/trader/src/AppV2/Containers/Positions/positions.scss @@ -38,8 +38,7 @@ $TABS_HEIGHT: var(--core-size-2400); } &__filter { &__wrapper { - padding-block: var(--core-spacing-400); - padding-inline-start: var(--core-spacing-400); + padding: var(--core-spacing-400); display: flex; gap: var(--core-spacing-400); overflow-x: auto; From 82a2c354f30d2a4ddd613ef56f83e26fbe15b661 Mon Sep 17 00:00:00 2001 From: Maryia <103177211+maryia-deriv@users.noreply.github.com> Date: Wed, 29 May 2024 13:00:11 +0300 Subject: [PATCH 47/54] fix: positions count in footer to not show 0 (#78) --- .../AppV2/Components/BottomNav/bottom-nav.tsx | 28 +++++++++++-------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/packages/trader/src/AppV2/Components/BottomNav/bottom-nav.tsx b/packages/trader/src/AppV2/Components/BottomNav/bottom-nav.tsx index 51de0e9df296..3b2ba26fa434 100644 --- a/packages/trader/src/AppV2/Components/BottomNav/bottom-nav.tsx +++ b/packages/trader/src/AppV2/Components/BottomNav/bottom-nav.tsx @@ -43,21 +43,27 @@ const BottomNav = observer(({ children, className, selectedItemIdx = 0, setSelec label: , }, { - icon: ( - + icon: + active_positions_count > 0 ? ( + + + + ) : ( - - ), + ), label: , }, { From fc343cd57b74285f2b208542531f60a77d4e7214 Mon Sep 17 00:00:00 2001 From: kate-deriv Date: Wed, 29 May 2024 13:26:18 +0300 Subject: [PATCH 48/54] refactor: total profit loss --- .../Components/ContractCard/contract-card.scss | 4 +++- .../ContractCard/contract-cards-sections.tsx | 8 ++++++-- .../TotalProfitLoss/total-profit-loss.tsx | 10 ++++++++-- .../Containers/Positions/positions-content.tsx | 18 ++++++++++++------ 4 files changed, 29 insertions(+), 11 deletions(-) diff --git a/packages/trader/src/AppV2/Components/ContractCard/contract-card.scss b/packages/trader/src/AppV2/Components/ContractCard/contract-card.scss index 7520f61aff05..c61d236cf929 100644 --- a/packages/trader/src/AppV2/Components/ContractCard/contract-card.scss +++ b/packages/trader/src/AppV2/Components/ContractCard/contract-card.scss @@ -231,7 +231,9 @@ .contract-cards { &-sections { - margin-block-end: var(--core-size-1900); + &--has-bottom-margin { + margin-block-end: var(--core-size-1900); + } } &-section { &__title { diff --git a/packages/trader/src/AppV2/Components/ContractCard/contract-cards-sections.tsx b/packages/trader/src/AppV2/Components/ContractCard/contract-cards-sections.tsx index df23da306160..7a03ddee39a5 100644 --- a/packages/trader/src/AppV2/Components/ContractCard/contract-cards-sections.tsx +++ b/packages/trader/src/AppV2/Components/ContractCard/contract-cards-sections.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import clsx from 'clsx'; import { Text } from '@deriv-com/quill-ui'; import { Loading } from '@deriv/components'; import { toMoment } from '@deriv/shared'; @@ -7,10 +8,11 @@ import ContractCardList from './contract-card-list'; type TContractCardsSections = { isLoadingMore?: boolean; + hasBottomMargin?: boolean; positions?: TClosedPosition[]; }; -const ContractCardsSections = ({ isLoadingMore, positions }: TContractCardsSections) => { +const ContractCardsSections = ({ isLoadingMore, hasBottomMargin, positions }: TContractCardsSections) => { const formatTime = (time: number) => toMoment(time).format('DD MMM YYYY'); const dates = positions?.map(element => { @@ -22,7 +24,9 @@ const ContractCardsSections = ({ isLoadingMore, positions }: TContractCardsSecti if (!positions?.length) return null; return ( -
+
{uniqueDates.map(date => (
diff --git a/packages/trader/src/AppV2/Components/TotalProfitLoss/total-profit-loss.tsx b/packages/trader/src/AppV2/Components/TotalProfitLoss/total-profit-loss.tsx index ec37a8213080..f285dd461dd1 100644 --- a/packages/trader/src/AppV2/Components/TotalProfitLoss/total-profit-loss.tsx +++ b/packages/trader/src/AppV2/Components/TotalProfitLoss/total-profit-loss.tsx @@ -3,17 +3,23 @@ import clsx from 'clsx'; import { Text } from '@deriv-com/quill-ui'; import { getCardLabels } from '@deriv/shared'; import { Money } from '@deriv/components'; +import { Localize } from '@deriv/translations'; type TTotalProfitLossProps = { currency?: string; hasBottomAlignment?: boolean; + positionsCount?: number; totalProfitLoss: number; }; -const TotalProfitLoss = ({ currency, hasBottomAlignment, totalProfitLoss }: TTotalProfitLossProps) => ( +const TotalProfitLoss = ({ currency, hasBottomAlignment, positionsCount, totalProfitLoss }: TTotalProfitLossProps) => (
- {getCardLabels().TOTAL_PROFIT_LOSS} + {hasBottomAlignment ? ( + + ) : ( + getCardLabels().TOTAL_PROFIT_LOSS + )} ) => { @@ -67,6 +68,7 @@ const PositionsContent = observer(({ hasButtonsDemo, isClosedTab, setHasButtonsD ) : ( { isClosedTab ? onClosedTabMount() : onOpenTabMount(); @@ -133,11 +136,14 @@ const PositionsContent = observer(({ hasButtonsDemo, isClosedTab, setHasButtonsD ) : ( shouldShowContractCards && ( - + {shouldShowTakeProfit && ( + + )} {contractCards} ) From 85f0fdd470fcd574345e513a01262589d9229ff3 Mon Sep 17 00:00:00 2001 From: kate-deriv Date: Wed, 29 May 2024 13:36:37 +0300 Subject: [PATCH 49/54] chore: add tests for tpl and refactor date picker --- .../trader/src/AppV2/Components/Filter/time-filter.tsx | 5 ++++- .../TotalProfitLoss/__tests__/total-profit-loss.spec.tsx | 9 ++++++++- .../Components/TotalProfitLoss/total-profit-loss.tsx | 9 +++++++-- 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/packages/trader/src/AppV2/Components/Filter/time-filter.tsx b/packages/trader/src/AppV2/Components/Filter/time-filter.tsx index aaef8aac9edb..ad8b55571cb6 100644 --- a/packages/trader/src/AppV2/Components/Filter/time-filter.tsx +++ b/packages/trader/src/AppV2/Components/Filter/time-filter.tsx @@ -148,7 +148,10 @@ const TimeFilter = ({ setShowDatePicker(false)} + onClose={() => { + setShowDatePicker(false); + setIsDropdownOpen(false); + }} setCustomTimeRangeFilter={setCustomTimeRangeFilter} /> )} diff --git a/packages/trader/src/AppV2/Components/TotalProfitLoss/__tests__/total-profit-loss.spec.tsx b/packages/trader/src/AppV2/Components/TotalProfitLoss/__tests__/total-profit-loss.spec.tsx index 422ac43e2e97..2f504d995136 100644 --- a/packages/trader/src/AppV2/Components/TotalProfitLoss/__tests__/total-profit-loss.spec.tsx +++ b/packages/trader/src/AppV2/Components/TotalProfitLoss/__tests__/total-profit-loss.spec.tsx @@ -23,9 +23,16 @@ describe('TotalProfitLoss', () => { expect(screen.getByText(/BYN/i)).toBeInTheDocument(); }); - it('should render component with specific className if hasBottomAlignment is true', () => { + it('should render component with another text and with specific className if hasBottomAlignment is true', () => { render(); expect(screen.getByTestId('dt_total_profit_loss')).toHaveClass('total-profit-loss bottom'); + expect(screen.getByText(/Last contracts:/)).toBeInTheDocument(); + }); + + it('should reflect correct amount of contracts in text if hasBottomAlignment is true and positionsCount was passed', () => { + render(); + + expect(screen.getByText(/Last 50 contracts:/)).toBeInTheDocument(); }); }); diff --git a/packages/trader/src/AppV2/Components/TotalProfitLoss/total-profit-loss.tsx b/packages/trader/src/AppV2/Components/TotalProfitLoss/total-profit-loss.tsx index f285dd461dd1..a19dd1a26463 100644 --- a/packages/trader/src/AppV2/Components/TotalProfitLoss/total-profit-loss.tsx +++ b/packages/trader/src/AppV2/Components/TotalProfitLoss/total-profit-loss.tsx @@ -8,11 +8,16 @@ import { Localize } from '@deriv/translations'; type TTotalProfitLossProps = { currency?: string; hasBottomAlignment?: boolean; - positionsCount?: number; + positionsCount?: number | string; totalProfitLoss: number; }; -const TotalProfitLoss = ({ currency, hasBottomAlignment, positionsCount, totalProfitLoss }: TTotalProfitLossProps) => ( +const TotalProfitLoss = ({ + currency, + hasBottomAlignment, + positionsCount = '', + totalProfitLoss, +}: TTotalProfitLossProps) => (
{hasBottomAlignment ? ( From 75e0e405ec1af1bc342a79734459db2b35b949a9 Mon Sep 17 00:00:00 2001 From: kate-deriv Date: Wed, 29 May 2024 14:02:21 +0300 Subject: [PATCH 50/54] refactor: add loadre inside of empty positions --- .../Components/EmptyPositions/empty-positions.tsx | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/trader/src/AppV2/Components/EmptyPositions/empty-positions.tsx b/packages/trader/src/AppV2/Components/EmptyPositions/empty-positions.tsx index f5601ae6c6a5..f20ca64528b7 100644 --- a/packages/trader/src/AppV2/Components/EmptyPositions/empty-positions.tsx +++ b/packages/trader/src/AppV2/Components/EmptyPositions/empty-positions.tsx @@ -1,5 +1,6 @@ import React from 'react'; import { Text } from '@deriv-com/quill-ui'; +import { Loading } from '@deriv/components'; import { StandaloneBriefcaseFillIcon, StandaloneSearchFillIcon } from '@deriv/quill-icons'; import { Localize } from '@deriv/translations'; @@ -9,8 +10,19 @@ export type TEmptyPositionsProps = { }; const EmptyPositions = ({ isClosedTab, noMatchesFound }: TEmptyPositionsProps) => { + const [showLoader, setShowLoader] = React.useState(true); + + React.useEffect(() => { + const demoTimeout = setTimeout(() => setShowLoader(false), 500); + return () => { + clearTimeout(demoTimeout); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + const Icon = noMatchesFound ? StandaloneSearchFillIcon : StandaloneBriefcaseFillIcon; + if (showLoader) return ; return (
From 3b69cd1360db90795cc7660a7f44a081e081aa4a Mon Sep 17 00:00:00 2001 From: kate-deriv Date: Wed, 29 May 2024 14:16:14 +0300 Subject: [PATCH 51/54] fix: tests --- .../__tests__/empty-positions.spec.tsx | 32 ++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/packages/trader/src/AppV2/Components/EmptyPositions/__tests__/empty-positions.spec.tsx b/packages/trader/src/AppV2/Components/EmptyPositions/__tests__/empty-positions.spec.tsx index c5fc07361abe..d8828d058a57 100644 --- a/packages/trader/src/AppV2/Components/EmptyPositions/__tests__/empty-positions.spec.tsx +++ b/packages/trader/src/AppV2/Components/EmptyPositions/__tests__/empty-positions.spec.tsx @@ -1,30 +1,60 @@ import React from 'react'; -import { render, screen } from '@testing-library/react'; +import { act, render, screen } from '@testing-library/react'; import EmptyPositions from '../empty-positions'; describe('EmptyPositions', () => { const iconId = 'dt_empty_state_icon'; + it('should render Loader before timer ends', () => { + render(); + + expect(screen.getByTestId('dt_initial_loader')).toBeInTheDocument(); + }); + it('should render "No open trades" content when isClosedTab prop is false', () => { + jest.useFakeTimers(); render(); + + act(() => { + jest.advanceTimersByTime(500); + }); + expect(screen.getByText('No open trades')).toBeInTheDocument(); expect(screen.getByText('Your open trades will appear here.')).toBeInTheDocument(); }); it('should render "No closed trades" content when isClosedTab prop is true', () => { + jest.useFakeTimers(); render(); + + act(() => { + jest.advanceTimersByTime(500); + }); + expect(screen.getByText('No closed trades')).toBeInTheDocument(); expect(screen.getByText('Your closed trades will be shown here.')).toBeInTheDocument(); }); it('should render "No matches found" content when noMatchesFound prop is true', () => { + jest.useFakeTimers(); render(); + + act(() => { + jest.advanceTimersByTime(500); + }); + expect(screen.getByText('No matches found')).toBeInTheDocument(); expect(screen.getByText(/Try changing or removing filters/i)).toBeInTheDocument(); }); it('should render an empty state icon regardless of props', () => { + jest.useFakeTimers(); const { rerender } = render(); + + act(() => { + jest.advanceTimersByTime(500); + }); + expect(screen.getByTestId(iconId)).toBeInTheDocument(); rerender(); expect(screen.getByTestId(iconId)).toBeInTheDocument(); From 81ee2d0ff5ec2ccdf940ce3d7ff139e6fb25c2b4 Mon Sep 17 00:00:00 2001 From: Maryia <103177211+maryia-deriv@users.noreply.github.com> Date: Wed, 29 May 2024 14:40:32 +0300 Subject: [PATCH 52/54] Maryia/positions-redesign/fix: loader on infinite scroll in Closed tab + make redirectTo prop optional in ContractCard (#79) * fix: place loading after contract cards sections * chore: make redirection optional when clicking on contract card --- .../contract-card-status-timer.spec.tsx | 6 ++- .../__tests__/contract-card.spec.tsx | 46 +++++++++++++------ .../ContractCard/contract-card-list.tsx | 2 +- .../Components/ContractCard/contract-card.tsx | 9 ++-- .../ContractCard/contract-cards-sections.tsx | 41 +++++++++-------- 5 files changed, 65 insertions(+), 39 deletions(-) diff --git a/packages/trader/src/AppV2/Components/ContractCard/__tests__/contract-card-status-timer.spec.tsx b/packages/trader/src/AppV2/Components/ContractCard/__tests__/contract-card-status-timer.spec.tsx index 2f503b79b38b..a916beadad99 100644 --- a/packages/trader/src/AppV2/Components/ContractCard/__tests__/contract-card-status-timer.spec.tsx +++ b/packages/trader/src/AppV2/Components/ContractCard/__tests__/contract-card-status-timer.spec.tsx @@ -3,8 +3,10 @@ import { render, screen } from '@testing-library/react'; import { getCardLabels, toMoment } from '@deriv/shared'; import { ContractCardStatusTimer } from '../contract-card-status-timer'; +const mockedNow = Math.floor(Date.now() / 1000); + describe('ContractCardStatusTimer', () => { - const mockProps = { date_expiry: Date.now() / 1000 + 1000 }; + const mockProps = { date_expiry: mockedNow + 1000 }; it('should render Closed status if date_expiry is not passed', () => { render(); @@ -16,7 +18,7 @@ describe('ContractCardStatusTimer', () => { expect(container).toBeEmptyDOMElement(); }); it('should render remaining time if date_expiry and serverTime are passed', () => { - render(); + render(); expect(screen.getByText('00:16:40')).toBeInTheDocument(); }); diff --git a/packages/trader/src/AppV2/Components/ContractCard/__tests__/contract-card.spec.tsx b/packages/trader/src/AppV2/Components/ContractCard/__tests__/contract-card.spec.tsx index e55be656b7bd..bb3d490c991d 100644 --- a/packages/trader/src/AppV2/Components/ContractCard/__tests__/contract-card.spec.tsx +++ b/packages/trader/src/AppV2/Components/ContractCard/__tests__/contract-card.spec.tsx @@ -8,6 +8,8 @@ import { TPortfolioPosition } from '@deriv/stores/types'; import { TClosedPosition } from 'AppV2/Containers/Positions/positions-content'; import ContractCard from '../contract-card'; +const mockedNow = Math.floor(Date.now() / 1000); + const closedPositions = [ { contract_info: { @@ -46,8 +48,8 @@ const openPositions = [ current_spot: 681.76, current_spot_display_value: '681.76', current_spot_time: 1716220628, - date_expiry: Date.now() / 1000 + 1000, - date_settlement: Date.now() / 1000 + 1000, + date_expiry: mockedNow + 1000, + date_settlement: mockedNow + 1000, date_start: 1716220562, display_name: 'Volatility 100 (1s) Index', entry_spot: 682.6, @@ -55,7 +57,7 @@ const openPositions = [ entry_tick: 682.6, entry_tick_display_value: '682.60', entry_tick_time: 1716220563, - expiry_time: Date.now() / 1000 + 1000, + expiry_time: mockedNow + 1000, id: '917d1b48-305b-a2f4-5b9c-7fb1f2c6c145', is_expired: 0, is_forward_starting: 0, @@ -71,7 +73,7 @@ const openPositions = [ profit: -2.62, profit_percentage: -29.11, purchase_time: 1716220562, - shortcode: `CALL_1HZ100V_17.61_1716220562_${Date.now() / 1000 + 1000}F_S0P_0`, + shortcode: `CALL_1HZ100V_17.61_1716220562_${mockedNow + 1000}F_S0P_0`, status: 'open', transaction_ids: { buy: 484286139408, @@ -110,8 +112,8 @@ const openPositions = [ current_spot: 681.71, current_spot_display_value: '681.71', current_spot_time: 1716220672, - date_expiry: Date.now() / 1000 + 1000, - date_settlement: Date.now() / 1000 + 1000, + date_expiry: mockedNow + 1000, + date_settlement: mockedNow + 1000, date_start: 1716220583, display_name: 'Volatility 100 (1s) Index', entry_spot: 682.23, @@ -119,7 +121,7 @@ const openPositions = [ entry_tick: 682.23, entry_tick_display_value: '682.23', entry_tick_time: 1716220584, - expiry_time: Date.now() / 1000 + 1000, + expiry_time: mockedNow + 1000, id: '917d1b48-305b-a2f4-5b9c-7fb1f2c6c145', is_expired: 0, is_forward_starting: 0, @@ -143,7 +145,7 @@ const openPositions = [ profit: -0.1, profit_percentage: -1.11, purchase_time: 1716220583, - shortcode: `MULTUP_1HZ100V_9.00_10_1716220583_${Date.now() / 1000 + 1000}_60m_0.00_N1`, + shortcode: `MULTUP_1HZ100V_9.00_10_1716220583_${mockedNow + 1000}_60m_0.00_N1`, status: 'open', transaction_ids: { buy: 484286215128, @@ -189,8 +191,8 @@ const openPositions = [ current_spot_high_barrier: '683.016', current_spot_low_barrier: '682.424', current_spot_time: 1716220720, - date_expiry: Date.now() / 1000 + 1000, - date_settlement: Date.now() / 1000 + 1000, + date_expiry: mockedNow + 1000, + date_settlement: mockedNow + 1000, date_start: 1716220710, display_name: 'Volatility 100 (1s) Index', entry_spot: 682.58, @@ -198,7 +200,7 @@ const openPositions = [ entry_tick: 682.58, entry_tick_display_value: '682.58', entry_tick_time: 1716220711, - expiry_time: Date.now() / 1000 + 1000, + expiry_time: mockedNow + 1000, growth_rate: 0.01, high_barrier: '683.046', id: '917d1b48-305b-a2f4-5b9c-7fb1f2c6c145', @@ -306,15 +308,16 @@ describe('ContractCard', () => { currency: 'USD', hasActionButtons: true, isSellRequested: false, - redirectTo: getContractPath(openPositions[0].contract_info.contract_id), - serverTime: toMoment(Date.now() / 1000), + serverTime: toMoment(mockedNow), }; const mockedContractCard = (props = mockProps) => ( ); - + beforeEach(() => { + history.push('/'); + }); it('should not render component if contractInfo prop is empty/missing contract_type', () => { const { container } = render(mockedContractCard({ ...mockProps, contractInfo: {} })); @@ -434,4 +437,19 @@ describe('ContractCard', () => { expect(mockedOnClick).toHaveBeenCalledTimes(1); expect(history.location.pathname).toBe(redirectTo); }); + it('should call onClick when a card is clicked, but should not redirect anywhere if redirectTo prop is missing', () => { + const mockedOnClick = jest.fn(); + const redirectTo = getContractPath(Number(closedPositions[0].contract_info.contract_id)); + render( + mockedContractCard({ + ...mockProps, + contractInfo: closedPositions[0].contract_info, + onClick: mockedOnClick, + }) + ); + const card = screen.getByText('Turbos Up'); + userEvent.click(card); + expect(mockedOnClick).toHaveBeenCalledTimes(1); + expect(history.location.pathname).not.toBe(redirectTo); + }); }); diff --git a/packages/trader/src/AppV2/Components/ContractCard/contract-card-list.tsx b/packages/trader/src/AppV2/Components/ContractCard/contract-card-list.tsx index 047c09e7bb5b..ad479751a4de 100644 --- a/packages/trader/src/AppV2/Components/ContractCard/contract-card-list.tsx +++ b/packages/trader/src/AppV2/Components/ContractCard/contract-card-list.tsx @@ -52,7 +52,7 @@ const ContractCardList = ({ isSellRequested={(position as TPortfolioPosition).is_sell_requested} onCancel={() => id && onClickCancel?.(id)} onClose={() => id && onClickSell?.(id)} - redirectTo={getContractPath(Number(id))} + redirectTo={id ? getContractPath(id) : ''} {...rest} /> ); diff --git a/packages/trader/src/AppV2/Components/ContractCard/contract-card.tsx b/packages/trader/src/AppV2/Components/ContractCard/contract-card.tsx index dd2073a628f6..e31c1660aeda 100644 --- a/packages/trader/src/AppV2/Components/ContractCard/contract-card.tsx +++ b/packages/trader/src/AppV2/Components/ContractCard/contract-card.tsx @@ -27,10 +27,10 @@ type TContractCardProps = TContractCardStatusTimerProps & { currency?: string; hasActionButtons?: boolean; isSellRequested?: boolean; - onClick?: (e?: React.MouseEvent) => void; + onClick?: (e?: React.MouseEvent) => void; onCancel?: (e?: React.MouseEvent) => void; onClose?: (e?: React.MouseEvent) => void; - redirectTo: string; + redirectTo?: string; serverTime?: TRootStore['common']['server_time']; }; @@ -78,6 +78,7 @@ const ContractCard = ({ const validToSell = isValidToSell(contractInfo as TContractInfo) && !isSellRequested; const isCancelButtonPressed = isSellRequested && isCanceling; const isCloseButtonPressed = isSellRequested && isClosing; + const Component = redirectTo ? BinaryLink : 'div'; const handleSwipe = (direction: string) => { const isLeft = direction === DIRECTION.LEFT; @@ -111,7 +112,7 @@ const ContractCard = ({ if (!contract_type) return null; return (
-
)} - +
); }; diff --git a/packages/trader/src/AppV2/Components/ContractCard/contract-cards-sections.tsx b/packages/trader/src/AppV2/Components/ContractCard/contract-cards-sections.tsx index 7a03ddee39a5..bf2065bf837e 100644 --- a/packages/trader/src/AppV2/Components/ContractCard/contract-cards-sections.tsx +++ b/packages/trader/src/AppV2/Components/ContractCard/contract-cards-sections.tsx @@ -24,24 +24,29 @@ const ContractCardsSections = ({ isLoadingMore, hasBottomMargin, positions }: TC if (!positions?.length) return null; return ( -
- {uniqueDates.map(date => ( -
- - {date} - - { - const purchaseTime = position.contract_info.purchase_time_unix; - return purchaseTime && formatTime(purchaseTime) === date; - })} - /> - {isLoadingMore && } -
- ))} -
+ +
+ {uniqueDates.map(date => ( +
+ + {date} + + { + const purchaseTime = position.contract_info.purchase_time_unix; + return purchaseTime && formatTime(purchaseTime) === date; + })} + /> +
+ ))} +
+ {isLoadingMore && } +
); }; From 1a677ebcffaa5938d99904e4101aaf3261c90009 Mon Sep 17 00:00:00 2001 From: kate-deriv Date: Wed, 29 May 2024 15:48:36 +0300 Subject: [PATCH 53/54] chore: rename timet --- .../src/AppV2/Components/EmptyPositions/empty-positions.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/trader/src/AppV2/Components/EmptyPositions/empty-positions.tsx b/packages/trader/src/AppV2/Components/EmptyPositions/empty-positions.tsx index f20ca64528b7..3c5016fa14ef 100644 --- a/packages/trader/src/AppV2/Components/EmptyPositions/empty-positions.tsx +++ b/packages/trader/src/AppV2/Components/EmptyPositions/empty-positions.tsx @@ -13,9 +13,9 @@ const EmptyPositions = ({ isClosedTab, noMatchesFound }: TEmptyPositionsProps) = const [showLoader, setShowLoader] = React.useState(true); React.useEffect(() => { - const demoTimeout = setTimeout(() => setShowLoader(false), 500); + const timeout = setTimeout(() => setShowLoader(false), 500); return () => { - clearTimeout(demoTimeout); + clearTimeout(timeout); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); From ebcd0dfeba91c49083880a45831e88e57e52a94d Mon Sep 17 00:00:00 2001 From: kate-deriv <121025168+kate-deriv@users.noreply.github.com> Date: Wed, 29 May 2024 17:16:38 +0300 Subject: [PATCH 54/54] DTRA-1279 / Kate / Add a single date selection (#80) * feat: add partial range * refactor: tests * refactor: callback --- .../DatePicker/__tests__/date-picker.spec.tsx | 13 ++++++++++++ .../Components/DatePicker/date-picker.tsx | 20 +++++++++++++++---- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/packages/trader/src/AppV2/Components/DatePicker/__tests__/date-picker.spec.tsx b/packages/trader/src/AppV2/Components/DatePicker/__tests__/date-picker.spec.tsx index 07d8df80cd61..e0b151fd3def 100644 --- a/packages/trader/src/AppV2/Components/DatePicker/__tests__/date-picker.spec.tsx +++ b/packages/trader/src/AppV2/Components/DatePicker/__tests__/date-picker.spec.tsx @@ -41,4 +41,17 @@ describe('DateRangePicker', () => { expect(mockProps.handleDateChange).toBeCalled(); expect(mockProps.onClose).toBeCalled(); }); + + it('should call setCustomTimeRangeFilter, handleDateChange and onClose if user choses a single date and clicks on Apply button', () => { + render(); + + const fromDate = screen.getByText('1'); + const applyButton = screen.getByText(footer); + userEvent.click(fromDate); + userEvent.click(applyButton); + + expect(mockProps.setCustomTimeRangeFilter).toBeCalled(); + expect(mockProps.handleDateChange).toBeCalled(); + expect(mockProps.onClose).toBeCalled(); + }); }); diff --git a/packages/trader/src/AppV2/Components/DatePicker/date-picker.tsx b/packages/trader/src/AppV2/Components/DatePicker/date-picker.tsx index 40d3309e419c..85ba226c1eff 100644 --- a/packages/trader/src/AppV2/Components/DatePicker/date-picker.tsx +++ b/packages/trader/src/AppV2/Components/DatePicker/date-picker.tsx @@ -1,5 +1,6 @@ import React from 'react'; import { ActionSheet, DatePicker } from '@deriv-com/quill-ui'; +import moment from 'moment'; import { toMoment } from '@deriv/shared'; import { Localize } from '@deriv/translations'; import { DEFAULT_DATE_FORMATTING_CONFIG } from 'AppV2/Utils/positions-utils'; @@ -16,22 +17,33 @@ const DateRangePicker = ({ handleDateChange, isOpen, onClose, setCustomTimeRange const onApply = () => { setCustomTimeRangeFilter(chosenRangeString); - if (Array.isArray(chosenRange) && chosenRange.length) - handleDateChange({ from: toMoment(chosenRange[0]), to: toMoment(chosenRange[1]) }); + if (Array.isArray(chosenRange) && chosenRange.length) { + handleDateChange({ + from: toMoment(chosenRange[0]), + to: chosenRange[1] ? toMoment(chosenRange[1]) : moment(chosenRange[0]).endOf('day'), + }); + } onClose(); }; + const onFormattedDate = (value: string) => { + const trimmedValue = value.trim(); + const partialRange = trimmedValue.endsWith('-'); + setChosenRangeString(partialRange ? trimmedValue.substring(0, trimmedValue.length - 1) : trimmedValue); + }; + return ( } /> setChosenRangeString(value)} - onChange={value => setChosenRange(value)} + onFormattedDate={onFormattedDate} + onChange={setChosenRange} optionsConfig={DEFAULT_DATE_FORMATTING_CONFIG} tileDisabled={({ date }) => Date.parse(date.toDateString()) > Date.parse(toMoment().toString())} />