feat(trading): store current tab selection (#3148)
This commit is contained in:
parent
b2dd053621
commit
1d178b940f
@ -282,7 +282,7 @@ export const LiquidityViewContainer = ({
|
|||||||
<div className="break-word">{marketId}</div>
|
<div className="break-word">{marketId}</div>
|
||||||
</HeaderStat>
|
</HeaderStat>
|
||||||
</Header>
|
</Header>
|
||||||
<Tabs active={getActiveDefaultId()}>
|
<Tabs defaultValue={getActiveDefaultId()}>
|
||||||
<Tab
|
<Tab
|
||||||
id={LiquidityTabs.MyLiquidityProvision}
|
id={LiquidityTabs.MyLiquidityProvision}
|
||||||
name={t('My liquidity provision')}
|
name={t('My liquidity provision')}
|
||||||
|
@ -14,7 +14,7 @@ import { DepthChartContainer } from '@vegaprotocol/market-depth';
|
|||||||
import { CandlesChartContainer } from '@vegaprotocol/candles-chart';
|
import { CandlesChartContainer } from '@vegaprotocol/candles-chart';
|
||||||
import {
|
import {
|
||||||
Tab,
|
Tab,
|
||||||
Tabs,
|
LocalStoragePersistTabs as Tabs,
|
||||||
ResizableGrid,
|
ResizableGrid,
|
||||||
ResizableGridPanel,
|
ResizableGridPanel,
|
||||||
Splash,
|
Splash,
|
||||||
@ -96,7 +96,7 @@ const MarketBottomPanel = memo(
|
|||||||
minSize={50}
|
minSize={50}
|
||||||
>
|
>
|
||||||
<TradeGridChild>
|
<TradeGridChild>
|
||||||
<Tabs>
|
<Tabs storageKey="console-trade-grid-orders">
|
||||||
<Tab id="orders" name={t('Orders')}>
|
<Tab id="orders" name={t('Orders')}>
|
||||||
<VegaWalletContainer>
|
<VegaWalletContainer>
|
||||||
<TradingViews.Orders
|
<TradingViews.Orders
|
||||||
@ -122,7 +122,7 @@ const MarketBottomPanel = memo(
|
|||||||
minSize={50}
|
minSize={50}
|
||||||
>
|
>
|
||||||
<TradeGridChild>
|
<TradeGridChild>
|
||||||
<Tabs>
|
<Tabs storageKey="console-trade-grid-positions">
|
||||||
<Tab id="positions" name={t('Positions')}>
|
<Tab id="positions" name={t('Positions')}>
|
||||||
<VegaWalletContainer>
|
<VegaWalletContainer>
|
||||||
<TradingViews.Positions
|
<TradingViews.Positions
|
||||||
@ -146,7 +146,7 @@ const MarketBottomPanel = memo(
|
|||||||
</ResizableGrid>
|
</ResizableGrid>
|
||||||
) : (
|
) : (
|
||||||
<TradeGridChild>
|
<TradeGridChild>
|
||||||
<Tabs>
|
<Tabs storageKey="console-trade-grid-positions">
|
||||||
<Tab id="positions" name={t('Positions')}>
|
<Tab id="positions" name={t('Positions')}>
|
||||||
<VegaWalletContainer>
|
<VegaWalletContainer>
|
||||||
<TradingViews.Positions onMarketClick={onMarketClick} />
|
<TradingViews.Positions onMarketClick={onMarketClick} />
|
||||||
@ -201,7 +201,7 @@ const MainGrid = memo(
|
|||||||
preferredSize="50%"
|
preferredSize="50%"
|
||||||
>
|
>
|
||||||
<TradeGridChild>
|
<TradeGridChild>
|
||||||
<Tabs>
|
<Tabs storageKey="console-trade-grid-chart">
|
||||||
<Tab id="chart" name={t('Chart')}>
|
<Tab id="chart" name={t('Chart')}>
|
||||||
<TradingViews.Candles marketId={marketId} />
|
<TradingViews.Candles marketId={marketId} />
|
||||||
</Tab>
|
</Tab>
|
||||||
@ -220,7 +220,7 @@ const MainGrid = memo(
|
|||||||
minSize={300}
|
minSize={300}
|
||||||
>
|
>
|
||||||
<TradeGridChild>
|
<TradeGridChild>
|
||||||
<Tabs>
|
<Tabs storageKey="console-trade-grid-ticket">
|
||||||
<Tab id="ticket" name={t('Ticket')}>
|
<Tab id="ticket" name={t('Ticket')}>
|
||||||
<TradingViews.Ticket
|
<TradingViews.Ticket
|
||||||
marketId={marketId}
|
marketId={marketId}
|
||||||
@ -244,7 +244,7 @@ const MainGrid = memo(
|
|||||||
minSize={200}
|
minSize={200}
|
||||||
>
|
>
|
||||||
<TradeGridChild>
|
<TradeGridChild>
|
||||||
<Tabs>
|
<Tabs storageKey="console-trade-grid-orderbook">
|
||||||
<Tab id="orderbook" name={t('Orderbook')}>
|
<Tab id="orderbook" name={t('Orderbook')}>
|
||||||
<TradingViews.Orderbook marketId={marketId} />
|
<TradingViews.Orderbook marketId={marketId} />
|
||||||
</Tab>
|
</Tab>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { titlefy } from '@vegaprotocol/utils';
|
import { titlefy } from '@vegaprotocol/utils';
|
||||||
import { t } from '@vegaprotocol/i18n';
|
import { t } from '@vegaprotocol/i18n';
|
||||||
import { Tabs, Tab } from '@vegaprotocol/ui-toolkit';
|
import { LocalStoragePersistTabs as Tabs, Tab } from '@vegaprotocol/ui-toolkit';
|
||||||
import { Markets } from './markets';
|
import { Markets } from './markets';
|
||||||
import { Proposed } from './proposed';
|
import { Proposed } from './proposed';
|
||||||
import { usePageTitleStore } from '../../stores';
|
import { usePageTitleStore } from '../../stores';
|
||||||
@ -14,7 +14,7 @@ export const MarketsPage = () => {
|
|||||||
updateTitle(titlefy(['Markets']));
|
updateTitle(titlefy(['Markets']));
|
||||||
}, [updateTitle]);
|
}, [updateTitle]);
|
||||||
return (
|
return (
|
||||||
<Tabs>
|
<Tabs storageKey="console-markets">
|
||||||
<Tab id="all-markets" name={t('All markets')}>
|
<Tab id="all-markets" name={t('All markets')}>
|
||||||
<Markets />
|
<Markets />
|
||||||
</Tab>
|
</Tab>
|
||||||
|
@ -2,7 +2,11 @@ import { titlefy } from '@vegaprotocol/utils';
|
|||||||
import { t } from '@vegaprotocol/i18n';
|
import { t } from '@vegaprotocol/i18n';
|
||||||
import { PositionsContainer } from '@vegaprotocol/positions';
|
import { PositionsContainer } from '@vegaprotocol/positions';
|
||||||
import { OrderListContainer } from '@vegaprotocol/orders';
|
import { OrderListContainer } from '@vegaprotocol/orders';
|
||||||
import { ResizableGridPanel, Tab, Tabs } from '@vegaprotocol/ui-toolkit';
|
import {
|
||||||
|
ResizableGridPanel,
|
||||||
|
Tab,
|
||||||
|
LocalStoragePersistTabs as Tabs,
|
||||||
|
} from '@vegaprotocol/ui-toolkit';
|
||||||
import { WithdrawalsContainer } from './withdrawals-container';
|
import { WithdrawalsContainer } from './withdrawals-container';
|
||||||
import { FillsContainer } from '@vegaprotocol/fills';
|
import { FillsContainer } from '@vegaprotocol/fills';
|
||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
@ -41,7 +45,7 @@ export const Portfolio = () => {
|
|||||||
<ResizableGrid vertical>
|
<ResizableGrid vertical>
|
||||||
<ResizableGridPanel minSize={75}>
|
<ResizableGridPanel minSize={75}>
|
||||||
<PortfolioGridChild>
|
<PortfolioGridChild>
|
||||||
<Tabs>
|
<Tabs storageKey="console-portfolio-account-history">
|
||||||
<Tab id="account-history" name={t('Account history')}>
|
<Tab id="account-history" name={t('Account history')}>
|
||||||
<VegaWalletContainer>
|
<VegaWalletContainer>
|
||||||
<AccountHistoryContainer />
|
<AccountHistoryContainer />
|
||||||
@ -79,7 +83,7 @@ export const Portfolio = () => {
|
|||||||
minSize={50}
|
minSize={50}
|
||||||
>
|
>
|
||||||
<PortfolioGridChild>
|
<PortfolioGridChild>
|
||||||
<Tabs>
|
<Tabs storageKey="console-portfolio-collateral">
|
||||||
<Tab id="collateral" name={t('Collateral')}>
|
<Tab id="collateral" name={t('Collateral')}>
|
||||||
<VegaWalletContainer>
|
<VegaWalletContainer>
|
||||||
<AccountsContainer />
|
<AccountsContainer />
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { act, renderHook } from '@testing-library/react';
|
import { act, renderHook } from '@testing-library/react';
|
||||||
import { useLocalStorage } from './use-local-storage';
|
import { useLocalStorage, useLocalStorageSnapshot } from './use-local-storage';
|
||||||
|
|
||||||
describe('useLocalStorage', () => {
|
describe('useLocalStorage', () => {
|
||||||
afterEach(() => window.localStorage.clear());
|
afterEach(() => window.localStorage.clear());
|
||||||
@ -50,3 +50,51 @@ describe('useLocalStorage', () => {
|
|||||||
expect(B.current[0]).toBeNull();
|
expect(B.current[0]).toBeNull();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('useLocalStorageSnapshot', () => {
|
||||||
|
afterEach(() => window.localStorage.clear());
|
||||||
|
it("should return null if there's no value set", () => {
|
||||||
|
const { result } = renderHook(() => useLocalStorageSnapshot('test'));
|
||||||
|
const [value] = result.current;
|
||||||
|
expect(value).toBeNull();
|
||||||
|
});
|
||||||
|
it('should return already saved value', () => {
|
||||||
|
window.localStorage.setItem('test', '123');
|
||||||
|
const { result } = renderHook(() => useLocalStorageSnapshot('test'));
|
||||||
|
const [value] = result.current;
|
||||||
|
expect(value).toEqual('123');
|
||||||
|
});
|
||||||
|
it('should save given value', () => {
|
||||||
|
const { result } = renderHook(() => useLocalStorageSnapshot('test'));
|
||||||
|
const setValue = result.current[1];
|
||||||
|
expect(result.current[0]).toBeNull();
|
||||||
|
act(() => setValue('123'));
|
||||||
|
expect(result.current[0]).toEqual('123');
|
||||||
|
act(() => setValue('456'));
|
||||||
|
expect(result.current[0]).toEqual('456');
|
||||||
|
});
|
||||||
|
it('should remove given value', () => {
|
||||||
|
const { result } = renderHook(() => useLocalStorageSnapshot('test'));
|
||||||
|
const setValue = result.current[1];
|
||||||
|
const removeValue = result.current[2];
|
||||||
|
act(() => setValue('123'));
|
||||||
|
expect(result.current[0]).toEqual('123');
|
||||||
|
act(() => removeValue());
|
||||||
|
expect(result.current[0]).toBeNull();
|
||||||
|
});
|
||||||
|
it('should not return value set by storage event (by another tab)', () => {
|
||||||
|
const { result: A } = renderHook(() => useLocalStorageSnapshot('test-a'));
|
||||||
|
act(() => {
|
||||||
|
window.localStorage.setItem('test-a', '123');
|
||||||
|
window.dispatchEvent(
|
||||||
|
new StorageEvent('storage', {
|
||||||
|
key: 'test-a',
|
||||||
|
oldValue: window.localStorage.getItem('test-a'),
|
||||||
|
storageArea: window.localStorage,
|
||||||
|
newValue: '123',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
expect(A.current[0]).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
@ -1,4 +1,10 @@
|
|||||||
import { useCallback, useEffect, useMemo, useSyncExternalStore } from 'react';
|
import {
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useSyncExternalStore,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
import { LocalStorage } from '@vegaprotocol/utils';
|
import { LocalStorage } from '@vegaprotocol/utils';
|
||||||
|
|
||||||
type LocalStorageCallback = (key: string) => void;
|
type LocalStorageCallback = (key: string) => void;
|
||||||
@ -10,6 +16,12 @@ const unregisterCallback = (callback: LocalStorageCallback) =>
|
|||||||
const triggerCallbacks = (key: string) =>
|
const triggerCallbacks = (key: string) =>
|
||||||
LOCAL_STORAGE_CALLBACKS.forEach((cb) => cb(key));
|
LOCAL_STORAGE_CALLBACKS.forEach((cb) => cb(key));
|
||||||
|
|
||||||
|
type UseLocalStorageHook = [
|
||||||
|
string | null | undefined,
|
||||||
|
(value: string) => void,
|
||||||
|
() => void
|
||||||
|
];
|
||||||
|
|
||||||
export const useLocalStorage = (key: string) => {
|
export const useLocalStorage = (key: string) => {
|
||||||
const subscribe = useCallback(
|
const subscribe = useCallback(
|
||||||
(onStoreChange: () => void) => {
|
(onStoreChange: () => void) => {
|
||||||
@ -52,7 +64,27 @@ export const useLocalStorage = (key: string) => {
|
|||||||
return () => window.removeEventListener('storage', onStorage);
|
return () => window.removeEventListener('storage', onStorage);
|
||||||
}, [key, onStorage]);
|
}, [key, onStorage]);
|
||||||
|
|
||||||
return useMemo<
|
return useMemo<UseLocalStorageHook>(
|
||||||
[string | null | undefined, (value: string) => void, () => void]
|
() => [value, setValue, removeValue],
|
||||||
>(() => [value, setValue, removeValue], [removeValue, setValue, value]);
|
[removeValue, setValue, value]
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useLocalStorageSnapshot = (key: string) => {
|
||||||
|
const [value, setStoredValue] = useState(LocalStorage.getItem(key));
|
||||||
|
const setValue = useCallback(
|
||||||
|
(value: string) => {
|
||||||
|
LocalStorage.setItem(key, value);
|
||||||
|
setStoredValue(value);
|
||||||
|
},
|
||||||
|
[key]
|
||||||
|
);
|
||||||
|
const removeValue = useCallback(() => {
|
||||||
|
LocalStorage.removeItem(key);
|
||||||
|
setStoredValue(null);
|
||||||
|
}, [key]);
|
||||||
|
return useMemo<UseLocalStorageHook>(
|
||||||
|
() => [value, setValue, removeValue],
|
||||||
|
[removeValue, setValue, value]
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,23 +1,31 @@
|
|||||||
import * as TabsPrimitive from '@radix-ui/react-tabs';
|
import * as TabsPrimitive from '@radix-ui/react-tabs';
|
||||||
|
import { useLocalStorageSnapshot } from '@vegaprotocol/react-helpers';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import type { ReactElement, ReactNode } from 'react';
|
import type { ReactElement, ReactNode } from 'react';
|
||||||
import { Children, isValidElement, useState } from 'react';
|
import { Children, isValidElement, useState } from 'react';
|
||||||
|
export interface TabsProps extends TabsPrimitive.TabsProps {
|
||||||
interface TabsProps {
|
|
||||||
children: ReactElement<TabProps>[];
|
children: ReactElement<TabProps>[];
|
||||||
active?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Tabs = ({ children, active: activeDefaultId }: TabsProps) => {
|
export const Tabs = ({
|
||||||
|
children,
|
||||||
|
defaultValue,
|
||||||
|
value,
|
||||||
|
onValueChange,
|
||||||
|
...props
|
||||||
|
}: TabsProps) => {
|
||||||
const [activeTab, setActiveTab] = useState<string>(() => {
|
const [activeTab, setActiveTab] = useState<string>(() => {
|
||||||
return activeDefaultId ?? children[0].props.id;
|
if (defaultValue) {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
return children[0].props.id;
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TabsPrimitive.Root
|
<TabsPrimitive.Root
|
||||||
value={activeTab}
|
{...props}
|
||||||
|
value={value || activeTab}
|
||||||
|
onValueChange={onValueChange || setActiveTab}
|
||||||
className="h-full grid grid-rows-[min-content_1fr]"
|
className="h-full grid grid-rows-[min-content_1fr]"
|
||||||
onValueChange={(value) => setActiveTab(value)}
|
|
||||||
>
|
>
|
||||||
<div className="border-b border-default">
|
<div className="border-b border-default">
|
||||||
<TabsPrimitive.List
|
<TabsPrimitive.List
|
||||||
@ -26,7 +34,7 @@ export const Tabs = ({ children, active: activeDefaultId }: TabsProps) => {
|
|||||||
>
|
>
|
||||||
{Children.map(children, (child) => {
|
{Children.map(children, (child) => {
|
||||||
if (!isValidElement(child) || child.props.hidden) return null;
|
if (!isValidElement(child) || child.props.hidden) return null;
|
||||||
const isActive = child.props.id === activeTab;
|
const isActive = child.props.id === (value || activeTab);
|
||||||
const triggerClass = classNames(
|
const triggerClass = classNames(
|
||||||
'relative px-4 py-1 border-r border-default',
|
'relative px-4 py-1 border-r border-default',
|
||||||
'uppercase',
|
'uppercase',
|
||||||
@ -82,3 +90,15 @@ interface TabProps {
|
|||||||
export const Tab = ({ children, ...props }: TabProps) => {
|
export const Tab = ({ children, ...props }: TabProps) => {
|
||||||
return <div {...props}>{children}</div>;
|
return <div {...props}>{children}</div>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const LocalStoragePersistTabs = ({
|
||||||
|
storageKey,
|
||||||
|
...props
|
||||||
|
}: Omit<TabsProps, 'value' | 'onValueChange'> & { storageKey: string }) => {
|
||||||
|
const [value, onValueChange] = useLocalStorageSnapshot(
|
||||||
|
`active-tab-${storageKey}`
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<Tabs {...props} value={value || undefined} onValueChange={onValueChange} />
|
||||||
|
);
|
||||||
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user