Commit 9cac9f82 authored by Charles Bachmeier's avatar Charles Bachmeier Committed by GitHub

feat: [ListV2] error and warning states (#5921)

* update error message for price inputs

* add grid and button warning states

* add list below floor warning modal

* only warn for prices 20% below floor

* highlight unentered price in red on button press

* missing dependency

* updated modal name and mobile height

* add new file

* fix column presence

* rookie mistake

* bulk zustand imports

* below floor threshold

* move issue check higher

* cleanup mouseEvent

* rename color var

---------
Co-authored-by: default avatarCharles Bachmeier <charlie@genie.xyz>
parent 5def0dd1
import { Plural, t } from '@lingui/macro'
import { NftListV2Variant, useNftListV2Flag } from 'featureFlags/flags/nftListV2'
import { useOnClickOutside } from 'hooks/useOnClickOutside' import { useOnClickOutside } from 'hooks/useOnClickOutside'
import ms from 'ms.macro' import ms from 'ms.macro'
import { Box } from 'nft/components/Box' import { Box } from 'nft/components/Box'
import { Row } from 'nft/components/Flex' import { Row } from 'nft/components/Flex'
import { ArrowRightIcon, HazardIcon, LoadingIcon, XMarkIcon } from 'nft/components/icons' import { ArrowRightIcon, HazardIcon, LoadingIcon, XMarkIcon } from 'nft/components/icons'
import { BelowFloorWarningModal } from 'nft/components/profile/list/Modal/BelowFloorWarningModal'
import { bodySmall } from 'nft/css/common.css' import { bodySmall } from 'nft/css/common.css'
import { themeVars } from 'nft/css/sprinkles.css' import { themeVars } from 'nft/css/sprinkles.css'
import { useNFTList, useSellAsset } from 'nft/hooks' import { useNFTList, useSellAsset } from 'nft/hooks'
import { Listing, ListingStatus, WalletAsset } from 'nft/types' import { Listing, ListingStatus, WalletAsset } from 'nft/types'
import { pluralize } from 'nft/utils/roundAndPluralize' import { pluralize } from 'nft/utils/roundAndPluralize'
import { useEffect, useMemo, useRef, useState } from 'react' import { useEffect, useMemo, useRef, useState } from 'react'
import { useTheme } from 'styled-components/macro'
import shallow from 'zustand/shallow'
import * as styles from './ListingModal.css' import * as styles from './ListingModal.css'
import { getListings } from './utils' import { getListings } from './utils'
const BELOW_FLOOR_PRICE_THRESHOLD = 0.8
interface ListingButtonProps { interface ListingButtonProps {
onClick: () => void onClick: () => void
buttonText: string buttonText: string
...@@ -20,18 +27,46 @@ interface ListingButtonProps { ...@@ -20,18 +27,46 @@ interface ListingButtonProps {
} }
export const ListingButton = ({ onClick, buttonText, showWarningOverride = false }: ListingButtonProps) => { export const ListingButton = ({ onClick, buttonText, showWarningOverride = false }: ListingButtonProps) => {
const sellAssets = useSellAsset((state) => state.sellAssets) const {
const addMarketplaceWarning = useSellAsset((state) => state.addMarketplaceWarning) addMarketplaceWarning,
const removeAllMarketplaceWarnings = useSellAsset((state) => state.removeAllMarketplaceWarnings) sellAssets,
const listingStatus = useNFTList((state) => state.listingStatus) removeAllMarketplaceWarnings,
const setListingStatus = useNFTList((state) => state.setListingStatus) showResolveIssues,
const setListings = useNFTList((state) => state.setListings) toggleShowResolveIssues,
const setCollectionsRequiringApproval = useNFTList((state) => state.setCollectionsRequiringApproval) } = useSellAsset(
({
addMarketplaceWarning,
sellAssets,
removeAllMarketplaceWarnings,
showResolveIssues,
toggleShowResolveIssues,
}) => ({
addMarketplaceWarning,
sellAssets,
removeAllMarketplaceWarnings,
showResolveIssues,
toggleShowResolveIssues,
}),
shallow
)
const { listingStatus, setListingStatus, setListings, setCollectionsRequiringApproval } = useNFTList(
({ listingStatus, setListingStatus, setListings, setCollectionsRequiringApproval }) => ({
listingStatus,
setListingStatus,
setListings,
setCollectionsRequiringApproval,
}),
shallow
)
const isNftListV2 = useNftListV2Flag() === NftListV2Variant.Enabled
const [showWarning, setShowWarning] = useState(false) const [showWarning, setShowWarning] = useState(false)
const [canContinue, setCanContinue] = useState(false) const [canContinue, setCanContinue] = useState(false)
const [issues, setIssues] = useState(0)
const theme = useTheme()
const warningRef = useRef<HTMLDivElement>(null) const warningRef = useRef<HTMLDivElement>(null)
useOnClickOutside(warningRef, () => { useOnClickOutside(warningRef, () => {
setShowWarning(false) !isNftListV2 && setShowWarning(false)
}) })
useEffect(() => { useEffect(() => {
...@@ -71,13 +106,30 @@ export const ListingButton = ({ onClick, buttonText, showWarningOverride = false ...@@ -71,13 +106,30 @@ export const ListingButton = ({ onClick, buttonText, showWarningOverride = false
for (const listing of asset.newListings) { for (const listing of asset.newListings) {
if (!listing.price) listingsMissingPrice.push([asset, listing]) if (!listing.price) listingsMissingPrice.push([asset, listing])
else if (isNaN(listing.price) || listing.price < 0) invalidPrices.push([asset, listing]) else if (isNaN(listing.price) || listing.price < 0) invalidPrices.push([asset, listing])
else if (listing.price < (asset?.floorPrice ?? 0) && !listing.overrideFloorPrice) else if (
listing.price < (asset?.floorPrice ?? 0) * BELOW_FLOOR_PRICE_THRESHOLD &&
!listing.overrideFloorPrice
)
listingsBelowFloor.push([asset, listing]) listingsBelowFloor.push([asset, listing])
else if (asset.floor_sell_order_price && listing.price > asset.floor_sell_order_price) else if (asset.floor_sell_order_price && listing.price > asset.floor_sell_order_price)
listingsAboveSellOrderFloor.push([asset, listing]) listingsAboveSellOrderFloor.push([asset, listing])
} }
} }
} }
// set number of issues
if (isNftListV2) {
const foundIssues =
Number(missingExpiration) +
Number(overMaxExpiration) +
listingsMissingPrice.length +
listingsAboveSellOrderFloor.length
setIssues(foundIssues)
!foundIssues && showResolveIssues && toggleShowResolveIssues()
// Only show Resolve Issue text if there was a user submitted error (ie not when page loads with no prices set)
if ((missingExpiration || overMaxExpiration || listingsAboveSellOrderFloor.length) && !showResolveIssues)
toggleShowResolveIssues()
}
const continueCheck = listingsBelowFloor.length === 0 && listingsAboveSellOrderFloor.length === 0 const continueCheck = listingsBelowFloor.length === 0 && listingsAboveSellOrderFloor.length === 0
setCanContinue(continueCheck) setCanContinue(continueCheck)
return [ return [
...@@ -90,7 +142,7 @@ export const ListingButton = ({ onClick, buttonText, showWarningOverride = false ...@@ -90,7 +142,7 @@ export const ListingButton = ({ onClick, buttonText, showWarningOverride = false
listingsAboveSellOrderFloor, listingsAboveSellOrderFloor,
invalidPrices, invalidPrices,
] ]
}, [sellAssets]) }, [isNftListV2, sellAssets, showResolveIssues, toggleShowResolveIssues])
const [disableListButton, warningMessage] = useMemo(() => { const [disableListButton, warningMessage] = useMemo(() => {
const disableListButton = const disableListButton =
...@@ -158,11 +210,15 @@ export const ListingButton = ({ onClick, buttonText, showWarningOverride = false ...@@ -158,11 +210,15 @@ export const ListingButton = ({ onClick, buttonText, showWarningOverride = false
} }
const warningWrappedClick = () => { const warningWrappedClick = () => {
if ((!disableListButton && canContinue) || showWarningOverride) onClick() if ((!disableListButton && canContinue) || showWarningOverride) {
else addWarningMessages() if (issues && isNftListV2 && !showResolveIssues) toggleShowResolveIssues()
else if (listingsBelowFloor.length) setShowWarning(true)
else onClick()
} else addWarningMessages()
} }
return ( return (
<>
<Box position="relative" width="full"> <Box position="relative" width="full">
{!showWarningOverride && showWarning && warningMessage.length > 0 && ( {!showWarningOverride && showWarning && warningMessage.length > 0 && (
<Row <Row
...@@ -210,21 +266,21 @@ export const ListingButton = ({ onClick, buttonText, showWarningOverride = false ...@@ -210,21 +266,21 @@ export const ListingButton = ({ onClick, buttonText, showWarningOverride = false
<Box <Box
as="button" as="button"
border="none" border="none"
backgroundColor="accentAction" backgroundColor={showResolveIssues ? 'accentFailure' : 'accentAction'}
cursor={ cursor={
[ListingStatus.APPROVED, ListingStatus.PENDING, ListingStatus.SIGNING].includes(listingStatus) || [ListingStatus.APPROVED, ListingStatus.PENDING, ListingStatus.SIGNING].includes(listingStatus) ||
disableListButton disableListButton
? 'default' ? 'default'
: 'pointer' : 'pointer'
} }
color="explicitWhite"
className={styles.button} className={styles.button}
onClick={() => listingStatus !== ListingStatus.APPROVED && warningWrappedClick()} onClick={() => listingStatus !== ListingStatus.APPROVED && warningWrappedClick()}
type="button" type="button"
style={{ style={{
color: showResolveIssues ? theme.accentTextDarkPrimary : theme.white,
opacity: opacity:
![ListingStatus.DEFINED, ListingStatus.FAILED, ListingStatus.CONTINUE].includes(listingStatus) || ![ListingStatus.DEFINED, ListingStatus.FAILED, ListingStatus.CONTINUE].includes(listingStatus) ||
disableListButton (disableListButton && !showResolveIssues)
? 0.3 ? 0.3
: 1, : 1,
}} }}
...@@ -242,10 +298,20 @@ export const ListingButton = ({ onClick, buttonText, showWarningOverride = false ...@@ -242,10 +298,20 @@ export const ListingButton = ({ onClick, buttonText, showWarningOverride = false
'Try again' 'Try again'
) : listingStatus === ListingStatus.CONTINUE ? ( ) : listingStatus === ListingStatus.CONTINUE ? (
'Continue' 'Continue'
) : showResolveIssues ? (
<Plural value={issues !== 1 ? 2 : 1} _1="Resolve issue" other={t`Resolve ${issues} issues`} />
) : ( ) : (
buttonText buttonText
)} )}
</Box> </Box>
</Box> </Box>
{showWarning && (
<BelowFloorWarningModal
listingsBelowFloor={listingsBelowFloor}
closeModal={() => setShowWarning(false)}
startListing={onClick}
/>
)}
</>
) )
} }
...@@ -314,6 +314,7 @@ export const ListPage = () => { ...@@ -314,6 +314,7 @@ export const ListPage = () => {
<ListingButton <ListingButton
onClick={handleV2Click} onClick={handleV2Click}
buttonText={anyListingsMissingPrice && !isMobile ? t`Set prices to continue` : t`Start listing`} buttonText={anyListingsMissingPrice && !isMobile ? t`Set prices to continue` : t`Start listing`}
showWarningOverride={true}
/> />
</ListingButtonWrapper> </ListingButtonWrapper>
</ProceedsAndButtonWrapper> </ProceedsAndButtonWrapper>
......
...@@ -22,7 +22,7 @@ const PastPriceInfo = styled(Column)` ...@@ -22,7 +22,7 @@ const PastPriceInfo = styled(Column)`
display: none; display: none;
flex: 1; flex: 1;
@media screen and (min-width: ${BREAKPOINTS.xxl}px) { @media screen and (min-width: ${BREAKPOINTS.xl}px) {
display: flex; display: flex;
} }
` `
......
import { Plural, t, Trans } from '@lingui/macro'
import { ButtonPrimary } from 'components/Button'
import Column from 'components/Column'
import { Portal } from 'nft/components/common/Portal'
import { Overlay } from 'nft/components/modals/Overlay'
import { Listing, WalletAsset } from 'nft/types'
import React from 'react'
import { AlertTriangle, X } from 'react-feather'
import styled, { useTheme } from 'styled-components/macro'
import { BREAKPOINTS, ThemedText } from 'theme'
import { Z_INDEX } from 'theme/zIndex'
const ModalWrapper = styled(Column)`
position: fixed;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 420px;
z-index: ${Z_INDEX.modal};
background: ${({ theme }) => theme.backgroundSurface};
border-radius: 20px;
border: 1px solid ${({ theme }) => theme.backgroundOutline};
box-shadow: ${({ theme }) => theme.deepShadow};
padding: 20px 24px 24px;
display: flex;
flex-direction: column;
gap: 12px;
@media screen and (max-width: ${BREAKPOINTS.sm}px) {
width: 100%;
}
`
const CloseIconWrapper = styled.div`
display: flex;
width: 100%;
justify-content: flex-end;
`
const CloseIcon = styled(X)`
cursor: pointer;
&:hover {
opacity: 0.6;
}
`
const HazardIconWrap = styled.div`
display: flex;
width: 100%;
justify-content: center;
align-items: center;
padding: 32px 120px;
`
const ContinueButton = styled(ButtonPrimary)`
font-weight: 600;
font-size: 20px;
line-height: 24px;
margin-top: 12px;
`
const EditListings = styled.span`
font-weight: 600;
font-size: 16px;
line-height: 20px;
color: ${({ theme }) => theme.accentAction};
text-align: center;
cursor: pointer;
padding: 12px 16px;
&:hover {
opacity: 0.6;
}
`
export const BelowFloorWarningModal = ({
listingsBelowFloor,
closeModal,
startListing,
}: {
listingsBelowFloor: [WalletAsset, Listing][]
closeModal: () => void
startListing: () => void
}) => {
const theme = useTheme()
const clickContinue = (e: React.MouseEvent) => {
e.preventDefault()
e.stopPropagation()
startListing()
closeModal()
}
return (
<Portal>
<ModalWrapper>
<CloseIconWrapper>
<CloseIcon width={24} height={24} onClick={closeModal} />{' '}
</CloseIconWrapper>
<HazardIconWrap>
<AlertTriangle height={90} width={90} color={theme.accentCritical} />
</HazardIconWrap>
<ThemedText.HeadlineSmall lineHeight="28px" textAlign="center">
<Trans>Low listing price</Trans>
</ThemedText.HeadlineSmall>
<ThemedText.BodyPrimary textAlign="center">
<Plural
value={listingsBelowFloor.length !== 1 ? 2 : 1}
_1={t`One NFT is listed ${(
(1 - (listingsBelowFloor[0][1].price ?? 0) / (listingsBelowFloor[0][0].floorPrice ?? 0)) *
100
).toFixed(0)}% `}
other={t`${listingsBelowFloor.length} NFTs are listed significantly `}
/>
&nbsp;
<Trans>below the collection’s floor price. Are you sure you want to continue?</Trans>
</ThemedText.BodyPrimary>
<ContinueButton onClick={clickContinue}>
<Trans>Continue</Trans>
</ContinueButton>
<EditListings onClick={closeModal}>
<Trans>Edit listings</Trans>
</EditListings>
</ModalWrapper>
<Overlay onClick={closeModal} />
</Portal>
)
}
...@@ -8,6 +8,7 @@ import { useSellAsset } from 'nft/hooks' ...@@ -8,6 +8,7 @@ import { useSellAsset } from 'nft/hooks'
import { ListingWarning, WalletAsset } from 'nft/types' import { ListingWarning, WalletAsset } from 'nft/types'
import { formatEth } from 'nft/utils/currency' import { formatEth } from 'nft/utils/currency'
import { Dispatch, FormEvent, useEffect, useRef, useState } from 'react' import { Dispatch, FormEvent, useEffect, useRef, useState } from 'react'
import { AlertTriangle } from 'react-feather'
import styled, { useTheme } from 'styled-components/macro' import styled, { useTheme } from 'styled-components/macro'
import { BREAKPOINTS } from 'theme' import { BREAKPOINTS } from 'theme'
import { colors } from 'theme/colors' import { colors } from 'theme/colors'
...@@ -42,7 +43,11 @@ const GlobalPriceIcon = styled.div` ...@@ -42,7 +43,11 @@ const GlobalPriceIcon = styled.div`
background-color: ${({ theme }) => theme.backgroundSurface}; background-color: ${({ theme }) => theme.backgroundSurface};
` `
const WarningMessage = styled(Row)<{ warningType: WarningType }>` const WarningRow = styled(Row)`
gap: 4px;
`
const WarningMessage = styled(Row)<{ $color: string }>`
top: 52px; top: 52px;
width: max-content; width: max-content;
position: absolute; position: absolute;
...@@ -50,17 +55,16 @@ const WarningMessage = styled(Row)<{ warningType: WarningType }>` ...@@ -50,17 +55,16 @@ const WarningMessage = styled(Row)<{ warningType: WarningType }>`
font-weight: 600; font-weight: 600;
font-size: 10px; font-size: 10px;
line-height: 12px; line-height: 12px;
color: ${({ warningType, theme }) => (warningType === WarningType.BELOW_FLOOR ? colors.red400 : theme.textSecondary)}; color: ${({ $color }) => $color};
@media screen and (min-width: ${BREAKPOINTS.md}px) { @media screen and (min-width: ${BREAKPOINTS.md}px) {
right: unset; right: unset;
} }
` `
const WarningAction = styled.div<{ warningType: WarningType }>` const WarningAction = styled.div`
margin-left: 8px;
cursor: pointer; cursor: pointer;
color: ${({ warningType, theme }) => (warningType === WarningType.BELOW_FLOOR ? theme.accentAction : colors.red400)}; color: ${({ theme }) => theme.accentAction};
` `
enum WarningType { enum WarningType {
...@@ -73,10 +77,10 @@ const getWarningMessage = (warning: WarningType) => { ...@@ -73,10 +77,10 @@ const getWarningMessage = (warning: WarningType) => {
let message = <></> let message = <></>
switch (warning) { switch (warning) {
case WarningType.BELOW_FLOOR: case WarningType.BELOW_FLOOR:
message = <Trans>LISTING BELOW FLOOR </Trans> message = <Trans>below floor price.</Trans>
break break
case WarningType.ALREADY_LISTED: case WarningType.ALREADY_LISTED:
message = <Trans>ALREADY LISTED FOR </Trans> message = <Trans>Already listed at</Trans>
break break
} }
return message return message
...@@ -107,6 +111,7 @@ export const PriceTextInput = ({ ...@@ -107,6 +111,7 @@ export const PriceTextInput = ({
const [warningType, setWarningType] = useState(WarningType.NONE) const [warningType, setWarningType] = useState(WarningType.NONE)
const removeMarketplaceWarning = useSellAsset((state) => state.removeMarketplaceWarning) const removeMarketplaceWarning = useSellAsset((state) => state.removeMarketplaceWarning)
const removeSellAsset = useSellAsset((state) => state.removeSellAsset) const removeSellAsset = useSellAsset((state) => state.removeSellAsset)
const showResolveIssues = useSellAsset((state) => state.showResolveIssues)
const inputRef = useRef() as React.MutableRefObject<HTMLInputElement> const inputRef = useRef() as React.MutableRefObject<HTMLInputElement>
const theme = useTheme() const theme = useTheme()
...@@ -121,9 +126,16 @@ export const PriceTextInput = ({ ...@@ -121,9 +126,16 @@ export const PriceTextInput = ({
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [listPrice]) }, [listPrice])
const borderColor = const percentBelowFloor = (1 - (listPrice ?? 0) / (asset.floorPrice ?? 0)) * 100
warningType !== WarningType.NONE && !focused
const warningColor =
showResolveIssues && !listPrice
? colors.red400
: warningType !== WarningType.NONE && !focused
? (warningType === WarningType.BELOW_FLOOR && percentBelowFloor >= 20) ||
warningType === WarningType.ALREADY_LISTED
? colors.red400 ? colors.red400
: theme.accentWarning
: isGlobalPrice : isGlobalPrice
? theme.accentAction ? theme.accentAction
: listPrice != null : listPrice != null
...@@ -132,7 +144,7 @@ export const PriceTextInput = ({ ...@@ -132,7 +144,7 @@ export const PriceTextInput = ({
return ( return (
<PriceTextInputWrapper> <PriceTextInputWrapper>
<InputWrapper borderColor={borderColor}> <InputWrapper borderColor={warningColor}>
<NumericInput <NumericInput
as="input" as="input"
pattern="[0-9]" pattern="[0-9]"
...@@ -164,27 +176,27 @@ export const PriceTextInput = ({ ...@@ -164,27 +176,27 @@ export const PriceTextInput = ({
</GlobalPriceIcon> </GlobalPriceIcon>
)} )}
</InputWrapper> </InputWrapper>
<WarningMessage warningType={warningType}> <WarningMessage $color={warningColor}>
{warning {warning
? warning.message ? warning.message
: warningType !== WarningType.NONE && ( : warningType !== WarningType.NONE && (
<> <WarningRow>
<AlertTriangle height={16} width={16} color={warningColor} />
<span>
{warningType === WarningType.BELOW_FLOOR && `${percentBelowFloor.toFixed(0)}% `}
{getWarningMessage(warningType)} {getWarningMessage(warningType)}
&nbsp; &nbsp;
{warningType === WarningType.BELOW_FLOOR {warningType === WarningType.ALREADY_LISTED && `${formatEth(asset?.floor_sell_order_price ?? 0)} ETH`}
? formatEth(asset?.floorPrice ?? 0) </span>
: formatEth(asset?.floor_sell_order_price ?? 0)}
ETH
<WarningAction <WarningAction
warningType={warningType}
onClick={() => { onClick={() => {
warningType === WarningType.ALREADY_LISTED && removeSellAsset(asset) warningType === WarningType.ALREADY_LISTED && removeSellAsset(asset)
setWarningType(WarningType.NONE) setWarningType(WarningType.NONE)
}} }}
> >
{warningType === WarningType.BELOW_FLOOR ? <Trans>DISMISS</Trans> : <Trans>REMOVE ITEM</Trans>} {warningType === WarningType.BELOW_FLOOR ? <Trans>Dismiss</Trans> : <Trans>Remove item</Trans>}
</WarningAction> </WarningAction>
</> </WarningRow>
)} )}
</WarningMessage> </WarningMessage>
</PriceTextInputWrapper> </PriceTextInputWrapper>
......
...@@ -5,6 +5,7 @@ import { ListingMarket, ListingWarning, WalletAsset } from '../types' ...@@ -5,6 +5,7 @@ import { ListingMarket, ListingWarning, WalletAsset } from '../types'
interface SellAssetState { interface SellAssetState {
sellAssets: WalletAsset[] sellAssets: WalletAsset[]
showResolveIssues: boolean
selectSellAsset: (asset: WalletAsset) => void selectSellAsset: (asset: WalletAsset) => void
removeSellAsset: (asset: WalletAsset) => void removeSellAsset: (asset: WalletAsset) => void
reset: () => void reset: () => void
...@@ -16,12 +17,14 @@ interface SellAssetState { ...@@ -16,12 +17,14 @@ interface SellAssetState {
addMarketplaceWarning: (asset: WalletAsset, warning: ListingWarning) => void addMarketplaceWarning: (asset: WalletAsset, warning: ListingWarning) => void
removeMarketplaceWarning: (asset: WalletAsset, warning: ListingWarning, setGlobalOverride?: boolean) => void removeMarketplaceWarning: (asset: WalletAsset, warning: ListingWarning, setGlobalOverride?: boolean) => void
removeAllMarketplaceWarnings: () => void removeAllMarketplaceWarnings: () => void
toggleShowResolveIssues: () => void
} }
export const useSellAsset = create<SellAssetState>()( export const useSellAsset = create<SellAssetState>()(
devtools( devtools(
(set) => ({ (set) => ({
sellAssets: [], sellAssets: [],
showResolveIssues: false,
selectSellAsset: (asset) => selectSellAsset: (asset) =>
set(({ sellAssets }) => { set(({ sellAssets }) => {
if (sellAssets.length === 0) return { sellAssets: [asset] } if (sellAssets.length === 0) return { sellAssets: [asset] }
...@@ -153,6 +156,11 @@ export const useSellAsset = create<SellAssetState>()( ...@@ -153,6 +156,11 @@ export const useSellAsset = create<SellAssetState>()(
return { sellAssets: assetsCopy } return { sellAssets: assetsCopy }
}) })
}, },
toggleShowResolveIssues: () => {
set(({ showResolveIssues }) => {
return { showResolveIssues: !showResolveIssues }
})
},
}), }),
{ name: 'useSelectAsset' } { name: 'useSelectAsset' }
) )
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment