Skip to content

Commit

Permalink
fix(form): check for invalid image value, provide reset option
Browse files Browse the repository at this point in the history
  • Loading branch information
rexxars committed Aug 13, 2022
1 parent 5e096f1 commit b7b308f
Show file tree
Hide file tree
Showing 3 changed files with 92 additions and 39 deletions.
83 changes: 50 additions & 33 deletions packages/sanity/src/form/inputs/files/ImageInput/ImageInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ import {FormInput} from '../../../FormInput'
import {MemberField, MemberFieldError, MemberFieldSet} from '../../../members'
import {ImageActionsMenu} from './ImageActionsMenu'
import {ImagePreview} from './ImagePreview'
import {isImageSource} from '@sanity/asset-utils'
import {InvalidImageWarning} from './InvalidImageWarning'

export interface Image extends Partial<BaseImage> {
_upload?: UploadState
Expand Down Expand Up @@ -339,6 +341,10 @@ export class ImageInput extends React.PureComponent<ImageInputProps, ImageInputS
this.setState({isStale: true})
}

handleClearField = () => {
this.props.onChange([unset(['asset']), unset(['crop']), unset(['hotspot'])])
}

handleSelectFiles = (files: File[]) => {
const {directUploads, readOnly} = this.props
const {hoveringFiles} = this.state
Expand Down Expand Up @@ -394,27 +400,28 @@ export class ImageInput extends React.PureComponent<ImageInputProps, ImageInputS
const {value, schemaType, readOnly, directUploads, imageUrlBuilder, resolveUploader} =
this.props

if (!value) {
if (!value || !isImageSource(value)) {
return null
}

const {hoveringFiles} = this.state

const acceptedFiles = hoveringFiles.filter((file) => resolveUploader(schemaType, file))
const rejectedFilesCount = hoveringFiles.length - acceptedFiles.length
const imageUrl = imageUrlBuilder
.width(2000)
.fit('max')
.image(value)
.dpr(getDevicePixelRatio())
.auto('format')
.url()

return (
<ImagePreview
drag={!value?._upload && hoveringFiles.length > 0}
isRejected={rejectedFilesCount > 0 || !directUploads}
readOnly={readOnly}
src={imageUrlBuilder
.width(2000)
.fit('max')
.image(value)
.dpr(getDevicePixelRatio())
.auto('format')
.url()}
src={imageUrl}
alt="Preview of uploaded image"
/>
)
Expand Down Expand Up @@ -475,30 +482,36 @@ export class ImageInput extends React.PureComponent<ImageInputProps, ImageInputS

return (
<WithReferencedAsset observeAsset={observeAsset} reference={asset}>
{(assetDocument) => (
<ImageActionsMenu
isMenuOpen={isMenuOpen}
onEdit={this.handleOpenDialog}
showEdit={showAdvancedEditButton}
onMenuOpen={(isOpen) => this.setState({isMenuOpen: isOpen})}
>
<ActionsMenu
onUpload={this.handleSelectFiles}
browse={browseMenuItem}
onReset={this.handleRemoveButtonClick}
downloadUrl={imageUrlBuilder
.image(value.asset!)
.forceDownload(
assetDocument.originalFilename || `download.${assetDocument.extension}`
)
.url()}
copyUrl={imageUrlBuilder.image(value.asset!).url()}
readOnly={readOnly}
directUploads={directUploads}
accept={accept}
/>
</ImageActionsMenu>
)}
{({_id, originalFilename, extension}) => {
let copyUrl: string | undefined
let downloadUrl: string | undefined

if (isImageSource(value)) {
const filename = originalFilename || `download.${extension}`
downloadUrl = imageUrlBuilder.image(_id).forceDownload(filename).url()
copyUrl = imageUrlBuilder.image(_id).url()
}

return (
<ImageActionsMenu
isMenuOpen={isMenuOpen}
onEdit={this.handleOpenDialog}
showEdit={showAdvancedEditButton}
onMenuOpen={(isOpen) => this.setState({isMenuOpen: isOpen})}
>
<ActionsMenu
onUpload={this.handleSelectFiles}
browse={browseMenuItem}
onReset={this.handleRemoveButtonClick}
downloadUrl={downloadUrl}
copyUrl={copyUrl}
readOnly={readOnly}
directUploads={directUploads}
accept={accept}
/>
</ImageActionsMenu>
)
}}
</WithReferencedAsset>
)
}
Expand Down Expand Up @@ -677,12 +690,16 @@ export class ImageInput extends React.PureComponent<ImageInputProps, ImageInputS
}

renderAsset() {
const {value, changed, readOnly, onFocus, onBlur} = this.props
const {value, readOnly, onFocus, onBlur} = this.props

const {hoveringFiles, isStale} = this.state

const hasValueOrUpload = Boolean(value?._upload || value?.asset)

if (value && typeof value.asset !== 'undefined' && !value?._upload && !isImageSource(value)) {
return () => <InvalidImageWarning onClearValue={this.handleClearField} />
}

// todo: convert this to a functional component and use this with useCallback
// it currently has to return a new function on every render in order to pick up state from this component
return (inputProps: InputProps) => (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import {ResetIcon, WarningOutlineIcon} from '@sanity/icons'
import {Card, Flex, Box, Text, Stack, Button} from '@sanity/ui'
import React from 'react'
import styled from 'styled-components'

type Props = {
onClearValue?: () => void
}

const ButtonWrapper = styled(Button)`
width: 100%;
`

export function InvalidImageWarning({onClearValue}: Props) {
return (
<Card tone="caution" padding={4} border radius={2}>
<Flex gap={4} marginBottom={4}>
<Box>
<Text size={1}>
<WarningOutlineIcon />
</Text>
</Box>
<Stack space={3}>
<Text size={1} weight="semibold">
Invalid image value
</Text>
<Text size={1}>
The value of this field is not a valid image. Resetting this field will let you choose a
new image.
</Text>
</Stack>
</Flex>
<ButtonWrapper icon={ResetIcon} text="Reset value" onClick={onClearValue} mode="ghost" />
</Card>
)
}
12 changes: 6 additions & 6 deletions packages/sanity/src/form/inputs/files/common/ActionsMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ interface Props {
onReset: MouseEventHandler<HTMLDivElement>
accept: string
directUploads?: boolean
downloadUrl: string
copyUrl: string
downloadUrl?: string
copyUrl?: string
}

export function ActionsMenu(props: Props) {
Expand All @@ -21,7 +21,7 @@ export function ActionsMenu(props: Props) {
const {push: pushToast} = useToast()

const handleCopyURL = useCallback(() => {
navigator.clipboard.writeText(copyUrl)
navigator.clipboard.writeText(copyUrl || '')
pushToast({closable: true, status: 'success', title: 'The url is copied to the clipboard'})
}, [pushToast, copyUrl])

Expand All @@ -44,9 +44,9 @@ export function ActionsMenu(props: Props) {
/>
{browse}

<MenuDivider />
<MenuItem as="a" icon={DownloadIcon} text="Download" href={downloadUrl} />
<MenuItem icon={ClipboardIcon} text="Copy URL" onClick={handleCopyURL} />
{(downloadUrl || copyUrl) && <MenuDivider />}
{downloadUrl && <MenuItem as="a" icon={DownloadIcon} text="Download" href={downloadUrl} />}
{copyUrl && <MenuItem icon={ClipboardIcon} text="Copy URL" onClick={handleCopyURL} />}

<MenuDivider />
<MenuItem
Expand Down

0 comments on commit b7b308f

Please sign in to comment.