mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-11-22 09:57:02 +01:00
[FLASK] Expanded Snap authorship (#18775)
This commit is contained in:
parent
a4a5b28f2e
commit
6126c156ea
31
app/_locales/en/messages.json
generated
31
app/_locales/en/messages.json
generated
@ -1066,6 +1066,10 @@
|
|||||||
"description": {
|
"description": {
|
||||||
"message": "Description"
|
"message": "Description"
|
||||||
},
|
},
|
||||||
|
"descriptionFromSnap": {
|
||||||
|
"message": "Description from $1",
|
||||||
|
"description": "$1 represents the name of the snap"
|
||||||
|
},
|
||||||
"desktopConnectionCriticalErrorDescription": {
|
"desktopConnectionCriticalErrorDescription": {
|
||||||
"message": "This error could be intermittent, so try restarting the extension or disable MetaMask Desktop."
|
"message": "This error could be intermittent, so try restarting the extension or disable MetaMask Desktop."
|
||||||
},
|
},
|
||||||
@ -1331,10 +1335,7 @@
|
|||||||
"message": "Enable smart transactions"
|
"message": "Enable smart transactions"
|
||||||
},
|
},
|
||||||
"enableSnap": {
|
"enableSnap": {
|
||||||
"message": "Enable snap"
|
"message": "Enable"
|
||||||
},
|
|
||||||
"enableSnapDescription": {
|
|
||||||
"message": "Your installed snap will only have access to its permissions and run if it’s enabled."
|
|
||||||
},
|
},
|
||||||
"enableToken": {
|
"enableToken": {
|
||||||
"message": "enable $1",
|
"message": "enable $1",
|
||||||
@ -1851,6 +1852,13 @@
|
|||||||
"install": {
|
"install": {
|
||||||
"message": "Install"
|
"message": "Install"
|
||||||
},
|
},
|
||||||
|
"installOrigin": {
|
||||||
|
"message": "Install origin"
|
||||||
|
},
|
||||||
|
"installedOn": {
|
||||||
|
"message": "Installed on $1",
|
||||||
|
"description": "$1 is the date when the snap has been installed"
|
||||||
|
},
|
||||||
"institutionalFeatures": {
|
"institutionalFeatures": {
|
||||||
"message": "Institutional Features"
|
"message": "Institutional Features"
|
||||||
},
|
},
|
||||||
@ -2192,6 +2200,9 @@
|
|||||||
"mmiAuthenticate": {
|
"mmiAuthenticate": {
|
||||||
"message": "The page at $1 would like to authorise the following project’s compliance settings in MetaMask Institutional"
|
"message": "The page at $1 would like to authorise the following project’s compliance settings in MetaMask Institutional"
|
||||||
},
|
},
|
||||||
|
"more": {
|
||||||
|
"message": "more"
|
||||||
|
},
|
||||||
"moreComingSoon": {
|
"moreComingSoon": {
|
||||||
"message": "More coming soon..."
|
"message": "More coming soon..."
|
||||||
},
|
},
|
||||||
@ -3576,6 +3587,10 @@
|
|||||||
"settingsSearchMatchingNotFound": {
|
"settingsSearchMatchingNotFound": {
|
||||||
"message": "No matching results found."
|
"message": "No matching results found."
|
||||||
},
|
},
|
||||||
|
"shortVersion": {
|
||||||
|
"message": "v$1",
|
||||||
|
"description": "$1 is the version number to show"
|
||||||
|
},
|
||||||
"show": {
|
"show": {
|
||||||
"message": "Show"
|
"message": "Show"
|
||||||
},
|
},
|
||||||
@ -4510,7 +4525,6 @@
|
|||||||
"transactionFailed": {
|
"transactionFailed": {
|
||||||
"message": "Transaction Failed"
|
"message": "Transaction Failed"
|
||||||
},
|
},
|
||||||
|
|
||||||
"transactionFee": {
|
"transactionFee": {
|
||||||
"message": "Transaction fee"
|
"message": "Transaction fee"
|
||||||
},
|
},
|
||||||
@ -4737,6 +4751,9 @@
|
|||||||
"message": "Verify this token on $1 and make sure this is the token you want to trade.",
|
"message": "Verify this token on $1 and make sure this is the token you want to trade.",
|
||||||
"description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\""
|
"description": "Points the user to etherscan as a place they can verify information about a token. $1 is replaced with the translation for \"etherscan\""
|
||||||
},
|
},
|
||||||
|
"version": {
|
||||||
|
"message": "Version"
|
||||||
|
},
|
||||||
"view": {
|
"view": {
|
||||||
"message": "View"
|
"message": "View"
|
||||||
},
|
},
|
||||||
@ -4870,10 +4887,6 @@
|
|||||||
"message": "You've added all the popular networks. You can discover more networks $1 Or you can $2",
|
"message": "You've added all the popular networks. You can discover more networks $1 Or you can $2",
|
||||||
"description": "$1 is a link with the text 'here' and $2 is a button with the text 'add more networks manually'"
|
"description": "$1 is a link with the text 'here' and $2 is a button with the text 'add more networks manually'"
|
||||||
},
|
},
|
||||||
"youInstalled": {
|
|
||||||
"message": "You installed",
|
|
||||||
"description": "Part of version description for installed snap"
|
|
||||||
},
|
|
||||||
"youNeedToAllowCameraAccess": {
|
"youNeedToAllowCameraAccess": {
|
||||||
"message": "You need to allow camera access to use this feature."
|
"message": "You need to allow camera access to use this feature."
|
||||||
},
|
},
|
||||||
|
@ -43,6 +43,7 @@
|
|||||||
@import 'snaps/snap-settings-card/index';
|
@import 'snaps/snap-settings-card/index';
|
||||||
@import 'snaps/update-snap-permission-list/index';
|
@import 'snaps/update-snap-permission-list/index';
|
||||||
@import 'snaps/copyable/index';
|
@import 'snaps/copyable/index';
|
||||||
|
@import 'snaps/snap-version/index';
|
||||||
@import 'gas-details-item/index';
|
@import 'gas-details-item/index';
|
||||||
@import 'gas-details-item/gas-details-item-title/index';
|
@import 'gas-details-item/gas-details-item-title/index';
|
||||||
@import 'gas-timing/index';
|
@import 'gas-timing/index';
|
||||||
|
@ -2,30 +2,42 @@ import React from 'react';
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import { getSnapPrefix } from '@metamask/snaps-utils';
|
import { getSnapPrefix } from '@metamask/snaps-utils';
|
||||||
import { useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import Box from '../../../ui/box';
|
import Box from '../../../ui/box';
|
||||||
import {
|
import {
|
||||||
BackgroundColor,
|
BackgroundColor,
|
||||||
TextColor,
|
TextColor,
|
||||||
IconColor,
|
|
||||||
FLEX_DIRECTION,
|
FLEX_DIRECTION,
|
||||||
TextVariant,
|
TextVariant,
|
||||||
BorderColor,
|
BorderColor,
|
||||||
AlignItems,
|
AlignItems,
|
||||||
DISPLAY,
|
DISPLAY,
|
||||||
BorderRadius,
|
|
||||||
BLOCK_SIZES,
|
BLOCK_SIZES,
|
||||||
|
JustifyContent,
|
||||||
|
BorderStyle,
|
||||||
|
Color,
|
||||||
|
BorderRadius,
|
||||||
} from '../../../../helpers/constants/design-system';
|
} from '../../../../helpers/constants/design-system';
|
||||||
import {
|
import {
|
||||||
|
formatDate,
|
||||||
getSnapName,
|
getSnapName,
|
||||||
removeSnapIdPrefix,
|
removeSnapIdPrefix,
|
||||||
} from '../../../../helpers/utils/util';
|
} from '../../../../helpers/utils/util';
|
||||||
import { ButtonIcon, IconName, Text } from '../../../component-library';
|
|
||||||
|
|
||||||
|
import { Text, ButtonLink } from '../../../component-library';
|
||||||
import { getTargetSubjectMetadata } from '../../../../selectors';
|
import { getTargetSubjectMetadata } from '../../../../selectors';
|
||||||
import SnapAvatar from '../snap-avatar';
|
import SnapAvatar from '../snap-avatar';
|
||||||
|
import { useI18nContext } from '../../../../hooks/useI18nContext';
|
||||||
|
import Tooltip from '../../../ui/tooltip/tooltip';
|
||||||
|
import ToggleButton from '../../../ui/toggle-button';
|
||||||
|
import { disableSnap, enableSnap } from '../../../../store/actions';
|
||||||
|
import { useOriginMetadata } from '../../../../hooks/useOriginMetadata';
|
||||||
|
import SnapVersion from '../snap-version/snap-version';
|
||||||
|
|
||||||
|
const SnapAuthorship = ({ snapId, className, expanded = false, snap }) => {
|
||||||
|
const t = useI18nContext();
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
const SnapAuthorship = ({ snapId, className }) => {
|
|
||||||
// We're using optional chaining with snapId, because with the current implementation
|
// We're using optional chaining with snapId, because with the current implementation
|
||||||
// of snap update in the snap controller, we do not have reference to snapId when an
|
// of snap update in the snap controller, we do not have reference to snapId when an
|
||||||
// update request is rejected because the reference comes from the request itself and not subject metadata
|
// update request is rejected because the reference comes from the request itself and not subject metadata
|
||||||
@ -43,48 +55,127 @@ const SnapAuthorship = ({ snapId, className }) => {
|
|||||||
|
|
||||||
const friendlyName = snapId && getSnapName(snapId, subjectMetadata);
|
const friendlyName = snapId && getSnapName(snapId, subjectMetadata);
|
||||||
|
|
||||||
|
// Expanded data
|
||||||
|
const versionHistory = snap?.versionHistory ?? [];
|
||||||
|
const installInfo = versionHistory.length
|
||||||
|
? versionHistory[versionHistory.length - 1]
|
||||||
|
: undefined;
|
||||||
|
const installOrigin = useOriginMetadata(installInfo?.origin);
|
||||||
|
|
||||||
|
const onToggle = () => {
|
||||||
|
if (snap?.enabled) {
|
||||||
|
dispatch(disableSnap(snap?.id));
|
||||||
|
} else {
|
||||||
|
dispatch(enableSnap(snap?.id));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
className={classnames('snaps-authorship', className)}
|
className={classnames('snaps-authorship', className)}
|
||||||
backgroundColor={BackgroundColor.backgroundDefault}
|
backgroundColor={BackgroundColor.backgroundDefault}
|
||||||
borderColor={BorderColor.borderDefault}
|
borderColor={BorderColor.borderDefault}
|
||||||
borderWidth={1}
|
borderWidth={1}
|
||||||
alignItems={AlignItems.center}
|
|
||||||
paddingLeft={2}
|
|
||||||
paddingTop={2}
|
|
||||||
paddingBottom={2}
|
|
||||||
paddingRight={4}
|
|
||||||
borderRadius={BorderRadius.pill}
|
|
||||||
display={DISPLAY.FLEX}
|
|
||||||
width={BLOCK_SIZES.FULL}
|
width={BLOCK_SIZES.FULL}
|
||||||
|
borderRadius={expanded ? BorderRadius.LG : BorderRadius.pill}
|
||||||
>
|
>
|
||||||
<Box>
|
|
||||||
<SnapAvatar snapId={snapId} />
|
|
||||||
</Box>
|
|
||||||
<Box
|
<Box
|
||||||
marginLeft={4}
|
alignItems={AlignItems.center}
|
||||||
marginRight={2}
|
|
||||||
display={DISPLAY.FLEX}
|
display={DISPLAY.FLEX}
|
||||||
flexDirection={FLEX_DIRECTION.COLUMN}
|
width={BLOCK_SIZES.FULL}
|
||||||
style={{ overflow: 'hidden' }}
|
paddingLeft={expanded ? 4 : 2}
|
||||||
|
paddingRight={expanded ? 4 : 2}
|
||||||
|
paddingTop={expanded ? 3 : 1}
|
||||||
|
paddingBottom={expanded ? 3 : 1}
|
||||||
>
|
>
|
||||||
<Text ellipsis>{friendlyName}</Text>
|
<Box>
|
||||||
<Text
|
<SnapAvatar snapId={snapId} />
|
||||||
ellipsis
|
</Box>
|
||||||
variant={TextVariant.bodySm}
|
<Box
|
||||||
color={TextColor.textAlternative}
|
marginLeft={2}
|
||||||
|
marginRight={expanded ? 0 : 2}
|
||||||
|
display={DISPLAY.FLEX}
|
||||||
|
flexDirection={FLEX_DIRECTION.COLUMN}
|
||||||
|
style={{ overflow: 'hidden' }}
|
||||||
>
|
>
|
||||||
{packageName}
|
<Text ellipsis>{friendlyName}</Text>
|
||||||
</Text>
|
<Text
|
||||||
|
ellipsis
|
||||||
|
variant={TextVariant.bodySm}
|
||||||
|
color={TextColor.textAlternative}
|
||||||
|
>
|
||||||
|
{packageName}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
{!expanded && (
|
||||||
|
<Box marginLeft="auto">
|
||||||
|
<SnapVersion version={subjectMetadata?.version} url={url} />
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
<ButtonIcon
|
{expanded && (
|
||||||
rel="noopener noreferrer"
|
<Box flexDirection={FLEX_DIRECTION.COLUMN} width={BLOCK_SIZES.FULL}>
|
||||||
target="_blank"
|
<Box
|
||||||
href={url}
|
flexDirection={FLEX_DIRECTION.ROW}
|
||||||
iconName={IconName.Export}
|
justifyContent={JustifyContent.spaceBetween}
|
||||||
color={IconColor.infoDefault}
|
paddingLeft={4}
|
||||||
style={{ marginLeft: 'auto' }}
|
paddingTop={4}
|
||||||
/>
|
paddingBottom={4}
|
||||||
|
borderColor={BorderColor.borderDefault}
|
||||||
|
width={BLOCK_SIZES.FULL}
|
||||||
|
style={{
|
||||||
|
borderLeft: BorderStyle.none,
|
||||||
|
borderRight: BorderStyle.none,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text variant={TextVariant.bodyMdBold}>{t('enableSnap')}</Text>
|
||||||
|
<Box style={{ maxWidth: '52px' }}>
|
||||||
|
<Tooltip interactive position="left" html={t('snapsToggle')}>
|
||||||
|
<ToggleButton value={snap?.enabled} onToggle={onToggle} />
|
||||||
|
</Tooltip>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
flexDirection={FLEX_DIRECTION.COLUMN}
|
||||||
|
padding={4}
|
||||||
|
width={BLOCK_SIZES.FULL}
|
||||||
|
>
|
||||||
|
{installOrigin && installInfo && (
|
||||||
|
<Box
|
||||||
|
flexDirection={FLEX_DIRECTION.ROW}
|
||||||
|
justifyContent={JustifyContent.spaceBetween}
|
||||||
|
width={BLOCK_SIZES.FULL}
|
||||||
|
>
|
||||||
|
<Text variant={TextVariant.bodyMdBold}>
|
||||||
|
{t('installOrigin')}
|
||||||
|
</Text>
|
||||||
|
<Box
|
||||||
|
flexDirection={FLEX_DIRECTION.COLUMN}
|
||||||
|
alignItems={AlignItems.flexEnd}
|
||||||
|
>
|
||||||
|
<ButtonLink href={installOrigin.origin} target="_blank">
|
||||||
|
{installOrigin.host}
|
||||||
|
</ButtonLink>
|
||||||
|
<Text color={Color.textMuted}>
|
||||||
|
{t('installedOn', [
|
||||||
|
formatDate(installInfo.date, 'dd MMM yyyy'),
|
||||||
|
])}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
<Box
|
||||||
|
flexDirection={FLEX_DIRECTION.ROW}
|
||||||
|
justifyContent={JustifyContent.spaceBetween}
|
||||||
|
alignItems={AlignItems.center}
|
||||||
|
marginTop={4}
|
||||||
|
>
|
||||||
|
<Text variant={TextVariant.bodyMdBold}>{t('version')}</Text>
|
||||||
|
<SnapVersion version={snap?.version} url={url} />
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -98,6 +189,14 @@ SnapAuthorship.propTypes = {
|
|||||||
* The className of the SnapAuthorship
|
* The className of the SnapAuthorship
|
||||||
*/
|
*/
|
||||||
className: PropTypes.string,
|
className: PropTypes.string,
|
||||||
|
/**
|
||||||
|
* If the authorship component should be expanded
|
||||||
|
*/
|
||||||
|
expanded: PropTypes.bool,
|
||||||
|
/**
|
||||||
|
* The snap object. Can be undefined if the component is not expanded
|
||||||
|
*/
|
||||||
|
snap: PropTypes.object,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default SnapAuthorship;
|
export default SnapAuthorship;
|
||||||
|
@ -50,10 +50,10 @@ const SnapAvatar = ({ snapId, className }) => {
|
|||||||
position={BadgeWrapperPosition.bottomRight}
|
position={BadgeWrapperPosition.bottomRight}
|
||||||
>
|
>
|
||||||
{iconUrl ? (
|
{iconUrl ? (
|
||||||
<AvatarFavicon size={Size.LG} src={iconUrl} name={friendlyName} />
|
<AvatarFavicon size={Size.MD} src={iconUrl} name={friendlyName} />
|
||||||
) : (
|
) : (
|
||||||
<AvatarBase
|
<AvatarBase
|
||||||
size={Size.LG}
|
size={Size.MD}
|
||||||
display={DISPLAY.FLEX}
|
display={DISPLAY.FLEX}
|
||||||
alignItems={AlignItems.center}
|
alignItems={AlignItems.center}
|
||||||
justifyContent={JustifyContent.center}
|
justifyContent={JustifyContent.center}
|
||||||
|
1
ui/components/app/snaps/snap-version/index.js
Normal file
1
ui/components/app/snaps/snap-version/index.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { default } from './snap-version';
|
15
ui/components/app/snaps/snap-version/index.scss
Normal file
15
ui/components/app/snaps/snap-version/index.scss
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
.snap-version {
|
||||||
|
&:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__wrapper {
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--color-info-muted);
|
||||||
|
|
||||||
|
& * {
|
||||||
|
color: var(--color-info-default);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
72
ui/components/app/snaps/snap-version/snap-version.js
Normal file
72
ui/components/app/snaps/snap-version/snap-version.js
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import {
|
||||||
|
AlignItems,
|
||||||
|
BackgroundColor,
|
||||||
|
BorderRadius,
|
||||||
|
Color,
|
||||||
|
FLEX_DIRECTION,
|
||||||
|
TextVariant,
|
||||||
|
} from '../../../../helpers/constants/design-system';
|
||||||
|
import Box from '../../../ui/box';
|
||||||
|
import {
|
||||||
|
BUTTON_TYPES,
|
||||||
|
Button,
|
||||||
|
Icon,
|
||||||
|
IconName,
|
||||||
|
IconSize,
|
||||||
|
Text,
|
||||||
|
} from '../../../component-library';
|
||||||
|
import Preloader from '../../../ui/icon/preloader/preloader-icon.component';
|
||||||
|
import { useI18nContext } from '../../../../hooks/useI18nContext';
|
||||||
|
|
||||||
|
const SnapVersion = ({ version, url }) => {
|
||||||
|
const t = useI18nContext();
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
type={BUTTON_TYPES.LINK}
|
||||||
|
href={url}
|
||||||
|
target="_blank"
|
||||||
|
className="snap-version"
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
className="snap-version__wrapper"
|
||||||
|
flexDirection={FLEX_DIRECTION.ROW}
|
||||||
|
alignItems={AlignItems.center}
|
||||||
|
backgroundColor={BackgroundColor.backgroundAlternative}
|
||||||
|
borderRadius={BorderRadius.pill}
|
||||||
|
paddingTop={1}
|
||||||
|
paddingBottom={1}
|
||||||
|
paddingLeft={2}
|
||||||
|
paddingRight={2}
|
||||||
|
>
|
||||||
|
{version ? (
|
||||||
|
<Text color={Color.textAlternative} variant={TextVariant.bodyMd}>
|
||||||
|
{t('shortVersion', [version])}
|
||||||
|
</Text>
|
||||||
|
) : (
|
||||||
|
<Preloader size={18} />
|
||||||
|
)}
|
||||||
|
<Icon
|
||||||
|
name={IconName.Export}
|
||||||
|
color={Color.textAlternative}
|
||||||
|
size={IconSize.Sm}
|
||||||
|
marginLeft={1}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
SnapVersion.propTypes = {
|
||||||
|
/**
|
||||||
|
* The version of the snap
|
||||||
|
*/
|
||||||
|
version: PropTypes.string,
|
||||||
|
/**
|
||||||
|
* The url to the snap package
|
||||||
|
*/
|
||||||
|
url: PropTypes.string,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SnapVersion;
|
20
ui/components/app/snaps/snap-version/snap-version.stories.js
Normal file
20
ui/components/app/snaps/snap-version/snap-version.stories.js
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import SnapVersion from '.';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: 'Components/App/Snaps/SnapVersion',
|
||||||
|
component: SnapVersion,
|
||||||
|
};
|
||||||
|
export const DefaultStory = (args) => <SnapVersion {...args} />;
|
||||||
|
|
||||||
|
DefaultStory.args = {
|
||||||
|
version: '1.4.2',
|
||||||
|
url: 'https://www.npmjs.com/package/@metamask/test-snap-error',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const LoadingStory = (args) => <SnapVersion {...args} />;
|
||||||
|
|
||||||
|
LoadingStory.args = {
|
||||||
|
version: undefined,
|
||||||
|
url: 'https://www.npmjs.com/package/@metamask/test-snap-error',
|
||||||
|
};
|
27
ui/components/app/snaps/snap-version/snap-version.test.js
Normal file
27
ui/components/app/snaps/snap-version/snap-version.test.js
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { renderWithLocalization } from '../../../../../test/lib/render-helpers';
|
||||||
|
import SnapVersion from './snap-version';
|
||||||
|
|
||||||
|
describe('SnapVersion', () => {
|
||||||
|
const args = {
|
||||||
|
version: '1.4.2',
|
||||||
|
url: 'https://www.npmjs.com/package/@metamask/test-snap-error',
|
||||||
|
};
|
||||||
|
|
||||||
|
it('should render the SnapVersion without crashing and display a version', () => {
|
||||||
|
const { getByText, container } = renderWithLocalization(
|
||||||
|
<SnapVersion {...args} />,
|
||||||
|
);
|
||||||
|
expect(getByText(`v${args.version}`)).toBeDefined();
|
||||||
|
expect(container.firstChild).toHaveAttribute('href', args.url);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have a loading state if no version is passed', () => {
|
||||||
|
args.version = undefined;
|
||||||
|
|
||||||
|
const { container } = renderWithLocalization(<SnapVersion {...args} />);
|
||||||
|
|
||||||
|
expect(container.getElementsByClassName('preloader__icon')).toHaveLength(1);
|
||||||
|
expect(container.firstChild).toHaveAttribute('href', args.url);
|
||||||
|
});
|
||||||
|
});
|
@ -3,6 +3,7 @@ export enum DelineatorType {
|
|||||||
// eslint-disable-next-line @typescript-eslint/no-shadow
|
// eslint-disable-next-line @typescript-eslint/no-shadow
|
||||||
Error = 'error',
|
Error = 'error',
|
||||||
Insights = 'insights',
|
Insights = 'insights',
|
||||||
|
Description = 'description',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getDelineatorTitle = (type: DelineatorType) => {
|
export const getDelineatorTitle = (type: DelineatorType) => {
|
||||||
@ -11,6 +12,8 @@ export const getDelineatorTitle = (type: DelineatorType) => {
|
|||||||
return 'errorWithSnap';
|
return 'errorWithSnap';
|
||||||
case DelineatorType.Insights:
|
case DelineatorType.Insights:
|
||||||
return 'insightsFromSnap';
|
return 'insightsFromSnap';
|
||||||
|
case DelineatorType.Description:
|
||||||
|
return 'descriptionFromSnap';
|
||||||
default:
|
default:
|
||||||
return 'contentFromSnap';
|
return 'contentFromSnap';
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
.snap-install {
|
.snap-install {
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
|
|
||||||
.headers {
|
.content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|
||||||
.loader-container {
|
.loader-container {
|
||||||
|
@ -84,13 +84,13 @@ export default function SnapInstall({
|
|||||||
flexDirection={FLEX_DIRECTION.COLUMN}
|
flexDirection={FLEX_DIRECTION.COLUMN}
|
||||||
>
|
>
|
||||||
<Box
|
<Box
|
||||||
className="headers"
|
className="header"
|
||||||
alignItems={AlignItems.center}
|
alignItems={AlignItems.center}
|
||||||
|
paddingLeft={4}
|
||||||
|
paddingRight={4}
|
||||||
flexDirection={FLEX_DIRECTION.COLUMN}
|
flexDirection={FLEX_DIRECTION.COLUMN}
|
||||||
>
|
>
|
||||||
<Box paddingLeft={4} paddingRight={4}>
|
<SnapAuthorship snapId={targetSubjectMetadata.origin} />
|
||||||
<SnapAuthorship snapId={targetSubjectMetadata.origin} />
|
|
||||||
</Box>
|
|
||||||
{!hasError && (
|
{!hasError && (
|
||||||
<Text
|
<Text
|
||||||
variant={TextVariant.headingLg}
|
variant={TextVariant.headingLg}
|
||||||
@ -100,6 +100,8 @@ export default function SnapInstall({
|
|||||||
{t('snapInstall')}
|
{t('snapInstall')}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
|
</Box>
|
||||||
|
<Box className="content">
|
||||||
{isLoading && (
|
{isLoading && (
|
||||||
<Box
|
<Box
|
||||||
className="loader-container"
|
className="loader-container"
|
||||||
@ -116,7 +118,7 @@ export default function SnapInstall({
|
|||||||
{hasPermissions && (
|
{hasPermissions && (
|
||||||
<>
|
<>
|
||||||
<Text
|
<Text
|
||||||
className="headers__permission-description"
|
className="content__permission-description"
|
||||||
paddingBottom={4}
|
paddingBottom={4}
|
||||||
paddingLeft={4}
|
paddingLeft={4}
|
||||||
paddingRight={4}
|
paddingRight={4}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
.snap-update {
|
.snap-update {
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
|
|
||||||
.headers {
|
.content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|
||||||
.loader-container {
|
.loader-container {
|
||||||
|
@ -90,7 +90,7 @@ export default function SnapUpdate({
|
|||||||
flexDirection={FLEX_DIRECTION.COLUMN}
|
flexDirection={FLEX_DIRECTION.COLUMN}
|
||||||
>
|
>
|
||||||
<Box
|
<Box
|
||||||
className="headers"
|
className="header"
|
||||||
paddingLeft={4}
|
paddingLeft={4}
|
||||||
paddingRight={4}
|
paddingRight={4}
|
||||||
alignItems={AlignItems.center}
|
alignItems={AlignItems.center}
|
||||||
@ -106,6 +106,8 @@ export default function SnapUpdate({
|
|||||||
{t('snapUpdate')}
|
{t('snapUpdate')}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
|
</Box>
|
||||||
|
<Box className="content">
|
||||||
{isLoading && (
|
{isLoading && (
|
||||||
<Box
|
<Box
|
||||||
className="loader-container"
|
className="loader-container"
|
||||||
@ -122,8 +124,10 @@ export default function SnapUpdate({
|
|||||||
{hasPermissions && (
|
{hasPermissions && (
|
||||||
<>
|
<>
|
||||||
<Text
|
<Text
|
||||||
className="headers__permission-description"
|
className="content__permission-description"
|
||||||
paddingBottom={4}
|
paddingBottom={4}
|
||||||
|
paddingLeft={4}
|
||||||
|
paddingRight={4}
|
||||||
textAlign={TEXT_ALIGN.CENTER}
|
textAlign={TEXT_ALIGN.CENTER}
|
||||||
>
|
>
|
||||||
{t('snapUpdateRequestsPermission', [
|
{t('snapUpdateRequestsPermission', [
|
||||||
|
@ -1,19 +1,36 @@
|
|||||||
.view-snap {
|
.view-snap {
|
||||||
max-width: 475px;
|
max-width: 475px;
|
||||||
|
|
||||||
&__version_info {
|
&__description {
|
||||||
&__version-number {
|
&__wrapper {
|
||||||
font-weight: bold;
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
max-height: 5.5rem;
|
||||||
|
|
||||||
|
@include screen-md-min {
|
||||||
|
max-height: 6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.open {
|
||||||
|
max-height: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__link {
|
&__more-button {
|
||||||
vertical-align: top;
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
width: unset;
|
||||||
|
padding: 0;
|
||||||
|
padding-left: 32px;
|
||||||
|
background: linear-gradient(90deg, rgba(255, 255, 255, 0) 0%, var(--color-background-default) 33%);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__enable {
|
|
||||||
&__tooltip_wrapper {
|
&__permissions {
|
||||||
max-width: 52px;
|
.permission-cell {
|
||||||
|
margin: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,28 +1,26 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
import { useHistory, useLocation } from 'react-router-dom';
|
import { useHistory, useLocation } from 'react-router-dom';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import {
|
import {
|
||||||
SnapCaveatType,
|
SnapCaveatType,
|
||||||
WALLET_SNAP_PERMISSION_KEY,
|
WALLET_SNAP_PERMISSION_KEY,
|
||||||
} from '@metamask/rpc-methods';
|
} from '@metamask/rpc-methods';
|
||||||
import { getSnapPrefix } from '@metamask/snaps-utils';
|
import classnames from 'classnames';
|
||||||
import Button from '../../../../components/ui/button';
|
import Button from '../../../../components/ui/button';
|
||||||
import { useI18nContext } from '../../../../hooks/useI18nContext';
|
import { useI18nContext } from '../../../../hooks/useI18nContext';
|
||||||
import {
|
import {
|
||||||
Size,
|
Color,
|
||||||
|
FLEX_WRAP,
|
||||||
TextColor,
|
TextColor,
|
||||||
TextVariant,
|
TextVariant,
|
||||||
} from '../../../../helpers/constants/design-system';
|
} from '../../../../helpers/constants/design-system';
|
||||||
import SnapAuthorship from '../../../../components/app/snaps/snap-authorship';
|
import SnapAuthorship from '../../../../components/app/snaps/snap-authorship';
|
||||||
import Box from '../../../../components/ui/box';
|
import Box from '../../../../components/ui/box';
|
||||||
import SnapRemoveWarning from '../../../../components/app/snaps/snap-remove-warning';
|
import SnapRemoveWarning from '../../../../components/app/snaps/snap-remove-warning';
|
||||||
import ToggleButton from '../../../../components/ui/toggle-button';
|
|
||||||
import ConnectedSitesList from '../../../../components/app/connected-sites-list';
|
import ConnectedSitesList from '../../../../components/app/connected-sites-list';
|
||||||
import Tooltip from '../../../../components/ui/tooltip';
|
|
||||||
import { SNAPS_LIST_ROUTE } from '../../../../helpers/constants/routes';
|
import { SNAPS_LIST_ROUTE } from '../../../../helpers/constants/routes';
|
||||||
import {
|
import {
|
||||||
disableSnap,
|
|
||||||
enableSnap,
|
|
||||||
removeSnap,
|
removeSnap,
|
||||||
removePermissionsFor,
|
removePermissionsFor,
|
||||||
updateCaveat,
|
updateCaveat,
|
||||||
@ -34,18 +32,17 @@ import {
|
|||||||
getPermissionSubjects,
|
getPermissionSubjects,
|
||||||
getTargetSubjectMetadata,
|
getTargetSubjectMetadata,
|
||||||
} from '../../../../selectors';
|
} from '../../../../selectors';
|
||||||
import {
|
import { getSnapName } from '../../../../helpers/utils/util';
|
||||||
formatDate,
|
import { Text, BUTTON_TYPES } from '../../../../components/component-library';
|
||||||
getSnapName,
|
|
||||||
removeSnapIdPrefix,
|
|
||||||
} from '../../../../helpers/utils/util';
|
|
||||||
import { ButtonLink, Text } from '../../../../components/component-library';
|
|
||||||
import SnapPermissionsList from '../../../../components/app/snaps/snap-permissions-list';
|
import SnapPermissionsList from '../../../../components/app/snaps/snap-permissions-list';
|
||||||
|
import { SnapDelineator } from '../../../../components/app/snaps/snap-delineator';
|
||||||
|
import { DelineatorType } from '../../../../helpers/constants/flask';
|
||||||
|
|
||||||
function ViewSnap() {
|
function ViewSnap() {
|
||||||
const t = useI18nContext();
|
const t = useI18nContext();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
const descriptionRef = useRef(null);
|
||||||
const { pathname } = location;
|
const { pathname } = location;
|
||||||
// The snap ID is in URI-encoded form in the last path segment of the URL.
|
// The snap ID is in URI-encoded form in the last path segment of the URL.
|
||||||
const decodedSnapId = decodeURIComponent(pathname.match(/[^/]+$/u)[0]);
|
const decodedSnapId = decodeURIComponent(pathname.match(/[^/]+$/u)[0]);
|
||||||
@ -55,6 +52,8 @@ function ViewSnap() {
|
|||||||
.find((snapState) => snapState.id === decodedSnapId);
|
.find((snapState) => snapState.id === decodedSnapId);
|
||||||
|
|
||||||
const [isShowingRemoveWarning, setIsShowingRemoveWarning] = useState(false);
|
const [isShowingRemoveWarning, setIsShowingRemoveWarning] = useState(false);
|
||||||
|
const [isDescriptionOpen, setIsDescriptionOpen] = useState(false);
|
||||||
|
const [isOverflowing, setIsOverflowing] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!snap) {
|
if (!snap) {
|
||||||
@ -62,6 +61,14 @@ function ViewSnap() {
|
|||||||
}
|
}
|
||||||
}, [history, snap]);
|
}, [history, snap]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIsOverflowing(
|
||||||
|
descriptionRef.current &&
|
||||||
|
descriptionRef.current.offsetHeight <
|
||||||
|
descriptionRef.current.scrollHeight,
|
||||||
|
);
|
||||||
|
}, [descriptionRef]);
|
||||||
|
|
||||||
const connectedSubjects = useSelector((state) =>
|
const connectedSubjects = useSelector((state) =>
|
||||||
getSubjectsWithSnapPermission(state, snap?.id),
|
getSubjectsWithSnapPermission(state, snap?.id),
|
||||||
);
|
);
|
||||||
@ -74,14 +81,6 @@ function ViewSnap() {
|
|||||||
);
|
);
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
const onToggle = () => {
|
|
||||||
if (snap.enabled) {
|
|
||||||
dispatch(disableSnap(snap.id));
|
|
||||||
} else {
|
|
||||||
dispatch(enableSnap(snap.id));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onDisconnect = (connectedOrigin, snapId) => {
|
const onDisconnect = (connectedOrigin, snapId) => {
|
||||||
const caveatValue =
|
const caveatValue =
|
||||||
subjects[connectedOrigin].permissions[WALLET_SNAP_PERMISSION_KEY]
|
subjects[connectedOrigin].permissions[WALLET_SNAP_PERMISSION_KEY]
|
||||||
@ -110,121 +109,51 @@ function ViewSnap() {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const versionHistory = snap.versionHistory ?? [];
|
|
||||||
const installInfo = versionHistory.length
|
|
||||||
? versionHistory[versionHistory.length - 1]
|
|
||||||
: undefined;
|
|
||||||
const packageName = snap.id && removeSnapIdPrefix(snap.id);
|
|
||||||
const snapPrefix = snap.id && getSnapPrefix(snap.id);
|
|
||||||
const isNPM = snapPrefix === 'npm:';
|
|
||||||
const url = isNPM
|
|
||||||
? `https://www.npmjs.com/package/${packageName}`
|
|
||||||
: packageName;
|
|
||||||
const snapName = getSnapName(snap.id, targetSubjectMetadata);
|
const snapName = getSnapName(snap.id, targetSubjectMetadata);
|
||||||
|
|
||||||
|
const shouldDisplayMoreButton = isOverflowing && !isDescriptionOpen;
|
||||||
|
const handleMoreClick = () => {
|
||||||
|
setIsDescriptionOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
className="view-snap"
|
className="view-snap"
|
||||||
paddingBottom={8}
|
paddingBottom={[4, 8]}
|
||||||
paddingLeft={3}
|
paddingTop={[4, 8]}
|
||||||
paddingRight={3}
|
paddingLeft={4}
|
||||||
|
paddingRight={4}
|
||||||
>
|
>
|
||||||
<Box
|
<SnapAuthorship snapId={snap.id} snap={snap} expanded />
|
||||||
className="view-snap__header"
|
<Box className="view-snap__description" marginTop={[4, 7]}>
|
||||||
paddingTop={8}
|
<SnapDelineator type={DelineatorType.Description} snapName={snapName}>
|
||||||
marginLeft={4}
|
<Box
|
||||||
marginRight={4}
|
className={classnames('view-snap__description__wrapper', {
|
||||||
>
|
open: isDescriptionOpen,
|
||||||
<SnapAuthorship snapId={snap.id} />
|
})}
|
||||||
</Box>
|
ref={descriptionRef}
|
||||||
<Box
|
|
||||||
className="view-snap__description"
|
|
||||||
marginTop={4}
|
|
||||||
marginLeft={4}
|
|
||||||
marginRight={4}
|
|
||||||
>
|
|
||||||
<Text variant={TextVariant.bodyMd} color={TextColor.textDefault}>
|
|
||||||
{snap.manifest.description}
|
|
||||||
</Text>
|
|
||||||
</Box>
|
|
||||||
<Box
|
|
||||||
className="view-snap__version_info"
|
|
||||||
marginTop={2}
|
|
||||||
marginLeft={4}
|
|
||||||
marginRight={4}
|
|
||||||
>
|
|
||||||
<Text variant={TextVariant.bodyMd} color={TextColor.textDefault}>
|
|
||||||
{`${t('youInstalled')} `}
|
|
||||||
<span className="view-snap__version_info__version-number">
|
|
||||||
v{snap.version}
|
|
||||||
</span>
|
|
||||||
{` ${t('ofTextNofM')} `}
|
|
||||||
<ButtonLink
|
|
||||||
size={Size.auto}
|
|
||||||
href={url}
|
|
||||||
target="_blank"
|
|
||||||
className="view-snap__version_info__link"
|
|
||||||
>
|
>
|
||||||
{packageName}
|
<Text>{snap?.manifest.description}</Text>
|
||||||
</ButtonLink>
|
{shouldDisplayMoreButton && (
|
||||||
{installInfo && ` ${t('from').toLowerCase()} `}
|
<Button
|
||||||
{installInfo && (
|
className="view-snap__description__more-button"
|
||||||
<ButtonLink
|
type={BUTTON_TYPES.LINK}
|
||||||
size={Size.auto}
|
onClick={handleMoreClick}
|
||||||
href={installInfo.origin}
|
>
|
||||||
target="_blank"
|
<Text color={Color.infoDefault}>{t('more')}</Text>
|
||||||
className="view-snap__version_info__link"
|
</Button>
|
||||||
>
|
)}
|
||||||
{installInfo.origin}
|
</Box>
|
||||||
</ButtonLink>
|
</SnapDelineator>
|
||||||
)}
|
|
||||||
{installInfo &&
|
|
||||||
` ${t('on').toLowerCase()} ${formatDate(
|
|
||||||
installInfo.date,
|
|
||||||
'dd MMM yyyy',
|
|
||||||
)}`}
|
|
||||||
.
|
|
||||||
</Text>
|
|
||||||
</Box>
|
|
||||||
<Box
|
|
||||||
className="view-snap__enable"
|
|
||||||
marginTop={12}
|
|
||||||
marginLeft={4}
|
|
||||||
marginRight={4}
|
|
||||||
>
|
|
||||||
<Text variant={TextVariant.bodyLgMedium}>{t('enableSnap')}</Text>
|
|
||||||
<Text
|
|
||||||
variant={TextVariant.bodyMd}
|
|
||||||
color={TextColor.textDefault}
|
|
||||||
marginBottom={4}
|
|
||||||
>
|
|
||||||
{t('enableSnapDescription')}
|
|
||||||
</Text>
|
|
||||||
<Box className="view-snap__enable__tooltip_wrapper">
|
|
||||||
<Tooltip interactive position="left" html={t('snapsToggle')}>
|
|
||||||
<ToggleButton
|
|
||||||
value={snap.enabled}
|
|
||||||
onToggle={onToggle}
|
|
||||||
className="view-snap__toggle-button"
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
</Box>
|
||||||
<Box className="view-snap__permissions" marginTop={12}>
|
<Box className="view-snap__permissions" marginTop={12}>
|
||||||
<Text variant={TextVariant.bodyLgMedium} marginLeft={4} marginRight={4}>
|
<Text variant={TextVariant.bodyLgMedium}>{t('permissions')}</Text>
|
||||||
{t('permissions')}
|
|
||||||
</Text>
|
|
||||||
<SnapPermissionsList
|
<SnapPermissionsList
|
||||||
permissions={permissions ?? {}}
|
permissions={permissions ?? {}}
|
||||||
targetSubjectMetadata={targetSubjectMetadata}
|
targetSubjectMetadata={targetSubjectMetadata}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
<Box
|
<Box className="view-snap__connected-sites" marginTop={12}>
|
||||||
className="view-snap__connected-sites"
|
|
||||||
marginTop={12}
|
|
||||||
marginLeft={4}
|
|
||||||
marginRight={4}
|
|
||||||
>
|
|
||||||
<Text variant={TextVariant.bodyLgMedium} marginBottom={4}>
|
<Text variant={TextVariant.bodyLgMedium} marginBottom={4}>
|
||||||
{t('connectedSites')}
|
{t('connectedSites')}
|
||||||
</Text>
|
</Text>
|
||||||
@ -235,12 +164,7 @@ function ViewSnap() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
<Box
|
<Box className="view-snap__remove" marginTop={12}>
|
||||||
className="view-snap__remove"
|
|
||||||
marginTop={12}
|
|
||||||
marginLeft={4}
|
|
||||||
marginRight={4}
|
|
||||||
>
|
|
||||||
<Text variant={TextVariant.bodyLgMedium} color={TextColor.textDefault}>
|
<Text variant={TextVariant.bodyLgMedium} color={TextColor.textDefault}>
|
||||||
{t('removeSnap')}
|
{t('removeSnap')}
|
||||||
</Text>
|
</Text>
|
||||||
@ -253,7 +177,13 @@ function ViewSnap() {
|
|||||||
type="danger"
|
type="danger"
|
||||||
onClick={() => setIsShowingRemoveWarning(true)}
|
onClick={() => setIsShowingRemoveWarning(true)}
|
||||||
>
|
>
|
||||||
<Text variant={TextVariant.bodyMd} color={TextColor.errorDefault}>
|
<Text
|
||||||
|
variant={TextVariant.bodyMd}
|
||||||
|
color={TextColor.errorDefault}
|
||||||
|
flexWrap={FLEX_WRAP.NO_WRAP}
|
||||||
|
ellipsis
|
||||||
|
style={{ overflow: 'hidden' }}
|
||||||
|
>
|
||||||
{`${t('remove')} ${snapName}`}
|
{`${t('remove')} ${snapName}`}
|
||||||
</Text>
|
</Text>
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -48,12 +48,7 @@ describe('ViewSnap', () => {
|
|||||||
// Snap version info
|
// Snap version info
|
||||||
expect(getByText('v5.1.2')).toBeDefined();
|
expect(getByText('v5.1.2')).toBeDefined();
|
||||||
// Enable Snap
|
// Enable Snap
|
||||||
expect(getByText('Enable snap')).toBeDefined();
|
expect(getByText('Enable')).toBeDefined();
|
||||||
expect(
|
|
||||||
getByText(
|
|
||||||
'Your installed snap will only have access to its permissions and run if it’s enabled.',
|
|
||||||
),
|
|
||||||
).toBeDefined();
|
|
||||||
expect(container.getElementsByClassName('toggle-button')?.length).toBe(1);
|
expect(container.getElementsByClassName('toggle-button')?.length).toBe(1);
|
||||||
// Permissions
|
// Permissions
|
||||||
expect(getByText('Permissions')).toBeDefined();
|
expect(getByText('Permissions')).toBeDefined();
|
||||||
|
Loading…
Reference in New Issue
Block a user