Skip to content

Commit

Permalink
[form-builder] Make it possible to select existing image asset (#230)
Browse files Browse the repository at this point in the history
  • Loading branch information
bjoerge committed Oct 12, 2017
1 parent ad7b49e commit b0b0688
Show file tree
Hide file tree
Showing 7 changed files with 161 additions and 41 deletions.
8 changes: 6 additions & 2 deletions packages/@sanity/components/sanity.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@
"name": "part:@sanity/components/imageinput/fieldset",
"description": "Image input fieldset"
},
{
"name": "part:@sanity/components/imageinput/default",
"description": "Default image input"
},
{
"name": "part:@sanity/components/imageinput/image-select",
"description": "Abstract Image selector. Replaces <Input type=file />"
Expand Down Expand Up @@ -552,8 +556,8 @@
"path": "buttons/styles/InInput.css"
},
{
"implements": "part:@sanity/components/imageinput/fieldset",
"path": "imageinput/ImageInputFieldset.js"
"implements": "part:@sanity/components/imageinput/default",
"path": "imageinput/ImageInput.js"
},
{
"implements": "part:@sanity/components/fileinput/default",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import React from 'react'
import styles from 'part:@sanity/components/imageinput/fieldset-style'
import ProgressCircle from 'part:@sanity/components/progress/circle'
import UploadIcon from 'part:@sanity/base/upload-icon'
import Fieldset from 'part:@sanity/components/fieldsets/default'
import ImageLoader from 'part:@sanity/components/utilities/image-loader'
import HotspotImage from '@sanity/imagetool/HotspotImage'
import ImageSelect from 'part:@sanity/components/imageinput/image-select'
Expand All @@ -20,7 +19,7 @@ const DEFAULT_HOTSPOT = {
y: 0.5
}

export default class ImageInputFieldset extends React.PureComponent {
export default class ImageInput extends React.PureComponent {
static propTypes = {
status: PropTypes.oneOf(['ready', 'complete', 'pending', 'error']),
legend: PropTypes.string,
Expand All @@ -44,7 +43,6 @@ export default class ImageInputFieldset extends React.PureComponent {
}

static defaultProps = {
level: 1,
status: 'ready',
showContent: true
}
Expand All @@ -60,7 +58,6 @@ export default class ImageInputFieldset extends React.PureComponent {
const {
legend,
description,
level,
hotspotImage,
fieldName,
percent,
Expand All @@ -76,7 +73,7 @@ export default class ImageInputFieldset extends React.PureComponent {
}

return (
<Fieldset legend={legend} description={description} level={level} className={`${styles[`level${level}`] || ''}`}>
<div>
<div className={`${styles.grid} ${showContent ? styles.hasContent : styles.noContent}`}>
<div
className={`
Expand Down Expand Up @@ -212,7 +209,7 @@ export default class ImageInputFieldset extends React.PureComponent {
)
}
</div>
</Fieldset>
</div>
)
}
}
2 changes: 1 addition & 1 deletion packages/@sanity/components/src/imageinput/story.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React from 'react'
import {storiesOf, action} from 'part:@sanity/storybook'
import {withKnobs, number, text, boolean, select, object} from 'part:@sanity/storybook/addons/knobs'
import Sanity from 'part:@sanity/storybook/addons/sanity'
import ImageInput from 'part:@sanity/components/imageinput/fieldset'
import ImageInput from 'part:@sanity/components/imageinput/default'
import ImageSelect from 'part:@sanity/components/imageinput/image-select'
import buttonStyles from 'part:@sanity/components/buttons/default-style'
import Button from 'part:@sanity/components/buttons/default'
Expand Down
99 changes: 68 additions & 31 deletions packages/@sanity/form-builder/src/inputs/Image/ImageInput.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
// @flow
import PropTypes from 'prop-types'
// @flow weak
import React from 'react'
import {omit, groupBy, get} from 'lodash'

import Button from 'part:@sanity/components/buttons/default'
import Dialog from 'part:@sanity/components/dialogs/fullscreen'
import ImageInputFieldset from 'part:@sanity/components/imageinput/fieldset'
import DefaultImageInput from 'part:@sanity/components/imageinput/default'
import ImageLoader from 'part:@sanity/components/utilities/image-loader'
import Fieldset from 'part:@sanity/components/fieldsets/default'

import Field from '../Object/Field'
import ImageTool from '@sanity/imagetool'
import HotspotImage from '@sanity/imagetool/HotspotImage'
import {DEFAULT_CROP} from '@sanity/imagetool/constants'
import subscriptionManager from '../../utils/subscriptionManager'
import PatchEvent, {set, setIfMissing, unset} from '../../PatchEvent'
import SelectAsset from './SelectAsset'

const DEFAULT_HOTSPOT = {
height: 1,
Expand All @@ -36,11 +38,12 @@ function getInitialState() {
progress: null,
uploadingImage: null,
materializedImage: null,
isAdvancedEditOpen: false
isAdvancedEditOpen: false,
isSelectAssetOpen: false
}
}

export default class ImageInput extends React.PureComponent {
export default class ImageInput extends React.PureComponent<*> {
_unmounted: boolean

static propTypes = {
Expand Down Expand Up @@ -71,7 +74,7 @@ export default class ImageInput extends React.PureComponent {
const currentRef = get(this.props, 'value.asset')
const nextRef = get(nextProps, 'value.asset')

const shouldUpdate = currentRef !== nextRef && get(currentRef, '_ref') !== get(nextRef, '_ref')
const shouldUpdate = currentRef !== nextRef || get(currentRef, '_ref') !== get(nextRef, '_ref')

if (shouldUpdate) {
this.setState(omit(getInitialState(), 'materializedImage', 'uploadingImage'))
Expand All @@ -95,9 +98,6 @@ export default class ImageInput extends React.PureComponent {
this.setState({materializedImage: null})
return
}
if (this.state.materializedImage && this.state.materializedImage._id === reference._id) {
return
}
const {materializeReferenceFn} = this.props
this.subscriptions.replace('materialize', materializeReferenceFn(reference._ref).subscribe(materialized => {
this.setState({materializedImage: materialized})
Expand Down Expand Up @@ -181,7 +181,7 @@ export default class ImageInput extends React.PureComponent {
})
}

handleFieldChange = (event : PatchEvent, field) => {
handleFieldChange = (event: PatchEvent, field) => {
const {onChange, type} = this.props

onChange(event
Expand Down Expand Up @@ -315,8 +315,37 @@ export default class ImageInput extends React.PureComponent {
})
}

handleOpenSelectAsset = () => {
this.setState({
isSelectAssetOpen: true
})
}

handleCloseSelectAsset = () => {
this.setState({
isSelectAssetOpen: false
})
}

handleSelectAsset = asset => {
const {onChange, type} = this.props
onChange(PatchEvent.from([
setIfMissing({
_type: type.name
}),
set({
_type: 'reference',
_ref: asset._id
}, ['asset'])
]))

this.setState({
isSelectAssetOpen: false
})
}

render() {
const {status, progress, isAdvancedEditOpen} = this.state
const {status, progress, isAdvancedEditOpen, isSelectAssetOpen} = this.state
const {
type,
level,
Expand Down Expand Up @@ -347,27 +376,35 @@ export default class ImageInput extends React.PureComponent {
const hasAdvancedFields = fieldGroups.other.length > 0
const onEdit = hasAdvancedFields ? this.handleOpenAdvancedEdit : null
return (
<ImageInputFieldset
status={status}
legend={type.title}
level={level}
percent={progress && progress.percent}
onSelect={this.handleSelect}
onCancel={this.handleCancel}
onClear={this.handleClearValue}
onEdit={onEdit}
showContent={fieldGroups.highlighted.length > 0}
multiple={false}
accept={accept || 'image/*'}
hotspotImage={{
hotspot: isImageToolEnabled ? (value && value.hotspot) : DEFAULT_HOTSPOT,
crop: isImageToolEnabled ? (value && value.crop) : DEFAULT_CROP,
imageUrl: imageUrl
}}
>
{fieldGroups.highlighted.length > 0 && this.renderFields(fieldGroups.highlighted)}
{isAdvancedEditOpen && this.renderAdvancedEdit(fieldGroups.highlighted.concat(fieldGroups.other))}
</ImageInputFieldset>
<Fieldset legend={type.title} description={type.description} level={level}>
<DefaultImageInput
status={status}
legend={type.title}
level={level}
percent={progress && progress.percent}
onSelect={this.handleSelect}
onCancel={this.handleCancel}
onClear={this.handleClearValue}
onEdit={onEdit}
showContent={fieldGroups.highlighted.length > 0}
multiple={false}
accept={accept || 'image/*'}
hotspotImage={{
hotspot: isImageToolEnabled ? (value && value.hotspot) : DEFAULT_HOTSPOT,
crop: isImageToolEnabled ? (value && value.crop) : DEFAULT_CROP,
imageUrl: imageUrl
}}
>
{fieldGroups.highlighted.length > 0 && this.renderFields(fieldGroups.highlighted)}
{isSelectAssetOpen && (
<Dialog title="Select image" onClose={this.handleCloseSelectAsset} isOpen>
<SelectAsset onSelect={this.handleSelectAsset} />
</Dialog>
)}
{isAdvancedEditOpen && this.renderAdvancedEdit(fieldGroups.highlighted.concat(fieldGroups.other))}
</DefaultImageInput>
<Button onClick={this.handleOpenSelectAsset}>Select from library…</Button>
</Fieldset>
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.item {
composes: listItemStates from 'part:@sanity/base/theme/layout/backgrounds-style';
}
79 changes: 79 additions & 0 deletions packages/@sanity/form-builder/src/inputs/Image/SelectAsset.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// @flow
import React from 'react'
import client from 'part:@sanity/base/client'
import {List as GridList, Item as GridListItem} from 'part:@sanity/components/lists/grid'
import Button from 'part:@sanity/components/buttons/default'
import styles from './SelectAsset.css'

const PER_PAGE = 200

type Asset = {
_id: string,
url: string
}
type State = {
assets: Array<Asset>,
isLastPage: boolean
}
type Props = {
onSelect: Asset => void
}
export default class SelectAsset extends React.Component<Props, State> {

state = {
assets: [],
isLastPage: false
}

pageNo: number
pageNo = 0

fetchPage(pageNo: number) {
const start = pageNo * PER_PAGE
const end = start + PER_PAGE
const slice = `${start}...${end}`
return client.fetch(`*[_type == "sanity.imageAsset"][${slice}] {_id, url}`).then(result => {
this.setState(prevState => ({
isLastPage: result.length === 0,
assets: prevState.assets.concat(result)
}))
})
}

componentDidMount() {
this.fetchPage(this.pageNo)
}

handleSelectItem = (event: SyntheticEvent<*>) => {
const assetId = event.currentTarget.getAttribute('data-id')
const selected = this.state.assets.find(doc => doc._id === assetId)
if (selected) {
this.props.onSelect(selected)
}
}
fetchNextPage = () => {
this.fetchPage(++this.pageNo)
}

render() {
const {assets, isLastPage} = this.state
return (
<div>
<GridList>
{assets.map(asset => {
return (
<GridListItem className={styles.item} key={asset._id} data-id={asset._id} onClick={this.handleSelectItem}>
<div>
<img src={`${asset.url}?w=100`} />
</div>
</GridListItem>
)
})}
</GridList>
{isLastPage
? <span>Nothing more to load…</span>
: <Button onClick={this.fetchNextPage}>Load more…</Button>}
</div>
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import ImageInput from './ImageInput'
import {omit} from 'lodash'

export default function createImageInput({upload, materializeReference}) {
return class CustomImageInput extends React.PureComponent {
return class CustomImageInput extends React.Component {
static propTypes = omit(ImageInput.propTypes, 'uploadFn', 'materializeReferenceFn');
static valueContainer = ImageInput.valueContainer;

Expand Down

0 comments on commit b0b0688

Please sign in to comment.