diff --git a/packages/trader/src/AppV2/Components/DraggableList/draggable-list.scss b/packages/trader/src/AppV2/Components/DraggableList/draggable-list.scss index b11d0d02a707..ab37b738c099 100644 --- a/packages/trader/src/AppV2/Components/DraggableList/draggable-list.scss +++ b/packages/trader/src/AppV2/Components/DraggableList/draggable-list.scss @@ -26,6 +26,11 @@ border: var(--core-spacing-75) solid var(--core-color-solid-slate-75); background-color: var(--core-color-solid-slate-75); } + + button { + background-color: transparent; + border: none; + } } &-category { diff --git a/packages/trader/src/AppV2/Components/DraggableList/draggable-list.tsx b/packages/trader/src/AppV2/Components/DraggableList/draggable-list.tsx index d0ea50cc65e0..5d55fa5c82f6 100644 --- a/packages/trader/src/AppV2/Components/DraggableList/draggable-list.tsx +++ b/packages/trader/src/AppV2/Components/DraggableList/draggable-list.tsx @@ -20,11 +20,11 @@ export type TDraggableListCategory = { export type TDraggableListProps = { categories: TDraggableListCategory[]; onRightIconClick: (item: TDraggableListItem) => void; - onSave?: () => void; + onAction?: () => void; onDrag?: (categories: TDraggableListCategory[]) => void; }; -const DraggableList: React.FC = ({ categories, onRightIconClick, onSave, onDrag }) => { +const DraggableList: React.FC = ({ categories, onRightIconClick, onAction, onDrag }) => { const [category_list, setCategoryList] = useState(categories); const [draggedItemId, setDraggedItemId] = useState(null); @@ -79,15 +79,15 @@ const DraggableList: React.FC = ({ categories, onRightIconC
- {category.title} + {category?.title} - {onSave && ( + {onAction && ( {category.button_title || } diff --git a/packages/trader/src/AppV2/Components/TradeTypeList/__tests__/trade-type-list-item.spec.tsx b/packages/trader/src/AppV2/Components/TradeTypeList/__tests__/trade-type-list-item.spec.tsx index 05c3a2832f17..9958ccbf46b5 100644 --- a/packages/trader/src/AppV2/Components/TradeTypeList/__tests__/trade-type-list-item.spec.tsx +++ b/packages/trader/src/AppV2/Components/TradeTypeList/__tests__/trade-type-list-item.spec.tsx @@ -5,7 +5,7 @@ import TradeTypeListItem from '../trade-type-list-item'; describe('TradeTypeListItem', () => { it('renders with default right icon', () => { - render(); + render(); expect(screen.getByText('Test Title')).toBeInTheDocument(); expect(screen.getByRole('img')).toBeInTheDocument(); @@ -15,13 +15,7 @@ describe('TradeTypeListItem', () => { const custom_left_icon = Custom Left Icon; const custom_right_icon = Custom Right Icon; - render( - - ); + render(); expect(screen.getByText('Custom Left Icon')).toBeInTheDocument(); expect(screen.getByText('Custom Right Icon')).toBeInTheDocument(); @@ -31,10 +25,10 @@ describe('TradeTypeListItem', () => { const handle_left_icon_click = jest.fn(); render( - Left Icon} - onLeftIconClick={handle_left_icon_click} + Left Icon} + onLeftIconClick={handle_left_icon_click} /> ); @@ -47,7 +41,7 @@ describe('TradeTypeListItem', () => { it('calls onRightIconClick when right icon is clicked', async () => { const handle_right_icon_click = jest.fn(); - render(); + render(); const right_icon = screen.getByRole('img'); await userEvent.click(right_icon); diff --git a/packages/trader/src/AppV2/Components/TradeTypeList/trade-type-list-item.tsx b/packages/trader/src/AppV2/Components/TradeTypeList/trade-type-list-item.tsx index 1ab6e9c3b4ec..2b66c4ab2f05 100644 --- a/packages/trader/src/AppV2/Components/TradeTypeList/trade-type-list-item.tsx +++ b/packages/trader/src/AppV2/Components/TradeTypeList/trade-type-list-item.tsx @@ -1,35 +1,53 @@ import React from 'react'; import { StandaloneCirclePlusFillIcon } from '@deriv/quill-icons'; +import clsx from 'clsx'; type TTradeTypeListItemProps = { title: string; + selected?: boolean; leftIcon?: React.ReactNode; rightIcon?: React.ReactNode; onLeftIconClick?: () => void; onRightIconClick?: () => void; + onTradeTypeClick?: (e: React.MouseEvent) => void; }; const TradeTypeListItem: React.FC = ({ title, + selected, leftIcon, rightIcon, onLeftIconClick, onRightIconClick, + onTradeTypeClick, }) => { const default_icon = ; return ( -
+ )}
{title}
- -
+ {onRightIconClick && ( + + )} + ); }; diff --git a/packages/trader/src/AppV2/Components/TradeTypeList/trade-type-list.scss b/packages/trader/src/AppV2/Components/TradeTypeList/trade-type-list.scss index d01e4cb7740f..c26fb9adca93 100644 --- a/packages/trader/src/AppV2/Components/TradeTypeList/trade-type-list.scss +++ b/packages/trader/src/AppV2/Components/TradeTypeList/trade-type-list.scss @@ -3,25 +3,48 @@ display: flex; justify-content: space-between; align-items: center; - padding: var(--core-spacing-200) var(--core-spacing-300); + padding: var(--core-spacing-200) var(--core-spacing-800); height: var(--core-spacing-2400); + border-radius: var(--core-spacing-500); + background-color: transparent; + border: none; + width: 100%; &__title { font-size: var(--core-fontSize-75); } + + &--selected { + background-color: var(--core-color-solid-slate-1400); + color: var(--core-color-solid-slate-50); + } + + button { + background-color: transparent; + border: none; + } } &-category { - margin-bottom: var(--core-spacing-1000); + padding-bottom: var(--core-spacing-400); + border-bottom: var(--core-borderWidth-75) solid var(--core-color-opacity-black-100); + + &-header { + display: flex; + justify-content: space-between; + padding: 0 var(--core-spacing-600) 0 var(--core-spacing-800); + + &-title, &-button { + margin-bottom: var(--core-spacing-100); + } + } &__title { font-size: var(--core-fontSize-75); } - &__droppable-area { - display: flex; - flex-direction: column; - padding: var(--core-spacing-500); + &__items { + margin-top: var(--core-spacing-400);; } } } diff --git a/packages/trader/src/AppV2/Components/TradeTypeList/trade-type-list.tsx b/packages/trader/src/AppV2/Components/TradeTypeList/trade-type-list.tsx index 7c0df5aab5fb..ca107c604671 100644 --- a/packages/trader/src/AppV2/Components/TradeTypeList/trade-type-list.tsx +++ b/packages/trader/src/AppV2/Components/TradeTypeList/trade-type-list.tsx @@ -2,6 +2,7 @@ import React, { useState } from 'react'; import TradeTypeListItem from './trade-type-list-item'; import { Text } from '@deriv-com/quill-ui'; import './trade-type-list.scss'; +import { Localize } from '@deriv/translations'; type TTradeTypeItem = { id: string; @@ -12,15 +13,29 @@ type TTradeTypeItem = { type TTradeTypeCategory = { id: string; title?: string; + button_title?: string; items: TTradeTypeItem[]; }; type TTradeTypeListProps = { - categories: TTradeTypeCategory[]; - onRightIconClick: (item: TTradeTypeItem) => void; + categories?: TTradeTypeCategory[]; + selected_item?: string; + selectable?: boolean; + onRightIconClick?: (item: TTradeTypeItem) => void; + onTradeTypeClick?: (e: React.MouseEvent) => void; + onAction?: () => void; + should_show_title?: boolean; }; -const TradeTypeList: React.FC = ({ categories, onRightIconClick }) => { +const TradeTypeList: React.FC = ({ + categories, + selected_item, + selectable, + onRightIconClick, + onTradeTypeClick, + onAction, + should_show_title = true, +}) => { const [category_list, setCategoryList] = useState(categories); React.useEffect(() => { @@ -29,15 +44,33 @@ const TradeTypeList: React.FC = ({ categories, onRightIconC return (
- {category_list.map(category => ( + {category_list?.map(category => (
- - {category?.title} - -
- {category.items.map(item => ( -
- onRightIconClick(item)} /> +
+ + {should_show_title && category?.title} + + {onAction && ( + + {category.button_title || } + + )} +
+
+ {category.items.map((item: TTradeTypeItem) => ( +
+ onRightIconClick(item))} + onTradeTypeClick={onTradeTypeClick} + />
))}
diff --git a/packages/trader/src/AppV2/Containers/Trade/__tests__/trade-types.spec.tsx b/packages/trader/src/AppV2/Containers/Trade/__tests__/trade-types.spec.tsx new file mode 100644 index 000000000000..9a79f047b02e --- /dev/null +++ b/packages/trader/src/AppV2/Containers/Trade/__tests__/trade-types.spec.tsx @@ -0,0 +1,100 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { mockStore } from '@deriv/stores'; +import TradeTypes from '../trade-types'; +import TraderProviders from '../../../../trader-providers'; +import { getTradeTypesList } from 'AppV2/Utils/trade-types-utils'; + +jest.mock('AppV2/Utils/trade-types-utils'); + +const mockGetTradeTypesList = getTradeTypesList as jest.MockedFunction; + +const contract_types_list = { + rise_fall: { + name: 'Rise/Fall', + categories: [ + { text: 'Rise', value: 'rise' }, + { text: 'Fall', value: 'fall' }, + ], + }, + vanilla: { + name: 'Vanilla', + categories: [ + { text: 'Vanilla Call', value: 'vanilla_call' }, + { text: 'Vanilla Put', value: 'vanilla_put' }, + ], + }, +}; + +const default_mock_store = { + modules: { + trade: { + contract_type: 'rise_fall', + contract_types_list, + onMount: jest.fn(), + onUnmount: jest.fn(), + }, + }, +}; + +const mockTradeTypes = (mocked_store = mockStore(default_mock_store)) => { + return ( + + + + ); +}; + +describe('TradeTypes', () => { + beforeEach(() => { + mockGetTradeTypesList.mockReturnValue([ + { value: 'rise', text: 'Rise' }, + { value: 'fall', text: 'Fall' }, + { value: 'vanilla_call', text: 'Vanilla Call' }, + { value: 'vanilla_put', text: 'Vanilla Put' }, + ]); + }); + + it('should render the TradeTypes component with pinned and other trade types', () => { + render(mockTradeTypes()); + + expect(screen.getByText('View all')).toBeInTheDocument(); + expect(screen.getByText('Rise')).toBeInTheDocument(); + }); + + it('should open ActionSheet when View all button is clicked', async () => { + render(mockTradeTypes()); + + await userEvent.click(screen.getByText('View all')); + + expect(screen.getByText('Trade types')).toBeInTheDocument(); + expect(screen.getByText('Fall')).toBeInTheDocument(); + }); + + it('should handle adding and removing pinned trade types', async () => { + render(mockTradeTypes()); + + await userEvent.click(screen.getByText('View all')); + await userEvent.click(screen.getByText('Customize')); + const addButton = screen.getAllByTestId('dt_trade_type_list_item_right_icon')[0]; + await userEvent.click(addButton); + + const removeButton = screen.getAllByTestId('dt_draggable_list_item_icon')[0]; + await userEvent.click(removeButton); + + expect(screen.getByText('Trade types')).toBeInTheDocument(); + }); + + it('should mount and unmount correctly', () => { + const { unmount } = render(mockTradeTypes()); + + expect(default_mock_store.modules.trade.onMount).toHaveBeenCalled(); + unmount(); + expect(default_mock_store.modules.trade.onUnmount).toHaveBeenCalled(); + }); +}); diff --git a/packages/trader/src/AppV2/Containers/Trade/all-trade-types.tsx b/packages/trader/src/AppV2/Containers/Trade/all-trade-types.tsx deleted file mode 100644 index 4cc3d5f825f9..000000000000 --- a/packages/trader/src/AppV2/Containers/Trade/all-trade-types.tsx +++ /dev/null @@ -1,194 +0,0 @@ -import React from 'react'; -import { ActionSheet } from '@deriv-com/quill-ui'; -import { observer } from '@deriv/stores'; -import BottomNav from 'AppV2/Components/BottomNav'; -import { DraggableList } from 'AppV2/Components/DraggableList'; -import { TradeTypeList } from 'AppV2/Components/TradeTypeList'; -import { useTraderStore } from 'Stores/useTraderStores'; -import { Localize } from '@deriv/translations'; - -const EditTradeTypes = observer(() => { - const { contract_types_list, onMount, onUnmount } = useTraderStore(); - - type TCategory = { - text: string; - value: string; - }; - - type TItem = { - id: string; - title: string; - icon?: React.ReactNode; - }; - - type TDataObject = { - [key: string]: { - categories: TCategory[]; - }; - }; - - type TResultItem = { - id: string; - title?: string; - button_title?: string; - onButtonClick?: () => void; - items: TItem[]; - }; - - const createArrayFromCategories = (data: TDataObject): TItem[] => { - const result: TItem[] = []; - let id_counter = 1; - - for (const key in data) { - if (data.hasOwnProperty(key) && data[key].categories) { - data[key].categories.forEach(category => { - result.push({ - id: id_counter.toString(), - title: category.text, - }); - id_counter++; - }); - } - } - - return result; - }; - - const [other_trade_types, setOtherTradeTypes] = React.useState([]); - const [pinned_trade_types, setPinnedTradeTypes] = React.useState([]); - - const [open, setOpen] = React.useState(); - - const handleOpenTradeTypes = () => { - const saved_other_trade_types = localStorage.getItem('other_trade_types'); - const saved_pinned_trade_types = localStorage.getItem('pinned_trade_types'); - - if (saved_other_trade_types) { - setOtherTradeTypes(JSON.parse(saved_other_trade_types)); - } - - if (saved_pinned_trade_types) { - setPinnedTradeTypes(JSON.parse(saved_pinned_trade_types)); - } - - setOpen(true); - }; - - const handleAddPinnedClick = (item: TItem) => { - setOtherTradeTypes(prev_categories => - prev_categories.map(category => ({ - ...category, - items: category.items.filter(i => i.id !== item.id).sort((a, b) => a.title.localeCompare(b.title)), - })) - ); - setPinnedTradeTypes(prev_pinned => { - const updated_pinned = [...prev_pinned]; - const pinned_category = updated_pinned.find(cat => cat.id === 'pinned'); - - if (pinned_category) { - pinned_category.items.push(item); - } else { - updated_pinned.push({ - id: 'pinned', - title: 'Pinned', - items: [item], - }); - } - - return updated_pinned; - }); - }; - - const handleRemovePinnedClick = (item: TItem) => { - setPinnedTradeTypes(prev_categories => - prev_categories.map(category => ({ - ...category, - items: category.items.filter(i => i.id !== item.id), - })) - ); - setOtherTradeTypes(prev_others => { - const updated_others = [...prev_others]; - const other_category = updated_others.find(cat => cat.id === 'other'); - - if (other_category) { - other_category.items.unshift(item); - } else { - updated_others.push({ - id: 'other', - items: [item], - }); - } - - updated_others.map(category => ({ - ...category, - items: category.items.sort((a, b) => a.title.localeCompare(b.title)), - })); - - return updated_others; - }); - }; - - React.useEffect(() => { - const formatted_items = createArrayFromCategories(contract_types_list); - - setOtherTradeTypes([ - { - id: 'other', - items: formatted_items - .filter(item => !pinned_trade_types[0]?.items.some(pinned_item => pinned_item.id === item.id)) - .sort((a, b) => a.title.localeCompare(b.title)), - }, - ]); - }, [contract_types_list]); - - React.useEffect(() => { - const saved_other_trade_types = localStorage.getItem('other_trade_types'); - const saved_pinned_trade_types = localStorage.getItem('pinned_trade_types'); - - onMount(); - - if (saved_other_trade_types) { - setOtherTradeTypes(JSON.parse(saved_other_trade_types)); - } - - if (saved_pinned_trade_types) { - setPinnedTradeTypes(JSON.parse(saved_pinned_trade_types)); - } - - return () => { - onUnmount(); - }; - }, []); - - const savePinnedToLocalStorage = () => { - localStorage.setItem('pinned_trade_types', JSON.stringify(pinned_trade_types)); - localStorage.setItem('other_trade_types', JSON.stringify(other_trade_types)); - setOpen(false); - }; - - const handleOnDrag = (categories: TResultItem[]) => { - setPinnedTradeTypes(categories); - }; - - return ( - - - - - } /> - - - - - - - - ); -}); - -export default EditTradeTypes; diff --git a/packages/trader/src/AppV2/Containers/Trade/trade-types.tsx b/packages/trader/src/AppV2/Containers/Trade/trade-types.tsx index 572b2e334ac6..469b97c1b2f0 100644 --- a/packages/trader/src/AppV2/Containers/Trade/trade-types.tsx +++ b/packages/trader/src/AppV2/Containers/Trade/trade-types.tsx @@ -1,14 +1,170 @@ import React from 'react'; import { useTraderStore } from 'Stores/useTraderStores'; -import { Chip, Text } from '@deriv-com/quill-ui'; +import { Chip, Text, ActionSheet } from '@deriv-com/quill-ui'; +import { DraggableList } from 'AppV2/Components/DraggableList'; +import { TradeTypeList } from 'AppV2/Components/TradeTypeList'; import { getTradeTypesList } from 'AppV2/Utils/trade-types-utils'; +import { Localize, localize } from '@deriv/translations'; -type TTemporaryTradeTypesProps = { +type TTradeTypesProps = { onTradeTypeSelect: (e: React.MouseEvent) => void; trade_types: ReturnType; } & Pick, 'contract_type'>; -const TemporaryTradeTypes = ({ contract_type, onTradeTypeSelect, trade_types }: TTemporaryTradeTypesProps) => { +type TItem = { + id: string; + title: string; + icon?: React.ReactNode; +}; + +type TResultItem = { + id: string; + title?: string; + button_title?: string; + onButtonClick?: () => void; + items: TItem[]; +}; + +const TradeTypes = ({ contract_type, onTradeTypeSelect, trade_types }: TTradeTypesProps) => { + const [is_open, setIsOpen] = React.useState(false); + const [is_editing, setIsEditing] = React.useState(false); + + const { onMount, onUnmount } = useTraderStore(); + + const createArrayFromCategories = (data: any): TItem[] => { + const result: TItem[] = []; + + data.forEach((category: { value: string; text: string }) => { + result.push({ + id: category.value, + title: category.text, + }); + }); + + return result; + }; + + const saved_other_trade_types = JSON.parse(localStorage.getItem('other_trade_types') ?? '[]'); + const saved_pinned_trade_types = JSON.parse(localStorage.getItem('pinned_trade_types') ?? '[]'); + + const [other_trade_types, setOtherTradeTypes] = React.useState(saved_other_trade_types); + const [pinned_trade_types, setPinnedTradeTypes] = React.useState(saved_pinned_trade_types); + + const handleCloseTradeTypes = () => { + setIsOpen(false); + setIsEditing(false); + }; + + const handleAddPinnedClick = (item: TItem) => { + setOtherTradeTypes(prev_categories => + prev_categories.map(category => ({ + ...category, + items: category.items.filter(i => i.id !== item.id).sort((a, b) => a.title?.localeCompare(b.title)), + })) + ); + setPinnedTradeTypes(prev_pinned => { + const updated_pinned = [...prev_pinned]; + const pinned_category = updated_pinned.find(cat => cat.id === 'pinned'); + + if (pinned_category) { + pinned_category.items.push(item); + } else { + updated_pinned.push({ + id: 'pinned', + title: localize('Pinned'), + items: [item], + }); + } + + return updated_pinned; + }); + }; + + const handleRemovePinnedClick = (item: TItem) => { + setPinnedTradeTypes(prev_categories => + prev_categories.map(category => ({ + ...category, + items: category.items.filter(i => i.id !== item.id), + })) + ); + setOtherTradeTypes(prev_others => { + const updated_others = [...prev_others]; + const other_category = updated_others.find(cat => cat.id === 'other'); + + if (other_category) { + other_category.items.unshift(item); + } else { + updated_others.push({ + id: 'other', + items: [item], + }); + } + + updated_others.map(category => ({ + ...category, + items: category.items.sort((a, b) => a.title?.localeCompare(b.title)), + })); + + return updated_others; + }); + }; + + React.useEffect(() => { + const formatted_items = createArrayFromCategories(trade_types); + const default_pinned_trade_types = [ + { + id: 'pinned', + title: localize('Pinned'), + items: formatted_items.slice(0, 1), + }, + ]; + const default_other_trade_types = [ + { + id: 'other', + items: formatted_items + .filter(item => !pinned_trade_types[0]?.items.some(pinned_item => pinned_item.id === item.id)) + .filter( + item => !default_pinned_trade_types[0]?.items.some(pinned_item => pinned_item.id === item.id) + ) + .sort((a, b) => a.title?.localeCompare(b.title)), + }, + ]; + + if (saved_pinned_trade_types.length < 1) { + setPinnedTradeTypes(default_pinned_trade_types); + localStorage.setItem('pinned_trade_types', JSON.stringify(default_pinned_trade_types)); + } + + setOtherTradeTypes(default_other_trade_types); + localStorage.setItem('other_trade_types', JSON.stringify(default_other_trade_types)); + }, [trade_types]); + + React.useEffect(() => { + onMount(); + + if (saved_pinned_trade_types.length > 0) { + setPinnedTradeTypes(saved_pinned_trade_types); + } + + if (saved_other_trade_types.length > 0) { + setOtherTradeTypes(saved_other_trade_types); + } + + return () => { + onUnmount(); + }; + }, []); + + const savePinnedToLocalStorage = () => { + localStorage.setItem('pinned_trade_types', JSON.stringify(pinned_trade_types)); + localStorage.setItem('other_trade_types', JSON.stringify(other_trade_types)); + setIsOpen(false); + }; + + const handleOnDrag = (categories: TResultItem[]) => { + setPinnedTradeTypes(categories); + }; + const isTradeTypeSelected = (value: string) => [contract_type, value].every(type => type.startsWith('vanilla')) || [contract_type, value].every(type => type.startsWith('turbos')) || @@ -16,13 +172,50 @@ const TemporaryTradeTypes = ({ contract_type, onTradeTypeSelect, trade_types }: contract_type === value; return (
- {trade_types.map(({ text, value }) => ( - - {text} - - ))} + {saved_pinned_trade_types.length > 0 && + saved_pinned_trade_types[0].items.map(({ title, id }: TItem) => ( + + {title} + + ))} + setIsOpen(true)} className='trade__trade-types-header'> + + {} + + + + + } /> + + {is_editing ? ( + + ) : ( + setIsEditing(true)} + onTradeTypeClick={onTradeTypeSelect} + selected_item={contract_type} + should_show_title={false} + selectable + /> + )} + + + +
); }; -export default React.memo(TemporaryTradeTypes); +export default React.memo(TradeTypes); diff --git a/packages/trader/src/AppV2/Containers/Trade/trade.scss b/packages/trader/src/AppV2/Containers/Trade/trade.scss index a1072eaefa73..75b54851c697 100644 --- a/packages/trader/src/AppV2/Containers/Trade/trade.scss +++ b/packages/trader/src/AppV2/Containers/Trade/trade.scss @@ -25,6 +25,10 @@ button { background-color: transparent; } + + &-header { + margin: auto 0; + } } &__assets { padding: 0 var(--core-spacing-800); diff --git a/packages/trader/src/AppV2/Containers/Trade/trade.tsx b/packages/trader/src/AppV2/Containers/Trade/trade.tsx index 442e71b4e503..f7b704600714 100644 --- a/packages/trader/src/AppV2/Containers/Trade/trade.tsx +++ b/packages/trader/src/AppV2/Containers/Trade/trade.tsx @@ -11,7 +11,7 @@ import { TradeParametersContainer, TradeParameters } from 'AppV2/Components/Trad import CurrentSpot from 'AppV2/Components/CurrentSpot'; import { TradeChart } from '../Chart'; import { isDigitTradeType } from 'Modules/Trading/Helpers/digits'; -import TemporaryTradeTypes from './trade-types'; +import TradeTypes from './trade-types'; import LastDigitPrediction from 'AppV2/Components/TradeParameters/LastDigitPrediction'; import MarketSelector from 'AppV2/Components/MarketSelector'; @@ -67,7 +67,7 @@ const Trade = observer(() => { {symbols.length && trade_types.length ? (
-