diff --git a/e2e-tests/popup/buy-cspr/buy-cspr.spec.ts b/e2e-tests/popup/buy-cspr/buy-cspr.spec.ts index 6cd24a872..e10eec6fb 100644 --- a/e2e-tests/popup/buy-cspr/buy-cspr.spec.ts +++ b/e2e-tests/popup/buy-cspr/buy-cspr.spec.ts @@ -23,13 +23,9 @@ popup.describe('Popup UI: buy cspr', () => { await popupPage.getByRole('button', { name: 'Next' }).click(); await popupExpect( - popupPage.getByRole('heading', { name: 'Pick provider' }) + popupPage.getByRole('heading', { name: 'Review provider option' }) ).toBeVisible(); - await popupExpect( - popupPage.getByRole('button', { name: 'Confirm' }) - ).toBeDisabled(); - await popupPage.getByText('Topper by Uphold').click(); await popupExpect( @@ -47,7 +43,7 @@ popup.describe('Popup UI: buy cspr', () => { } ); - popup( + popup.skip( 'should redirect to Ramp provider page', async ({ popupPage, unlockVault, context }) => { await unlockVault(); diff --git a/e2e-tests/popup/stakes/redelagation.spec.ts b/e2e-tests/popup/stakes/redelagation.spec.ts index dc7b53f52..3c2b7fecb 100644 --- a/e2e-tests/popup/stakes/redelagation.spec.ts +++ b/e2e-tests/popup/stakes/redelagation.spec.ts @@ -38,6 +38,10 @@ popup.describe('Popup UI: Redelegation', () => { await popupPage.getByPlaceholder('0.00', { exact: true }).fill('500'); + await popupExpect( + popupPage.getByRole('button', { name: 'Next' }) + ).not.toBeDisabled(); + await popupPage.getByRole('button', { name: 'Next' }).click(); await popupPage.getByText('Delegate', { exact: true }).click(); diff --git a/package-lock.json b/package-lock.json index 295f27d2a..e942802b4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -49,6 +49,7 @@ "react-query": "^3.39.3", "react-redux": "8.0.5", "react-router-dom": "6.16.0", + "react-virtualized": "^9.22.5", "redux": "4.2.1", "redux-saga": "1.2.3", "reselect": "4.1.7", @@ -79,6 +80,7 @@ "@types/node": "^20.9.0", "@types/react": "^18.2.55", "@types/react-dom": "^18.2.18", + "@types/react-virtualized": "^9.21.30", "@types/styled-components": "^5.1.26", "babel-eslint": "^10.1.0", "babel-loader": "9.1.0", @@ -7409,6 +7411,16 @@ "@types/react": "*" } }, + "node_modules/@types/react-virtualized": { + "version": "9.21.30", + "resolved": "https://registry.npmjs.org/@types/react-virtualized/-/react-virtualized-9.21.30.tgz", + "integrity": "sha512-4l2TFLQ8BCjNDQlvH85tU6gctuZoEdgYzENQyZHpgTHU7hoLzYgPSOALMAeA58LOWua8AzC6wBivPj1lfl6JgQ==", + "dev": true, + "dependencies": { + "@types/prop-types": "*", + "@types/react": "*" + } + }, "node_modules/@types/responselike": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", @@ -11079,6 +11091,14 @@ "safe-buffer": "~5.1.0" } }, + "node_modules/clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "engines": { + "node": ">=6" + } + }, "node_modules/co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -13023,7 +13043,6 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", - "dev": true, "dependencies": { "@babel/runtime": "^7.8.7", "csstype": "^3.0.2" @@ -24192,6 +24211,11 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, + "node_modules/react-lifecycles-compat": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", + "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" + }, "node_modules/react-loading-skeleton": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/react-loading-skeleton/-/react-loading-skeleton-3.3.1.tgz", @@ -24351,6 +24375,23 @@ "react-dom": ">=16.6.0" } }, + "node_modules/react-virtualized": { + "version": "9.22.5", + "resolved": "https://registry.npmjs.org/react-virtualized/-/react-virtualized-9.22.5.tgz", + "integrity": "sha512-YqQMRzlVANBv1L/7r63OHa2b0ZsAaDp1UhVNEdUaXI8A5u6hTpA5NYtUueLH2rFuY/27mTGIBl7ZhqFKzw18YQ==", + "dependencies": { + "@babel/runtime": "^7.7.2", + "clsx": "^1.0.4", + "dom-helpers": "^5.1.3", + "loose-envify": "^1.4.0", + "prop-types": "^15.7.2", + "react-lifecycles-compat": "^3.0.4" + }, + "peerDependencies": { + "react": "^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0", + "react-dom": "^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0" + } + }, "node_modules/readable-stream": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", @@ -34938,6 +34979,16 @@ "@types/react": "*" } }, + "@types/react-virtualized": { + "version": "9.21.30", + "resolved": "https://registry.npmjs.org/@types/react-virtualized/-/react-virtualized-9.21.30.tgz", + "integrity": "sha512-4l2TFLQ8BCjNDQlvH85tU6gctuZoEdgYzENQyZHpgTHU7hoLzYgPSOALMAeA58LOWua8AzC6wBivPj1lfl6JgQ==", + "dev": true, + "requires": { + "@types/prop-types": "*", + "@types/react": "*" + } + }, "@types/responselike": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", @@ -37727,6 +37778,11 @@ } } }, + "clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==" + }, "co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -39181,7 +39237,6 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", - "dev": true, "requires": { "@babel/runtime": "^7.8.7", "csstype": "^3.0.2" @@ -47526,6 +47581,11 @@ "react-base16-styling": "^0.10.0" } }, + "react-lifecycles-compat": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", + "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" + }, "react-loading-skeleton": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/react-loading-skeleton/-/react-loading-skeleton-3.3.1.tgz", @@ -47621,6 +47681,19 @@ "prop-types": "^15.6.2" } }, + "react-virtualized": { + "version": "9.22.5", + "resolved": "https://registry.npmjs.org/react-virtualized/-/react-virtualized-9.22.5.tgz", + "integrity": "sha512-YqQMRzlVANBv1L/7r63OHa2b0ZsAaDp1UhVNEdUaXI8A5u6hTpA5NYtUueLH2rFuY/27mTGIBl7ZhqFKzw18YQ==", + "requires": { + "@babel/runtime": "^7.7.2", + "clsx": "^1.0.4", + "dom-helpers": "^5.1.3", + "loose-envify": "^1.4.0", + "prop-types": "^15.7.2", + "react-lifecycles-compat": "^3.0.4" + } + }, "readable-stream": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", diff --git a/package.json b/package.json index 52825b703..051438944 100644 --- a/package.json +++ b/package.json @@ -93,6 +93,7 @@ "react-query": "^3.39.3", "react-redux": "8.0.5", "react-router-dom": "6.16.0", + "react-virtualized": "^9.22.5", "redux": "4.2.1", "redux-saga": "1.2.3", "reselect": "4.1.7", @@ -123,6 +124,7 @@ "@types/node": "^20.9.0", "@types/react": "^18.2.55", "@types/react-dom": "^18.2.18", + "@types/react-virtualized": "^9.21.30", "@types/styled-components": "^5.1.26", "babel-eslint": "^10.1.0", "babel-loader": "9.1.0", diff --git a/src/apps/popup/pages/create-account/content.tsx b/src/apps/popup/pages/create-account/content.tsx index 16248ad3b..de700fd6a 100644 --- a/src/apps/popup/pages/create-account/content.tsx +++ b/src/apps/popup/pages/create-account/content.tsx @@ -52,7 +52,6 @@ export function CreateAccountPageContent({ {...register('name')} error={!!errorMessage} validationText={errorMessage} - autoComplete="off" autoFocus /> diff --git a/src/apps/popup/pages/stakes/components/validator-list.tsx b/src/apps/popup/pages/stakes/components/validator-list.tsx new file mode 100644 index 000000000..a6741425b --- /dev/null +++ b/src/apps/popup/pages/stakes/components/validator-list.tsx @@ -0,0 +1,83 @@ +import React from 'react'; +import { Trans, useTranslation } from 'react-i18next'; +import AutoSizer from 'react-virtualized/dist/commonjs/AutoSizer'; +import List from 'react-virtualized/dist/commonjs/List'; +import styled from 'styled-components'; + +import { + DropdownHeader, + SpacingSize, + VerticalSpaceContainer +} from '@libs/layout'; +import { ValidatorResultWithId } from '@libs/services/validators-service'; +import { Tile, Typography, ValidatorPlate } from '@libs/ui/components'; + +interface ValidatorListProps { + filteredValidatorsList: ValidatorResultWithId[]; + handleValidatorClick: (validator: ValidatorResultWithId) => void; + totalStake: 'total_stake' | 'user_stake'; +} + +const Container = styled.div``; + +export const ValidatorList = ({ + filteredValidatorsList, + handleValidatorClick, + totalStake +}: ValidatorListProps) => { + const { t } = useTranslation(); + + return ( + + + + + Validator + + + Total stake, fee, delegators + + + + {({ width }) => ( + { + const validator = filteredValidatorsList[index]; + const logo = + validator?.account_info?.info?.owner?.branding?.logo?.svg || + validator?.account_info?.info?.owner?.branding?.logo + ?.png_256 || + validator?.account_info?.info?.owner?.branding?.logo + ?.png_1024; + + return ( + + { + handleValidatorClick(validator); + }} + withBorder + /> + + ); + }} + /> + )} + + + + ); +}; diff --git a/src/apps/popup/pages/stakes/redelegate-validator-dropdown-input.tsx b/src/apps/popup/pages/stakes/redelegate-validator-dropdown-input.tsx index 3cba5a4c4..03648693f 100644 --- a/src/apps/popup/pages/stakes/redelegate-validator-dropdown-input.tsx +++ b/src/apps/popup/pages/stakes/redelegate-validator-dropdown-input.tsx @@ -1,25 +1,19 @@ import React, { useEffect, useState } from 'react'; import { UseFormReturn, useWatch } from 'react-hook-form'; -import { Trans, useTranslation } from 'react-i18next'; +import { useTranslation } from 'react-i18next'; +import { ValidatorList } from '@popup/pages/stakes/components/validator-list'; import { useFilteredValidators } from '@popup/pages/stakes/utils'; import { useClickAway } from '@hooks/use-click-away'; import { AlignedFlexRow, - DropdownHeader, SpacingSize, VerticalSpaceContainer } from '@libs/layout'; import { ValidatorResultWithId } from '@libs/services/validators-service/types'; -import { - Input, - List, - SvgIcon, - Typography, - ValidatorPlate -} from '@libs/ui/components'; +import { Input, SvgIcon, ValidatorPlate } from '@libs/ui/components'; import { StakeNewValidatorFormValues } from '@libs/ui/forms/stakes-form'; interface ValidatorDropdownInputProps { @@ -96,6 +90,16 @@ export const RedelegateValidatorDropdownInput = ({ validatorList ); + const handleValidatorClick = (validator: ValidatorResultWithId) => { + setValue('newValidatorPublicKey', validator.public_key); + setStakeAmount(validator.user_stake!); + + setValidator(validator); + + setIsOpenValidatorPublicKeysList(false); + setShowValidatorPlate(true); + }; + return showValidatorPlate && validator ? ( {isOpenValidatorPublicKeysList && ( - ( - - - Validator - - - Total stake, fee, delegators - - - )} - renderRow={validator => { - const logo = - validator?.account_info?.info?.owner?.branding?.logo?.svg || - validator?.account_info?.info?.owner?.branding?.logo?.png_256 || - validator?.account_info?.info?.owner?.branding?.logo?.png_1024; - - return ( - { - setValue('newValidatorPublicKey', validator.public_key); - setStakeAmount(validator.user_stake!); - - setValidator(validator); - - setIsOpenValidatorPublicKeysList(false); - setShowValidatorPlate(true); - }} - /> - ); - }} - marginLeftForItemSeparatorLine={56} - marginLeftForHeaderSeparatorLine={0} + )} diff --git a/src/apps/popup/pages/stakes/utils.ts b/src/apps/popup/pages/stakes/utils.ts index c7662d42b..cb011e327 100644 --- a/src/apps/popup/pages/stakes/utils.ts +++ b/src/apps/popup/pages/stakes/utils.ts @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useSelector } from 'react-redux'; @@ -64,30 +64,22 @@ export const useFilteredValidators = ( inputValue: string | undefined, validatorList: ValidatorResultWithId[] | null ) => { - const filterValidators = useCallback( - ( - inputValue: string | undefined, - validatorList: ValidatorResultWithId[] | null - ): ValidatorResultWithId[] | [] => { - if (!validatorList) return []; - if (!inputValue) return validatorList; - - const lowerCaseInput = inputValue.toLowerCase(); - - const isIncluded = (stringToCheck: string | undefined) => - stringToCheck?.toLowerCase().includes(lowerCaseInput); - - return validatorList.filter(validator => { - const { public_key } = validator; - const ownerName = validator?.account_info?.info?.owner?.name; - - return isIncluded(ownerName) || isIncluded(public_key); - }); - }, - [] - ); + return useMemo(() => { + if (!validatorList) return []; + if (!inputValue) return validatorList; + + const lowerCaseInput = inputValue.toLowerCase(); + + const isIncluded = (stringToCheck: string | undefined) => + stringToCheck?.toLowerCase().includes(lowerCaseInput); - return filterValidators(inputValue, validatorList); + return validatorList.filter(validator => { + const { public_key } = validator; + const ownerName = validator?.account_info?.info?.owner?.name; + + return isIncluded(ownerName) || isIncluded(public_key); + }); + }, [inputValue, validatorList]); }; export const useStakeActionTexts = ( diff --git a/src/apps/popup/pages/stakes/validator-dropdown-input.tsx b/src/apps/popup/pages/stakes/validator-dropdown-input.tsx index 8b28643b0..f7b043706 100644 --- a/src/apps/popup/pages/stakes/validator-dropdown-input.tsx +++ b/src/apps/popup/pages/stakes/validator-dropdown-input.tsx @@ -1,28 +1,21 @@ import React, { useEffect, useState } from 'react'; import { UseFormReturn, useWatch } from 'react-hook-form'; -import { Trans, useTranslation } from 'react-i18next'; +import { useTranslation } from 'react-i18next'; import { AuctionManagerEntryPoint } from '@src/constants'; +import { ValidatorList } from '@popup/pages/stakes/components/validator-list'; import { useFilteredValidators } from '@popup/pages/stakes/utils'; import { useClickAway } from '@hooks/use-click-away'; import { AlignedFlexRow, - DropdownHeader, SpacingSize, VerticalSpaceContainer } from '@libs/layout'; import { ValidatorResultWithId } from '@libs/services/validators-service/types'; -import { - Input, - List, - Spinner, - SvgIcon, - Typography, - ValidatorPlate -} from '@libs/ui/components'; +import { Input, Spinner, SvgIcon, ValidatorPlate } from '@libs/ui/components'; import { StakeValidatorFormValues } from '@libs/ui/forms/stakes-form'; interface ValidatorDropdownInputProps { @@ -118,6 +111,16 @@ export const ValidatorDropdownInput = ({ } }, [stakeType]); + const handleValidatorClick = (validator: ValidatorResultWithId) => { + setValue('validatorPublicKey', validator.public_key); + setStakeAmount(validator.user_stake!); + + setValidator(validator); + + setIsOpenValidatorPublicKeysList(false); + setShowValidatorPlate(true); + }; + return showValidatorPlate && validator ? ( {loading && } {isOpenValidatorPublicKeysList && !loading && ( - ( - - - Validator - - - Total stake, fee, delegators - - - )} - renderRow={validator => { - const logo = - validator?.account_info?.info?.owner?.branding?.logo?.svg || - validator?.account_info?.info?.owner?.branding?.logo?.png_256 || - validator?.account_info?.info?.owner?.branding?.logo?.png_1024; - - return ( - { - setValue('validatorPublicKey', validator.public_key); - setStakeAmount(validator.user_stake!); - - setValidator(validator); - - setIsOpenValidatorPublicKeysList(false); - setShowValidatorPlate(true); - }} - /> - ); - }} - marginLeftForItemSeparatorLine={56} - marginLeftForHeaderSeparatorLine={0} + )} diff --git a/src/libs/layout/containers.ts b/src/libs/layout/containers.ts index b1cd25ec4..aa0b0870d 100644 --- a/src/libs/layout/containers.ts +++ b/src/libs/layout/containers.ts @@ -332,6 +332,7 @@ export const DropdownHeader = styled(AlignedSpaceBetweenFlexRow)` border-top-left-radius: ${({ theme }) => theme.borderRadius.base}px; border-top-right-radius: ${({ theme }) => theme.borderRadius.base}px; + border-bottom: ${({ theme }) => `0.5px solid ${theme.color.borderPrimary}`}; background-color: ${({ theme }) => theme.color.backgroundPrimary}; `; diff --git a/src/libs/ui/components/input/input.tsx b/src/libs/ui/components/input/input.tsx index 0f1ec2851..638deaa77 100644 --- a/src/libs/ui/components/input/input.tsx +++ b/src/libs/ui/components/input/input.tsx @@ -167,7 +167,7 @@ export const Input = forwardRef(function Input( onFocus, dataTestId, readOnly, - autoComplete, + autoComplete = 'off', secondaryBackground, autoFocus, ...restProps diff --git a/src/libs/ui/components/list/list.tsx b/src/libs/ui/components/list/list.tsx index 4ff895b55..14e1db7c7 100644 --- a/src/libs/ui/components/list/list.tsx +++ b/src/libs/ui/components/list/list.tsx @@ -34,7 +34,7 @@ const FlexColumn = styled.div` flex-direction: column; `; -export const RowsContainer = styled.div` +const RowsContainer = styled.div` & > * + *:before { ${borderBottomPseudoElementRules}; } diff --git a/src/libs/ui/components/recipient-dropdown-input/recipient-dropdown-input.tsx b/src/libs/ui/components/recipient-dropdown-input/recipient-dropdown-input.tsx index fff364a61..b41742f0a 100644 --- a/src/libs/ui/components/recipient-dropdown-input/recipient-dropdown-input.tsx +++ b/src/libs/ui/components/recipient-dropdown-input/recipient-dropdown-input.tsx @@ -169,7 +169,6 @@ export const RecipientDropdownInput = ({ {...register('recipientPublicKey')} error={!!errors?.recipientPublicKey} validationText={errors?.recipientPublicKey?.message} - autoComplete="off" /> {isOpenRecentRecipientPublicKeysList && optionsRow.length ? ( diff --git a/src/libs/ui/components/validator-plate/validator-plate.tsx b/src/libs/ui/components/validator-plate/validator-plate.tsx index f68b6d88b..96b7c0a2f 100644 --- a/src/libs/ui/components/validator-plate/validator-plate.tsx +++ b/src/libs/ui/components/validator-plate/validator-plate.tsx @@ -23,16 +23,24 @@ import { } from '@libs/ui/components'; import { formatNumber, motesToCSPR } from '@libs/ui/utils/formatters'; -const ValidatorPlateContainer = styled(AlignedSpaceBetweenFlexRow)<{ +const ValidatorPlateContainer = styled(AlignedFlexRow)<{ onClick?: () => void; withBackground?: boolean; + withBorder?: boolean; }>` cursor: ${({ onClick }) => (onClick ? 'pointer' : 'initial')}; background: ${({ withBackground, theme }) => withBackground ? theme.color.backgroundPrimary : 'transparent'}; border-radius: ${({ theme }) => theme.borderRadius.base}px; + padding: ${({ withBorder }) => (withBorder ? '11px 0 0 16px' : '11px 16px')}; +`; - padding: 8px 16px; +const InfoContainer = styled(AlignedSpaceBetweenFlexRow)<{ + withBorder?: boolean; +}>` + border-bottom: ${({ withBorder, theme }) => + withBorder ? `0.5px solid ${theme.color.borderPrimary}` : 'none'}; + padding: ${({ withBorder }) => (withBorder ? '0 16px 11px 0' : '0')}; `; const NameContainer = styled(FlexColumn)` @@ -59,6 +67,7 @@ interface ValidatorPlateProps { validatorLabel?: string; error?: FieldError; totalStake?: string; + withBorder?: boolean; } export const ValidatorPlate = ({ @@ -71,7 +80,8 @@ export const ValidatorPlate = ({ delegatorsNumber, validatorLabel, error, - totalStake + totalStake, + withBorder }: ValidatorPlateProps) => { const [formattedTotalStake, setFormattedTotalStake] = useState(''); @@ -107,7 +117,7 @@ export const ValidatorPlate = ({ )} - + - + {name} @@ -129,14 +139,16 @@ export const ValidatorPlate = ({ - - {logoUrl ? ( - {name} - ) : ( - - )} - + {logoUrl ? ( + {name} + ) : ( + + )} + + - + {name} - - - - {`${formattedTotalStake} CSPR`} - - - - {`${formattedFee}% fee`} - - - {getFormattedDelegatorsNumber()} delegators + + + {`${formattedTotalStake} CSPR`} - - + + + {`${formattedFee}% fee`} + + + {getFormattedDelegatorsNumber()} delegators + + + + );