diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 000000000..05ddf0fbe --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,20 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Debug Jest", + "type": "node", + "request": "launch", + "program": "${workspaceFolder}/node_modules/@nrwl/cli/bin/nx.js", + "args": [ + "test", + "accounts", + "--codeCoverage=false", + "--testFile=summary-row.spec.ts", + "--watch" + ], + "cwd": "${workspaceFolder}", + "console": "integratedTerminal" + } + ] +} diff --git a/libs/accounts/.babelrc b/libs/accounts/.babelrc new file mode 100644 index 000000000..9f6abe49d --- /dev/null +++ b/libs/accounts/.babelrc @@ -0,0 +1,6 @@ +{ + "presets": [ + "@nrwl/next/babel" + ], + "plugins": [] +} diff --git a/libs/accounts/.eslintrc.json b/libs/accounts/.eslintrc.json new file mode 100644 index 000000000..db820c5d0 --- /dev/null +++ b/libs/accounts/.eslintrc.json @@ -0,0 +1,18 @@ +{ + "extends": ["plugin:@nrwl/nx/react", "../../.eslintrc.json"], + "ignorePatterns": ["!**/*", "__generated__"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + } + ] +} diff --git a/libs/accounts/README.md b/libs/accounts/README.md new file mode 100644 index 000000000..fac6c0eff --- /dev/null +++ b/libs/accounts/README.md @@ -0,0 +1,7 @@ +# accounts + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test accounts` to execute the unit tests via [Jest](https://jestjs.io). diff --git a/libs/accounts/jest.config.js b/libs/accounts/jest.config.js new file mode 100644 index 000000000..fd4b91f52 --- /dev/null +++ b/libs/accounts/jest.config.js @@ -0,0 +1,15 @@ +module.exports = { + displayName: 'positions', + preset: '../../jest.preset.js', + globals: { + 'ts-jest': { + tsconfig: '/tsconfig.spec.json', + }, + }, + transform: { + '^.+\\.[tj]sx?$': 'ts-jest', + }, + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], + coverageDirectory: '../../coverage/libs/positions', + setupFilesAfterEnv: ['./src/setup-tests.ts'], +}; diff --git a/libs/accounts/package.json b/libs/accounts/package.json new file mode 100644 index 000000000..27644568a --- /dev/null +++ b/libs/accounts/package.json @@ -0,0 +1,4 @@ +{ + "name": "@vegaprotocol/accounts", + "version": "0.0.1" +} diff --git a/libs/accounts/project.json b/libs/accounts/project.json new file mode 100644 index 000000000..796e2fc78 --- /dev/null +++ b/libs/accounts/project.json @@ -0,0 +1,43 @@ +{ + "root": "libs/accounts", + "sourceRoot": "libs/accounts/src", + "projectType": "library", + "tags": [], + "targets": { + "build": { + "executor": "@nrwl/web:rollup", + "outputs": ["{options.outputPath}"], + "options": { + "outputPath": "dist/libs/accounts", + "tsConfig": "libs/accounts/tsconfig.lib.json", + "project": "libs/accounts/package.json", + "entryFile": "libs/accounts/src/index.ts", + "external": ["react/jsx-runtime"], + "rollupConfig": "@nrwl/react/plugins/bundle-rollup", + "compiler": "babel", + "assets": [ + { + "glob": "libs/accounts/README.md", + "input": ".", + "output": "." + } + ] + } + }, + "lint": { + "executor": "@nrwl/linter:eslint", + "outputs": ["{options.outputFile}"], + "options": { + "lintFilePatterns": ["libs/accounts/**/*.{ts,tsx,js,jsx}"] + } + }, + "test": { + "executor": "@nrwl/jest:jest", + "outputs": ["coverage/libs/accounts"], + "options": { + "jestConfig": "libs/accounts/jest.config.js", + "passWithNoTests": true + } + } + } +} diff --git a/libs/accounts/src/index.ts b/libs/accounts/src/index.ts new file mode 100644 index 000000000..cbc4641cf --- /dev/null +++ b/libs/accounts/src/index.ts @@ -0,0 +1,3 @@ +export * from './lib/accounts-table'; +export * from './lib/accounts-container'; +export * from './lib/accounts-data-provider'; diff --git a/libs/accounts/src/lib/__generated__/AccountFields.ts b/libs/accounts/src/lib/__generated__/AccountFields.ts new file mode 100644 index 000000000..7d4193866 --- /dev/null +++ b/libs/accounts/src/lib/__generated__/AccountFields.ts @@ -0,0 +1,58 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +import { AccountType } from "./../../../../types/src/__generated__/globalTypes"; + +// ==================================================== +// GraphQL fragment: AccountFields +// ==================================================== + +export interface AccountFields_market { + __typename: "Market"; + /** + * Market ID + */ + id: string; + /** + * Market full name + */ + name: string; +} + +export interface AccountFields_asset { + __typename: "Asset"; + /** + * The id of the asset + */ + id: string; + /** + * The symbol of the asset (e.g: GBP) + */ + symbol: string; + /** + * The precision of the asset + */ + decimals: number; +} + +export interface AccountFields { + __typename: "Account"; + /** + * Account type (General, Margin, etc) + */ + type: AccountType; + /** + * Balance as string - current account balance (approx. as balances can be updated several times per second) + */ + balance: string; + /** + * Market (only relevant to margin accounts) + */ + market: AccountFields_market | null; + /** + * Asset, the 'currency' + */ + asset: AccountFields_asset; +} diff --git a/libs/accounts/src/lib/__generated__/AccountSubscribe.ts b/libs/accounts/src/lib/__generated__/AccountSubscribe.ts new file mode 100644 index 000000000..18ab89139 --- /dev/null +++ b/libs/accounts/src/lib/__generated__/AccountSubscribe.ts @@ -0,0 +1,69 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +import { AccountType } from "./../../../../types/src/__generated__/globalTypes"; + +// ==================================================== +// GraphQL subscription operation: AccountSubscribe +// ==================================================== + +export interface AccountSubscribe_accounts_market { + __typename: "Market"; + /** + * Market ID + */ + id: string; + /** + * Market full name + */ + name: string; +} + +export interface AccountSubscribe_accounts_asset { + __typename: "Asset"; + /** + * The id of the asset + */ + id: string; + /** + * The symbol of the asset (e.g: GBP) + */ + symbol: string; + /** + * The precision of the asset + */ + decimals: number; +} + +export interface AccountSubscribe_accounts { + __typename: "Account"; + /** + * Account type (General, Margin, etc) + */ + type: AccountType; + /** + * Balance as string - current account balance (approx. as balances can be updated several times per second) + */ + balance: string; + /** + * Market (only relevant to margin accounts) + */ + market: AccountSubscribe_accounts_market | null; + /** + * Asset, the 'currency' + */ + asset: AccountSubscribe_accounts_asset; +} + +export interface AccountSubscribe { + /** + * Subscribe to the accounts updates + */ + accounts: AccountSubscribe_accounts; +} + +export interface AccountSubscribeVariables { + partyId: string; +} diff --git a/libs/accounts/src/lib/__generated__/Accounts.ts b/libs/accounts/src/lib/__generated__/Accounts.ts new file mode 100644 index 000000000..2f83c1b63 --- /dev/null +++ b/libs/accounts/src/lib/__generated__/Accounts.ts @@ -0,0 +1,81 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +import { AccountType } from "./../../../../types/src/__generated__/globalTypes"; + +// ==================================================== +// GraphQL query operation: Accounts +// ==================================================== + +export interface Accounts_party_accounts_market { + __typename: "Market"; + /** + * Market ID + */ + id: string; + /** + * Market full name + */ + name: string; +} + +export interface Accounts_party_accounts_asset { + __typename: "Asset"; + /** + * The id of the asset + */ + id: string; + /** + * The symbol of the asset (e.g: GBP) + */ + symbol: string; + /** + * The precision of the asset + */ + decimals: number; +} + +export interface Accounts_party_accounts { + __typename: "Account"; + /** + * Account type (General, Margin, etc) + */ + type: AccountType; + /** + * Balance as string - current account balance (approx. as balances can be updated several times per second) + */ + balance: string; + /** + * Market (only relevant to margin accounts) + */ + market: Accounts_party_accounts_market | null; + /** + * Asset, the 'currency' + */ + asset: Accounts_party_accounts_asset; +} + +export interface Accounts_party { + __typename: "Party"; + /** + * Party identifier + */ + id: string; + /** + * Collateral accounts relating to a party + */ + accounts: Accounts_party_accounts[] | null; +} + +export interface Accounts { + /** + * An entity that is trading on the VEGA network + */ + party: Accounts_party | null; +} + +export interface AccountsVariables { + partyId: string; +} diff --git a/libs/accounts/src/lib/accounts-container.tsx b/libs/accounts/src/lib/accounts-container.tsx new file mode 100644 index 000000000..8d6b27809 --- /dev/null +++ b/libs/accounts/src/lib/accounts-container.tsx @@ -0,0 +1,18 @@ +import { t } from '@vegaprotocol/react-helpers'; +import { Splash } from '@vegaprotocol/ui-toolkit'; +import { useVegaWallet } from '@vegaprotocol/wallet'; +import { AccountsManager } from './accounts-manager'; + +export const AccountsContainer = () => { + const { keypair } = useVegaWallet(); + + if (!keypair) { + return ( + +

{t('Please connect Vega wallet')}

+
+ ); + } + + return ; +}; diff --git a/libs/accounts/src/lib/accounts-data-provider.ts b/libs/accounts/src/lib/accounts-data-provider.ts new file mode 100644 index 000000000..a330d4c9f --- /dev/null +++ b/libs/accounts/src/lib/accounts-data-provider.ts @@ -0,0 +1,77 @@ +import { gql } from '@apollo/client'; +import type { + Accounts, + Accounts_party_accounts, +} from './__generated__/Accounts'; +import { makeDataProvider } from '@vegaprotocol/react-helpers'; + +import type { + AccountSubscribe, + AccountSubscribe_accounts, +} from './__generated__/AccountSubscribe'; + +const ACCOUNTS_FRAGMENT = gql` + fragment AccountFields on Account { + type + balance + market { + id + name + } + asset { + id + symbol + decimals + } + } +`; + +const ACCOUNTS_QUERY = gql` + ${ACCOUNTS_FRAGMENT} + query Accounts($partyId: ID!) { + party(id: $partyId) { + id + accounts { + ...AccountFields + } + } + } +`; + +export const ACCOUNTS_SUB = gql` + ${ACCOUNTS_FRAGMENT} + subscription AccountSubscribe($partyId: ID!) { + accounts(partyId: $partyId) { + ...AccountFields + } + } +`; + +export const getId = ( + data: Accounts_party_accounts | AccountSubscribe_accounts +) => `${data.type}-${data.asset.symbol}-${data.market?.id ?? 'null'}`; + +const update = ( + draft: Accounts_party_accounts[], + delta: AccountSubscribe_accounts +) => { + const id = getId(delta); + const index = draft.findIndex((a) => getId(a) === id); + if (index !== -1) { + draft[index] = delta; + } else { + draft.push(delta); + } +}; +const getData = (responseData: Accounts): Accounts_party_accounts[] | null => + responseData.party ? responseData.party.accounts : null; +const getDelta = ( + subscriptionData: AccountSubscribe +): AccountSubscribe_accounts => subscriptionData.accounts; + +export const accountsDataProvider = makeDataProvider< + Accounts, + Accounts_party_accounts[], + AccountSubscribe, + AccountSubscribe_accounts +>(ACCOUNTS_QUERY, ACCOUNTS_SUB, update, getData, getDelta); diff --git a/libs/accounts/src/lib/accounts-manager.tsx b/libs/accounts/src/lib/accounts-manager.tsx new file mode 100644 index 000000000..7600e5660 --- /dev/null +++ b/libs/accounts/src/lib/accounts-manager.tsx @@ -0,0 +1,73 @@ +import { useRef, useCallback, useMemo } from 'react'; +import { produce } from 'immer'; +import merge from 'lodash/merge'; +import { AsyncRenderer } from '@vegaprotocol/ui-toolkit'; +import { useDataProvider, addSummaryRows } from '@vegaprotocol/react-helpers'; +import type { AccountSubscribe_accounts } from './__generated__/AccountSubscribe'; +import type { Accounts_party_accounts } from './__generated__/Accounts'; + +import type { AgGridReact } from 'ag-grid-react'; +import { + AccountsTable, + getGroupId, + getGroupSummaryRow, +} from './accounts-table'; +import { accountsDataProvider, getId } from './accounts-data-provider'; + +interface AccountsManagerProps { + partyId: string; +} + +export const AccountsManager = ({ partyId }: AccountsManagerProps) => { + const gridRef = useRef(null); + const variables = useMemo(() => ({ partyId }), [partyId]); + const update = useCallback( + (delta: AccountSubscribe_accounts) => { + const update: Accounts_party_accounts[] = []; + const add: Accounts_party_accounts[] = []; + if (!gridRef.current) { + return false; + } + const rowNode = gridRef.current.api.getRowNode(getId(delta)); + if (rowNode) { + const updatedData = produce( + rowNode.data, + (draft: Accounts_party_accounts) => { + merge(draft, delta); + } + ); + if (updatedData !== rowNode.data) { + update.push(updatedData); + } + } else { + add.push(delta); + } + if (update.length || add.length) { + gridRef.current.api.applyTransactionAsync({ + update, + add, + addIndex: 0, + }); + } + if (add.length) { + addSummaryRows( + gridRef.current.api, + gridRef.current.columnApi, + getGroupId, + getGroupSummaryRow + ); + } + return true; + }, + [gridRef] + ); + const { data, error, loading } = useDataProvider< + Accounts_party_accounts[], + AccountSubscribe_accounts + >(accountsDataProvider, update, variables); + return ( + + {(data) => } + + ); +}; diff --git a/libs/accounts/src/lib/accounts-table.spec.tsx b/libs/accounts/src/lib/accounts-table.spec.tsx new file mode 100644 index 000000000..2567b3727 --- /dev/null +++ b/libs/accounts/src/lib/accounts-table.spec.tsx @@ -0,0 +1,56 @@ +import AccountsTable from './accounts-table'; +import { act, render, screen } from '@testing-library/react'; +import type { Accounts_party_accounts } from './__generated__/Accounts'; +import { AccountType } from '@vegaprotocol/types'; + +const singleRow: Accounts_party_accounts = { + __typename: 'Account', + type: AccountType.Margin, + balance: '125600000', + market: { + __typename: 'Market', + name: 'BTCUSD Monthly (30 Jun 2022)', + id: '10cd0a793ad2887b340940337fa6d97a212e0e517fe8e9eab2b5ef3a38633f35', + }, + asset: { + __typename: 'Asset', + id: '5cfa87844724df6069b94e4c8a6f03af21907d7bc251593d08e4251043ee9f7c', + symbol: 'tBTC', + decimals: 5, + }, +}; +const singleRowData = [singleRow]; + +test('should render successfully', async () => { + await act(async () => { + const { baseElement } = render(); + expect(baseElement).toBeTruthy(); + }); +}); +test('Render correct columns', async () => { + await act(async () => { + render(); + }); + + const headers = screen.getAllByRole('columnheader'); + expect(headers).toHaveLength(4); + expect( + headers.map((h) => h.querySelector('[ref="eText"]')?.textContent?.trim()) + ).toEqual(['Asset', 'Type', 'Market', 'Balance']); +}); + +test('Correct formatting applied', async () => { + await act(async () => { + render(); + }); + const cells = screen.getAllByRole('gridcell'); + const expectedValues = [ + 'tBTC', + singleRow.type, + 'BTCUSD Monthly (30 Jun 2022)', + '1,256.00000', + ]; + cells.forEach((cell, i) => { + expect(cell).toHaveTextContent(expectedValues[i]); + }); +}); diff --git a/libs/accounts/src/lib/accounts-table.tsx b/libs/accounts/src/lib/accounts-table.tsx new file mode 100644 index 000000000..e713e8c4f --- /dev/null +++ b/libs/accounts/src/lib/accounts-table.tsx @@ -0,0 +1,147 @@ +import { forwardRef } from 'react'; +import type { ColumnApi, ValueFormatterParams } from 'ag-grid-community'; +import { + PriceCell, + formatNumber, + t, + addSummaryRows, +} from '@vegaprotocol/react-helpers'; +import type { SummaryRow } from '@vegaprotocol/react-helpers'; +import { AgGridDynamic as AgGrid } from '@vegaprotocol/ui-toolkit'; +import { AgGridColumn } from 'ag-grid-react'; +import type { AgGridReact } from 'ag-grid-react'; +import type { Accounts_party_accounts } from './__generated__/Accounts'; +import { getId as getRowNodeId } from './accounts-data-provider'; + +interface AccountsTableProps { + data: Accounts_party_accounts[] | null; +} + +interface AccountsTableValueFormatterParams extends ValueFormatterParams { + data: Accounts_party_accounts; +} + +export const getGroupId = ( + data: Accounts_party_accounts & SummaryRow, + columnApi: ColumnApi +) => { + if (data.__summaryRow) { + return null; + } + const sortColumnId = columnApi.getColumnState().find((c) => c.sort)?.colId; + switch (sortColumnId) { + case 'asset.symbol': + return data.asset.id; + } + return undefined; +}; + +export const getGroupSummaryRow = ( + data: Accounts_party_accounts[], + columnApi: ColumnApi +): Partial | null => { + if (!data.length) { + return null; + } + const sortColumnId = columnApi.getColumnState().find((c) => c.sort)?.colId; + switch (sortColumnId) { + case 'asset.symbol': + return { + __summaryRow: true, + balance: data + .reduce((a, i) => a + (parseFloat(i.balance) || 0), 0) + .toString(), + asset: data[0].asset, + }; + } + return null; +}; + +const comparator = ( + valueA: string, + valueB: string, + nodeA: { data: Accounts_party_accounts & SummaryRow }, + nodeB: { data: Accounts_party_accounts & SummaryRow }, + isInverted: boolean +) => { + if (valueA < valueB) { + return -1; + } + + if (valueA > valueB) { + return 1; + } + + if (nodeA.data.__summaryRow) { + return isInverted ? -1 : 1; + } + + if (nodeB.data.__summaryRow) { + return isInverted ? 1 : -1; + } + + return 0; +}; + +export const AccountsTable = forwardRef( + ({ data }, ref) => { + return ( + { + addSummaryRows(api, columnApi, getGroupId, getGroupSummaryRow); + }} + onGridReady={(event) => { + event.columnApi.applyColumnState({ + state: [ + { + colId: 'asset.symbol', + sort: 'asc', + }, + ], + }); + }} + > + + + + + formatNumber(value, data.asset.decimals) + } + /> + + ); + } +); + +export default AccountsTable; diff --git a/libs/accounts/src/setup-tests.ts b/libs/accounts/src/setup-tests.ts new file mode 100644 index 000000000..7b0828bfa --- /dev/null +++ b/libs/accounts/src/setup-tests.ts @@ -0,0 +1 @@ +import '@testing-library/jest-dom'; diff --git a/libs/accounts/tsconfig.json b/libs/accounts/tsconfig.json new file mode 100644 index 000000000..4c089585e --- /dev/null +++ b/libs/accounts/tsconfig.json @@ -0,0 +1,25 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "jsx": "react-jsx", + "allowJs": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/libs/accounts/tsconfig.lib.json b/libs/accounts/tsconfig.lib.json new file mode 100644 index 000000000..de828a9a8 --- /dev/null +++ b/libs/accounts/tsconfig.lib.json @@ -0,0 +1,22 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "types": ["node"] + }, + "files": [ + "../../node_modules/@nrwl/react/typings/cssmodule.d.ts", + "../../node_modules/@nrwl/next/typings/image.d.ts" + ], + "exclude": [ + "**/*.spec.ts", + "**/*.test.ts", + "**/*.spec.tsx", + "**/*.test.tsx", + "**/*.spec.js", + "**/*.test.js", + "**/*.spec.jsx", + "**/*.test.jsx" + ], + "include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"] +} diff --git a/libs/accounts/tsconfig.spec.json b/libs/accounts/tsconfig.spec.json new file mode 100644 index 000000000..0659641ca --- /dev/null +++ b/libs/accounts/tsconfig.spec.json @@ -0,0 +1,20 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "module": "commonjs", + "types": ["jest", "node", "@testing-library/jest-dom"] + }, + "include": [ + "**/*.test.ts", + "**/*.spec.ts", + "**/*.test.tsx", + "**/*.spec.tsx", + "**/*.test.js", + "**/*.spec.js", + "**/*.test.jsx", + "**/*.spec.jsx", + "**/*.d.ts", + "../react-helpers/src/lib/grid-cells/summary-row.spec.ts" + ] +} diff --git a/libs/deal-ticket/src/deal-ticket.spec.tsx b/libs/deal-ticket/src/deal-ticket.spec.tsx index 16b84a6f5..106b1000e 100644 --- a/libs/deal-ticket/src/deal-ticket.spec.tsx +++ b/libs/deal-ticket/src/deal-ticket.spec.tsx @@ -46,7 +46,8 @@ const transactionStatus = 'default'; function generateJsx() { return ( - + // eslint-disable-next-line @typescript-eslint/no-explicit-any + { [gridRef] ); const { data, error, loading } = useDataProvider< - Markets_markets, + Markets_markets[], Markets_markets_data >(marketsDataProvider, update); diff --git a/libs/market-list/src/lib/markets-data-provider.ts b/libs/market-list/src/lib/markets-data-provider.ts index 6ee73e94f..4d1e91210 100644 --- a/libs/market-list/src/lib/markets-data-provider.ts +++ b/libs/market-list/src/lib/markets-data-provider.ts @@ -69,7 +69,7 @@ const getDelta = (subscriptionData: MarketDataSub): MarketDataSub_marketData => export const marketsDataProvider = makeDataProvider< Markets, - Markets_markets, + Markets_markets[], MarketDataSub, MarketDataSub_marketData >(MARKETS_QUERY, MARKET_DATA_SUB, update, getData, getDelta); diff --git a/libs/positions/src/lib/positions-data-provider.ts b/libs/positions/src/lib/positions-data-provider.ts index d1d26a602..c811c2289 100644 --- a/libs/positions/src/lib/positions-data-provider.ts +++ b/libs/positions/src/lib/positions-data-provider.ts @@ -92,7 +92,7 @@ const getDelta = ( export const positionsDataProvider = makeDataProvider< Positions, - Positions_party_positions, + Positions_party_positions[], PositionSubscribe, PositionSubscribe_positions >(POSITION_QUERY, POSITIONS_SUB, update, getData, getDelta); diff --git a/libs/positions/src/lib/positions-manager.tsx b/libs/positions/src/lib/positions-manager.tsx index 5f299d981..70d057f91 100644 --- a/libs/positions/src/lib/positions-manager.tsx +++ b/libs/positions/src/lib/positions-manager.tsx @@ -50,7 +50,7 @@ export const PositionsManager = ({ partyId }: PositionsManagerProps) => { [gridRef] ); const { data, error, loading } = useDataProvider< - Positions_party_positions, + Positions_party_positions[], PositionSubscribe_positions >(positionsDataProvider, update, variables); return ( diff --git a/libs/react-helpers/src/hooks/use-apply-grid-transaction.spec.ts b/libs/react-helpers/src/hooks/use-apply-grid-transaction.spec.ts index e42f71e6c..e7c9249a9 100644 --- a/libs/react-helpers/src/hooks/use-apply-grid-transaction.spec.ts +++ b/libs/react-helpers/src/hooks/use-apply-grid-transaction.spec.ts @@ -24,7 +24,7 @@ function setup(items: Items, rowNodes: Items) { return undefined; }, }; - // eslint-disable-next-line + // eslint-disable-next-line @typescript-eslint/no-explicit-any renderHook(() => useApplyGridTransaction(items, gridApiMock as any)); return gridApiMock; } diff --git a/libs/react-helpers/src/hooks/use-data-provider.ts b/libs/react-helpers/src/hooks/use-data-provider.ts index dec3596a6..ec144a92e 100644 --- a/libs/react-helpers/src/hooks/use-data-provider.ts +++ b/libs/react-helpers/src/hooks/use-data-provider.ts @@ -9,7 +9,7 @@ export function useDataProvider( variables?: OperationVariables ) { const client = useApolloClient(); - const [data, setData] = useState(null); + const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(undefined); const initialized = useRef(false); diff --git a/libs/react-helpers/src/index.ts b/libs/react-helpers/src/index.ts index b003197c1..b876129db 100644 --- a/libs/react-helpers/src/index.ts +++ b/libs/react-helpers/src/index.ts @@ -1,6 +1,6 @@ export * from './lib/context'; export * from './lib/format'; -export * from './lib/grid-cells'; +export * from './lib/grid'; export * from './lib/storage'; export * from './lib/generic-data-provider'; export * from './lib/i18n'; diff --git a/libs/react-helpers/src/lib/generic-data-provider.ts b/libs/react-helpers/src/lib/generic-data-provider.ts index 1d8681645..04b65b4ff 100644 --- a/libs/react-helpers/src/lib/generic-data-provider.ts +++ b/libs/react-helpers/src/lib/generic-data-provider.ts @@ -12,7 +12,7 @@ import isEqual from 'lodash/isEqual'; export interface UpdateCallback { (arg: { - data: Data[] | null; + data: Data | null; error?: Error; loading: boolean; delta?: Delta; @@ -30,11 +30,11 @@ export interface Subscribe { type Query = DocumentNode | TypedDocumentNode; interface Update { - (draft: Draft[], delta: Delta): void; + (draft: Draft, delta: Delta): void; } interface GetData { - (subscriptionData: QueryData): Data[] | null; + (subscriptionData: QueryData): Data | null; } interface GetDelta { @@ -53,7 +53,7 @@ function makeDataProviderInternal( const updateQueue: Delta[] = []; let variables: OperationVariables | undefined = undefined; - let data: Data[] | null = null; + let data: Data | null = null; let error: Error | undefined = undefined; let loading = false; let client: ApolloClient | undefined = undefined; diff --git a/libs/react-helpers/src/lib/grid-cells/flash-cell.stories.tsx b/libs/react-helpers/src/lib/grid/flash-cell.stories.tsx similarity index 100% rename from libs/react-helpers/src/lib/grid-cells/flash-cell.stories.tsx rename to libs/react-helpers/src/lib/grid/flash-cell.stories.tsx diff --git a/libs/react-helpers/src/lib/grid-cells/flash-cell.test.tsx b/libs/react-helpers/src/lib/grid/flash-cell.test.tsx similarity index 92% rename from libs/react-helpers/src/lib/grid-cells/flash-cell.test.tsx rename to libs/react-helpers/src/lib/grid/flash-cell.test.tsx index 1b2ac4b6b..f5fe756c9 100644 --- a/libs/react-helpers/src/lib/grid-cells/flash-cell.test.tsx +++ b/libs/react-helpers/src/lib/grid/flash-cell.test.tsx @@ -10,7 +10,7 @@ describe('findFirstDiffPos', () => { it('Returns -1 if a string is undefined (just in case)', () => { const a = 'test'; - // eslint-disable-next-line + // eslint-disable-next-line @typescript-eslint/no-explicit-any const b = undefined as any as string; expect(findFirstDiffPos(a, b)).toEqual(-1); diff --git a/libs/react-helpers/src/lib/grid-cells/flash-cell.tsx b/libs/react-helpers/src/lib/grid/flash-cell.tsx similarity index 100% rename from libs/react-helpers/src/lib/grid-cells/flash-cell.tsx rename to libs/react-helpers/src/lib/grid/flash-cell.tsx diff --git a/libs/react-helpers/src/lib/grid-cells/index.tsx b/libs/react-helpers/src/lib/grid/index.tsx similarity index 65% rename from libs/react-helpers/src/lib/grid-cells/index.tsx rename to libs/react-helpers/src/lib/grid/index.tsx index aa17139a9..9c993e384 100644 --- a/libs/react-helpers/src/lib/grid-cells/index.tsx +++ b/libs/react-helpers/src/lib/grid/index.tsx @@ -1,2 +1,3 @@ export * from './flash-cell'; export * from './price-cell'; +export * from './summary-rows'; diff --git a/libs/react-helpers/src/lib/grid-cells/price-cell.test.tsx b/libs/react-helpers/src/lib/grid/price-cell.test.tsx similarity index 100% rename from libs/react-helpers/src/lib/grid-cells/price-cell.test.tsx rename to libs/react-helpers/src/lib/grid/price-cell.test.tsx diff --git a/libs/react-helpers/src/lib/grid-cells/price-cell.tsx b/libs/react-helpers/src/lib/grid/price-cell.tsx similarity index 100% rename from libs/react-helpers/src/lib/grid-cells/price-cell.tsx rename to libs/react-helpers/src/lib/grid/price-cell.tsx diff --git a/libs/react-helpers/src/lib/grid/summary-rows.spec.ts b/libs/react-helpers/src/lib/grid/summary-rows.spec.ts new file mode 100644 index 000000000..99cbf6277 --- /dev/null +++ b/libs/react-helpers/src/lib/grid/summary-rows.spec.ts @@ -0,0 +1,75 @@ +import type { GridApi, ColumnApi } from 'ag-grid-community'; +import { addSummaryRows } from './summary-rows'; + +type RowMock = { group: string; count: string }; + +const getGroupId = jest.fn(); +getGroupId.mockImplementation( + (data: { group: string; __summaryRow: boolean }) => + data.__summaryRow ? null : data.group +); +const getGroupSummaryRow = jest.fn(); +getGroupSummaryRow.mockImplementation( + (data: RowMock[]): Partial | null => { + if (!data.length) { + return null; + } + const row: Partial = { + count: data.reduce((a, c) => a + parseFloat(c.count), 0).toString(), + }; + return row; + } +); + +const api = { + forEachNodeAfterFilterAndSort: jest.fn(), + applyTransactionAsync: jest.fn(), +}; + +const columnsApi = {}; + +describe('addSummaryRows', () => { + it('should render search input and button', () => { + const nodes = [ + { data: { group: 'a', count: 10 } }, + { data: { group: 'a', count: 10, __summaryRow: true } }, + { data: { group: 'a', count: 20 } }, + { data: { group: 'b', count: 30 } }, + { data: { group: 'c', count: 40 } }, + { data: { group: 'c', count: 50 } }, + { data: { group: 'c', count: 60 } }, + { data: { group: 'd', count: 10, __summaryRow: true } }, + { data: { group: 'd', count: 70 } }, + { data: { group: 'd', count: 80 } }, + ]; + api.forEachNodeAfterFilterAndSort.mockImplementationOnce( + nodes.forEach.bind(nodes) + ); + addSummaryRows( + api as unknown as GridApi, + columnsApi as unknown as ColumnApi, + getGroupId, + getGroupSummaryRow + ); + expect(api.forEachNodeAfterFilterAndSort).toBeCalledTimes(1); + expect(api.applyTransactionAsync).toBeCalledTimes(5); + expect(api.applyTransactionAsync).toHaveBeenNthCalledWith(1, { + remove: [nodes[1].data], + }); + expect(api.applyTransactionAsync).toHaveBeenNthCalledWith(2, { + add: [{ count: '30' }], + addIndex: 2, + }); + expect(api.applyTransactionAsync).toHaveBeenNthCalledWith(3, { + remove: [nodes[7].data], + }); + expect(api.applyTransactionAsync).toHaveBeenNthCalledWith(4, { + add: [{ count: '150' }], + addIndex: 7, + }); + expect(api.applyTransactionAsync).toHaveBeenNthCalledWith(5, { + add: [{ count: '150' }], + addIndex: 10, + }); + }); +}); diff --git a/libs/react-helpers/src/lib/grid/summary-rows.ts b/libs/react-helpers/src/lib/grid/summary-rows.ts new file mode 100644 index 000000000..5f1f8e9a3 --- /dev/null +++ b/libs/react-helpers/src/lib/grid/summary-rows.ts @@ -0,0 +1,53 @@ +import type { ColumnApi, GridApi } from 'ag-grid-community'; + +export interface SummaryRow { + __summaryRow?: boolean; +} + +export function addSummaryRows( + api: GridApi, + columnApi: ColumnApi, + getGroupId: ( + data: T & SummaryRow, + columnApi: ColumnApi + ) => string | null | undefined, + getGroupSummaryRow: ( + data: (T & SummaryRow)[], + columnApi: ColumnApi + ) => Partial | null +) { + let currentGroupId: string | null | undefined = undefined; + let group: T[] = []; + let addIndex = 0; + api.forEachNodeAfterFilterAndSort((node) => { + const nodeGroupId = getGroupId(node.data, columnApi); + if (currentGroupId === undefined) { + currentGroupId = nodeGroupId; + } + if (node.data.__summaryRow) { + api.applyTransactionAsync({ + remove: [node.data], + }); + addIndex -= 1; + } else if (currentGroupId !== undefined && currentGroupId !== nodeGroupId) { + if (group.length > 1) { + api.applyTransactionAsync({ + add: [getGroupSummaryRow(group, columnApi)], + addIndex, + }); + addIndex += 1; + } + group = [node.data]; + currentGroupId = nodeGroupId; + } else if (currentGroupId) { + group.push(node.data); + } + addIndex += 1; + }); + if (group.length > 1) { + api.applyTransactionAsync({ + add: [getGroupSummaryRow(group, columnApi)], + addIndex, + }); + } +} diff --git a/tsconfig.base.json b/tsconfig.base.json index 884d84f05..6df97ad0e 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -15,6 +15,7 @@ "skipDefaultLibCheck": true, "baseUrl": ".", "paths": { + "@vegaprotocol/accounts": ["libs/accounts/src/index.ts"], "@vegaprotocol/cypress": ["libs/cypress/src/index.ts"], "@vegaprotocol/deal-ticket": ["libs/deal-ticket/src/index.ts"], "@vegaprotocol/deposits": ["libs/deposits/src/index.ts"], diff --git a/workspace.json b/workspace.json index 947bc4abc..858387239 100644 --- a/workspace.json +++ b/workspace.json @@ -1,6 +1,7 @@ { "version": 2, "projects": { + "accounts": "libs/accounts", "cypress": "libs/cypress", "deal-ticket": "libs/deal-ticket", "deposits": "libs/deposits",