chore: move hooks to react helpers

This commit is contained in:
Matthew Russell 2024-03-09 10:58:00 +00:00
parent 1d721dc748
commit 7b4c5c0fab
No known key found for this signature in database
25 changed files with 259 additions and 237 deletions

View File

@ -10,7 +10,7 @@ import {
addDecimalsFormatNumber,
formatNumber,
removePaginationWrapper,
suitableForSyntaxHighlighter,
validForSyntaxHighlighter,
} from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import { RouteTitle } from '../../components/route-title';
@ -134,7 +134,7 @@ export const NetworkParameterRow = ({
}: {
row: { key: string; value: string };
}) => {
const isSyntaxRow = suitableForSyntaxHighlighter(value);
const isSyntaxRow = validForSyntaxHighlighter(value);
useDocumentTitle(['Network Parameters']);
return (

View File

@ -1,7 +1,7 @@
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useForm } from 'react-hook-form';
import { suitableForSyntaxHighlighter } from '@vegaprotocol/utils';
import { validForSyntaxHighlighter } from '@vegaprotocol/utils';
import { useNetworkParams } from '@vegaprotocol/network-parameters';
import {
getClosingTimestamp,
@ -46,7 +46,7 @@ const SelectedNetworkParamCurrentValue = ({
<div className="mb-4">
<p className="text-sm text-white">{t('CurrentValue')}</p>
{suitableForSyntaxHighlighter(value) ? (
{validForSyntaxHighlighter(value) ? (
<SyntaxHighlighter data={JSON.parse(value)} />
) : (
<Input

View File

@ -8,7 +8,7 @@ import {
doesValueEquateToParam,
} from '@vegaprotocol/proposals';
import { useEnvironment, DocsLinks } from '@vegaprotocol/environment';
import { useValidateJson } from '@vegaprotocol/utils';
import { useValidateJson } from '@vegaprotocol/react-helpers';
import {
NetworkParams,
useNetworkParams,

View File

@ -7,7 +7,7 @@ import {
doesValueEquateToParam,
} from '@vegaprotocol/proposals';
import { useEnvironment, DocsLinks } from '@vegaprotocol/environment';
import { useValidateJson } from '@vegaprotocol/utils';
import { useValidateJson } from '@vegaprotocol/react-helpers';
import {
NetworkParams,
useNetworkParams,

View File

@ -14,7 +14,7 @@ import {
RoundedWrapper,
TextArea,
} from '@vegaprotocol/ui-toolkit';
import { useValidateJson } from '@vegaprotocol/utils';
import { useValidateJson } from '@vegaprotocol/react-helpers';
import {
NetworkParams,
useNetworkParams,

View File

@ -7,7 +7,7 @@ import {
doesValueEquateToParam,
} from '@vegaprotocol/proposals';
import { useEnvironment, DocsLinks } from '@vegaprotocol/environment';
import { useValidateJson } from '@vegaprotocol/utils';
import { useValidateJson } from '@vegaprotocol/react-helpers';
import {
NetworkParams,
useNetworkParams,

View File

@ -8,7 +8,7 @@ import {
useProposalSubmit,
} from '@vegaprotocol/proposals';
import { useEnvironment, DocsLinks } from '@vegaprotocol/environment';
import { useValidateJson } from '@vegaprotocol/utils';
import { useValidateJson } from '@vegaprotocol/react-helpers';
import {
NetworkParams,
useNetworkParams,

View File

@ -9,7 +9,7 @@ import {
VegaIcon,
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
import { URL_REGEX, isValidVegaPublicKey } from '@vegaprotocol/utils';
import { URL_REGEX, validVegaPublicKey } from '@vegaprotocol/utils';
import { type useReferralSetTransaction } from '../../lib/hooks/use-referral-set-transaction';
import { useT } from '../../lib/use-t';
@ -217,9 +217,7 @@ export const TeamForm = ({
validate: {
allowList: (value) => {
const publicKeys = parseAllowListText(value);
if (
publicKeys.every((pk) => isValidVegaPublicKey(pk))
) {
if (publicKeys.every((pk) => validVegaPublicKey(pk))) {
return true;
}
return t('Invalid public key found in allow list');

View File

@ -1,13 +1,15 @@
import sortBy from 'lodash/sortBy';
import {
useMaxSafe,
useRequired,
useVegaPublicKey,
addDecimal,
toBigNum,
removeDecimal,
addDecimalsFormatNumber,
} from '@vegaprotocol/utils';
import {
useMaxSafe,
useRequired,
useVegaPublicKey,
} from '@vegaprotocol/react-helpers';
import { useT } from './use-t';
import {
TradingFormGroup,

View File

@ -1,7 +1,8 @@
import { Controller, type Control } from 'react-hook-form';
import type { Market } from '@vegaprotocol/markets';
import type { OrderFormValues } from '../../hooks/use-form-values';
import { determinePriceStep, useValidateAmount } from '@vegaprotocol/utils';
import { determinePriceStep } from '@vegaprotocol/utils';
import { useValidateAmount } from '@vegaprotocol/react-helpers';
import {
TradingFormGroup,
Tooltip,

View File

@ -1,7 +1,7 @@
import { Controller, type Control } from 'react-hook-form';
import type { Market } from '@vegaprotocol/markets';
import type { OrderFormValues } from '../../hooks/use-form-values';
import { useValidateAmount } from '@vegaprotocol/utils';
import { useValidateAmount } from '@vegaprotocol/react-helpers';
import {
TradingFormGroup,
TradingInput,

View File

@ -8,8 +8,8 @@ import {
formatForInput,
formatValue,
removeDecimal,
useValidateAmount,
} from '@vegaprotocol/utils';
import { useValidateAmount } from '@vegaprotocol/react-helpers';
import { type Control, type UseFormWatch } from 'react-hook-form';
import { useForm, Controller, useController } from 'react-hook-form';
import * as Schema from '@vegaprotocol/types';

View File

@ -31,10 +31,10 @@ import { useOpenVolume } from '@vegaprotocol/positions';
import {
toBigNum,
removeDecimal,
useValidateAmount,
formatForInput,
formatValue,
} from '@vegaprotocol/utils';
import { useValidateAmount } from '@vegaprotocol/react-helpers';
import { activeOrdersProvider } from '@vegaprotocol/orders';
import {
getAsset,

View File

@ -1,16 +1,18 @@
import type { Asset, AssetFieldsFragment } from '@vegaprotocol/assets';
import { AssetOption } from '@vegaprotocol/assets';
import {
addDecimal,
isAssetTypeERC20,
formatNumber,
} from '@vegaprotocol/utils';
import {
useLocalStorage,
useEthereumAddress,
useRequired,
useVegaPublicKey,
useMinSafe,
useMaxSafe,
addDecimal,
isAssetTypeERC20,
formatNumber,
} from '@vegaprotocol/utils';
import { useLocalStorage } from '@vegaprotocol/react-helpers';
} from '@vegaprotocol/react-helpers';
import {
TradingFormGroup,
TradingInput,

View File

@ -3,10 +3,10 @@ import {
getDateTimeFormat,
addDecimal,
addDecimalsFormatNumber,
useValidateAmount,
determinePriceStep,
determineSizeStep,
} from '@vegaprotocol/utils';
import { useValidateAmount } from '@vegaprotocol/react-helpers';
import { Size } from '@vegaprotocol/datagrid';
import * as Schema from '@vegaprotocol/types';
import {

View File

@ -12,5 +12,6 @@ export * from './use-theme-switcher';
export * from './use-storybook-theme-observer';
export * from './use-yesterday';
export * from './use-previous';
export * from './use-validate';
export { useScript } from './use-script';
export { useUserAgent } from './use-user-agent';

View File

@ -0,0 +1,123 @@
import { useCallback } from 'react';
import BigNumber from 'bignumber.js';
import * as utils from '@vegaprotocol/utils';
// TODO: add i18n to react helpers
const useT = () => (str: string) => str;
export const useRequired = () => {
const t = useT();
return useCallback(
(value: string) => {
if (!utils.validRequired(value)) {
return t('Required');
}
return true;
},
[t]
);
};
export const useEthereumAddress = () => {
const t = useT();
return useCallback(
(value: string) => {
if (!utils.validEthAddress(value)) {
return t('Invalid Ethereum address');
}
return true;
},
[t]
);
};
export const useVegaPublicKey = () => {
const t = useT();
return useCallback(
(value: string) => {
if (!utils.validVegaPublicKey(value)) {
return t('Invalid Vega key');
}
return true;
},
[t]
);
};
export const useMinSafe = () => {
const t = useT();
return useCallback(
(min: BigNumber) => (value: string) => {
if (utils.validMinSafe(value, min)) {
return t('Value is below minimum');
}
return true;
},
[t]
);
};
export const useMaxSafe = () => {
const t = useT();
return useCallback(
(max: BigNumber) => (value: string) => {
if (utils.validMaxSafe(value, max)) {
return t('Value is above maximum');
}
return true;
},
[t]
);
};
export const useValidateJson = () => {
const t = useT();
return useCallback(
(value: string) => {
if (!utils.validJSON(value)) {
return t('Must be valid JSON');
}
return true;
},
[t]
);
};
export const useValidateUrl = () => {
const t = useT();
return useCallback(
(value: string) => {
if (!utils.validUrl(value)) {
return t('Invalid URL');
}
return true;
},
[t]
);
};
/** Used in deal ticket price/size amounts */
export const useValidateAmount = () => {
const t = useT();
return useCallback(
(step: number | string, field: string) => {
return (value?: string) => {
if (!utils.validStep(step, value)) {
if (new BigNumber(step).isEqualTo(1)) {
return t('{{field}} must be whole numbers for this market', {
field,
step,
});
}
return t('{{field}} must be a multiple of {{step}} for this market', {
field,
step,
});
}
return true;
};
},
[t]
);
};

View File

@ -1,6 +1,6 @@
{
"extends": ["../../.eslintrc.json"],
"ignorePatterns": ["!**/*", "__generated__"],
"ignorePatterns": ["!**/*"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
@ -9,16 +9,15 @@
"error",
{
"paths": [
"error",
"@apollo/client",
"@ethersproject",
"@vegaprotocol/data-provider",
"ag-grid-react",
"ag-grid-community",
"ethers",
"graphql",
"graphql-tag",
"graphql-ws",
"ethers",
"@ethersproject"
"graphql-ws"
],
"patterns": ["@sentry/*"]
}

View File

@ -1,40 +1,66 @@
import { renderHook } from '@testing-library/react';
import { useEthereumAddress, useVegaPublicKey } from './common';
it('ethereumAddress', () => {
const result = renderHook(useEthereumAddress);
const ethereumAddress = result.result.current;
const errorMessage = 'Invalid Ethereum address';
import { validEthAddress, validVegaPublicKey, validStep } from './common';
it('validEthAddress', () => {
const validAddress = '0x72c22822A19D20DE7e426fB84aa047399Ddd8853';
expect(ethereumAddress(validAddress)).toEqual(true);
expect(validEthAddress(validAddress)).toEqual(true);
const invalidChars = '0xzzc22822A19D20DE7e426fB84aa047399Ddd8853';
expect(ethereumAddress(invalidChars)).toEqual(errorMessage);
expect(validEthAddress(invalidChars)).toEqual(false);
const tooManyChars = '0x72c22822A19D20DE7e426fB84aa047399Ddd88531111111';
expect(ethereumAddress(tooManyChars)).toEqual(errorMessage);
expect(validEthAddress(tooManyChars)).toEqual(false);
const no0x = '1x72c22822A19D20DE7e426fB84aa047399Ddd8853';
expect(ethereumAddress(no0x)).toEqual(errorMessage);
expect(validEthAddress(no0x)).toEqual(false);
});
it('vegaPublicKey', () => {
const result = renderHook(useVegaPublicKey);
const vegaPublicKey = result.result.current;
const errorMessage = 'Invalid Vega key';
it('validVegaPublicKey', () => {
const validKey =
'70d14a321e02e71992fd115563df765000ccc4775cbe71a0e2f9ff5a3b9dc680';
expect(vegaPublicKey(validKey)).toEqual(true);
expect(validVegaPublicKey(validKey)).toEqual(true);
const invalidChars =
'zzz14a321e02e71992fd115563df765000ccc4775cbe71a0e2f9ff5a3b9dc680';
expect(vegaPublicKey(invalidChars)).toEqual(errorMessage);
expect(validVegaPublicKey(invalidChars)).toEqual(false);
const tooManyChars =
'70d14a321e02e71992fd115563df765000ccc4775cbe71a0e2f9ff5a3b9dc680111111';
expect(vegaPublicKey(tooManyChars)).toEqual(errorMessage);
expect(validVegaPublicKey(tooManyChars)).toEqual(false);
});
describe('validateAgainstStep', () => {
it('fails when step is an empty string', () => {
expect(validStep('', '1234')).toEqual(false);
});
it.each([
[0, 0],
[1234567890, 0],
[0.03, 0.03],
[0.09, 0.03],
[0.27, 0.03],
[1, 1],
[123, 1],
[4, 2],
[8, 2],
])(
'checks whether given value (%s) IS a multiple of given step (%s)',
(value, step) => {
expect(validStep(step, value)).toEqual(true);
}
);
it.each([
[1, 2],
[0.1, 0.003],
[1.11, 0.1],
[123.1, 1],
[222, 221],
[NaN, 1],
])(
'checks whether given value (%s) IS NOT a multiple of given step (%s)',
(value, step) => {
expect(validStep(step, value)).toEqual(false);
}
);
});

View File

@ -1,77 +1,52 @@
import BigNumber from 'bignumber.js';
import { useT } from '../use-t';
import { useCallback } from 'react';
export const useRequired = () => {
const t = useT();
return useCallback(
(value: string) => {
if (value === null || value === undefined || value === '') {
return t('Required');
}
return true;
},
[t]
);
};
export const useEthereumAddress = () => {
const t = useT();
return useCallback(
(value: string) => {
if (!/^0x[0-9a-fA-F]{40}$/i.test(value)) {
return t('Invalid Ethereum address');
}
return true;
},
[t]
);
export const validRequired = (value: string | number | undefined | null) => {
if (value === null || value === undefined || value === '') {
return false;
}
return true;
};
export const VEGA_ID_REGEX = /^[A-Fa-f0-9]{64}$/i;
export const isValidVegaPublicKey = (value: string) => {
export const validVegaPublicKey = (value: string) => {
return VEGA_ID_REGEX.test(value);
};
export const useVegaPublicKey = () => {
const t = useT();
return useCallback(
(value: string) => {
if (!isValidVegaPublicKey(value)) {
return t('Invalid Vega key');
}
return true;
},
[t]
);
export const URL_REGEX =
/^(https?:\/\/)?([a-zA-Z0-9.-]+(\.[a-zA-Z]{2,})+)(:[0-9]{1,5})?(\/[^\s]*)?$/;
export const validUrl = (value: string) => {
return URL_REGEX.test(value);
};
export const useMinSafe = () => {
const t = useT();
return useCallback(
(min: BigNumber) => (value: string) => {
if (new BigNumber(value).isLessThan(min)) {
return t('Value is below minimum');
}
return true;
},
[t]
);
export const ETH_ADDRESS = /^0x[0-9a-fA-F]{40}$/i;
export const validEthAddress = (value: string) => {
return ETH_ADDRESS.test(value);
};
export const useMaxSafe = () => {
const t = useT();
return useCallback(
(max: BigNumber) => (value: string) => {
if (new BigNumber(value).isGreaterThan(max)) {
return t('Value is above maximum');
}
return true;
},
[t]
);
export const validMinSafe = (
value: string | number | BigNumber,
min: string | number | BigNumber
) => {
return new BigNumber(value).isLessThan(min);
};
export const suitableForSyntaxHighlighter = (str: string) => {
export const validMaxSafe = (
value: string | number | BigNumber,
max: string | number | BigNumber
) => {
return new BigNumber(value).isGreaterThan(max);
};
export const validJSON = (value: string) => {
try {
JSON.parse(value);
return true;
} catch (e) {
return false;
}
};
export const validForSyntaxHighlighter = (str: string) => {
try {
const test = JSON.parse(str);
return test && Object.keys(test).length > 0;
@ -80,35 +55,17 @@ export const suitableForSyntaxHighlighter = (str: string) => {
}
};
export const useValidateJson = () => {
const t = useT();
return useCallback(
(value: string) => {
try {
JSON.parse(value);
return true;
} catch (e) {
return t('Must be valid JSON');
}
},
[t]
);
};
export const validStep = (step: string | number, input?: string | number) => {
const stepValue = new BigNumber(step);
if (stepValue.isNaN()) {
// unable to check if step is not a number
return false;
}
if (stepValue.isZero()) {
// every number is valid when step is zero
return true;
}
export const URL_REGEX =
/^(https?:\/\/)?([a-zA-Z0-9.-]+(\.[a-zA-Z]{2,})+)(:[0-9]{1,5})?(\/[^\s]*)?$/;
const isValidUrl = (value: string) => {
return URL_REGEX.test(value);
};
export const useValidateUrl = () => {
const t = useT();
return useCallback(
(value: string) => {
if (!isValidUrl(value)) {
return t('Invalid URL');
}
return true;
},
[t]
);
const value = new BigNumber(input || '');
return value.modulo(stepValue).isZero();
};

View File

@ -1,2 +1 @@
export * from './common';
export * from './validate-amount';

View File

@ -1,38 +0,0 @@
import { validateAgainstStep } from './validate-amount';
describe('validateAgainstStep', () => {
it('fails when step is an empty string', () => {
expect(validateAgainstStep('', '1234')).toEqual(false);
});
it.each([
[0, 0],
[1234567890, 0],
[0.03, 0.03],
[0.09, 0.03],
[0.27, 0.03],
[1, 1],
[123, 1],
[4, 2],
[8, 2],
])(
'checks whether given value (%s) IS a multiple of given step (%s)',
(value, step) => {
expect(validateAgainstStep(step, value)).toEqual(true);
}
);
it.each([
[1, 2],
[0.1, 0.003],
[1.11, 0.1],
[123.1, 1],
[222, 221],
[NaN, 1],
])(
'checks whether given value (%s) IS NOT a multiple of given step (%s)',
(value, step) => {
expect(validateAgainstStep(step, value)).toEqual(false);
}
);
});

View File

@ -1,50 +0,0 @@
import { useCallback } from 'react';
import { useT } from '../use-t';
import BigNumber from 'bignumber.js';
export const useValidateAmount = () => {
const t = useT();
return useCallback(
(step: number | string, field: string) => {
return (value?: string) => {
const isValid = validateAgainstStep(step, value);
if (!isValid) {
if (new BigNumber(step).isEqualTo(1)) {
return t('{{field}} must be whole numbers for this market', {
field,
step,
});
}
return t('{{field}} must be a multiple of {{step}} for this market', {
field,
step,
});
}
return true;
};
},
[t]
);
};
const isMultipleOf = (value: BigNumber, multipleOf: BigNumber) =>
value.modulo(multipleOf).isZero();
export const validateAgainstStep = (
step: string | number,
input?: string | number
) => {
const stepValue = new BigNumber(step);
if (stepValue.isNaN()) {
// unable to check if step is not a number
return false;
}
if (stepValue.isZero()) {
// every number is valid when step is zero
return true;
}
const value = new BigNumber(input || '');
return isMultipleOf(value, stepValue);
};

View File

@ -1,6 +1,6 @@
import { type StoreApi } from 'zustand';
import { type Store, type Connector } from '../types';
import { isValidVegaPublicKey } from '@vegaprotocol/utils';
import { validVegaPublicKey } from '@vegaprotocol/utils';
import {
ConnectorError,
chainIdError,
@ -40,7 +40,7 @@ export class ViewPartyConnector implements Connector {
throw userRejectedError();
}
if (!isValidVegaPublicKey(value)) {
if (!validVegaPublicKey(value)) {
throw connectError('invalid public key');
}

View File

@ -1,13 +1,15 @@
import type { Asset } from '@vegaprotocol/assets';
import { AssetOption } from '@vegaprotocol/assets';
import {
useEthereumAddress,
useRequired,
useMinSafe,
removeDecimal,
isAssetTypeERC20,
formatNumber,
} from '@vegaprotocol/utils';
import {
useEthereumAddress,
useRequired,
useMinSafe,
} from '@vegaprotocol/react-helpers';
import { useLocalStorage } from '@vegaprotocol/react-helpers';
import {
TradingFormGroup,