Merge branch 'main' of github.com:blockscout/frontend
@@ -20,7 +20,17 @@ export const base2: Blob = {
|
||||
],
|
||||
};
|
||||
|
||||
export const withoutData: Blob = {
|
||||
blob_data: null,
|
||||
hash: '0x0197fdb17195c176b23160f335daabd4b6a231aaaadd73ec567877c66a3affd3',
|
||||
kzg_commitment: null,
|
||||
kzg_proof: null,
|
||||
transaction_hashes: [
|
||||
{ block_consensus: true, transaction_hash: '0x22d597ebcf3e8d60096dd0363bc2f0f5e2df27ba1dacd696c51aa7c9409f3193' },
|
||||
],
|
||||
};
|
||||
|
||||
export const txBlobs: TxBlobs = {
|
||||
items: [ base1, base2 ],
|
||||
items: [ base1, base2, withoutData ],
|
||||
next_page_params: null,
|
||||
};
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
export interface TxBlob {
|
||||
hash: string;
|
||||
blob_data: string;
|
||||
kzg_commitment: string;
|
||||
kzg_proof: string;
|
||||
blob_data: string | null;
|
||||
kzg_commitment: string | null;
|
||||
kzg_proof: string | null;
|
||||
}
|
||||
|
||||
export type TxBlobs = {
|
||||
|
||||
@@ -10,6 +10,7 @@ import ClearButton from 'ui/shared/ClearButton';
|
||||
import ContractMethodFieldLabel from './ContractMethodFieldLabel';
|
||||
import ContractMethodMultiplyButton from './ContractMethodMultiplyButton';
|
||||
import useArgTypeMatchInt from './useArgTypeMatchInt';
|
||||
import useFormatFieldValue from './useFormatFieldValue';
|
||||
import useValidateField from './useValidateField';
|
||||
|
||||
interface Props {
|
||||
@@ -29,15 +30,22 @@ const ContractMethodFieldInput = ({ data, hideLabel, path: name, className, isDi
|
||||
|
||||
const argTypeMatchInt = useArgTypeMatchInt({ argType: data.type });
|
||||
const validate = useValidateField({ isOptional, argType: data.type, argTypeMatchInt });
|
||||
const format = useFormatFieldValue({ argType: data.type, argTypeMatchInt });
|
||||
|
||||
const { control, setValue, getValues } = useFormContext();
|
||||
const { field, fieldState } = useController({ control, name, rules: { validate, required: isOptional ? false : 'Field is required' } });
|
||||
const { field, fieldState } = useController({ control, name, rules: { validate } });
|
||||
|
||||
const inputBgColor = useColorModeValue('white', 'black');
|
||||
const nativeCoinRowBgColor = useColorModeValue('gray.100', 'gray.700');
|
||||
|
||||
const hasMultiplyButton = argTypeMatchInt && Number(argTypeMatchInt.power) >= 64;
|
||||
|
||||
const handleChange = React.useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const formattedValue = format(event.target.value);
|
||||
field.onChange(formattedValue); // data send back to hook form
|
||||
setValue(name, formattedValue); // UI state
|
||||
}, [ field, name, setValue, format ]);
|
||||
|
||||
const handleClear = React.useCallback(() => {
|
||||
setValue(name, '');
|
||||
ref.current?.focus();
|
||||
@@ -46,9 +54,9 @@ const ContractMethodFieldInput = ({ data, hideLabel, path: name, className, isDi
|
||||
const handleMultiplyButtonClick = React.useCallback((power: number) => {
|
||||
const zeroes = Array(power).fill('0').join('');
|
||||
const value = getValues(name);
|
||||
const newValue = value ? value + zeroes : '1' + zeroes;
|
||||
const newValue = format(value ? value + zeroes : '1' + zeroes);
|
||||
setValue(name, newValue);
|
||||
}, [ getValues, name, setValue ]);
|
||||
}, [ format, getValues, name, setValue ]);
|
||||
|
||||
const error = fieldState.error;
|
||||
|
||||
@@ -76,6 +84,7 @@ const ContractMethodFieldInput = ({ data, hideLabel, path: name, className, isDi
|
||||
allowNegative: !argTypeMatchInt.isUnsigned,
|
||||
} : {}) }
|
||||
ref={ ref }
|
||||
onChange={ handleChange }
|
||||
required={ !isOptional }
|
||||
isInvalid={ Boolean(error) }
|
||||
placeholder={ data.type }
|
||||
@@ -84,7 +93,7 @@ const ContractMethodFieldInput = ({ data, hideLabel, path: name, className, isDi
|
||||
paddingRight={ hasMultiplyButton ? '120px' : '40px' }
|
||||
/>
|
||||
<InputRightElement w="auto" right={ 1 }>
|
||||
{ typeof field.value === 'string' && field.value.replace('\n', '') && <ClearButton onClick={ handleClear } isDisabled={ isDisabled }/> }
|
||||
{ field.value !== undefined && field.value !== '' && <ClearButton onClick={ handleClear } isDisabled={ isDisabled }/> }
|
||||
{ hasMultiplyButton && <ContractMethodMultiplyButton onClick={ handleMultiplyButtonClick } isDisabled={ isDisabled }/> }
|
||||
</InputRightElement>
|
||||
</InputGroup>
|
||||
|
||||
@@ -20,11 +20,11 @@ const ContractMethodFormOutputs = ({ data }: Props) => {
|
||||
<p>
|
||||
{ data.map(({ type, name }, index) => {
|
||||
return (
|
||||
<>
|
||||
<React.Fragment key={ index }>
|
||||
<chakra.span fontWeight={ 500 }>{ name } </chakra.span>
|
||||
<span>{ name ? `(${ type })` : type }</span>
|
||||
{ index < data.length - 1 && <span>, </span> }
|
||||
</>
|
||||
</React.Fragment>
|
||||
);
|
||||
}) }
|
||||
</p>
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import React from 'react';
|
||||
|
||||
import type { SmartContractMethodArgType } from 'types/api/contract';
|
||||
|
||||
import type { MatchInt } from './useArgTypeMatchInt';
|
||||
|
||||
interface Params {
|
||||
argType: SmartContractMethodArgType;
|
||||
argTypeMatchInt: MatchInt | null;
|
||||
}
|
||||
|
||||
export default function useFormatFieldValue({ argType, argTypeMatchInt }: Params) {
|
||||
|
||||
return React.useCallback((value: string | undefined) => {
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (argTypeMatchInt) {
|
||||
const formattedString = value.replace(/\s/g, '');
|
||||
return parseInt(formattedString);
|
||||
}
|
||||
|
||||
if (argType === 'bool') {
|
||||
const formattedValue = value.toLowerCase();
|
||||
|
||||
switch (formattedValue) {
|
||||
case 'true': {
|
||||
return true;
|
||||
}
|
||||
|
||||
case 'false':{
|
||||
return false;
|
||||
}
|
||||
|
||||
default:
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
}, [ argType, argTypeMatchInt ]);
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import { getAddress, isAddress, isHex } from 'viem';
|
||||
import type { SmartContractMethodArgType } from 'types/api/contract';
|
||||
|
||||
import type { MatchInt } from './useArgTypeMatchInt';
|
||||
import { BYTES_REGEXP, formatBooleanValue } from './utils';
|
||||
import { BYTES_REGEXP } from './utils';
|
||||
|
||||
interface Params {
|
||||
argType: SmartContractMethodArgType;
|
||||
@@ -18,13 +18,15 @@ export default function useValidateField({ isOptional, argType, argTypeMatchInt
|
||||
return argType.match(BYTES_REGEXP);
|
||||
}, [ argType ]);
|
||||
|
||||
return React.useCallback((value: string | undefined) => {
|
||||
if (!value) {
|
||||
// some values are formatted before they are sent to the validator
|
||||
// see ./useFormatFieldValue.tsx hook
|
||||
return React.useCallback((value: string | number | boolean | undefined) => {
|
||||
if (value === undefined || value === '') {
|
||||
return isOptional ? true : 'Field is required';
|
||||
}
|
||||
|
||||
if (argType === 'address') {
|
||||
if (!isAddress(value)) {
|
||||
if (typeof value !== 'string' || !isAddress(value)) {
|
||||
return 'Invalid address format';
|
||||
}
|
||||
|
||||
@@ -39,13 +41,11 @@ export default function useValidateField({ isOptional, argType, argTypeMatchInt
|
||||
}
|
||||
|
||||
if (argTypeMatchInt) {
|
||||
const formattedValue = Number(value.replace(/\s/g, ''));
|
||||
|
||||
if (Object.is(formattedValue, NaN)) {
|
||||
if (typeof value !== 'number' || Object.is(value, NaN)) {
|
||||
return 'Invalid integer format';
|
||||
}
|
||||
|
||||
if (formattedValue > argTypeMatchInt.max || formattedValue < argTypeMatchInt.min) {
|
||||
if (value > argTypeMatchInt.max || value < argTypeMatchInt.min) {
|
||||
const lowerBoundary = argTypeMatchInt.isUnsigned ? '0' : `-1 * 2 ^ ${ Number(argTypeMatchInt.power) - 1 }`;
|
||||
const upperBoundary = argTypeMatchInt.isUnsigned ? `2 ^ ${ argTypeMatchInt.power } - 1` : `2 ^ ${ Number(argTypeMatchInt.power) - 1 } - 1`;
|
||||
return `Value should be in range from "${ lowerBoundary }" to "${ upperBoundary }" inclusively`;
|
||||
@@ -55,9 +55,8 @@ export default function useValidateField({ isOptional, argType, argTypeMatchInt
|
||||
}
|
||||
|
||||
if (argType === 'bool') {
|
||||
const formattedValue = formatBooleanValue(value);
|
||||
if (formattedValue === undefined) {
|
||||
return 'Invalid boolean format. Allowed values: 0, 1, true, false';
|
||||
if (typeof value !== 'boolean') {
|
||||
return 'Invalid boolean format. Allowed values: true, false';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,25 +17,6 @@ export const getIntBoundaries = (power: number, isUnsigned: boolean) => {
|
||||
return [ min, max ];
|
||||
};
|
||||
|
||||
export const formatBooleanValue = (value: string) => {
|
||||
const formattedValue = value.toLowerCase();
|
||||
|
||||
switch (formattedValue) {
|
||||
case 'true':
|
||||
case '1': {
|
||||
return 'true';
|
||||
}
|
||||
|
||||
case 'false':
|
||||
case '0': {
|
||||
return 'false';
|
||||
}
|
||||
|
||||
default:
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
export function transformFormDataToMethodArgs(formData: ContractMethodFormFields) {
|
||||
const result: Array<unknown> = [];
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Grid, Skeleton } from '@chakra-ui/react';
|
||||
import { Alert, Grid, GridItem, Skeleton } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import type { Blob } from 'types/api/blobs';
|
||||
@@ -17,45 +17,56 @@ interface Props {
|
||||
}
|
||||
|
||||
const BlobInfo = ({ data, isLoading }: Props) => {
|
||||
const size = data.blob_data.replace('0x', '').length / 2;
|
||||
|
||||
return (
|
||||
<Grid
|
||||
columnGap={ 8 }
|
||||
rowGap={ 3 }
|
||||
templateColumns={{ base: 'minmax(0, 1fr)', lg: '216px minmax(728px, auto)' }}
|
||||
>
|
||||
<DetailsInfoItem
|
||||
title="Proof"
|
||||
hint="Zero knowledge proof. Allows for quick verification of commitment"
|
||||
isLoading={ isLoading }
|
||||
>
|
||||
<Skeleton isLoaded={ !isLoading } overflow="hidden" whiteSpace="pre-wrap" wordBreak="break-all" lineHeight={ 6 } my="-2px">
|
||||
{ data.kzg_proof }
|
||||
<CopyToClipboard text={ data.kzg_proof } isLoading={ isLoading }/>
|
||||
</Skeleton>
|
||||
</DetailsInfoItem>
|
||||
<DetailsInfoItem
|
||||
title="Commitment"
|
||||
hint="Commitment to the data in the blob"
|
||||
isLoading={ isLoading }
|
||||
>
|
||||
<Skeleton isLoaded={ !isLoading } overflow="hidden" whiteSpace="pre-wrap" wordBreak="break-all" lineHeight={ 6 } my="-2px">
|
||||
{ data.kzg_commitment }
|
||||
<CopyToClipboard text={ data.kzg_commitment } isLoading={ isLoading }/>
|
||||
</Skeleton>
|
||||
</DetailsInfoItem>
|
||||
<DetailsInfoItem
|
||||
title="Size, bytes"
|
||||
hint="Blob size in bytes"
|
||||
isLoading={ isLoading }
|
||||
>
|
||||
<Skeleton isLoaded={ !isLoading } overflow="hidden" whiteSpace="pre-wrap" wordBreak="break-all">
|
||||
{ size.toLocaleString() }
|
||||
</Skeleton>
|
||||
</DetailsInfoItem>
|
||||
{ !data.blob_data && (
|
||||
<GridItem colSpan={{ base: undefined, lg: 2 }} mb={ 3 }>
|
||||
<Skeleton isLoaded={ !isLoading }>
|
||||
<Alert status="warning">This blob is not yet indexed</Alert>
|
||||
</Skeleton>
|
||||
</GridItem>
|
||||
) }
|
||||
{ data.kzg_proof && (
|
||||
<DetailsInfoItem
|
||||
title="Proof"
|
||||
hint="Zero knowledge proof. Allows for quick verification of commitment"
|
||||
isLoading={ isLoading }
|
||||
>
|
||||
<Skeleton isLoaded={ !isLoading } overflow="hidden" whiteSpace="pre-wrap" wordBreak="break-all" lineHeight={ 6 } my="-2px">
|
||||
{ data.kzg_proof }
|
||||
<CopyToClipboard text={ data.kzg_proof } isLoading={ isLoading }/>
|
||||
</Skeleton>
|
||||
</DetailsInfoItem>
|
||||
) }
|
||||
{ data.kzg_commitment && (
|
||||
<DetailsInfoItem
|
||||
title="Commitment"
|
||||
hint="Commitment to the data in the blob"
|
||||
isLoading={ isLoading }
|
||||
>
|
||||
<Skeleton isLoaded={ !isLoading } overflow="hidden" whiteSpace="pre-wrap" wordBreak="break-all" lineHeight={ 6 } my="-2px">
|
||||
{ data.kzg_commitment }
|
||||
<CopyToClipboard text={ data.kzg_commitment } isLoading={ isLoading }/>
|
||||
</Skeleton>
|
||||
</DetailsInfoItem>
|
||||
) }
|
||||
{ data.blob_data && (
|
||||
<DetailsInfoItem
|
||||
title="Size, bytes"
|
||||
hint="Blob size in bytes"
|
||||
isLoading={ isLoading }
|
||||
>
|
||||
<Skeleton isLoaded={ !isLoading } overflow="hidden" whiteSpace="pre-wrap" wordBreak="break-all">
|
||||
{ (data.blob_data.replace('0x', '').length / 2).toLocaleString() }
|
||||
</Skeleton>
|
||||
</DetailsInfoItem>
|
||||
) }
|
||||
|
||||
<DetailsInfoItemDivider/>
|
||||
{ data.blob_data && <DetailsInfoItemDivider/> }
|
||||
|
||||
{ data.transaction_hashes[0] && (
|
||||
<DetailsInfoItem
|
||||
@@ -68,9 +79,12 @@ const BlobInfo = ({ data, isLoading }: Props) => {
|
||||
) }
|
||||
<DetailsSponsoredItem isLoading={ isLoading }/>
|
||||
|
||||
<DetailsInfoItemDivider/>
|
||||
|
||||
<BlobData data={ data.blob_data } hash={ data.hash } isLoading={ isLoading }/>
|
||||
{ data.blob_data && (
|
||||
<>
|
||||
<DetailsInfoItemDivider/>
|
||||
<BlobData data={ data.blob_data } hash={ data.hash } isLoading={ isLoading }/>
|
||||
</>
|
||||
) }
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -19,7 +19,7 @@ const MarketplaceAppAlert = ({ internalWallet, isWalletConnected }: Props) => {
|
||||
icon = 'integration/full';
|
||||
text = 'Your wallet is connected with Blockscout';
|
||||
status = 'success';
|
||||
} else if (isWalletConnected) {
|
||||
} else if (!internalWallet) {
|
||||
icon = 'integration/partial';
|
||||
text = 'Connect your wallet in the app below';
|
||||
}
|
||||
|
||||
@@ -50,20 +50,23 @@ function sortApps(apps: Array<MarketplaceAppOverview>, favoriteApps: Array<strin
|
||||
export default function useMarketplaceApps(
|
||||
filter: string,
|
||||
selectedCategoryId: string = MarketplaceCategory.ALL,
|
||||
favoriteApps: Array<string> = [],
|
||||
favoriteApps: Array<string> | undefined = undefined,
|
||||
isFavoriteAppsLoaded: boolean = false, // eslint-disable-line @typescript-eslint/no-inferrable-types
|
||||
) {
|
||||
const fetch = useFetch();
|
||||
const apiFetch = useApiFetch();
|
||||
|
||||
// Update favorite apps only when selectedCategoryId changes to avoid sortApps to be called on each favorite app click
|
||||
const lastFavoriteAppsRef = React.useRef(favoriteApps);
|
||||
const [ snapshotFavoriteApps, setSnapshotFavoriteApps ] = React.useState<Array<string> | undefined>();
|
||||
|
||||
React.useEffect(() => {
|
||||
lastFavoriteAppsRef.current = favoriteApps;
|
||||
if (isFavoriteAppsLoaded) {
|
||||
setSnapshotFavoriteApps(favoriteApps);
|
||||
}
|
||||
}, [ selectedCategoryId, isFavoriteAppsLoaded ]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
const { isPlaceholderData, isError, error, data } = useQuery<unknown, ResourceError<unknown>, Array<MarketplaceAppOverview>>({
|
||||
queryKey: [ 'marketplace-dapps' ],
|
||||
queryKey: [ 'marketplace-dapps', snapshotFavoriteApps, favoriteApps ],
|
||||
queryFn: async() => {
|
||||
if (!feature.isEnabled) {
|
||||
return [];
|
||||
@@ -73,14 +76,14 @@ export default function useMarketplaceApps(
|
||||
return apiFetch('marketplace_dapps', { pathParams: { chainId: config.chain.id } });
|
||||
}
|
||||
},
|
||||
select: (data) => sortApps(data as Array<MarketplaceAppOverview>, lastFavoriteAppsRef.current),
|
||||
select: (data) => sortApps(data as Array<MarketplaceAppOverview>, snapshotFavoriteApps || []),
|
||||
placeholderData: feature.isEnabled ? Array(9).fill(MARKETPLACE_APP) : undefined,
|
||||
staleTime: Infinity,
|
||||
enabled: feature.isEnabled,
|
||||
enabled: feature.isEnabled && (!favoriteApps || Boolean(snapshotFavoriteApps)),
|
||||
});
|
||||
|
||||
const displayedApps = React.useMemo(() => {
|
||||
return data?.filter(app => isAppNameMatches(filter, app) && isAppCategoryMatches(selectedCategoryId, app, favoriteApps)) || [];
|
||||
return data?.filter(app => isAppNameMatches(filter, app) && isAppCategoryMatches(selectedCategoryId, app, favoriteApps || [])) || [];
|
||||
}, [ selectedCategoryId, data, filter, favoriteApps ]);
|
||||
|
||||
return React.useMemo(() => ({
|
||||
|
||||
@@ -47,3 +47,22 @@ test('base view +@mobile +@dark-mode', async({ mount, page }) => {
|
||||
maskColor: configs.maskColor,
|
||||
});
|
||||
});
|
||||
|
||||
test('without data', async({ mount, page }) => {
|
||||
await page.route(BLOB_API_URL, (route) => route.fulfill({
|
||||
status: 200,
|
||||
body: JSON.stringify(blobsMock.withoutData),
|
||||
}));
|
||||
|
||||
const component = await mount(
|
||||
<TestApp>
|
||||
<Blob/>
|
||||
</TestApp>,
|
||||
{ hooksConfig },
|
||||
);
|
||||
|
||||
await expect(component).toHaveScreenshot({
|
||||
mask: [ page.locator(configs.adsBannerSelector) ],
|
||||
maskColor: configs.maskColor,
|
||||
});
|
||||
});
|
||||
|
||||
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 38 KiB |
@@ -16,7 +16,7 @@ const ValidatorStatus = ({ state, isLoading }: Props) => {
|
||||
case 'probation':
|
||||
return <StatusTag type="pending" text="Probation" isLoading={ isLoading }/>;
|
||||
case 'inactive':
|
||||
return <StatusTag type="error" text="Failed" isLoading={ isLoading }/>;
|
||||
return <StatusTag type="error" text="Inactive" isLoading={ isLoading }/>;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 26 KiB |
@@ -13,7 +13,7 @@ interface Props {
|
||||
}
|
||||
|
||||
const TxBlobListItem = ({ data, isLoading }: Props) => {
|
||||
const size = data.blob_data.replace('0x', '').length / 2;
|
||||
const size = data.blob_data ? data.blob_data.replace('0x', '').length / 2 : '-';
|
||||
|
||||
return (
|
||||
<ListItemMobileGrid.Container>
|
||||
@@ -24,7 +24,7 @@ const TxBlobListItem = ({ data, isLoading }: Props) => {
|
||||
|
||||
<ListItemMobileGrid.Label isLoading={ isLoading }>Data type</ListItemMobileGrid.Label>
|
||||
<ListItemMobileGrid.Value>
|
||||
<BlobDataType isLoading={ isLoading } data={ data.blob_data }/>
|
||||
{ data.blob_data ? <BlobDataType isLoading={ isLoading } data={ data.blob_data }/> : '-' }
|
||||
</ListItemMobileGrid.Value>
|
||||
|
||||
<ListItemMobileGrid.Label isLoading={ isLoading }>Size, bytes</ListItemMobileGrid.Label>
|
||||
|
||||
@@ -12,7 +12,7 @@ interface Props {
|
||||
}
|
||||
|
||||
const TxBlobsTableItem = ({ data, isLoading }: Props) => {
|
||||
const size = data.blob_data.replace('0x', '').length / 2;
|
||||
const size = data.blob_data ? data.blob_data.replace('0x', '').length / 2 : '-';
|
||||
|
||||
return (
|
||||
<Tr alignItems="top">
|
||||
@@ -20,7 +20,7 @@ const TxBlobsTableItem = ({ data, isLoading }: Props) => {
|
||||
<BlobEntity hash={ data.hash } noIcon isLoading={ isLoading }/>
|
||||
</Td>
|
||||
<Td verticalAlign="middle">
|
||||
<BlobDataType isLoading={ isLoading } data={ data.blob_data }/>
|
||||
{ data.blob_data ? <BlobDataType isLoading={ isLoading } data={ data.blob_data }/> : '-' }
|
||||
</Td>
|
||||
<Td verticalAlign="middle">
|
||||
<Skeleton isLoaded={ !isLoading } display="inline-block">
|
||||
|
||||
@@ -36,7 +36,7 @@ const ValidatorsFilter = ({ onChange, defaultValue, isActive }: Props) => {
|
||||
<MenuItemOption value="all">All</MenuItemOption>
|
||||
<MenuItemOption value="active">Active</MenuItemOption>
|
||||
<MenuItemOption value="probation">Probation</MenuItemOption>
|
||||
<MenuItemOption value="inactive">Failed</MenuItemOption>
|
||||
<MenuItemOption value="inactive">Inactive</MenuItemOption>
|
||||
</MenuOptionGroup>
|
||||
</MenuList>
|
||||
</Menu>
|
||||
|
||||