Skip to content

Commit

Permalink
[field] Provide loading and error states for image diff
Browse files Browse the repository at this point in the history
  • Loading branch information
rexxars committed Oct 6, 2020
1 parent 6f68602 commit 174c546
Show file tree
Hide file tree
Showing 7 changed files with 81 additions and 83 deletions.
1 change: 1 addition & 0 deletions packages/@sanity/field/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"access": "public"
},
"dependencies": {
"@sanity/asset-utils": "^1.1.0",
"@sanity/base": "1.150.7",
"@sanity/components": "1.150.7",
"@sanity/diff": "1.150.7",
Expand Down
10 changes: 5 additions & 5 deletions packages/@sanity/field/src/types/image/diff/HotspotCropSVG.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import {Image, ImageCrop, ImageHotspot} from '@sanity/types'
import React from 'react'
import {DiffAnnotationTooltip, ObjectDiff, useDiffAnnotationColor} from '../../../diff'
import {hexToRgba} from './helpers'
import {Crop, Hotspot, Image} from './types'

interface HotspotCropSVGProps {
crop?: Crop
crop?: ImageCrop
diff: ObjectDiff<Image>
hash: string
hotspot?: Hotspot
hotspot?: ImageHotspot
width?: number
height?: number
}
Expand Down Expand Up @@ -79,7 +79,7 @@ function CropSVG({
width,
height,
...restProps
}: {crop: Crop; width: number; height: number} & Omit<
}: {crop: ImageCrop; width: number; height: number} & Omit<
React.SVGProps<SVGRectElement>,
'width' | 'height'
>) {
Expand All @@ -99,7 +99,7 @@ function HotspotSVG({
width,
height,
...restProps
}: {hotspot: Hotspot; offset?: number; width: number; height: number} & Omit<
}: {hotspot: ImageHotspot; offset?: number; width: number; height: number} & Omit<
React.SVGProps<SVGEllipseElement>,
'width' | 'height'
>) {
Expand Down
21 changes: 9 additions & 12 deletions packages/@sanity/field/src/types/image/diff/ImageFieldDiff.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import {Image} from '@sanity/types'
import * as React from 'react'
import {
DiffComponent,
Expand All @@ -7,11 +8,9 @@ import {
ChangeList,
getAnnotationAtPath
} from '../../../diff'
import {useRefValue} from '../../../diff/hooks'
import {ChangeArrow} from '../../../diff/components'
import ImagePreview from './ImagePreview'
import styles from './ImageFieldDiff.css'
import {Image, SanityImageAsset} from './types'

const IMAGE_META_FIELDS = ['crop', 'hotspot']
const BASE_IMAGE_FIELDS = ['asset', ...IMAGE_META_FIELDS]
Expand All @@ -20,14 +19,12 @@ export const ImageFieldDiff: DiffComponent<ObjectDiff<Image>> = ({diff, schemaTy
const {fromValue, toValue, fields, isChanged} = diff
const fromRef = fromValue?.asset?._ref
const toRef = toValue?.asset?._ref
const prev = useRefValue<SanityImageAsset>(fromRef)
const next = useRefValue<SanityImageAsset>(toRef)
const assetAnnotation = getAnnotationAtPath(diff, ['asset', '_ref'])

if (!isChanged) {
return next ? (
return toRef ? (
<DiffAnnotationCard className={styles.annotation} annotation={assetAnnotation}>
<ImagePreview is="to" asset={next} diff={diff} />
<ImagePreview id={toRef} is="to" diff={diff} />
</DiffAnnotationCard>
) : null
}
Expand Down Expand Up @@ -56,25 +53,25 @@ export const ImageFieldDiff: DiffComponent<ObjectDiff<Image>> = ({diff, schemaTy
const showMetaChange = didMetaChange && !didAssetChange

const imageDiff = (
<div className={styles.imageDiff} data-diff-layout={prev && next ? 'double' : 'single'}>
{prev && fromValue && (
<div className={styles.imageDiff} data-diff-layout={fromRef && toRef ? 'double' : 'single'}>
{fromValue && fromRef && (
<DiffAnnotationCard className={styles.annotation} annotation={assetAnnotation}>
<ImagePreview
is="from"
asset={prev}
id={fromRef}
diff={diff}
action={assetAction}
hotspot={showMetaChange && didHotspotChange ? fromValue.hotspot : undefined}
crop={showMetaChange && didCropChange ? fromValue.crop : undefined}
/>
</DiffAnnotationCard>
)}
{prev && next && <ChangeArrow />}
{next && toValue && (
{fromRef && toRef && <ChangeArrow />}
{toValue && toRef && (
<DiffAnnotationCard className={styles.annotation} annotation={assetAnnotation}>
<ImagePreview
is="to"
asset={next}
id={toRef}
diff={diff}
hotspot={showMetaChange && didHotspotChange ? toValue.hotspot : undefined}
crop={showMetaChange && didCropChange ? toValue.crop : undefined}
Expand Down
6 changes: 6 additions & 0 deletions packages/@sanity/field/src/types/image/diff/ImagePreview.css
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,12 @@
}
}

.error {
composes: image;
margin: 50px 0;
text-align: center;
}

.hotspotCrop {
display: block;
position: absolute;
Expand Down
61 changes: 44 additions & 17 deletions packages/@sanity/field/src/types/image/diff/ImagePreview.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,39 @@
import React from 'react'
import React, {SyntheticEvent} from 'react'
import {useDocumentValues} from '@sanity/base/hooks'
import {getImageDimensions, isDefaultCrop, isDefaultHotspot} from '@sanity/asset-utils'
import imageUrlBuilder from '@sanity/image-url'
import sanityClient from 'part:@sanity/base/client'
import ImageIcon from 'part:@sanity/base/image-icon'
import {MetaInfo} from '../../../diff'
import {isDefaultCrop, isDefaultHotspot, simpleHash} from './helpers'
import {getDeviceDpr, simpleHash} from './helpers'
import {HotspotCropSVG} from './HotspotCropSVG'
import {ImagePreviewProps} from './types'
import {ImagePreviewProps, MinimalAsset} from './types'

import styles from './ImagePreview.css'

const ASSET_FIELDS = ['originalFilename']
const imageBuilder = imageUrlBuilder(sanityClient)

// To trigger error state, change `src` attribute to random string ("foo")
// To trigger slow loading, use a throttling proxy (charles) or browser devtools

// To trigger deleted state, set `id` to valid, non-existant image asset ID,
// eg: 'image-1217bc35db5030739b7be571c79d3c401551911d-300x200-png'

export default function ImagePreview(props: ImagePreviewProps): React.ReactElement {
const {asset, action, diff, hotspot, crop, is} = props
const title = asset.originalFilename || 'Untitled'
const dimensions = asset.metadata?.dimensions
const {id, action, diff, hotspot, crop, is} = props
const [imageError, setImageError] = React.useState<SyntheticEvent<HTMLImageElement, Event>>()
const {value: asset} = useDocumentValues<MinimalAsset>(id, ASSET_FIELDS)
const dimensions = getImageDimensions(id)

// undefined = still loading, null = its gone
const assetIsDeleted = asset === null

const title = (asset && asset.originalFilename) || 'Untitled'
const imageSource = imageBuilder
.image(asset)
.height(300)
.image(id)
.height(190) // Should match container max-height
.dpr(getDeviceDpr())
.fit('max')

const assetChanged = diff.fromValue?.asset?._ref !== diff.toValue?.asset?._ref
Expand All @@ -29,21 +45,32 @@ export default function ImagePreview(props: ImagePreviewProps): React.ReactEleme
<div className={styles.root}>
<div className={styles.header}>
<div className={imageWrapperClassName}>
<img
className={styles.image}
src={imageSource.toString() || ''}
alt={title}
data-action={metaAction}
/>
{!assetIsDeleted && !imageError && (
<img
className={styles.image}
src={imageSource.toString() || ''}
alt={title}
data-action={metaAction}
onError={setImageError}
width={dimensions.width}
height={dimensions.height}
/>
)}

{(assetIsDeleted || imageError) && (
<div className={styles.error}>
{assetIsDeleted ? 'Image is deleted' : 'Error loading image'}
</div>
)}

<HotspotCropSVG
className={styles.hotspotCrop}
crop={crop && !isDefaultCrop(crop) ? crop : undefined}
diff={diff}
hash={simpleHash(`${imageSource.toString() || ''}-${is}`)}
hotspot={hotspot && !isDefaultHotspot(hotspot) ? hotspot : undefined}
width={dimensions?.width}
height={dimensions?.height}
width={dimensions.width}
height={dimensions.height}
/>
</div>
</div>
Expand All @@ -53,7 +80,7 @@ export default function ImagePreview(props: ImagePreviewProps): React.ReactEleme
<div>{metaAction}</div>
) : (
<div>
{dimensions.height} × {dimensions.width}
{dimensions.width} × {dimensions.height}
</div>
)}
</MetaInfo>
Expand Down
17 changes: 5 additions & 12 deletions packages/@sanity/field/src/types/image/diff/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import {Crop, Hotspot} from './types'

// @todo: use `polished` for this?
export function hexToRgba(hex: string, opacity: number): string {
const rgba = (/^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex) || ([] as string[]))
Expand All @@ -9,16 +7,6 @@ export function hexToRgba(hex: string, opacity: number): string {
return `rgba(${rgba.join(', ')})`
}

export function isDefaultCrop(crop: Crop) {
const {top, right, left, bottom} = crop
return top === 0 && right === 0 && left === 0 && bottom === 0
}

export function isDefaultHotspot(hotspot: Hotspot) {
const {x, y, width, height} = hotspot
return x === 0.5 && y === 0.5 && width === 1 && height === 1
}

// @todo: replace this
export function simpleHash(str: string): string {
let hash = 0
Expand All @@ -39,3 +27,8 @@ export function simpleHash(str: string): string {

return hash.toString()
}

export function getDeviceDpr(): number {
const base = Math.ceil(window.devicePixelRatio || 1)
return Math.min(3, Math.max(1, base))
}
48 changes: 11 additions & 37 deletions packages/@sanity/field/src/types/image/diff/types.ts
Original file line number Diff line number Diff line change
@@ -1,45 +1,19 @@
import {Image, ImageHotspot, ImageCrop, ImageDimensions} from '@sanity/types'
import {ObjectDiff} from '../../../diff'

export interface Hotspot {
height: number
width: number
x: number
y: number
}

export interface Crop {
bottom: number
left: number
right: number
top: number
}

export interface Image {
[key: string]: any
asset?: {_ref: string; _weak?: boolean}
crop?: Crop
hotspot?: Hotspot
export interface ImagePreviewProps {
id: string
diff: ObjectDiff<Image>
hotspot?: ImageHotspot
crop?: ImageCrop
is: 'from' | 'to'
action?: 'changed' | 'added' | 'removed'
}

export interface SanityImageAsset {
_id: string
export interface MinimalAsset {
url: string
path: string
originalFilename?: string
originalFilename: string
metadata: {
dimensions: {
width: number
height: number
aspectRatio: number
}
dimensions: ImageDimensions
}
}

export interface ImagePreviewProps {
diff: ObjectDiff<Image>
asset: SanityImageAsset
hotspot?: Hotspot
crop?: Crop
is: 'from' | 'to'
action?: 'changed' | 'added' | 'removed'
}

0 comments on commit 174c546

Please sign in to comment.