Merge branch 'develop' of github.com:vegaprotocol/frontend-monorepo into feat/lp-sla
This commit is contained in:
@@ -133,7 +133,7 @@ export const ErrorBoundary = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const GHOST = (
|
||||
export const GHOST = (
|
||||
<svg
|
||||
width="56"
|
||||
height="85"
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
import { BackgroundVideo } from '@vegaprotocol/ui-toolkit';
|
||||
import classNames from 'classnames';
|
||||
|
||||
const RestrictedPage = () => {
|
||||
const errorTitle = '451 Unavailable';
|
||||
const errorMessage =
|
||||
'Due to uncertainty about the legal and regulatory status of the content hosted on this site, it is not available to visitors in your jurisdiction.';
|
||||
|
||||
return (
|
||||
<div>
|
||||
<BackgroundVideo className="brightness-50" />
|
||||
<div
|
||||
className={classNames(
|
||||
'max-w-[620px] p-2 mt-[10vh]',
|
||||
'mx-auto my-0',
|
||||
'antialiased text-white',
|
||||
'overflow-hidden relative',
|
||||
'flex flex-col gap-2'
|
||||
)}
|
||||
>
|
||||
<div className="flex gap-4">
|
||||
<div>{GHOST}</div>
|
||||
<h1 className="text-[2.7rem] font-alpha calt break-words uppercase">
|
||||
{errorTitle}
|
||||
</h1>
|
||||
</div>
|
||||
<div className="text-sm mt-10 overflow-auto font-mono">
|
||||
{errorMessage}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const GHOST = (
|
||||
<svg
|
||||
width="56"
|
||||
height="85"
|
||||
viewBox="0 0 150 234"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M10.43 93.6821L0 104.097V172.435H5.13793V106.2L14.0779 97.3247L10.43 93.6821Z"
|
||||
fill="black"
|
||||
/>
|
||||
<path d="M105.328 18.521H7.70703V172.435H105.328V18.521Z" fill="white" />
|
||||
<path d="M38.5364 64.6953H33.3984V69.8258H38.5364V64.6953Z" fill="black" />
|
||||
<path d="M43.6731 69.8257H38.5352V74.9561H43.6731V69.8257Z" fill="black" />
|
||||
<path d="M48.8098 74.9561H43.6719V80.0865H48.8098V74.9561Z" fill="black" />
|
||||
<path d="M38.5364 74.9561H33.3984V80.0865H38.5364V74.9561Z" fill="black" />
|
||||
<path d="M48.8098 64.6953H43.6719V69.8258H48.8098V64.6953Z" fill="black" />
|
||||
<path d="M69.3606 64.6953H64.2227V69.8258H69.3606V64.6953Z" fill="black" />
|
||||
<path d="M74.5012 69.8257H69.3633V74.9561H74.5012V69.8257Z" fill="black" />
|
||||
<path d="M79.6379 74.9561H74.5V80.0865H79.6379V74.9561Z" fill="black" />
|
||||
<path d="M69.3606 74.9561H64.2227V80.0865H69.3606V74.9561Z" fill="black" />
|
||||
<path d="M79.6379 64.6953H74.5V69.8258H79.6379V64.6953Z" fill="black" />
|
||||
<path d="M79.6398 90.3477H33.3984V95.4781H79.6398V90.3477Z" fill="black" />
|
||||
<path d="M48.8098 172.435H43.6719V234H48.8098V172.435Z" fill="black" />
|
||||
<path d="M69.3606 172.435H64.2227V234H69.3606V172.435Z" fill="black" />
|
||||
<path
|
||||
d="M129.87 0.00408026L92.0273 14.3682L111.195 64.7203L149.038 50.3562L129.87 0.00408026Z"
|
||||
fill="#FF0081"
|
||||
/>
|
||||
<path
|
||||
d="M108.565 21.7836L103.762 23.6064L105.587 28.4024L110.39 26.5795L108.565 21.7836Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M115.189 24.7596L110.387 26.583L112.213 31.3785L117.015 29.5551L115.189 24.7596Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M121.826 27.7348L117.023 29.5576L118.849 34.3536L123.652 32.5307L121.826 27.7348Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M128.447 30.7059L123.645 32.5293L125.471 37.3247L130.273 35.5014L128.447 30.7059Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M135.084 33.6854L130.281 35.5083L132.107 40.3043L136.91 38.4814L135.084 33.6854Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M124.799 21.1156L119.996 22.939L121.822 27.7344L126.625 25.911L124.799 21.1156Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M127.775 14.492L122.973 16.3154L124.799 21.1109L129.601 19.2875L127.775 14.492Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M118.838 34.3499L114.035 36.1733L115.861 40.9688L120.664 39.1454L118.838 34.3499Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M115.857 40.9691L111.055 42.7925L112.881 47.5879L117.683 45.7645L115.857 40.9691Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M129.938 52.3822L139.906 91.5276L100.703 101.429L101.988 106.406L146.174 95.2215L134.922 51.0996L129.938 52.3822Z"
|
||||
fill="black"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default RestrictedPage;
|
||||
@@ -1,5 +1,6 @@
|
||||
export const Routes = {
|
||||
HOME: '/',
|
||||
RESTRICTED: '/restricted',
|
||||
TX: 'txs',
|
||||
BLOCKS: 'blocks',
|
||||
PARTIES: 'parties',
|
||||
|
||||
@@ -30,6 +30,7 @@ import { remove0x } from '@vegaprotocol/utils';
|
||||
import { PartyAccountsByAsset } from './parties/id/accounts';
|
||||
import { Disclaimer } from './pages/disclaimer';
|
||||
import { FLAGS } from '@vegaprotocol/environment';
|
||||
import RestrictedPage from './restricted';
|
||||
|
||||
export type Navigable = {
|
||||
path: string;
|
||||
@@ -356,6 +357,14 @@ export const routerConfig: Route[] = [
|
||||
...validators,
|
||||
],
|
||||
},
|
||||
{
|
||||
path: Routes.RESTRICTED,
|
||||
element: <RestrictedPage />,
|
||||
handle: {
|
||||
name: t('Restricted'),
|
||||
text: t('Restricted'),
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export const router = createBrowserRouter(routerConfig);
|
||||
|
||||
@@ -106,6 +106,7 @@ const Web3Container = ({
|
||||
useEthWithdrawApprovalsManager();
|
||||
return null;
|
||||
};
|
||||
|
||||
const [connectors, initializeConnectors] = useWeb3ConnectStore((store) => [
|
||||
store.connectors,
|
||||
store.initialize,
|
||||
@@ -239,6 +240,9 @@ const AppContainer = () => {
|
||||
store.setDialogOpen,
|
||||
]);
|
||||
|
||||
// Hacky skip all the loading & web3 init for geo restricted users
|
||||
const isRestricted = document?.location?.pathname?.includes('/restricted');
|
||||
|
||||
useEffect(() => {
|
||||
if (ENV.dsn && telemetryOn === 'true') {
|
||||
Sentry.init({
|
||||
@@ -304,6 +308,14 @@ const AppContainer = () => {
|
||||
}
|
||||
}, [GIT_COMMIT_HASH, GIT_BRANCH, VEGA_ENV, telemetryOn]);
|
||||
|
||||
if (isRestricted) {
|
||||
return (
|
||||
<Router>
|
||||
<AppRouter />
|
||||
</Router>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Router>
|
||||
<ScrollToTop />
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"pageTitleRedemptionTranche": "Redeem from Tranche",
|
||||
"pageTitleTranches": "Vesting tranches",
|
||||
"pageTitle404": "Page not found",
|
||||
"pageTitle451": "451 unavailable",
|
||||
"pageTitleNotPermitted": "Can not proceed!",
|
||||
"pageTitleDisassociate": "Disassociate $VEGA tokens from a Vega key",
|
||||
"pageTitleProposals": "Proposals",
|
||||
|
||||
+80
-3
@@ -12,18 +12,19 @@ import {
|
||||
generateProposal,
|
||||
generateYesVotes,
|
||||
} from '../../test-helpers/generate-proposals';
|
||||
import { ProposalHeader } from './proposal-header';
|
||||
import { ProposalHeader, NewTransferSummary } from './proposal-header';
|
||||
import {
|
||||
lastWeek,
|
||||
nextWeek,
|
||||
mockWalletContext,
|
||||
createUserVoteQueryMock,
|
||||
} from '../../test-helpers/mocks';
|
||||
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
|
||||
import type { MockedResponse } from '@apollo/client/testing';
|
||||
import { FLAGS } from '@vegaprotocol/environment';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import { VoteState } from '../vote-details/use-user-vote';
|
||||
import { useNewTransferProposalDetails } from '@vegaprotocol/proposals';
|
||||
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
|
||||
import type { MockedResponse } from '@apollo/client/testing';
|
||||
|
||||
jest.mock('@vegaprotocol/proposals', () => ({
|
||||
...jest.requireActual('@vegaprotocol/proposals'),
|
||||
@@ -31,6 +32,7 @@ jest.mock('@vegaprotocol/proposals', () => ({
|
||||
code: 'PARENT_CODE',
|
||||
parentMarketId: 'PARENT_ID',
|
||||
}),
|
||||
useNewTransferProposalDetails: jest.fn(),
|
||||
}));
|
||||
|
||||
const renderComponent = (
|
||||
@@ -418,3 +420,78 @@ describe('Proposal header', () => {
|
||||
expect(await screen.findByTestId('user-voted-yes')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
jest.mock('@vegaprotocol/proposals');
|
||||
|
||||
describe('<NewTransferSummary />', () => {
|
||||
it('renders null if no details are provided', () => {
|
||||
(useNewTransferProposalDetails as jest.Mock).mockReturnValue(null);
|
||||
const { container } = render(<NewTransferSummary proposalId="1" />);
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
|
||||
it('handles OneOffGovernanceTransfer', () => {
|
||||
(useNewTransferProposalDetails as jest.Mock).mockReturnValue({
|
||||
kind: { __typename: 'OneOffGovernanceTransfer', deliverOn: null },
|
||||
source: '0x123',
|
||||
sourceType: 'wallet',
|
||||
destination: '0x456',
|
||||
destinationType: 'contract',
|
||||
});
|
||||
const { getByText } = render(<NewTransferSummary proposalId="1" />);
|
||||
const textMatch = (content: string) => content.includes('One off transfer');
|
||||
expect(getByText(textMatch)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('handles RecurringGovernanceTransfer', () => {
|
||||
(useNewTransferProposalDetails as jest.Mock).mockReturnValue({
|
||||
kind: {
|
||||
__typename: 'RecurringGovernanceTransfer',
|
||||
startEpoch: 1,
|
||||
endEpoch: 5,
|
||||
},
|
||||
source: '0x123',
|
||||
sourceType: 'wallet',
|
||||
destination: '0x456',
|
||||
destinationType: 'contract',
|
||||
});
|
||||
const { getByText } = render(<NewTransferSummary proposalId="1" />);
|
||||
const textMatch = (content: string) =>
|
||||
content.includes('Recurring transfer');
|
||||
expect(getByText(textMatch)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should fallback to translated sourceType when source is not set', () => {
|
||||
(useNewTransferProposalDetails as jest.Mock).mockReturnValue({
|
||||
kind: {
|
||||
__typename: 'RecurringGovernanceTransfer',
|
||||
startEpoch: 1,
|
||||
endEpoch: 5,
|
||||
},
|
||||
source: undefined,
|
||||
sourceType: 'ACCOUNT_TYPE_GENERAL',
|
||||
destination: '0x456',
|
||||
destinationType: 'ACCOUNT_TYPE_GENERAL',
|
||||
});
|
||||
|
||||
render(<NewTransferSummary proposalId="1" />);
|
||||
expect(screen.getByText('General account')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should fallback to translated destinationType when destination is not set', () => {
|
||||
(useNewTransferProposalDetails as jest.Mock).mockReturnValue({
|
||||
kind: {
|
||||
__typename: 'RecurringGovernanceTransfer',
|
||||
startEpoch: 1,
|
||||
endEpoch: 5,
|
||||
},
|
||||
source: '0x123',
|
||||
sourceType: 'ACCOUNT_TYPE_GENERAL',
|
||||
destination: undefined,
|
||||
destinationType: 'ACCOUNT_TYPE_GLOBAL_INSURANCE',
|
||||
});
|
||||
|
||||
render(<NewTransferSummary proposalId="1" />);
|
||||
expect(screen.getByText('Global insurance account')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
+20
-5
@@ -237,7 +237,11 @@ export const ProposalHeader = ({
|
||||
);
|
||||
};
|
||||
|
||||
const SuccessorCode = ({ proposalId }: { proposalId?: string | null }) => {
|
||||
export const SuccessorCode = ({
|
||||
proposalId,
|
||||
}: {
|
||||
proposalId?: string | null;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const successor = useSuccessorMarketProposalDetails(proposalId);
|
||||
|
||||
@@ -254,7 +258,11 @@ const SuccessorCode = ({ proposalId }: { proposalId?: string | null }) => {
|
||||
) : null;
|
||||
};
|
||||
|
||||
const NewTransferSummary = ({ proposalId }: { proposalId?: string | null }) => {
|
||||
export const NewTransferSummary = ({
|
||||
proposalId,
|
||||
}: {
|
||||
proposalId?: string | null;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const details = useNewTransferProposalDetails(proposalId);
|
||||
|
||||
@@ -265,14 +273,21 @@ const NewTransferSummary = ({ proposalId }: { proposalId?: string | null }) => {
|
||||
{GovernanceTransferKindMapping[details.kind.__typename]}{' '}
|
||||
{t('transfer from')}{' '}
|
||||
<Lozenge>
|
||||
{truncateMiddle(details.source) || t(details.sourceType)}
|
||||
{details.source
|
||||
? truncateMiddle(details.source)
|
||||
: t(details.sourceType)}
|
||||
</Lozenge>{' '}
|
||||
{t('to')} <Lozenge>{truncateMiddle(details.destination)}</Lozenge>
|
||||
{t('to')}{' '}
|
||||
<Lozenge>
|
||||
{details.destination
|
||||
? truncateMiddle(details.destination)
|
||||
: t(details.destinationType)}
|
||||
</Lozenge>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
const CancelTransferSummary = ({
|
||||
export const CancelTransferSummary = ({
|
||||
proposalId,
|
||||
}: {
|
||||
proposalId?: string | null;
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDocumentTitle } from '../../hooks/use-document-title';
|
||||
import type { RouteChildProps } from '..';
|
||||
import { BackgroundVideo } from '@vegaprotocol/ui-toolkit';
|
||||
import classNames from 'classnames';
|
||||
|
||||
const Restricted = ({ name }: RouteChildProps) => {
|
||||
useDocumentTitle(name);
|
||||
const { t } = useTranslation();
|
||||
const errorMessage =
|
||||
'Due to uncertainty about the legal and regulatory status of the content hosted on this site, it is not available to visitors in your jurisdiction.';
|
||||
|
||||
return (
|
||||
<div>
|
||||
<BackgroundVideo className="brightness-50" />
|
||||
<div
|
||||
className={classNames(
|
||||
'max-w-[620px] p-2 mt-[10vh]',
|
||||
'mx-auto my-0',
|
||||
'antialiased text-white',
|
||||
'overflow-hidden relative',
|
||||
'flex flex-col gap-2'
|
||||
)}
|
||||
>
|
||||
<div className="flex gap-4">
|
||||
<div>{GHOST}</div>
|
||||
<h1 className="leading-relaxed mb-0 text-[2.7rem] font-alpha calt break-words uppercase">
|
||||
{t('pageTitle451')}
|
||||
</h1>
|
||||
</div>
|
||||
<div className="text-sm mt-10 overflow-auto font-mono">
|
||||
{errorMessage}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const GHOST = (
|
||||
<svg
|
||||
width="56"
|
||||
height="85"
|
||||
viewBox="0 0 150 234"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M10.43 93.6821L0 104.097V172.435H5.13793V106.2L14.0779 97.3247L10.43 93.6821Z"
|
||||
fill="black"
|
||||
/>
|
||||
<path d="M105.328 18.521H7.70703V172.435H105.328V18.521Z" fill="white" />
|
||||
<path d="M38.5364 64.6953H33.3984V69.8258H38.5364V64.6953Z" fill="black" />
|
||||
<path d="M43.6731 69.8257H38.5352V74.9561H43.6731V69.8257Z" fill="black" />
|
||||
<path d="M48.8098 74.9561H43.6719V80.0865H48.8098V74.9561Z" fill="black" />
|
||||
<path d="M38.5364 74.9561H33.3984V80.0865H38.5364V74.9561Z" fill="black" />
|
||||
<path d="M48.8098 64.6953H43.6719V69.8258H48.8098V64.6953Z" fill="black" />
|
||||
<path d="M69.3606 64.6953H64.2227V69.8258H69.3606V64.6953Z" fill="black" />
|
||||
<path d="M74.5012 69.8257H69.3633V74.9561H74.5012V69.8257Z" fill="black" />
|
||||
<path d="M79.6379 74.9561H74.5V80.0865H79.6379V74.9561Z" fill="black" />
|
||||
<path d="M69.3606 74.9561H64.2227V80.0865H69.3606V74.9561Z" fill="black" />
|
||||
<path d="M79.6379 64.6953H74.5V69.8258H79.6379V64.6953Z" fill="black" />
|
||||
<path d="M79.6398 90.3477H33.3984V95.4781H79.6398V90.3477Z" fill="black" />
|
||||
<path d="M48.8098 172.435H43.6719V234H48.8098V172.435Z" fill="black" />
|
||||
<path d="M69.3606 172.435H64.2227V234H69.3606V172.435Z" fill="black" />
|
||||
<path
|
||||
d="M129.87 0.00408026L92.0273 14.3682L111.195 64.7203L149.038 50.3562L129.87 0.00408026Z"
|
||||
fill="#FF0081"
|
||||
/>
|
||||
<path
|
||||
d="M108.565 21.7836L103.762 23.6064L105.587 28.4024L110.39 26.5795L108.565 21.7836Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M115.189 24.7596L110.387 26.583L112.213 31.3785L117.015 29.5551L115.189 24.7596Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M121.826 27.7348L117.023 29.5576L118.849 34.3536L123.652 32.5307L121.826 27.7348Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M128.447 30.7059L123.645 32.5293L125.471 37.3247L130.273 35.5014L128.447 30.7059Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M135.084 33.6854L130.281 35.5083L132.107 40.3043L136.91 38.4814L135.084 33.6854Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M124.799 21.1156L119.996 22.939L121.822 27.7344L126.625 25.911L124.799 21.1156Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M127.775 14.492L122.973 16.3154L124.799 21.1109L129.601 19.2875L127.775 14.492Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M118.838 34.3499L114.035 36.1733L115.861 40.9688L120.664 39.1454L118.838 34.3499Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M115.857 40.9691L111.055 42.7925L112.881 47.5879L117.683 45.7645L115.857 40.9691Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M129.938 52.3822L139.906 91.5276L100.703 101.429L101.988 106.406L146.174 95.2215L134.922 51.0996L129.938 52.3822Z"
|
||||
fill="black"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default Restricted;
|
||||
@@ -4,6 +4,7 @@ import Home from './token';
|
||||
import NotFound from './not-found';
|
||||
import NotPermitted from './not-permitted';
|
||||
import Routes from './routes';
|
||||
import Restricted from './restricted';
|
||||
|
||||
const LazyTranches = React.lazy(
|
||||
() =>
|
||||
@@ -361,8 +362,13 @@ const routerConfig = [
|
||||
element: <LazyDisclaimer name="Disclaimer" />,
|
||||
},
|
||||
{
|
||||
path: '*',
|
||||
path: Routes.RESTRICTED,
|
||||
// Not lazy as loaded when a user first hits the site
|
||||
element: <Restricted name="451 Unavailable" />,
|
||||
},
|
||||
{
|
||||
path: '*',
|
||||
// Also not lazy as loaded when a user first hits the site
|
||||
element: <NotFound name="NotFound" />,
|
||||
},
|
||||
...redirects,
|
||||
|
||||
@@ -7,6 +7,7 @@ const Routes = {
|
||||
PROPOSALS_REJECTED: '/proposals/rejected',
|
||||
PROTOCOL_UPGRADES: '/protocol-upgrades',
|
||||
NOT_PERMITTED: '/not-permitted',
|
||||
RESTRICTED: '/restricted',
|
||||
NOT_FOUND: '/not-found',
|
||||
CONTRACTS: '/contracts',
|
||||
TOKEN: '/token',
|
||||
@@ -33,6 +34,10 @@ export const TOP_LEVEL_ROUTES = [
|
||||
name: 'Rewards',
|
||||
path: Routes.REWARDS,
|
||||
},
|
||||
{
|
||||
name: 'Restricted',
|
||||
path: Routes.RESTRICTED,
|
||||
},
|
||||
];
|
||||
|
||||
export const TOKEN_DROPDOWN_ROUTES = [
|
||||
|
||||
@@ -1,529 +0,0 @@
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import type { OrderAmendment, OrderCancellation } from '@vegaprotocol/wallet';
|
||||
import {
|
||||
updateOrder,
|
||||
getSubscriptionMocks,
|
||||
} from '../support/order-update-subscription';
|
||||
import {
|
||||
testOrderCancellation,
|
||||
testOrderAmendment,
|
||||
} from '../support/order-validation';
|
||||
|
||||
const orderSymbol = 'instrument-code';
|
||||
const orderSize = 'size';
|
||||
const orderType = 'type';
|
||||
const orderStatus = 'status';
|
||||
const orderRemaining = 'remaining';
|
||||
const orderPrice = 'price';
|
||||
const orderTimeInForce = 'timeInForce';
|
||||
const orderUpdatedAt = 'updatedAt';
|
||||
const cancelOrderBtn = 'cancel';
|
||||
const cancelAllOrdersBtn = 'cancelAll';
|
||||
const editOrderBtn = 'edit';
|
||||
|
||||
describe('orders list', { tags: '@smoke', testIsolation: true }, () => {
|
||||
beforeEach(() => {
|
||||
const subscriptionMocks = getSubscriptionMocks();
|
||||
cy.spy(subscriptionMocks, 'OrdersUpdate');
|
||||
cy.mockTradingPage();
|
||||
cy.mockSubscription(subscriptionMocks);
|
||||
cy.setVegaWallet();
|
||||
cy.visit('/#/markets/market-0');
|
||||
cy.getByTestId('All').click();
|
||||
cy.wait('@Markets');
|
||||
});
|
||||
|
||||
it('renders orders', () => {
|
||||
cy.getByTestId('tab-orders').should('be.visible');
|
||||
cy.getByTestId(cancelAllOrdersBtn).should('be.visible');
|
||||
cy.getByTestId(cancelOrderBtn).should('have.length.at.least', 1);
|
||||
cy.getByTestId(editOrderBtn).should('have.length.at.least', 1);
|
||||
cy.getByTestId('tab-orders').within(() => {
|
||||
cy.get(`[role='rowgroup']`)
|
||||
.first()
|
||||
.within(() => {
|
||||
cy.get(`[col-id='${orderSymbol}']`).each(($symbol) => {
|
||||
cy.wrap($symbol).invoke('text').should('not.be.empty');
|
||||
});
|
||||
|
||||
cy.get(`[col-id='${orderRemaining}']`).each(($remaining) => {
|
||||
cy.wrap($remaining).invoke('text').should('not.be.empty');
|
||||
});
|
||||
|
||||
cy.get(`[col-id='${orderSize}']`).each(($size) => {
|
||||
cy.wrap($size).invoke('text').should('not.be.empty');
|
||||
});
|
||||
|
||||
cy.get(`[col-id='${orderType}']`).each(($type) => {
|
||||
cy.wrap($type).invoke('text').should('not.be.empty');
|
||||
});
|
||||
|
||||
cy.get(`[col-id='${orderStatus}']`).each(($status) => {
|
||||
cy.wrap($status).invoke('text').should('not.be.empty');
|
||||
});
|
||||
|
||||
cy.get(`[col-id='${orderPrice}']`).each(($price) => {
|
||||
cy.wrap($price).invoke('text').should('not.be.empty');
|
||||
});
|
||||
|
||||
cy.get(`[col-id='${orderTimeInForce}']`).each(($timeInForce) => {
|
||||
cy.wrap($timeInForce).invoke('text').should('not.be.empty');
|
||||
});
|
||||
|
||||
cy.get(`[col-id='${orderUpdatedAt}']`).each(($dateTime) => {
|
||||
cy.wrap($dateTime).invoke('text').should('not.be.empty');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('partially filled orders should not show close/edit buttons', () => {
|
||||
const partiallyFilledId =
|
||||
'94aead3ca92dc932efcb503631b03a410e2a5d4606cae6083e2406dc38e52f78';
|
||||
|
||||
cy.getByTestId('tab-orders').should('be.visible');
|
||||
cy.get('.ag-header-container').within(() => {
|
||||
cy.get('[col-id="status"]').realHover();
|
||||
cy.get('[col-id="status"] .ag-icon-menu').click();
|
||||
});
|
||||
cy.contains('Partially Filled').click();
|
||||
cy.getByTestId('All').click();
|
||||
|
||||
cy.get(`[row-id="${partiallyFilledId}"]`)
|
||||
.eq(0)
|
||||
.within(() => {
|
||||
cy.get(`[col-id='${orderStatus}']`).should(
|
||||
'have.text',
|
||||
'Partially Filled'
|
||||
);
|
||||
cy.get(`[col-id='${orderRemaining}']`).should('have.text', '7');
|
||||
cy.get(`[col-id='${orderSize}']`).should('have.text', '-10');
|
||||
cy.getByTestId(cancelOrderBtn).should('not.exist');
|
||||
cy.getByTestId(editOrderBtn).should('not.exist');
|
||||
});
|
||||
});
|
||||
|
||||
it('orders are sorted by most recent order', () => {
|
||||
// 7003-MORD-002
|
||||
const expectedOrderList = [
|
||||
'BTCUSD.MF21',
|
||||
'SOLUSD',
|
||||
'AAPL.MF21',
|
||||
'BTCUSD.MF21',
|
||||
'BTCUSD.MF21',
|
||||
];
|
||||
cy.get('.ag-header-container').within(() => {
|
||||
cy.get('[col-id="status"]').realHover();
|
||||
cy.get('[col-id="status"] .ag-icon-menu').click();
|
||||
});
|
||||
cy.contains('Reset').click();
|
||||
cy.getByTestId('All').click();
|
||||
|
||||
cy.getByTestId('tab-orders')
|
||||
.get(
|
||||
`.ag-center-cols-container [col-id='${orderSymbol}'] [data-testid="market-code"]`
|
||||
)
|
||||
.should('have.length.at.least', expectedOrderList.length)
|
||||
.then(($symbols) => {
|
||||
const symbolNames: string[] = [];
|
||||
cy.wrap($symbols)
|
||||
.each(($symbol) => {
|
||||
cy.wrap($symbol)
|
||||
.invoke('text')
|
||||
.then((text) => {
|
||||
symbolNames.push(text);
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
expect(symbolNames).to.include.ordered.members(expectedOrderList);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('subscribe orders', { tags: '@smoke' }, () => {
|
||||
let orderId = '0';
|
||||
beforeEach(() => {
|
||||
const subscriptionMocks = getSubscriptionMocks();
|
||||
cy.spy(subscriptionMocks, 'OrdersUpdate');
|
||||
cy.mockTradingPage();
|
||||
cy.mockSubscription(subscriptionMocks);
|
||||
cy.setVegaWallet();
|
||||
cy.visit('/#/markets/market-0');
|
||||
cy.getByTestId('All').click();
|
||||
cy.getByTestId('tab-orders').within(() => {
|
||||
cy.get('[col-id="status"][role="columnheader"]')
|
||||
.focus()
|
||||
.find('.ag-header-cell-menu-button')
|
||||
.click();
|
||||
cy.get('.ag-filter-apply-panel-button').click();
|
||||
});
|
||||
orderId = (parseInt(orderId, 10) + 1).toString();
|
||||
});
|
||||
// 7002-SORD-053
|
||||
// 7002-SORD-040
|
||||
// 7003-MORD-001
|
||||
|
||||
it('must see an active order', () => {
|
||||
// 7002-SORD-041
|
||||
updateOrder({
|
||||
id: orderId,
|
||||
status: Schema.OrderStatus.STATUS_ACTIVE,
|
||||
});
|
||||
cy.getByTestId(`order-status-${orderId}`).should('have.text', 'Active');
|
||||
});
|
||||
|
||||
it('must see an expired order', () => {
|
||||
// 7002-SORD-042
|
||||
updateOrder({
|
||||
id: orderId,
|
||||
status: Schema.OrderStatus.STATUS_EXPIRED,
|
||||
});
|
||||
cy.getByTestId(`order-status-${orderId}`).should('have.text', 'Expired');
|
||||
});
|
||||
|
||||
it('must see a cancelled order', () => {
|
||||
// 7002-SORD-043
|
||||
// NOT COVERED: see the txn that cancelled it and a link to the block explorer, if cancelled by a user transaction.
|
||||
updateOrder({
|
||||
id: orderId,
|
||||
status: Schema.OrderStatus.STATUS_CANCELLED,
|
||||
});
|
||||
cy.getByTestId(`order-status-${orderId}`).should('have.text', 'Cancelled');
|
||||
});
|
||||
|
||||
it('must see a stopped order', () => {
|
||||
// 7002-SORD-044
|
||||
// NOT COVERED: see an explanation of why stopped
|
||||
updateOrder({
|
||||
id: orderId,
|
||||
status: Schema.OrderStatus.STATUS_STOPPED,
|
||||
});
|
||||
cy.getByTestId(`order-status-${orderId}`).should('have.text', 'Stopped');
|
||||
});
|
||||
|
||||
it('must see a partially filled order', () => {
|
||||
// 7002-SORD-045
|
||||
updateOrder({
|
||||
id: orderId,
|
||||
status: Schema.OrderStatus.STATUS_PARTIALLY_FILLED,
|
||||
size: '5',
|
||||
remaining: '1',
|
||||
});
|
||||
cy.getByTestId(`order-status-${orderId}`).should(
|
||||
'have.text',
|
||||
'Partially Filled'
|
||||
);
|
||||
cy.getByTestId(`order-status-${orderId}`)
|
||||
.parentsUntil(`.ag-row`)
|
||||
.siblings(`[col-id=${orderRemaining}]`)
|
||||
.should('have.text', '4');
|
||||
});
|
||||
|
||||
it('must see a filled order', () => {
|
||||
// 7002-SORD-046
|
||||
// 7003-MORD-020
|
||||
// NOT COVERED: Must be able to see/link to all trades that were created from this order
|
||||
updateOrder({
|
||||
id: orderId,
|
||||
status: Schema.OrderStatus.STATUS_FILLED,
|
||||
});
|
||||
cy.getByTestId(`order-status-${orderId}`).should('have.text', 'Filled');
|
||||
cy.get(`[col-id="${orderSymbol}"]`).contains('[title="Future"]', 'Futr');
|
||||
});
|
||||
|
||||
it('must see a rejected order', () => {
|
||||
// 7002-SORD-047
|
||||
updateOrder({
|
||||
id: orderId,
|
||||
status: Schema.OrderStatus.STATUS_REJECTED,
|
||||
rejectionReason: Schema.OrderRejectionReason.ORDER_ERROR_INTERNAL_ERROR,
|
||||
});
|
||||
cy.getByTestId(`order-status-${orderId}`).should(
|
||||
'have.text',
|
||||
'Rejected: Internal error'
|
||||
);
|
||||
});
|
||||
|
||||
it('must see a parked order', () => {
|
||||
// 7002-SORD-048
|
||||
updateOrder({
|
||||
id: orderId,
|
||||
status: Schema.OrderStatus.STATUS_PARKED,
|
||||
});
|
||||
cy.getByTestId(`order-status-${orderId}`).should(
|
||||
'have.text',
|
||||
'Parked: Internal error'
|
||||
);
|
||||
});
|
||||
|
||||
it('must see the size of the order and direction/side -', () => {
|
||||
// 7003-MORD-003
|
||||
// 7003-MORD-004
|
||||
updateOrder({
|
||||
id: orderId,
|
||||
size: '15',
|
||||
side: Schema.Side.SIDE_SELL,
|
||||
status: Schema.OrderStatus.STATUS_ACTIVE,
|
||||
});
|
||||
cy.get(`[row-id=${orderId}]`)
|
||||
.find(`[col-id="${orderSize}"]`)
|
||||
.should('have.text', '-15');
|
||||
});
|
||||
|
||||
it('must see the size of the order and direction/side +', () => {
|
||||
// 7003-MORD-003
|
||||
// 7003-MORD-004
|
||||
updateOrder({
|
||||
id: orderId,
|
||||
size: '5',
|
||||
side: Schema.Side.SIDE_BUY,
|
||||
status: Schema.OrderStatus.STATUS_ACTIVE,
|
||||
});
|
||||
cy.get(`[row-id=${orderId}]`)
|
||||
.find(`[col-id="${orderSize}"]`)
|
||||
.should('have.text', '+5');
|
||||
});
|
||||
|
||||
it('for limit typy must see the Limit price that was set on the order', () => {
|
||||
// 7003-MORD-005
|
||||
updateOrder({
|
||||
id: orderId,
|
||||
status: Schema.OrderStatus.STATUS_ACTIVE,
|
||||
});
|
||||
cy.get(`[row-id=${orderId}]`)
|
||||
.find('[col-id="price"]')
|
||||
.should('have.text', '200.00');
|
||||
});
|
||||
|
||||
it('must see a pegged order - ask', () => {
|
||||
updateOrder({
|
||||
id: orderId,
|
||||
side: Schema.Side.SIDE_BUY,
|
||||
peggedOrder: {
|
||||
__typename: 'PeggedOrder',
|
||||
reference: Schema.PeggedReference.PEGGED_REFERENCE_BEST_ASK,
|
||||
offset: '250000',
|
||||
},
|
||||
});
|
||||
cy.get(`[row-id=${orderId}]`)
|
||||
.find('[col-id="type"]')
|
||||
.should('have.text', 'Ask - 2.50 Peg limit');
|
||||
});
|
||||
|
||||
it('must see a pegged order - bid', () => {
|
||||
updateOrder({
|
||||
id: orderId,
|
||||
side: Schema.Side.SIDE_SELL,
|
||||
peggedOrder: {
|
||||
__typename: 'PeggedOrder',
|
||||
reference: Schema.PeggedReference.PEGGED_REFERENCE_BEST_BID,
|
||||
offset: '100',
|
||||
},
|
||||
});
|
||||
cy.get(`[row-id=${orderId}]`)
|
||||
.find('[col-id="type"]')
|
||||
.should('have.text', 'Bid + 0.001 Peg limit');
|
||||
});
|
||||
|
||||
it('must see a pegged order - mid', () => {
|
||||
updateOrder({
|
||||
id: orderId,
|
||||
side: Schema.Side.SIDE_SELL,
|
||||
peggedOrder: {
|
||||
__typename: 'PeggedOrder',
|
||||
reference: Schema.PeggedReference.PEGGED_REFERENCE_MID,
|
||||
offset: '0.5',
|
||||
},
|
||||
});
|
||||
cy.get(`[row-id=${orderId}]`)
|
||||
.find('[col-id="type"]')
|
||||
.should('have.text', 'Mid + 0.00001 Peg limit');
|
||||
});
|
||||
|
||||
it('for market typy must not see a price for active or parked orders', () => {
|
||||
// 7003-MORD-005
|
||||
updateOrder({
|
||||
id: orderId,
|
||||
type: Schema.OrderType.TYPE_MARKET,
|
||||
status: Schema.OrderStatus.STATUS_PARKED,
|
||||
peggedOrder: null,
|
||||
});
|
||||
cy.get(`[row-id=${orderId}]`)
|
||||
.find('[col-id="price"]')
|
||||
.should('have.text', '-');
|
||||
});
|
||||
|
||||
it('must see the time in force applied to the order', () => {
|
||||
// 7003-MORD-006
|
||||
updateOrder({
|
||||
id: orderId,
|
||||
type: Schema.OrderType.TYPE_MARKET,
|
||||
status: Schema.OrderStatus.STATUS_ACTIVE,
|
||||
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_GTC,
|
||||
});
|
||||
cy.get(`[row-id=${orderId}]`)
|
||||
.find(`[col-id='${orderTimeInForce}']`)
|
||||
.should('have.text', 'GTC');
|
||||
});
|
||||
|
||||
it('for Active order when is part of a liquidity or peg shape, must not see an option to amend the individual order ', () => {
|
||||
// 7003-MORD-008
|
||||
updateOrder({
|
||||
id: orderId,
|
||||
status: Schema.OrderStatus.STATUS_ACTIVE,
|
||||
peggedOrder: {},
|
||||
liquidityProvisionId: '6536',
|
||||
});
|
||||
cy.get(`[row-id=${orderId}]`)
|
||||
.find(`[data-testid="cancel"]`)
|
||||
.should('not.exist');
|
||||
cy.get(`[row-id=${orderId}]`)
|
||||
.find(`[data-testid="edit"]`)
|
||||
.should('not.exist');
|
||||
});
|
||||
|
||||
it('for Active order when is part of a liquidity, must not see an option to amend the individual order ', () => {
|
||||
// 7003-MORD-008
|
||||
updateOrder({
|
||||
id: orderId,
|
||||
status: Schema.OrderStatus.STATUS_ACTIVE,
|
||||
peggedOrder: {},
|
||||
liquidityProvisionId: '6536',
|
||||
});
|
||||
cy.get(`[row-id=${orderId}]`)
|
||||
.find(`[data-testid="cancel"]`)
|
||||
.should('not.exist');
|
||||
cy.get(`[row-id=${orderId}]`)
|
||||
.find(`[data-testid="edit"]`)
|
||||
.should('not.exist');
|
||||
});
|
||||
|
||||
it('for Active order when is part of a peg shape, must not see an option to amend the individual order ', () => {
|
||||
// 7003-MORD-008
|
||||
updateOrder({
|
||||
id: orderId,
|
||||
status: Schema.OrderStatus.STATUS_ACTIVE,
|
||||
peggedOrder: {},
|
||||
});
|
||||
cy.get(`[row-id=${orderId}]`)
|
||||
.find(`[data-testid="cancel"]`)
|
||||
.should('not.exist');
|
||||
cy.get(`[row-id=${orderId}]`)
|
||||
.find(`[data-testid="edit"]`)
|
||||
.should('not.exist');
|
||||
});
|
||||
});
|
||||
|
||||
describe('amend and cancel order', { tags: '@smoke' }, () => {
|
||||
beforeEach(() => {
|
||||
const subscriptionMocks = getSubscriptionMocks();
|
||||
cy.spy(subscriptionMocks, 'OrdersUpdate');
|
||||
cy.mockTradingPage();
|
||||
cy.mockSubscription(subscriptionMocks);
|
||||
cy.setVegaWallet();
|
||||
cy.visit('/#/markets/market-0');
|
||||
cy.getByTestId('All').click();
|
||||
cy.getByTestId('tab-orders').within(() => {
|
||||
cy.get('[col-id="status"][role="columnheader"]')
|
||||
.focus()
|
||||
.find('.ag-header-cell-menu-button')
|
||||
.click();
|
||||
cy.get('.ag-filter-apply-panel-button').click();
|
||||
});
|
||||
cy.mockVegaWalletTransaction();
|
||||
});
|
||||
|
||||
const orderId = '1234567890';
|
||||
|
||||
// this test is flakey
|
||||
it('must be able to amend the price of an order', () => {
|
||||
// 7003-MORD-007
|
||||
// 7003-MORD-012
|
||||
// 7003-MORD-014
|
||||
// 7003-MORD-015
|
||||
updateOrder({
|
||||
id: orderId,
|
||||
status: Schema.OrderStatus.STATUS_ACTIVE,
|
||||
peggedOrder: null,
|
||||
liquidityProvisionId: null,
|
||||
});
|
||||
cy.get(`[row-id=${orderId}]`)
|
||||
.find('[data-testid="icon-edit"]')
|
||||
.then(($btn) => {
|
||||
cy.wrap($btn).click();
|
||||
cy.getByTestId('dialog-title').should('have.text', 'Edit order');
|
||||
cy.get('#limitPrice').focus().clear().type('100');
|
||||
cy.getByTestId('edit-order').find('[type="submit"]').click();
|
||||
const order: OrderAmendment = {
|
||||
orderId: orderId,
|
||||
marketId: 'market-0',
|
||||
price: '10000000',
|
||||
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_GTC,
|
||||
sizeDelta: 0,
|
||||
};
|
||||
testOrderAmendment(order);
|
||||
});
|
||||
});
|
||||
it('must be able to cancel an individual order', () => {
|
||||
// 7003-MORD-007
|
||||
// 7003-MORD-009
|
||||
// 7003-MORD-010
|
||||
// 7003-MORD-011
|
||||
updateOrder({
|
||||
id: orderId,
|
||||
status: Schema.OrderStatus.STATUS_ACTIVE,
|
||||
peggedOrder: null,
|
||||
liquidityProvisionId: null,
|
||||
});
|
||||
cy.get(`[row-id=${orderId}]`)
|
||||
.find(`[data-testid="icon-cross"]`)
|
||||
.then(($btn) => {
|
||||
cy.wrap($btn).click({ force: true });
|
||||
const order: OrderCancellation = {
|
||||
orderId: orderId,
|
||||
marketId: 'market-0',
|
||||
};
|
||||
testOrderCancellation(order);
|
||||
});
|
||||
});
|
||||
it('must be able to cancel all orders on all markets', () => {
|
||||
// 7003-MORD-009
|
||||
// 7003-MORD-010
|
||||
// 7003-MORD-011
|
||||
updateOrder({
|
||||
id: orderId,
|
||||
status: Schema.OrderStatus.STATUS_ACTIVE,
|
||||
peggedOrder: null,
|
||||
liquidityProvisionId: null,
|
||||
});
|
||||
cy.get(`[data-testid="cancelAll"]`)
|
||||
.should('have.text', 'Cancel all')
|
||||
.then(($btn) => {
|
||||
cy.wrap($btn).click({ force: true });
|
||||
const order: OrderCancellation = {};
|
||||
testOrderCancellation(order);
|
||||
});
|
||||
});
|
||||
it('must be warned (pre-submit) if the input price has too many digits after the decimal place for the market', () => {
|
||||
// 7003-MORD-013
|
||||
updateOrder({
|
||||
id: orderId,
|
||||
status: Schema.OrderStatus.STATUS_ACTIVE,
|
||||
peggedOrder: null,
|
||||
liquidityProvisionId: null,
|
||||
});
|
||||
cy.get(`[row-id=${orderId}]`)
|
||||
.find('[data-testid="icon-edit"]')
|
||||
.then(($btn) => {
|
||||
cy.wrap($btn).click({ force: true });
|
||||
cy.getByTestId('dialog-title').should('have.text', 'Edit order');
|
||||
cy.get('#limitPrice').focus().clear().type('0.111111');
|
||||
cy.getByTestId('edit-order').find('[type="submit"]').click();
|
||||
cy.getByTestId('input-error-text').should(
|
||||
'have.text',
|
||||
'Price accepts up to 5 decimal places'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -86,6 +86,7 @@ export const MarketHeaderStats = ({ market }: MarketHeaderStatsProps) => {
|
||||
<MarketLiquiditySupplied
|
||||
marketId={market.id}
|
||||
assetDecimals={asset?.decimals || 0}
|
||||
quantum={asset.quantum}
|
||||
/>
|
||||
{market.tradableInstrument.instrument.product.__typename === 'Future' && (
|
||||
<HeaderStat
|
||||
|
||||
@@ -123,7 +123,7 @@ const MainGrid = memo(
|
||||
<Tab
|
||||
id="open-orders"
|
||||
name={t('Open')}
|
||||
menu={<TradingViews.activeOrders.menu marketId={marketId} />}
|
||||
menu={<TradingViews.activeOrders.menu />}
|
||||
>
|
||||
<TradingViews.orders.component filter={Filter.Open} />
|
||||
</Tab>
|
||||
@@ -136,7 +136,7 @@ const MainGrid = memo(
|
||||
<Tab
|
||||
id="orders"
|
||||
name={t('All')}
|
||||
menu={<TradingViews.orders.menu marketId={marketId} />}
|
||||
menu={<TradingViews.orders.menu />}
|
||||
>
|
||||
<TradingViews.orders.component />
|
||||
</Tab>
|
||||
|
||||
@@ -43,7 +43,7 @@ export const TradePanels = ({ market, pinnedAsset }: TradePanelsProps) => {
|
||||
|
||||
return (
|
||||
<div className="flex gap-1 p-1 bg-vega-clight-800 dark:bg-vega-cdark-800 border-b border-default">
|
||||
<Menu marketId={market?.id || ''} />
|
||||
<Menu />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
query AccountHistory(
|
||||
$partyId: ID!
|
||||
$assetId: ID!
|
||||
$accountTypes: [AccountType!]
|
||||
$dateRange: DateRange
|
||||
$marketIds: [ID!]
|
||||
) {
|
||||
balanceChanges(
|
||||
filter: {
|
||||
partyIds: [$partyId]
|
||||
accountTypes: $accountTypes
|
||||
assetId: $assetId
|
||||
marketIds: $marketIds
|
||||
}
|
||||
dateRange: $dateRange
|
||||
) {
|
||||
edges {
|
||||
node {
|
||||
timestamp
|
||||
partyId
|
||||
balance
|
||||
marketId
|
||||
assetId
|
||||
accountType
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
query AccountsWithBalance($partyId: ID!, $dateRange: DateRange) {
|
||||
balanceChanges(filter: { partyIds: [$partyId] }, dateRange: $dateRange) {
|
||||
edges {
|
||||
node {
|
||||
assetId
|
||||
accountType
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
import * as Types from '@vegaprotocol/types';
|
||||
|
||||
import { gql } from '@apollo/client';
|
||||
import * as Apollo from '@apollo/client';
|
||||
const defaultOptions = {} as const;
|
||||
export type AccountHistoryQueryVariables = Types.Exact<{
|
||||
partyId: Types.Scalars['ID'];
|
||||
assetId: Types.Scalars['ID'];
|
||||
accountTypes?: Types.InputMaybe<Array<Types.AccountType> | Types.AccountType>;
|
||||
dateRange?: Types.InputMaybe<Types.DateRange>;
|
||||
marketIds?: Types.InputMaybe<Array<Types.Scalars['ID']> | Types.Scalars['ID']>;
|
||||
}>;
|
||||
|
||||
|
||||
export type AccountHistoryQuery = { __typename?: 'Query', balanceChanges: { __typename?: 'AggregatedBalanceConnection', edges: Array<{ __typename?: 'AggregatedBalanceEdge', node: { __typename?: 'AggregatedBalance', timestamp: any, partyId?: string | null, balance: string, marketId?: string | null, assetId?: string | null, accountType?: Types.AccountType | null } } | null> } };
|
||||
|
||||
export type AccountsWithBalanceQueryVariables = Types.Exact<{
|
||||
partyId: Types.Scalars['ID'];
|
||||
dateRange?: Types.InputMaybe<Types.DateRange>;
|
||||
}>;
|
||||
|
||||
|
||||
export type AccountsWithBalanceQuery = { __typename?: 'Query', balanceChanges: { __typename?: 'AggregatedBalanceConnection', edges: Array<{ __typename?: 'AggregatedBalanceEdge', node: { __typename?: 'AggregatedBalance', assetId?: string | null, accountType?: Types.AccountType | null } } | null> } };
|
||||
|
||||
|
||||
export const AccountHistoryDocument = gql`
|
||||
query AccountHistory($partyId: ID!, $assetId: ID!, $accountTypes: [AccountType!], $dateRange: DateRange, $marketIds: [ID!]) {
|
||||
balanceChanges(
|
||||
filter: {partyIds: [$partyId], accountTypes: $accountTypes, assetId: $assetId, marketIds: $marketIds}
|
||||
dateRange: $dateRange
|
||||
) {
|
||||
edges {
|
||||
node {
|
||||
timestamp
|
||||
partyId
|
||||
balance
|
||||
marketId
|
||||
assetId
|
||||
accountType
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* __useAccountHistoryQuery__
|
||||
*
|
||||
* To run a query within a React component, call `useAccountHistoryQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useAccountHistoryQuery` returns an object from Apollo Client that contains loading, error, and data properties
|
||||
* you can use to render your UI.
|
||||
*
|
||||
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
|
||||
*
|
||||
* @example
|
||||
* const { data, loading, error } = useAccountHistoryQuery({
|
||||
* variables: {
|
||||
* partyId: // value for 'partyId'
|
||||
* assetId: // value for 'assetId'
|
||||
* accountTypes: // value for 'accountTypes'
|
||||
* dateRange: // value for 'dateRange'
|
||||
* marketIds: // value for 'marketIds'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useAccountHistoryQuery(baseOptions: Apollo.QueryHookOptions<AccountHistoryQuery, AccountHistoryQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useQuery<AccountHistoryQuery, AccountHistoryQueryVariables>(AccountHistoryDocument, options);
|
||||
}
|
||||
export function useAccountHistoryLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<AccountHistoryQuery, AccountHistoryQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useLazyQuery<AccountHistoryQuery, AccountHistoryQueryVariables>(AccountHistoryDocument, options);
|
||||
}
|
||||
export type AccountHistoryQueryHookResult = ReturnType<typeof useAccountHistoryQuery>;
|
||||
export type AccountHistoryLazyQueryHookResult = ReturnType<typeof useAccountHistoryLazyQuery>;
|
||||
export type AccountHistoryQueryResult = Apollo.QueryResult<AccountHistoryQuery, AccountHistoryQueryVariables>;
|
||||
export const AccountsWithBalanceDocument = gql`
|
||||
query AccountsWithBalance($partyId: ID!, $dateRange: DateRange) {
|
||||
balanceChanges(filter: {partyIds: [$partyId]}, dateRange: $dateRange) {
|
||||
edges {
|
||||
node {
|
||||
assetId
|
||||
accountType
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* __useAccountsWithBalanceQuery__
|
||||
*
|
||||
* To run a query within a React component, call `useAccountsWithBalanceQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useAccountsWithBalanceQuery` returns an object from Apollo Client that contains loading, error, and data properties
|
||||
* you can use to render your UI.
|
||||
*
|
||||
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
|
||||
*
|
||||
* @example
|
||||
* const { data, loading, error } = useAccountsWithBalanceQuery({
|
||||
* variables: {
|
||||
* partyId: // value for 'partyId'
|
||||
* dateRange: // value for 'dateRange'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useAccountsWithBalanceQuery(baseOptions: Apollo.QueryHookOptions<AccountsWithBalanceQuery, AccountsWithBalanceQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useQuery<AccountsWithBalanceQuery, AccountsWithBalanceQueryVariables>(AccountsWithBalanceDocument, options);
|
||||
}
|
||||
export function useAccountsWithBalanceLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<AccountsWithBalanceQuery, AccountsWithBalanceQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useLazyQuery<AccountsWithBalanceQuery, AccountsWithBalanceQueryVariables>(AccountsWithBalanceDocument, options);
|
||||
}
|
||||
export type AccountsWithBalanceQueryHookResult = ReturnType<typeof useAccountsWithBalanceQuery>;
|
||||
export type AccountsWithBalanceLazyQueryHookResult = ReturnType<typeof useAccountsWithBalanceLazyQuery>;
|
||||
export type AccountsWithBalanceQueryResult = Apollo.QueryResult<AccountsWithBalanceQuery, AccountsWithBalanceQueryVariables>;
|
||||
@@ -1,349 +0,0 @@
|
||||
import { addDecimal, fromNanoSeconds } from '@vegaprotocol/utils';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import compact from 'lodash/compact';
|
||||
import uniqBy from 'lodash/uniqBy';
|
||||
import type { ChangeEvent } from 'react';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import type { AccountHistoryQuery } from './__generated__/AccountHistory';
|
||||
import { useAccountHistoryQuery } from './__generated__/AccountHistory';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import type { AssetFieldsFragment } from '@vegaprotocol/assets';
|
||||
import { useAssetsDataProvider } from '@vegaprotocol/assets';
|
||||
import {
|
||||
AsyncRenderer,
|
||||
Splash,
|
||||
Toggle,
|
||||
TradingButton,
|
||||
TradingDropdown,
|
||||
TradingDropdownContent,
|
||||
TradingDropdownItem,
|
||||
TradingDropdownTrigger,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { AccountTypeMapping } from '@vegaprotocol/types';
|
||||
import { PriceChart } from 'pennant';
|
||||
import 'pennant/dist/style.css';
|
||||
import type { Account } from '@vegaprotocol/accounts';
|
||||
import { accountsDataProvider } from '@vegaprotocol/accounts';
|
||||
import {
|
||||
useLocalStorageSnapshot,
|
||||
useThemeSwitcher,
|
||||
} from '@vegaprotocol/react-helpers';
|
||||
import { useDataProvider } from '@vegaprotocol/data-provider';
|
||||
import { getAsset, type Market } from '@vegaprotocol/markets';
|
||||
|
||||
export const DateRange = {
|
||||
RANGE_1D: '1D',
|
||||
RANGE_7D: '7D',
|
||||
RANGE_1M: '1M',
|
||||
RANGE_3M: '3M',
|
||||
RANGE_1Y: '1Y',
|
||||
RANGE_YTD: 'YTD',
|
||||
RANGE_ALL: 'All',
|
||||
};
|
||||
|
||||
const dateRangeToggleItems = Object.entries(DateRange).map(([_, value]) => ({
|
||||
label: t(value),
|
||||
value: value,
|
||||
}));
|
||||
|
||||
export const calculateStartDate = (range: string): string | undefined => {
|
||||
const now = new Date();
|
||||
switch (range) {
|
||||
case DateRange.RANGE_1D:
|
||||
return new Date(now.setDate(now.getDate() - 1)).toISOString();
|
||||
case DateRange.RANGE_7D:
|
||||
return new Date(now.setDate(now.getDate() - 7)).toISOString();
|
||||
case DateRange.RANGE_1M:
|
||||
return new Date(now.setMonth(now.getMonth() - 1)).toISOString();
|
||||
case DateRange.RANGE_3M:
|
||||
return new Date(now.setMonth(now.getMonth() - 3)).toISOString();
|
||||
case DateRange.RANGE_1Y:
|
||||
return new Date(now.setFullYear(now.getFullYear() - 1)).toISOString();
|
||||
case DateRange.RANGE_YTD:
|
||||
return new Date(now.setMonth(0)).toISOString();
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
export const AccountHistoryContainer = () => {
|
||||
const { pubKey } = useVegaWallet();
|
||||
const { data: assets } = useAssetsDataProvider();
|
||||
|
||||
if (!pubKey) {
|
||||
return <Splash>{t('Please connect Vega wallet')}</Splash>;
|
||||
}
|
||||
|
||||
return (
|
||||
<AsyncRenderer loading={!assets} error={undefined} data={assets}>
|
||||
{assets && <AccountHistoryManager pubKey={pubKey} assetData={assets} />}
|
||||
</AsyncRenderer>
|
||||
);
|
||||
};
|
||||
|
||||
const AccountHistoryManager = ({
|
||||
pubKey,
|
||||
assetData,
|
||||
}: {
|
||||
pubKey: string;
|
||||
assetData: AssetFieldsFragment[];
|
||||
}) => {
|
||||
const [accountType, setAccountType] = useState<Schema.AccountType>(
|
||||
Schema.AccountType.ACCOUNT_TYPE_GENERAL
|
||||
);
|
||||
|
||||
const variablesForOneTimeQuery = useMemo(
|
||||
() => ({
|
||||
partyId: pubKey,
|
||||
}),
|
||||
[pubKey]
|
||||
);
|
||||
|
||||
const { data: accounts } = useDataProvider({
|
||||
dataProvider: accountsDataProvider,
|
||||
variables: variablesForOneTimeQuery,
|
||||
skip: !pubKey,
|
||||
});
|
||||
|
||||
const assetIds = useMemo(
|
||||
() => accounts?.map((e) => e?.asset?.id) || [],
|
||||
[accounts]
|
||||
);
|
||||
|
||||
const assets = useMemo(
|
||||
() =>
|
||||
assetData
|
||||
.filter((a) => assetIds.includes(a.id))
|
||||
.sort((a, b) => a.name.localeCompare(b.name)),
|
||||
[assetData, assetIds]
|
||||
);
|
||||
const [assetId, setAssetId] = useLocalStorageSnapshot(
|
||||
'account-history-active-asset-id'
|
||||
);
|
||||
|
||||
const asset = useMemo(
|
||||
() => assets.find((a) => a.id === assetId) || assets[0],
|
||||
[assetId, assets]
|
||||
);
|
||||
|
||||
const [range, setRange] = useState<typeof DateRange[keyof typeof DateRange]>(
|
||||
DateRange.RANGE_1M
|
||||
);
|
||||
const [market, setMarket] = useState<Market | null>(null);
|
||||
|
||||
const marketFilterCb = useCallback(
|
||||
(item: Market) => {
|
||||
const itemAsset = getAsset(item);
|
||||
return !asset?.id || itemAsset?.id === asset?.id;
|
||||
},
|
||||
[asset?.id]
|
||||
);
|
||||
const markets = useMemo<Market[] | null>(() => {
|
||||
const arr =
|
||||
accounts
|
||||
?.filter((item: Account) => Boolean(item && item.market))
|
||||
.map<Market>((item) => item.market as Market) ?? null;
|
||||
return arr
|
||||
? uniqBy(arr.filter(marketFilterCb), 'id').sort((a, b) =>
|
||||
a.tradableInstrument.instrument.code.localeCompare(
|
||||
b.tradableInstrument.instrument.code
|
||||
)
|
||||
)
|
||||
: null;
|
||||
}, [accounts, marketFilterCb]);
|
||||
|
||||
const resolveMarket = useCallback(
|
||||
(m: Market) => {
|
||||
setMarket(m);
|
||||
const itemAsset = getAsset(m);
|
||||
const newAssetId = itemAsset?.id;
|
||||
const newAsset = assets.find((item) => item.id === newAssetId);
|
||||
if ((!asset || (assets && newAssetId !== asset.id)) && newAsset) {
|
||||
setAssetId(newAsset.id);
|
||||
}
|
||||
},
|
||||
[asset, assets, setAssetId]
|
||||
);
|
||||
|
||||
const variables = useMemo(
|
||||
() => ({
|
||||
partyId: pubKey,
|
||||
assetId: asset?.id || '',
|
||||
accountTypes: accountType ? [accountType] : undefined,
|
||||
dateRange:
|
||||
range === 'All' ? undefined : { start: calculateStartDate(range) },
|
||||
marketIds: market?.id ? [market.id] : undefined,
|
||||
}),
|
||||
[pubKey, asset, accountType, range, market?.id]
|
||||
);
|
||||
|
||||
const { data } = useAccountHistoryQuery({
|
||||
variables,
|
||||
skip: !asset || !pubKey,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full gap-2">
|
||||
<div className="flex flex-wrap justify-between px-1 pt-2 gap-2">
|
||||
<div className="flex items-center gap-1 shrink-0">
|
||||
<TradingDropdown
|
||||
trigger={
|
||||
<TradingDropdownTrigger>
|
||||
<TradingButton size="small">
|
||||
{accountType
|
||||
? `${
|
||||
AccountTypeMapping[
|
||||
accountType as keyof typeof Schema.AccountType
|
||||
]
|
||||
} Account`
|
||||
: t('Select account type')}
|
||||
</TradingButton>
|
||||
</TradingDropdownTrigger>
|
||||
}
|
||||
>
|
||||
<TradingDropdownContent>
|
||||
{[
|
||||
Schema.AccountType.ACCOUNT_TYPE_GENERAL,
|
||||
Schema.AccountType.ACCOUNT_TYPE_BOND,
|
||||
Schema.AccountType.ACCOUNT_TYPE_MARGIN,
|
||||
].map((type) => (
|
||||
<TradingDropdownItem
|
||||
key={type}
|
||||
onClick={() => {
|
||||
setAccountType(type as Schema.AccountType);
|
||||
|
||||
// if not a margin account clear any market selection
|
||||
if (type !== Schema.AccountType.ACCOUNT_TYPE_MARGIN) {
|
||||
setMarket(null);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{AccountTypeMapping[type as keyof typeof Schema.AccountType]}
|
||||
</TradingDropdownItem>
|
||||
))}
|
||||
</TradingDropdownContent>
|
||||
</TradingDropdown>
|
||||
|
||||
<TradingDropdown
|
||||
trigger={
|
||||
<TradingDropdownTrigger>
|
||||
<TradingButton size="small">
|
||||
{asset ? asset.symbol : t('Select asset')}
|
||||
</TradingButton>
|
||||
</TradingDropdownTrigger>
|
||||
}
|
||||
>
|
||||
<TradingDropdownContent>
|
||||
{assets.map((a) => (
|
||||
<TradingDropdownItem
|
||||
key={a.id}
|
||||
onClick={() => {
|
||||
setAssetId(a.id);
|
||||
|
||||
// if the selected asset is different to the selected market clear the market
|
||||
if (market && a.id !== getAsset(market).id) {
|
||||
setMarket(null);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{a.symbol}
|
||||
</TradingDropdownItem>
|
||||
))}
|
||||
</TradingDropdownContent>
|
||||
</TradingDropdown>
|
||||
<TradingDropdown
|
||||
trigger={
|
||||
<TradingDropdownTrigger>
|
||||
<TradingButton size="small">
|
||||
{market
|
||||
? market.tradableInstrument.instrument.code
|
||||
: t('Select market')}
|
||||
</TradingButton>
|
||||
</TradingDropdownTrigger>
|
||||
}
|
||||
>
|
||||
<TradingDropdownContent>
|
||||
{market && (
|
||||
<TradingDropdownItem key="0" onClick={() => setMarket(null)}>
|
||||
{t('All markets')}
|
||||
</TradingDropdownItem>
|
||||
)}
|
||||
{markets?.map((m) => (
|
||||
<TradingDropdownItem
|
||||
key={m.id}
|
||||
onClick={() => resolveMarket(m)}
|
||||
>
|
||||
{m.tradableInstrument.instrument.code}
|
||||
</TradingDropdownItem>
|
||||
))}
|
||||
</TradingDropdownContent>
|
||||
</TradingDropdown>
|
||||
</div>
|
||||
<div className="justify-items-end">
|
||||
<Toggle
|
||||
id="account-history-date-range"
|
||||
name="account-history-date-range"
|
||||
toggles={dateRangeToggleItems}
|
||||
checkedValue={range}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
||||
setRange(e.target.value as keyof typeof DateRange)
|
||||
}
|
||||
size="sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
{asset && (
|
||||
<div className="h-full">
|
||||
<AccountHistoryChart
|
||||
data={data}
|
||||
accountType={accountType}
|
||||
asset={asset}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const AccountHistoryChart = ({
|
||||
data,
|
||||
accountType,
|
||||
asset,
|
||||
}: {
|
||||
data: AccountHistoryQuery | undefined;
|
||||
accountType: Schema.AccountType;
|
||||
asset: AssetFieldsFragment;
|
||||
}) => {
|
||||
const { theme } = useThemeSwitcher();
|
||||
const values: { cols: [string, string]; rows: [Date, number][] } | null =
|
||||
useMemo(() => {
|
||||
if (!data?.balanceChanges.edges.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const valuesData = compact(data.balanceChanges.edges)
|
||||
.reduce((acc, edge) => {
|
||||
if (edge.node.accountType === accountType) {
|
||||
acc?.push({
|
||||
datetime: fromNanoSeconds(edge.node.timestamp),
|
||||
balance: Number(addDecimal(edge.node.balance, asset.decimals)),
|
||||
});
|
||||
}
|
||||
return acc;
|
||||
}, [] as { datetime: Date; balance: number }[])
|
||||
.reverse();
|
||||
return {
|
||||
cols: ['Date', `${asset.symbol} account balance`],
|
||||
rows: compact(valuesData).map((d) => [d.datetime, d.balance]),
|
||||
};
|
||||
}, [accountType, asset.decimals, asset.symbol, data?.balanceChanges.edges]);
|
||||
|
||||
if (!data || !values?.rows.length) {
|
||||
return <Splash> {t('No account history data')}</Splash>;
|
||||
}
|
||||
|
||||
return <PriceChart data={values} theme={theme} />;
|
||||
};
|
||||
@@ -15,7 +15,6 @@ import { PositionsMenu } from '../../components/positions-menu';
|
||||
import { WithdrawalsContainer } from '../../components/withdrawals-container';
|
||||
import { OrdersContainer } from '../../components/orders-container';
|
||||
import { LedgerContainer } from '../../components/ledger-container';
|
||||
import { AccountHistoryContainer } from './account-history-container';
|
||||
import {
|
||||
ResizableGrid,
|
||||
ResizableGridPanel,
|
||||
@@ -66,10 +65,7 @@ export const Portfolio = () => {
|
||||
<ResizableGrid vertical onChange={handleOnLayoutChange}>
|
||||
<ResizableGridPanel minSize={75}>
|
||||
<PortfolioGridChild>
|
||||
<Tabs storageKey="console-portfolio-top">
|
||||
<Tab id="account-history" name={t('Account history')}>
|
||||
<AccountHistoryContainer />
|
||||
</Tab>
|
||||
<Tabs storageKey="console-portfolio-top-1">
|
||||
<Tab
|
||||
id="positions"
|
||||
name={t('Positions')}
|
||||
|
||||
@@ -9,10 +9,36 @@ import { useMemo } from 'react';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { useThemeSwitcher } from '@vegaprotocol/react-helpers';
|
||||
import { Splash } from '@vegaprotocol/ui-toolkit';
|
||||
import {
|
||||
DateRange,
|
||||
calculateStartDate,
|
||||
} from '../../client-pages/portfolio/account-history-container';
|
||||
|
||||
const calculateStartDate = (range: string): string | undefined => {
|
||||
const now = new Date();
|
||||
switch (range) {
|
||||
case DateRange.RANGE_1D:
|
||||
return new Date(now.setDate(now.getDate() - 1)).toISOString();
|
||||
case DateRange.RANGE_7D:
|
||||
return new Date(now.setDate(now.getDate() - 7)).toISOString();
|
||||
case DateRange.RANGE_1M:
|
||||
return new Date(now.setMonth(now.getMonth() - 1)).toISOString();
|
||||
case DateRange.RANGE_3M:
|
||||
return new Date(now.setMonth(now.getMonth() - 3)).toISOString();
|
||||
case DateRange.RANGE_1Y:
|
||||
return new Date(now.setFullYear(now.getFullYear() - 1)).toISOString();
|
||||
case DateRange.RANGE_YTD:
|
||||
return new Date(now.setMonth(0)).toISOString();
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
const DateRange = {
|
||||
RANGE_1D: '1D',
|
||||
RANGE_7D: '7D',
|
||||
RANGE_1M: '1M',
|
||||
RANGE_3M: '3M',
|
||||
RANGE_1Y: '1Y',
|
||||
RANGE_YTD: 'YTD',
|
||||
RANGE_ALL: 'All',
|
||||
};
|
||||
|
||||
export const FundingContainer = ({ marketId }: { marketId: string }) => {
|
||||
const { theme } = useThemeSwitcher();
|
||||
|
||||
@@ -18,7 +18,7 @@ import BigNumber from 'bignumber.js';
|
||||
import { useCheckLiquidityStatus } from '@vegaprotocol/liquidity';
|
||||
import { AuctionTrigger, MarketTradingMode } from '@vegaprotocol/types';
|
||||
import {
|
||||
addDecimalsFormatNumber,
|
||||
addDecimalsFormatNumberQuantum,
|
||||
formatNumberPercentage,
|
||||
} from '@vegaprotocol/utils';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
@@ -30,12 +30,14 @@ interface Props {
|
||||
marketId?: string;
|
||||
noUpdate?: boolean;
|
||||
assetDecimals: number;
|
||||
quantum: string;
|
||||
}
|
||||
|
||||
export const MarketLiquiditySupplied = ({
|
||||
marketId,
|
||||
assetDecimals,
|
||||
noUpdate = false,
|
||||
quantum,
|
||||
}: Props) => {
|
||||
const [market, setMarket] = useState<MarketData>();
|
||||
const { params } = useNetworkParams([
|
||||
@@ -79,11 +81,12 @@ export const MarketLiquiditySupplied = ({
|
||||
});
|
||||
|
||||
const supplied = market?.suppliedStake
|
||||
? addDecimalsFormatNumber(
|
||||
? addDecimalsFormatNumberQuantum(
|
||||
new BigNumber(market?.suppliedStake)
|
||||
.multipliedBy(stakeToCcyVolume || 1)
|
||||
.toString(),
|
||||
assetDecimals
|
||||
assetDecimals,
|
||||
quantum
|
||||
)
|
||||
: '-';
|
||||
|
||||
@@ -108,9 +111,10 @@ export const MarketLiquiditySupplied = ({
|
||||
<span>{t('Supplied stake')}</span>
|
||||
<span>
|
||||
{market?.suppliedStake
|
||||
? addDecimalsFormatNumber(
|
||||
new BigNumber(market?.suppliedStake).toString(),
|
||||
assetDecimals
|
||||
? addDecimalsFormatNumberQuantum(
|
||||
market?.suppliedStake,
|
||||
assetDecimals,
|
||||
quantum
|
||||
)
|
||||
: '-'}
|
||||
</span>
|
||||
@@ -119,9 +123,10 @@ export const MarketLiquiditySupplied = ({
|
||||
<span>{t('Target stake')}</span>
|
||||
<span>
|
||||
{market?.targetStake
|
||||
? addDecimalsFormatNumber(
|
||||
new BigNumber(market?.targetStake).toString(),
|
||||
assetDecimals
|
||||
? addDecimalsFormatNumberQuantum(
|
||||
market?.targetStake,
|
||||
assetDecimals,
|
||||
quantum
|
||||
)
|
||||
: '-'}
|
||||
</span>
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { Fragment, useState } from 'react';
|
||||
import type { NewMarketSuccessorFieldsFragment } from '@vegaprotocol/proposals';
|
||||
import { useMarketViewProposals } from '@vegaprotocol/proposals';
|
||||
import {
|
||||
marketViewProposalsDataProvider,
|
||||
type NewMarketSuccessorFieldsFragment,
|
||||
} from '@vegaprotocol/proposals';
|
||||
|
||||
import {
|
||||
ExternalLink,
|
||||
Intent,
|
||||
@@ -9,17 +12,20 @@ import {
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { DApp, TOKEN_PROPOSAL, useLinks } from '@vegaprotocol/environment';
|
||||
import * as Types from '@vegaprotocol/types';
|
||||
import { useDataProvider } from '@vegaprotocol/data-provider';
|
||||
|
||||
export const MarketSuccessorProposalBanner = ({
|
||||
marketId,
|
||||
}: {
|
||||
marketId?: string;
|
||||
}) => {
|
||||
const proposals = useMarketViewProposals({
|
||||
const { data: proposals } = useDataProvider({
|
||||
dataProvider: marketViewProposalsDataProvider,
|
||||
skip: !marketId,
|
||||
inState: Types.ProposalState.STATE_OPEN,
|
||||
proposalType: Types.ProposalType.TYPE_NEW_MARKET,
|
||||
typename: 'NewMarket',
|
||||
variables: {
|
||||
inState: Types.ProposalState.STATE_OPEN,
|
||||
proposalType: Types.ProposalType.TYPE_NEW_MARKET,
|
||||
},
|
||||
});
|
||||
|
||||
const successors =
|
||||
|
||||
@@ -20,10 +20,13 @@ const marketMock = {
|
||||
},
|
||||
} as Market;
|
||||
|
||||
const proposalMock: MockedResponse<MarketViewProposalsQuery> = {
|
||||
const passedProposalMock: MockedResponse<MarketViewProposalsQuery> = {
|
||||
request: {
|
||||
query: MarketViewProposalsDocument,
|
||||
variables: { inState: Types.ProposalState.STATE_PASSED },
|
||||
variables: {
|
||||
inState: Types.ProposalState.STATE_PASSED,
|
||||
proposalType: Types.ProposalType.TYPE_UPDATE_MARKET_STATE,
|
||||
},
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
@@ -31,7 +34,7 @@ const proposalMock: MockedResponse<MarketViewProposalsQuery> = {
|
||||
edges: [
|
||||
{
|
||||
node: {
|
||||
id: 'first-id',
|
||||
id: '1',
|
||||
state: Types.ProposalState.STATE_PASSED,
|
||||
terms: {
|
||||
closingDatetime: '2023-09-27T11:48:18Z',
|
||||
@@ -56,7 +59,7 @@ const proposalMock: MockedResponse<MarketViewProposalsQuery> = {
|
||||
},
|
||||
{
|
||||
node: {
|
||||
id: 'second-id',
|
||||
id: '2',
|
||||
state: Types.ProposalState.STATE_PASSED,
|
||||
terms: {
|
||||
closingDatetime: '2023-09-27T11:48:18Z',
|
||||
@@ -84,7 +87,99 @@ const proposalMock: MockedResponse<MarketViewProposalsQuery> = {
|
||||
},
|
||||
},
|
||||
};
|
||||
const mocks: MockedResponse[] = [proposalMock];
|
||||
const openProposalMock: MockedResponse<MarketViewProposalsQuery> = {
|
||||
request: {
|
||||
query: MarketViewProposalsDocument,
|
||||
variables: {
|
||||
inState: Types.ProposalState.STATE_OPEN,
|
||||
proposalType: Types.ProposalType.TYPE_UPDATE_MARKET_STATE,
|
||||
},
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
proposalsConnection: {
|
||||
edges: [
|
||||
{
|
||||
node: {
|
||||
id: '3',
|
||||
state: Types.ProposalState.STATE_OPEN,
|
||||
terms: {
|
||||
closingDatetime: '2023-09-27T11:48:18Z',
|
||||
enactmentDatetime: '2023-10-01T11:48:18',
|
||||
change: {
|
||||
__typename: 'UpdateMarketState',
|
||||
updateType:
|
||||
Types.MarketUpdateType.MARKET_STATE_UPDATE_TYPE_TERMINATE,
|
||||
price: '',
|
||||
market: {
|
||||
id: 'market-3',
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
name: 'Market three name',
|
||||
code: 'Market three',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
node: {
|
||||
id: '4',
|
||||
state: Types.ProposalState.STATE_OPEN,
|
||||
terms: {
|
||||
closingDatetime: '2023-09-27T11:48:18Z',
|
||||
enactmentDatetime: '2023-10-11T11:48:18',
|
||||
change: {
|
||||
__typename: 'UpdateMarketState',
|
||||
updateType:
|
||||
Types.MarketUpdateType.MARKET_STATE_UPDATE_TYPE_TERMINATE,
|
||||
price: '',
|
||||
market: {
|
||||
id: 'market-3',
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
name: 'Market three name',
|
||||
code: 'Market three',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
node: {
|
||||
id: '5',
|
||||
state: Types.ProposalState.STATE_OPEN,
|
||||
terms: {
|
||||
closingDatetime: '2023-09-27T11:48:18Z',
|
||||
enactmentDatetime: '2023-10-01T11:48:18',
|
||||
change: {
|
||||
__typename: 'UpdateMarketState',
|
||||
updateType:
|
||||
Types.MarketUpdateType.MARKET_STATE_UPDATE_TYPE_TERMINATE,
|
||||
price: '',
|
||||
market: {
|
||||
id: 'market-4',
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
name: 'Market four name',
|
||||
code: 'Market four',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const mocks: MockedResponse[] = [passedProposalMock, openProposalMock];
|
||||
|
||||
describe('MarketTerminationBanner', () => {
|
||||
beforeAll(() => {
|
||||
@@ -108,4 +203,28 @@ describe('MarketTerminationBanner', () => {
|
||||
screen.getByTestId('termination-warning-banner-market-1')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render link to proposals', async () => {
|
||||
const { container } = render(
|
||||
<MockedProvider mocks={mocks}>
|
||||
<MarketTerminationBanner market={{ ...marketMock, id: 'market-3' }} />
|
||||
</MockedProvider>
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(container).not.toBeEmptyDOMElement();
|
||||
});
|
||||
expect(screen.getByText('View proposals')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render link to proposal', async () => {
|
||||
const { container } = render(
|
||||
<MockedProvider mocks={mocks}>
|
||||
<MarketTerminationBanner market={{ ...marketMock, id: 'market-4' }} />
|
||||
</MockedProvider>
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(container).not.toBeEmptyDOMElement();
|
||||
});
|
||||
expect(screen.getByText('View proposal')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,12 +1,67 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { format, formatDuration, intervalToDuration } from 'date-fns';
|
||||
import { Intent, NotificationBanner } from '@vegaprotocol/ui-toolkit';
|
||||
import { useMarketViewProposals } from '@vegaprotocol/proposals';
|
||||
import {
|
||||
ExternalLink,
|
||||
Intent,
|
||||
NotificationBanner,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import type { MarketViewProposalFieldsFragment } from '@vegaprotocol/proposals';
|
||||
import { marketViewProposalsDataProvider } from '@vegaprotocol/proposals';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import * as Types from '@vegaprotocol/types';
|
||||
import type { Market } from '@vegaprotocol/markets';
|
||||
import { getQuoteName } from '@vegaprotocol/markets';
|
||||
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
|
||||
import sortBy from 'lodash/sortBy';
|
||||
import {
|
||||
DApp,
|
||||
TOKEN_PROPOSAL,
|
||||
TOKEN_PROPOSALS,
|
||||
useLinks,
|
||||
} from '@vegaprotocol/environment';
|
||||
import { useDataProvider } from '@vegaprotocol/data-provider';
|
||||
|
||||
const filterProposals = (
|
||||
data: MarketViewProposalFieldsFragment[] | null,
|
||||
marketId: string,
|
||||
now: number
|
||||
) =>
|
||||
sortBy(
|
||||
(data || []).filter(
|
||||
(item) =>
|
||||
item.terms.change.__typename === 'UpdateMarketState' &&
|
||||
item.terms.change.market.id === marketId &&
|
||||
item.terms.change.updateType ===
|
||||
Types.MarketUpdateType.MARKET_STATE_UPDATE_TYPE_TERMINATE &&
|
||||
item.terms.enactmentDatetime &&
|
||||
new Date(item.terms.enactmentDatetime).getTime() > now
|
||||
),
|
||||
(item) => item.terms.enactmentDatetime
|
||||
);
|
||||
|
||||
const getMessageVariables = (proposal: MarketViewProposalFieldsFragment) => {
|
||||
const enactmentDatetime = new Date(proposal.terms.enactmentDatetime);
|
||||
const date = format(enactmentDatetime, 'dd MMMM');
|
||||
const duration = formatDuration(
|
||||
intervalToDuration({
|
||||
start: new Date(),
|
||||
end: enactmentDatetime,
|
||||
}),
|
||||
{
|
||||
format: ['days', 'hours'],
|
||||
}
|
||||
);
|
||||
const price =
|
||||
proposal.terms.change.__typename === 'UpdateMarketState'
|
||||
? proposal.terms.change.price
|
||||
: '';
|
||||
return {
|
||||
date,
|
||||
duration,
|
||||
price,
|
||||
};
|
||||
};
|
||||
|
||||
export const MarketTerminationBanner = ({
|
||||
market,
|
||||
@@ -15,57 +70,55 @@ export const MarketTerminationBanner = ({
|
||||
}) => {
|
||||
const [visible, setVisible] = useState(true);
|
||||
const skip = !market || !visible;
|
||||
const proposalsData = useMarketViewProposals({
|
||||
const { data: passedProposalsData } = useDataProvider({
|
||||
dataProvider: marketViewProposalsDataProvider,
|
||||
skip,
|
||||
inState: Types.ProposalState.STATE_PASSED,
|
||||
typename: 'UpdateMarketState',
|
||||
variables: {
|
||||
inState: Types.ProposalState.STATE_PASSED,
|
||||
proposalType: Types.ProposalType.TYPE_UPDATE_MARKET_STATE,
|
||||
},
|
||||
});
|
||||
|
||||
const { data: openProposalsData } = useDataProvider({
|
||||
dataProvider: marketViewProposalsDataProvider,
|
||||
skip,
|
||||
variables: {
|
||||
inState: Types.ProposalState.STATE_OPEN,
|
||||
proposalType: Types.ProposalType.TYPE_UPDATE_MARKET_STATE,
|
||||
},
|
||||
});
|
||||
|
||||
const governanceLink = useLinks(DApp.Governance);
|
||||
|
||||
if (!market) return null;
|
||||
const marketFound = (proposalsData || []).find(
|
||||
(item) =>
|
||||
item.terms.change.__typename === 'UpdateMarketState' &&
|
||||
item.terms.change.market.id === market.id &&
|
||||
item.terms.change.updateType ===
|
||||
Types.MarketUpdateType.MARKET_STATE_UPDATE_TYPE_TERMINATE &&
|
||||
item.state === Types.ProposalState.STATE_PASSED // subscription doesn't have state parameter
|
||||
);
|
||||
const now = Date.now();
|
||||
const passedProposals = filterProposals(passedProposalsData, market.id, now);
|
||||
const openProposals = filterProposals(openProposalsData, market.id, now);
|
||||
|
||||
const enactmentDatetime = new Date(marketFound?.terms.enactmentDatetime);
|
||||
const name =
|
||||
marketFound?.terms.change.__typename === 'UpdateMarketState'
|
||||
? marketFound.terms.change.market.tradableInstrument.instrument.code
|
||||
: '';
|
||||
const name = market.tradableInstrument.instrument.code;
|
||||
if (!passedProposals.length && !openProposals.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (name && enactmentDatetime.getTime() > Date.now()) {
|
||||
const dayMonthDate = format(enactmentDatetime, 'dd MMMM');
|
||||
const duration = intervalToDuration({
|
||||
start: new Date(),
|
||||
end: enactmentDatetime,
|
||||
});
|
||||
const formattedDuration = formatDuration(duration, {
|
||||
format: ['days', 'hours'],
|
||||
});
|
||||
const price =
|
||||
marketFound?.terms.change.__typename === 'UpdateMarketState'
|
||||
? marketFound.terms.change.price
|
||||
: '';
|
||||
const assetSymbol = getQuoteName(market);
|
||||
return (
|
||||
<NotificationBanner
|
||||
intent={Intent.Warning}
|
||||
onClose={() => {
|
||||
setVisible(false);
|
||||
}}
|
||||
data-testid={`termination-warning-banner-${market.id}`}
|
||||
>
|
||||
const assetSymbol = getQuoteName(market);
|
||||
const proposalLink =
|
||||
!passedProposals.length && openProposals[0]?.id
|
||||
? governanceLink(TOKEN_PROPOSAL.replace(':id', openProposals[0]?.id))
|
||||
: undefined;
|
||||
const proposalsLink =
|
||||
openProposals.length > 1 ? governanceLink(TOKEN_PROPOSALS) : undefined;
|
||||
let content: ReactNode;
|
||||
if (passedProposals.length) {
|
||||
const { date, duration, price } = getMessageVariables(passedProposals[0]);
|
||||
content = (
|
||||
<>
|
||||
<div className="uppercase mb-1">
|
||||
{t('Trading on Market %s will stop on %s', [name, dayMonthDate])}
|
||||
{t('Trading on Market %s will stop on %s', [name, date])}
|
||||
</div>
|
||||
<div>
|
||||
{t(
|
||||
'You will no longer be able to hold a position on this market when it closes in %s.',
|
||||
[formattedDuration]
|
||||
[duration]
|
||||
)}{' '}
|
||||
{price &&
|
||||
assetSymbol &&
|
||||
@@ -74,8 +127,57 @@ export const MarketTerminationBanner = ({
|
||||
assetSymbol,
|
||||
])}
|
||||
</div>
|
||||
</NotificationBanner>
|
||||
</>
|
||||
);
|
||||
} else if (openProposals.length > 1) {
|
||||
content = (
|
||||
<>
|
||||
<div className="uppercase mb-1">
|
||||
{t(
|
||||
'Trading on Market %s may stop. There are open proposals to close this market',
|
||||
[name]
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<ExternalLink href={proposalsLink}>
|
||||
{t('View proposals')}
|
||||
</ExternalLink>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
const { date, price } = getMessageVariables(openProposals[0]);
|
||||
content = (
|
||||
<>
|
||||
<div className="uppercase mb-1">
|
||||
{t(
|
||||
'Trading on Market %s may stop on %s. There is open proposal to close this market.',
|
||||
[name, date]
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
{price &&
|
||||
assetSymbol &&
|
||||
t('Proposed final price is %s %s.', [
|
||||
addDecimalsFormatNumber(price, market.decimalPlaces),
|
||||
assetSymbol,
|
||||
])}
|
||||
</div>
|
||||
<div>
|
||||
<ExternalLink href={proposalLink}>{t('View proposal')}</ExternalLink>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
return (
|
||||
<NotificationBanner
|
||||
intent={openProposals.length ? Intent.Warning : Intent.Info}
|
||||
onClose={() => {
|
||||
setVisible(false);
|
||||
}}
|
||||
data-testid={`termination-warning-banner-${market.id}`}
|
||||
>
|
||||
{content}
|
||||
</NotificationBanner>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -10,6 +10,19 @@ import type { DataGridStore } from '../../stores/datagrid-store-slice';
|
||||
import { OrderStatus } from '@vegaprotocol/types';
|
||||
import { Links } from '../../lib/links';
|
||||
|
||||
const resolveNoRowsMessage = (filter?: Filter) => {
|
||||
switch (filter) {
|
||||
case Filter.Open:
|
||||
return t('No open orders');
|
||||
case Filter.Closed:
|
||||
return t('No closed orders');
|
||||
case Filter.Rejected:
|
||||
return t('No rejected orders');
|
||||
default:
|
||||
return t('No orders');
|
||||
}
|
||||
};
|
||||
|
||||
export const FilterStatusValue = {
|
||||
[Filter.Open]: [OrderStatus.STATUS_ACTIVE, OrderStatus.STATUS_PARKED],
|
||||
[Filter.Closed]: [
|
||||
@@ -43,6 +56,7 @@ export const OrdersContainer = ({ filter }: OrderContainerProps) => {
|
||||
if (!pubKey) {
|
||||
return <Splash>{t('Please connect Vega wallet')}</Splash>;
|
||||
}
|
||||
const noRowsMessage = resolveNoRowsMessage(filter);
|
||||
|
||||
return (
|
||||
<OrderListManager
|
||||
@@ -56,6 +70,7 @@ export const OrdersContainer = ({ filter }: OrderContainerProps) => {
|
||||
}
|
||||
isReadOnly={isReadOnly}
|
||||
gridProps={gridStoreCallbacks}
|
||||
noRowsMessage={noRowsMessage}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -63,7 +63,9 @@ export const CandlesChartContainer = ({
|
||||
overlays,
|
||||
studies,
|
||||
notEnoughDataText: (
|
||||
<span className="text-xs text-center">{t('No data')}</span>
|
||||
<span className="text-xs text-center">
|
||||
{t('No open orders')}
|
||||
</span>
|
||||
),
|
||||
initialNumCandlesToDisplay: candlesCount,
|
||||
studySize: STUDY_SIZE,
|
||||
|
||||
@@ -43,7 +43,7 @@ import {
|
||||
REDUCE_ONLY_TOOLTIP,
|
||||
stopSubmit,
|
||||
getNotionalSize,
|
||||
getAssetUnit,
|
||||
getBaseQuoteUnit,
|
||||
} from './deal-ticket';
|
||||
import { TypeToggle } from './type-selector';
|
||||
import {
|
||||
@@ -854,7 +854,7 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
|
||||
}, [watch, market.id, updateStoredFormValues]);
|
||||
|
||||
const quoteName = getQuoteName(market);
|
||||
const assetUnit = getAssetUnit(
|
||||
const assetUnit = getBaseQuoteUnit(
|
||||
market.tradableInstrument.instrument.metadata.tags
|
||||
);
|
||||
|
||||
|
||||
@@ -127,7 +127,7 @@ const getDefaultValues = (
|
||||
...storedValues,
|
||||
});
|
||||
|
||||
export const getAssetUnit = (tags?: string[] | null) =>
|
||||
export const getBaseQuoteUnit = (tags?: string[] | null) =>
|
||||
tags
|
||||
?.find((tag) => tag.startsWith('base:') || tag.startsWith('ticker:'))
|
||||
?.replace(/^[^:]*:/, '');
|
||||
@@ -273,9 +273,7 @@ export const DealTicket = ({
|
||||
|
||||
const assetSymbol = getAsset(market).symbol;
|
||||
|
||||
const assetUnit = getAssetUnit(
|
||||
market.tradableInstrument.instrument.metadata.tags
|
||||
);
|
||||
const assetUnit = getQuoteName(market);
|
||||
|
||||
const summaryError = useMemo(() => {
|
||||
if (!pubKey) {
|
||||
|
||||
@@ -238,7 +238,7 @@ export const DepthChartContainer = ({ marketId }: DepthChartManagerProps) => {
|
||||
volumeFormat={volumeFormat}
|
||||
priceFormat={priceFormat}
|
||||
notEnoughDataText={
|
||||
<span className="text-xs text-center">{t('No data')}</span>
|
||||
<span className="text-xs text-center">{t('No open orders')}</span>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -11,14 +11,8 @@ import type {
|
||||
MarketDepthQuery,
|
||||
MarketDepthQueryVariables,
|
||||
MarketDepthUpdateSubscription,
|
||||
PriceLevelFieldsFragment,
|
||||
} from './__generated__/MarketDepth';
|
||||
|
||||
export type OrderbookData = {
|
||||
asks: PriceLevelFieldsFragment[];
|
||||
bids: PriceLevelFieldsFragment[];
|
||||
};
|
||||
|
||||
interface OrderbookManagerProps {
|
||||
marketId: string;
|
||||
onClick: (args: { price?: string; size?: string }) => void;
|
||||
|
||||
@@ -238,7 +238,7 @@ export const Orderbook = ({
|
||||
</>
|
||||
) : (
|
||||
<div className="absolute inset-0">
|
||||
<Splash>{t('No data')}</Splash>
|
||||
<Splash>{t('No open orders')}</Splash>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -4,10 +4,10 @@ import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import { useVegaTransactionStore } from '@vegaprotocol/web3';
|
||||
import { useHasAmendableOrder } from '../../order-hooks';
|
||||
|
||||
export const OpenOrdersMenu = ({ marketId }: { marketId: string }) => {
|
||||
export const OpenOrdersMenu = () => {
|
||||
const { isReadOnly } = useVegaWallet();
|
||||
const create = useVegaTransactionStore((state) => state.create);
|
||||
const hasAmendableOrder = useHasAmendableOrder(marketId);
|
||||
const hasAmendableOrder = useHasAmendableOrder();
|
||||
|
||||
if (isReadOnly) {
|
||||
return null;
|
||||
|
||||
@@ -239,11 +239,11 @@ export const hasActiveOrderProvider = makeDerivedDataProvider<
|
||||
export const hasAmendableOrderProvider = makeDerivedDataProvider<
|
||||
boolean,
|
||||
never,
|
||||
{ partyId: string; marketId?: string }
|
||||
{ partyId: string }
|
||||
>([activeOrdersProvider], (parts) => {
|
||||
const activeOrders = parts[0] as ReturnType<typeof getData>;
|
||||
const hasAmendableOrder = activeOrders.some(
|
||||
(order) => !(order.liquidityProvision || order.peggedOrder)
|
||||
(order) => !order.liquidityProvision
|
||||
);
|
||||
return hasAmendableOrder;
|
||||
});
|
||||
|
||||
@@ -1,23 +1,18 @@
|
||||
import { render, screen, act } from '@testing-library/react';
|
||||
import { OrderListManager } from './order-list-manager';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import type { OrderListManagerProps } from './order-list-manager';
|
||||
import { OrderListManager, Filter } from './order-list-manager';
|
||||
import * as useDataProviderHook from '@vegaprotocol/data-provider';
|
||||
import type { OrderFieldsFragment } from '../';
|
||||
import * as orderListMock from '../order-list/order-list';
|
||||
import { forwardRef } from 'react';
|
||||
import type { VegaWalletContextShape } from '@vegaprotocol/wallet';
|
||||
import { VegaWalletContext } from '@vegaprotocol/wallet';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
|
||||
// @ts-ignore OrderList is read only but we need to override with the forwardRef to
|
||||
// avoid warnings about padding refs
|
||||
orderListMock.OrderListTable = forwardRef(() => <div>OrderList</div>);
|
||||
|
||||
const generateJsx = () => {
|
||||
const generateJsx = (props: Partial<OrderListManagerProps> | null = null) => {
|
||||
const pubKey = '0x123';
|
||||
return (
|
||||
<MockedProvider>
|
||||
<VegaWalletContext.Provider value={{ pubKey } as VegaWalletContextShape}>
|
||||
<OrderListManager partyId={pubKey} isReadOnly={false} />
|
||||
<OrderListManager partyId={pubKey} isReadOnly={false} {...props} />
|
||||
</VegaWalletContext.Provider>
|
||||
</MockedProvider>
|
||||
);
|
||||
@@ -33,9 +28,46 @@ describe('OrderListManager', () => {
|
||||
reload: jest.fn(),
|
||||
load: jest.fn(),
|
||||
});
|
||||
await act(async () => {
|
||||
render(generateJsx());
|
||||
render(generateJsx());
|
||||
|
||||
expect(
|
||||
await screen.getByRole('treegrid', {
|
||||
name: (_, element) => element.classList.contains('ag-root'),
|
||||
})
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should display order info', async () => {
|
||||
jest.spyOn(useDataProviderHook, 'useDataProvider').mockReturnValue({
|
||||
data: [{ id: '1' } as OrderFieldsFragment],
|
||||
loading: false,
|
||||
error: new Error('Query error'),
|
||||
flush: jest.fn(),
|
||||
reload: jest.fn(),
|
||||
load: jest.fn(),
|
||||
});
|
||||
expect(await screen.findByText('OrderList')).toBeInTheDocument();
|
||||
|
||||
render(generateJsx());
|
||||
|
||||
expect(
|
||||
await screen.getByText('Something went wrong: Query error')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should display no rows overlay', async () => {
|
||||
jest.spyOn(useDataProviderHook, 'useDataProvider').mockReturnValue({
|
||||
data: [],
|
||||
loading: false,
|
||||
error: undefined,
|
||||
flush: jest.fn(),
|
||||
reload: jest.fn(),
|
||||
load: jest.fn(),
|
||||
});
|
||||
|
||||
render(
|
||||
generateJsx({ filter: Filter.Closed, noRowsMessage: 'no rows message' })
|
||||
);
|
||||
|
||||
expect(screen.getByText('no rows message')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { useCallback, useRef, useState } from 'react';
|
||||
import { useCallback, useRef, useState, useEffect } from 'react';
|
||||
import type { AgGridReact } from 'ag-grid-react';
|
||||
import { OrderListTable } from '../order-list/order-list';
|
||||
import type { FilterChangedEvent } from 'ag-grid-community';
|
||||
import { OrderListTable } from '../order-list';
|
||||
import type { useDataGridEvents } from '@vegaprotocol/datagrid';
|
||||
import { useDataProvider } from '@vegaprotocol/data-provider';
|
||||
import { ordersWithMarketProvider } from '../order-data-provider/order-data-provider';
|
||||
@@ -11,6 +12,7 @@ import type { OrderTxUpdateFieldsFragment } from '@vegaprotocol/web3';
|
||||
import { OrderEditDialog } from '../order-list/order-edit-dialog';
|
||||
import type { Order } from '../order-data-provider';
|
||||
import { OrderViewDialog } from '../order-list/order-view-dialog';
|
||||
import { Splash } from '@vegaprotocol/ui-toolkit';
|
||||
|
||||
export enum Filter {
|
||||
'Open' = 'Open',
|
||||
@@ -25,6 +27,7 @@ export interface OrderListManagerProps {
|
||||
isReadOnly: boolean;
|
||||
filter?: Filter;
|
||||
gridProps?: ReturnType<typeof useDataGridEvents>;
|
||||
noRowsMessage?: string;
|
||||
}
|
||||
|
||||
export const OrderListManager = ({
|
||||
@@ -34,6 +37,7 @@ export const OrderListManager = ({
|
||||
isReadOnly,
|
||||
filter,
|
||||
gridProps,
|
||||
noRowsMessage,
|
||||
}: OrderListManagerProps) => {
|
||||
const gridRef = useRef<AgGridReact | null>(null);
|
||||
const [editOrder, setEditOrder] = useState<Order | null>(null);
|
||||
@@ -56,6 +60,29 @@ export const OrderListManager = ({
|
||||
},
|
||||
});
|
||||
|
||||
const onFilterChanged = useCallback(
|
||||
(event: FilterChangedEvent) => {
|
||||
gridProps?.onFilterChanged?.(event);
|
||||
if (event.api) {
|
||||
const isEmpty = event.api.getDisplayedRowCount() === 0;
|
||||
if (isEmpty) {
|
||||
event.api.showNoRowsOverlay();
|
||||
} else {
|
||||
event.api.hideOverlay();
|
||||
}
|
||||
}
|
||||
},
|
||||
[gridProps]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!data || !data.length) {
|
||||
gridRef.current?.api?.showNoRowsOverlay();
|
||||
} else {
|
||||
gridRef.current?.api?.hideOverlay();
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
const cancel = useCallback(
|
||||
(order: Order) => {
|
||||
if (!order.market) return;
|
||||
@@ -69,6 +96,10 @@ export const OrderListManager = ({
|
||||
[create]
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return <Splash>{t(`Something went wrong: ${error.message}`)}</Splash>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="h-full relative">
|
||||
@@ -82,8 +113,9 @@ export const OrderListManager = ({
|
||||
onMarketClick={onMarketClick}
|
||||
onOrderTypeClick={onOrderTypeClick}
|
||||
isReadOnly={isReadOnly}
|
||||
overlayNoRowsTemplate={error ? error.message : t('No orders')}
|
||||
overlayNoRowsTemplate={noRowsMessage || t('No orders')}
|
||||
{...gridProps}
|
||||
onFilterChanged={onFilterChanged}
|
||||
/>
|
||||
</div>
|
||||
{editOrder && (
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { act, render, screen, within } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
import { OrderEditDialog } from './order-edit-dialog';
|
||||
import { limitOrder } from '../mocks';
|
||||
|
||||
describe('OrderEditDialog', () => {
|
||||
it('must be warned (pre-submit) if the input price has too many digits after the decimal place for the market', async () => {
|
||||
// 7003-MORD-013
|
||||
await act(async () => {
|
||||
render(
|
||||
<OrderEditDialog
|
||||
order={limitOrder}
|
||||
onChange={jest.fn()}
|
||||
isOpen={true}
|
||||
onSubmit={jest.fn()}
|
||||
/>
|
||||
);
|
||||
});
|
||||
const editOrder = await screen.findByTestId('edit-order');
|
||||
const limitPrice = within(editOrder).getByLabelText('Price');
|
||||
await userEvent.type(limitPrice, '0.111111');
|
||||
const submitButton = within(editOrder).getByRole('button', {
|
||||
name: 'Update',
|
||||
});
|
||||
await userEvent.click(submitButton);
|
||||
const inputErrorText = within(editOrder).getByTestId('input-error-text');
|
||||
expect(inputErrorText).toHaveTextContent(
|
||||
'Price accepts up to 1 decimal places'
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -46,6 +46,12 @@ const generateJsx = (
|
||||
|
||||
describe('OrderListTable', () => {
|
||||
it('should render correct columns', async () => {
|
||||
// 7003-MORD-001
|
||||
// 7003-MORD-002
|
||||
// 7003-MORD-003
|
||||
// 7003-MORD-004
|
||||
// 7003-MORD-005
|
||||
// 7003-MORD-006
|
||||
await act(async () => {
|
||||
render(generateJsx({ rowData: [marketOrder, limitOrder] }));
|
||||
});
|
||||
@@ -184,6 +190,7 @@ describe('OrderListTable', () => {
|
||||
|
||||
describe('amend cell', () => {
|
||||
it('allows cancelling and editing for permitted orders', async () => {
|
||||
// 7003-MORD-007
|
||||
const mockEdit = jest.fn();
|
||||
const mockCancel = jest.fn();
|
||||
const order = generateOrder({
|
||||
@@ -209,6 +216,7 @@ describe('OrderListTable', () => {
|
||||
});
|
||||
|
||||
it('does not allow cancelling and editing for permitted orders if read only', async () => {
|
||||
// 7003-MORD-008
|
||||
const mockEdit = jest.fn();
|
||||
const mockCancel = jest.fn();
|
||||
const order = generateOrder({
|
||||
@@ -233,6 +241,7 @@ describe('OrderListTable', () => {
|
||||
});
|
||||
|
||||
it('shows if an order is a liquidity provision order and does not show order actions', async () => {
|
||||
// 7003-MORD-008
|
||||
const order = generateOrder({
|
||||
type: Schema.OrderType.TYPE_LIMIT,
|
||||
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_GTC,
|
||||
@@ -251,6 +260,7 @@ describe('OrderListTable', () => {
|
||||
});
|
||||
|
||||
it('shows if an order is a pegged order and does not show order actions', async () => {
|
||||
// 7003-MORD-008
|
||||
const order = generateOrder({
|
||||
type: Schema.OrderType.TYPE_LIMIT,
|
||||
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_GTC,
|
||||
|
||||
@@ -34,7 +34,7 @@ import type {
|
||||
} from '@vegaprotocol/datagrid';
|
||||
import type { AgGridReact } from 'ag-grid-react';
|
||||
import type { Order } from '../order-data-provider';
|
||||
import { Filter } from '../order-list-manager';
|
||||
import { Filter } from '../order-list-manager/order-list-manager';
|
||||
import type { ColDef } from 'ag-grid-community';
|
||||
|
||||
const defaultColDef = {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { hasAmendableOrderProvider } from '../components/order-data-provider';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import { useDataProvider } from '@vegaprotocol/data-provider';
|
||||
|
||||
export const useHasAmendableOrder = (marketId?: string) => {
|
||||
export const useHasAmendableOrder = () => {
|
||||
const { pubKey } = useVegaWallet();
|
||||
const [hasAmendableOrder, setHasAmendableOrder] = useState(false);
|
||||
const update = useCallback(({ data }: { data: boolean | null }) => {
|
||||
@@ -15,7 +15,6 @@ export const useHasAmendableOrder = (marketId?: string) => {
|
||||
update,
|
||||
variables: {
|
||||
partyId: pubKey || '',
|
||||
marketId,
|
||||
},
|
||||
skip: !pubKey,
|
||||
});
|
||||
|
||||
@@ -1,2 +1,5 @@
|
||||
export * from './proposals-data-provider';
|
||||
export {
|
||||
marketViewProposalsDataProvider,
|
||||
proposalsDataProvider,
|
||||
} from './proposals-data-provider';
|
||||
export * from './__generated__/Proposals';
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import { update } from './proposals-data-provider';
|
||||
import type {
|
||||
MarketViewProposalFieldsFragment,
|
||||
MarketViewProposalsQueryVariables,
|
||||
} from './__generated__/Proposals';
|
||||
import * as Types from '@vegaprotocol/types';
|
||||
|
||||
describe('proposals data provider', () => {
|
||||
const proposal = {
|
||||
id: '1',
|
||||
state: Types.ProposalState.STATE_OPEN,
|
||||
terms: {
|
||||
change: {
|
||||
__typename: 'UpdateMarketState',
|
||||
},
|
||||
},
|
||||
} as MarketViewProposalFieldsFragment;
|
||||
const data = [proposal];
|
||||
const delta = { ...proposal };
|
||||
const reload = jest.fn();
|
||||
|
||||
const variables: MarketViewProposalsQueryVariables = {
|
||||
inState: Types.ProposalState.STATE_OPEN,
|
||||
proposalType: Types.ProposalType.TYPE_UPDATE_MARKET_STATE,
|
||||
};
|
||||
|
||||
it('update existing data', () => {
|
||||
expect(update(data, delta, reload, variables)?.[0]).toBe(delta);
|
||||
});
|
||||
|
||||
it('removes existing data if delta do not match inState', () => {
|
||||
expect(
|
||||
update(
|
||||
data,
|
||||
{ ...delta, state: Types.ProposalState.STATE_PASSED },
|
||||
reload,
|
||||
variables
|
||||
)?.length
|
||||
).toBe(0);
|
||||
});
|
||||
|
||||
it('do not add delta if it do not match inState', () => {
|
||||
expect(
|
||||
update(
|
||||
data,
|
||||
{ ...delta, id: '2', state: Types.ProposalState.STATE_PASSED },
|
||||
reload,
|
||||
variables
|
||||
)?.length
|
||||
).toBe(1);
|
||||
});
|
||||
|
||||
it('do not add delta if it do not match proposalType', () => {
|
||||
expect(
|
||||
update(
|
||||
data,
|
||||
{
|
||||
...delta,
|
||||
id: '2',
|
||||
terms: { ...delta.terms, change: { __typename: 'UpdateMarket' } },
|
||||
},
|
||||
reload,
|
||||
variables
|
||||
)?.length
|
||||
).toBe(1);
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,7 @@
|
||||
import type { Update } from '@vegaprotocol/data-provider';
|
||||
import { makeDataProvider } from '@vegaprotocol/data-provider';
|
||||
import produce from 'immer';
|
||||
import * as Types from '@vegaprotocol/types';
|
||||
import type {
|
||||
ProposalsListQuery,
|
||||
ProposalsListQueryVariables,
|
||||
@@ -8,6 +10,7 @@ import type {
|
||||
MarketViewProposalFieldsFragment,
|
||||
MarketViewProposalsQuery,
|
||||
MarketViewProposalsQueryVariables,
|
||||
MarketViewLiveProposalsSubscriptionVariables,
|
||||
} from './__generated__/Proposals';
|
||||
import {
|
||||
MarketViewLiveProposalsDocument,
|
||||
@@ -42,21 +45,61 @@ export const proposalsDataProvider = makeDataProvider<
|
||||
errors.every((e) => e.message.match(/failed to get asset for ID/)),
|
||||
});
|
||||
|
||||
const update = (
|
||||
const ProposalTypeMap: Record<
|
||||
Types.ProposalType,
|
||||
Types.ProposalChange['__typename']
|
||||
> = {
|
||||
[Types.ProposalType.TYPE_CANCEL_TRANSFER]: 'CancelTransfer',
|
||||
[Types.ProposalType.TYPE_NETWORK_PARAMETERS]: 'UpdateNetworkParameter',
|
||||
[Types.ProposalType.TYPE_NEW_ASSET]: 'NewAsset',
|
||||
[Types.ProposalType.TYPE_NEW_FREE_FORM]: 'NewFreeform',
|
||||
[Types.ProposalType.TYPE_NEW_MARKET]: 'NewMarket',
|
||||
[Types.ProposalType.TYPE_NEW_SPOT_MARKET]: 'NewSpotMarket',
|
||||
[Types.ProposalType.TYPE_NEW_TRANSFER]: 'NewTransfer',
|
||||
[Types.ProposalType.TYPE_UPDATE_ASSET]: 'UpdateAsset',
|
||||
[Types.ProposalType.TYPE_UPDATE_MARKET]: 'UpdateMarket',
|
||||
[Types.ProposalType.TYPE_UPDATE_MARKET_STATE]: 'UpdateMarketState',
|
||||
[Types.ProposalType.TYPE_UPDATE_REFERRAL_PROGRAM]: 'UpdateReferralProgram',
|
||||
[Types.ProposalType.TYPE_UPDATE_SPOT_MARKET]: 'UpdateSpotMarket',
|
||||
[Types.ProposalType.TYPE_UPDATE_VOLUME_DISCOUNT_PROGRAM]:
|
||||
'UpdateVolumeDiscountProgram',
|
||||
};
|
||||
|
||||
const matchFilter = (
|
||||
data: MarketViewProposalFieldsFragment,
|
||||
variables: MarketViewProposalsQueryVariables
|
||||
) => {
|
||||
return (
|
||||
(!variables.inState || data.state === variables.inState) &&
|
||||
(!variables.proposalType ||
|
||||
data.terms.change.__typename === ProposalTypeMap[variables.proposalType])
|
||||
);
|
||||
};
|
||||
|
||||
export const update: Update<
|
||||
MarketViewProposalFieldsFragment[] | null,
|
||||
MarketViewProposalFieldsFragment,
|
||||
MarketViewProposalsQueryVariables
|
||||
> = (
|
||||
data: MarketViewProposalFieldsFragment[] | null,
|
||||
delta: MarketViewProposalFieldsFragment
|
||||
delta: MarketViewProposalFieldsFragment,
|
||||
reload,
|
||||
variables
|
||||
) => {
|
||||
const updateData = produce(data || [], (draft) => {
|
||||
const { id } = delta;
|
||||
const index = draft.findIndex((item) => item.id === id);
|
||||
const match = matchFilter(delta, variables);
|
||||
if (index === -1) {
|
||||
draft.unshift(delta);
|
||||
if (match) {
|
||||
draft.unshift(delta);
|
||||
}
|
||||
} else {
|
||||
const currNode = draft[index];
|
||||
draft[index] = {
|
||||
...currNode,
|
||||
...delta,
|
||||
};
|
||||
if (match) {
|
||||
draft[index] = delta;
|
||||
} else {
|
||||
draft.splice(index, 1);
|
||||
}
|
||||
}
|
||||
});
|
||||
return updateData;
|
||||
@@ -66,12 +109,15 @@ const getMarketProposalsData = (
|
||||
responseData: MarketViewProposalsQuery | null
|
||||
) => removePaginationWrapper(responseData?.proposalsConnection?.edges) || [];
|
||||
|
||||
const subscriptionVariables: MarketViewLiveProposalsSubscriptionVariables = {};
|
||||
|
||||
export const marketViewProposalsDataProvider = makeDataProvider<
|
||||
MarketViewProposalsQuery,
|
||||
MarketViewProposalFieldsFragment[],
|
||||
MarketViewLiveProposalsSubscription,
|
||||
MarketViewProposalFieldsFragment,
|
||||
MarketViewProposalsQueryVariables
|
||||
MarketViewProposalsQueryVariables,
|
||||
MarketViewLiveProposalsSubscriptionVariables
|
||||
>({
|
||||
query: MarketViewProposalsDocument,
|
||||
subscriptionQuery: MarketViewLiveProposalsDocument,
|
||||
@@ -79,4 +125,5 @@ export const marketViewProposalsDataProvider = makeDataProvider<
|
||||
getDelta: (subscriptionData: MarketViewLiveProposalsSubscription) =>
|
||||
subscriptionData.proposals,
|
||||
getData: getMarketProposalsData,
|
||||
getSubscriptionVariables: () => subscriptionVariables,
|
||||
});
|
||||
|
||||
@@ -7,4 +7,3 @@ export * from './use-update-network-paramaters-toasts';
|
||||
export * from './use-successor-market-proposal-details';
|
||||
export * from './use-new-transfer-proposal-details';
|
||||
export * from './use-cancel-transfer-proposal-details';
|
||||
export * from './use-market-view-proposals';
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
import { useDataProvider } from '@vegaprotocol/data-provider';
|
||||
import { marketViewProposalsDataProvider } from '../proposals-data-provider';
|
||||
import type * as Types from '@vegaprotocol/types';
|
||||
|
||||
export const useMarketViewProposals = ({
|
||||
skip = false,
|
||||
typename,
|
||||
proposalType,
|
||||
inState,
|
||||
}: {
|
||||
typename: Types.ProposalChange['__typename'];
|
||||
skip?: boolean;
|
||||
inState: Types.ProposalState;
|
||||
proposalType?: Types.ProposalType;
|
||||
}) => {
|
||||
const variables = {
|
||||
inState,
|
||||
...(proposalType ? { proposalType } : null),
|
||||
};
|
||||
|
||||
const { data } = useDataProvider({
|
||||
dataProvider: marketViewProposalsDataProvider,
|
||||
skip,
|
||||
variables,
|
||||
});
|
||||
return (data || []).filter(
|
||||
(item) => item.terms.change.__typename === typename
|
||||
);
|
||||
};
|
||||
@@ -1,9 +1,9 @@
|
||||
import { proposalsDataProvider } from '..';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import { useDataProvider } from '@vegaprotocol/data-provider';
|
||||
import { useMemo } from 'react';
|
||||
import first from 'lodash/first';
|
||||
import type { ProposalListFieldsFragment } from '..';
|
||||
import { proposalsDataProvider } from '../proposals-data-provider';
|
||||
import type { ProposalListFieldsFragment } from '../proposals-data-provider';
|
||||
|
||||
type UseUpdateProposalProps = {
|
||||
id?: string;
|
||||
|
||||
@@ -58,7 +58,7 @@ type ThemeStore = {
|
||||
setTheme: (theme?: Theme) => void;
|
||||
};
|
||||
|
||||
const useThemeStore = create<ThemeStore>((set) => ({
|
||||
const useThemeStore = create<ThemeStore>()((set) => ({
|
||||
theme: getCurrentTheme(),
|
||||
setTheme: (newTheme) => {
|
||||
set((state) => {
|
||||
|
||||
Reference in New Issue
Block a user