Skip to content

Commit

Permalink
Improve SlugRelation control based on new SelectControl
Browse files Browse the repository at this point in the history
  • Loading branch information
leonardodino committed Nov 30, 2018
1 parent aa7248d commit c9758d8
Show file tree
Hide file tree
Showing 3 changed files with 78 additions and 73 deletions.
4 changes: 1 addition & 3 deletions packages/netlify-cms-widget-slug-relation/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,11 @@
"webpack-cli": "^3.1.0"
},
"peerDependencies": {
"emotion": "^9.2.6",
"immutable": "^3.7.6",
"lodash": "^4.17.10",
"netlify-cms-ui-default": "^2.0.0",
"prop-types": "^15.5.10",
"react": "^16.4.1",
"react-emotion": "^9.2.5",
"uuid": "^3.1.0"
"react-immutable-proptypes": "^2.1.0"
}
}
143 changes: 75 additions & 68 deletions packages/netlify-cms-widget-slug-relation/src/SlugRelationControl.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
import React, { createRef } from 'react';
import React from 'react';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import uuid from 'uuid/v4';
import { List, Map } from 'immutable';
import { debounce } from 'lodash';
import styled from 'react-emotion';
import AsyncSelect from 'react-select/lib/Async';
import { debounce, castArray } from 'lodash';
import Select from 'react-select/lib/Async';
import BaseOption from 'react-select/lib/components/Option';
import BaseSingleValue from 'react-select/lib/components/SingleValue';
import { colors } from 'netlify-cms-ui-default';
import EntryLoader from './EntryLoader'

const getOptionValue = option => option.value || option.slug || option
const getOptionValue = option => option && (option.value || option.slug || option)

// TODO: add support for setting custom identifier field #1543
// TODO: export a factory to create custom Option UI
Expand All @@ -28,8 +27,8 @@ const toJS = object => {
// TODO: export a factory to create custom Value UI
const ValueRenderer = props => (
<EntryLoader {...props}>
{({isLoading, title, error}) => {
if(isLoading) return '...'
{({isLoading, isFetching, title, error}) => {
if(isLoading || isFetching) return '...'
if(error) return error
return title || 'Unknown Entity'
}}
Expand All @@ -56,37 +55,61 @@ const SingleValue = ({children: entry, ...props}) => (
)

SingleValue.propTypes = {children: PropTypes.object}

const Select = styled(AsyncSelect)`z-index: 2;`
const Wrapper = styled.div`padding: 0 !important;`
const components = {SingleValue, MultiValueLabel, Option}

const styles = {
control: (provided) => ({
...provided,
border: '0px solid transparent',
borderRadius: '3px',
borderTopLeftRadius: '0',
control: styles => ({
...styles,
border: 0,
boxShadow: 'none',
minHeight: '54px',
padding: '9px 0 9px 12px',
}),
option: (styles, state) => ({
...styles,
backgroundColor: state.isSelected
? `${colors.active}`
: state.isFocused
? `${colors.activeBackground}`
: 'transparent',
paddingLeft: '22px',
}),
menu: styles => ({ ...styles, right: 0, zIndex: 2 }),
container: styles => ({ ...styles, padding: '0 !important' }),
indicatorSeparator: (styles, state) =>
state.hasValue && state.selectProps.isClearable
? { ...styles, backgroundColor: `${colors.textFieldBorder}` }
: { display: 'none' },
dropdownIndicator: styles => ({ ...styles, color: `${colors.controlLabel}` }),
clearIndicator: styles => ({ ...styles, color: `${colors.controlLabel}` }),
multiValue: styles => ({
...styles,
backgroundColor: colors.background,
}),
valueContainer: (provided) => ({
...provided,
padding: '8px 14px',
multiValueLabel: styles => ({
...styles,
color: colors.textLead,
fontWeight: 500,
}),
multiValueRemove: styles => ({
...styles,
color: colors.controlLabel,
':hover': {
color: colors.errorText,
backgroundColor: colors.errorBackground,
},
}),
}

const IndicatorSeparator = () => null
const DropdownIndicator = () => null
const entryMapper = ({data, slug}) => ({...data, value: slug, type: 'option'})
const getSlug = value => (value.value || value)
const getSlug = value => value && (value.value || value)

export default class RelationControl extends React.Component {
static defaultProps = {
queryHits: Map(),
}
static propTypes = {
onChange: PropTypes.func.isRequired,
forID: PropTypes.string,
forID: PropTypes.string.isRequired,
queryHits: ImmutablePropTypes.map.isRequired,
value: PropTypes.oneOfType([
ImmutablePropTypes.list.isRequired,
Expand All @@ -101,16 +124,13 @@ export default class RelationControl extends React.Component {
setInactiveStyle: PropTypes.func.isRequired,
}

controlID = uuid();
select = createRef();

componentDidUpdate(prevProps){
if(!this.callback) return
if(
this.props.queryHits !== prevProps.queryHits &&
this.props.queryHits.get(this.controlID)
this.props.queryHits.get(this.props.forID)
){
const queryHits = this.props.queryHits.get(this.controlID, [])
const queryHits = this.props.queryHits.get(this.props.forID, [])
return this.callback(queryHits.map(entryMapper))
}
}
Expand All @@ -120,14 +140,14 @@ export default class RelationControl extends React.Component {
)

loadOptions = debounce((term, callback) => {
const { field } = this.props;
const { field, forID } = this.props;
const collection = field.get('collection');
const searchFields = field.get('searchFields').toJS();
this.callback = value => {
callback(value)
this.callback = null
}
this.props.query(this.controlID, collection, searchFields, term)
this.props.query(forID, collection, searchFields, term)
}, 250);

handleMenuClose = () => {
Expand All @@ -145,48 +165,35 @@ export default class RelationControl extends React.Component {
return {entry: value}
}

shouldComponentUpdate(prevProps){
const current = this.props.queryHits.get(this.controlID);
const previous = prevProps.queryHits.get(this.controlID);
if(current !== previous) return true
if(prevProps.forID !== this.props.forID) return true
if(prevProps.classNameWrapper !== this.props.classNameWrapper) return true
return false
shouldComponentUpdate(){
return true
}

render() {
const {forID, classNameWrapper, value} = this.props;
const {setActiveStyle, setInactiveStyle} = this.props;
const multiple = !!this.props.field.get('multiple');
const defaultValue = toJS(value) || multiple ? [] : '';
const { field, value, forID, classNameWrapper, setActiveStyle, setInactiveStyle } = this.props;
const isMultiple = field.get('multiple', false);
const isClearable = !field.get('required', true) || isMultiple;
const defaultValue = castArray(toJS(value))

return (
<Wrapper className={classNameWrapper}>
<Select
isMulti={multiple}
innerRef={this.select}
defaultValue={defaultValue}
onChange={this.handleChange}
openMenuOnFocus={false}
openMenuOnClick={false}
isClearable={false}
components={{
IndicatorSeparator,
DropdownIndicator,
Option,
MultiValueLabel,
SingleValue,
}}
styles={styles}
cacheOptions={forID}
onFocus={setActiveStyle}
onBlur={setInactiveStyle}
formatOptionLabel={this.formatOptionLabel}
getOptionValue={getOptionValue}
onMenuClose={this.handleMenuClose}
loadOptions={this.loadOptions}
/>
</Wrapper>
<Select
inputId={forID}
className={classNameWrapper}
isMulti={isMultiple}
defaultValue={defaultValue}
onChange={this.handleChange}
openMenuOnClick={false}
isClearable={isClearable}
components={components}
styles={styles}
cacheOptions={forID}
onFocus={setActiveStyle}
onBlur={setInactiveStyle}
formatOptionLabel={this.formatOptionLabel}
getOptionValue={getOptionValue}
onMenuClose={this.handleMenuClose}
loadOptions={this.loadOptions}
/>
);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import React from 'react';
import PropTypes from 'prop-types';
import { WidgetPreviewContainer } from 'netlify-cms-ui-default';

const stringify = value => Array.isArray(value) ? value.join(', ') : value
const SlugRelationPreview = ({ value }) => (
const stringify = (value = null) => Array.isArray(value) ? value.join(', ') : value
const SlugRelationPreview = ({ value } = {}) => (
<WidgetPreviewContainer>{stringify(value)}</WidgetPreviewContainer>
);

Expand Down

0 comments on commit c9758d8

Please sign in to comment.