diff --git a/packages/netlify-cms-core/src/backend.js b/packages/netlify-cms-core/src/backend.js index e71b9ca6..e3ca4faf 100644 --- a/packages/netlify-cms-core/src/backend.js +++ b/packages/netlify-cms-core/src/backend.js @@ -396,7 +396,6 @@ class Backend { const entries = await this.listAllEntries(collection); const hits = fuzzy .filter(searchTerm, entries, { extract: extractSearchFields(searchFields) }) - .filter(entry => entry.score > 5) .sort(sortByScore) .map(f => f.original); return { query: searchTerm, hits }; diff --git a/packages/netlify-cms-core/src/components/Editor/EditorControlPane/Widget.js b/packages/netlify-cms-core/src/components/Editor/EditorControlPane/Widget.js index a01d14ac..e7963e98 100644 --- a/packages/netlify-cms-core/src/components/Editor/EditorControlPane/Widget.js +++ b/packages/netlify-cms-core/src/components/Editor/EditorControlPane/Widget.js @@ -66,8 +66,7 @@ export default class Widget extends Component { return ( this.props.value !== nextProps.value || this.props.classNameWrapper !== nextProps.classNameWrapper || - this.props.hasActiveStyle !== nextProps.hasActiveStyle || - this.props.queryHits !== nextProps.queryHits + this.props.hasActiveStyle !== nextProps.hasActiveStyle ); } diff --git a/packages/netlify-cms-ui-default/src/index.js b/packages/netlify-cms-ui-default/src/index.js index 6293eab4..9241df14 100644 --- a/packages/netlify-cms-ui-default/src/index.js +++ b/packages/netlify-cms-ui-default/src/index.js @@ -16,4 +16,5 @@ export { shadows, borders, transitions, + reactSelectStyles, } from './styles'; diff --git a/packages/netlify-cms-ui-default/src/styles.js b/packages/netlify-cms-ui-default/src/styles.js index 3433db87..343c3f49 100644 --- a/packages/netlify-cms-ui-default/src/styles.js +++ b/packages/netlify-cms-ui-default/src/styles.js @@ -1,6 +1,17 @@ import { css, injectGlobal } from 'react-emotion'; -export { fonts, colorsRaw, colors, lengths, components, buttons, shadows, borders, transitions }; +export { + fonts, + colorsRaw, + colors, + lengths, + components, + buttons, + shadows, + borders, + transitions, + reactSelectStyles, +}; /** * Font Stacks @@ -297,6 +308,49 @@ const components = { `, }; +const reactSelectStyles = { + control: styles => ({ + ...styles, + border: 0, + boxShadow: 'none', + 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, + }), + multiValueLabel: styles => ({ + ...styles, + color: colors.textLead, + fontWeight: 500, + }), + multiValueRemove: styles => ({ + ...styles, + color: colors.controlLabel, + ':hover': { + color: colors.errorText, + backgroundColor: colors.errorBackground, + }, + }), +}; + injectGlobal` *, *:before, *:after { box-sizing: border-box; diff --git a/packages/netlify-cms-widget-relation/package.json b/packages/netlify-cms-widget-relation/package.json index 8dd288a6..b611d85f 100644 --- a/packages/netlify-cms-widget-relation/package.json +++ b/packages/netlify-cms-widget-relation/package.json @@ -21,7 +21,7 @@ "build": "cross-env NODE_ENV=production webpack" }, "dependencies": { - "react-autosuggest": "^9.3.2" + "react-select": "^2.3.0" }, "devDependencies": { "cross-env": "^5.2.0", diff --git a/packages/netlify-cms-widget-relation/src/RelationControl.js b/packages/netlify-cms-widget-relation/src/RelationControl.js index c11eb056..1bc1b14d 100644 --- a/packages/netlify-cms-widget-relation/src/RelationControl.js +++ b/packages/netlify-cms-widget-relation/src/RelationControl.js @@ -1,83 +1,62 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { injectGlobal } from 'react-emotion'; -import Autosuggest from 'react-autosuggest'; -import uuid from 'uuid/v4'; -import { List } from 'immutable'; -import { debounce } from 'lodash'; -import { Loader, components } from 'netlify-cms-ui-default'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import AsyncSelect from 'react-select/lib/Async'; +import { find, isEmpty, last, debounce } from 'lodash'; +import { List, Map, fromJS } from 'immutable'; +import { reactSelectStyles } from 'netlify-cms-ui-default'; -injectGlobal` - .react-autosuggest__container { - position: relative; - } +function optionToString(option) { + return option && option.value ? option.value : ''; +} - .react-autosuggest__suggestions-container { - display: none; +function convertToOption(raw) { + if (typeof raw === 'string') { + return { label: raw, value: raw }; } + return Map.isMap(raw) ? raw.toJS() : raw; +} - .react-autosuggest__container--open .react-autosuggest__suggestions-container { - ${components.dropdownList} - position: absolute; - display: block; - top: 51px; - width: 100%; - z-index: 2; - } +function getSelectedValue({ value, options, isMultiple }) { + if (isMultiple) { + const selectedOptions = List.isList(value) ? value.toJS() : value; - .react-autosuggest__suggestion { - ${components.dropdownItem} - } + if (!selectedOptions || !Array.isArray(selectedOptions)) { + return null; + } - .react-autosuggest__suggestions-list { - margin: 0; - padding: 0; - list-style-type: none; + return selectedOptions + .map(i => options.find(o => o.value === (i.value || i))) + .filter(Boolean) + .map(convertToOption); + } else { + return find(options, ['value', value]) || null; } - - .react-autosuggest__suggestion { - cursor: pointer; - padding: 10px 20px; - } - - .react-autosuggest__suggestion--focused { - background-color: #ddd; - } -`; +} export default class RelationControl extends React.Component { + didInitialSearch = false; + static propTypes = { onChange: PropTypes.func.isRequired, forID: PropTypes.string.isRequired, value: PropTypes.node, - field: PropTypes.node, - isFetching: PropTypes.bool, + field: ImmutablePropTypes.map, fetchID: PropTypes.string, query: PropTypes.func.isRequired, - clearSearch: PropTypes.func.isRequired, queryHits: PropTypes.oneOfType([PropTypes.array, PropTypes.object]), classNameWrapper: PropTypes.string.isRequired, setActiveStyle: PropTypes.func.isRequired, setInactiveStyle: PropTypes.func.isRequired, }; - static defaultProps = { - value: '', - }; - - constructor(props, ctx) { - super(props, ctx); - this.controlID = uuid(); - this.didInitialSearch = false; - } - - componentDidMount() { - const { value, field } = this.props; - if (value) { - const collection = field.get('collection'); - const searchFields = field.get('searchFields').toJS(); - this.props.query(this.controlID, collection, searchFields, value); - } + shouldComponentUpdate(nextProps) { + return ( + this.props.value !== nextProps.value || + this.props.hasActiveStyle !== nextProps.hasActiveStyle || + this.props.queryHits !== nextProps.queryHits || + this.props.metadata !== nextProps.metadata + ); } componentDidUpdate(prevProps) { @@ -85,108 +64,126 @@ export default class RelationControl extends React.Component { * Load extra post data into the store after first query. */ if (this.didInitialSearch) return; - if ( - this.props.queryHits !== prevProps.queryHits && - this.props.queryHits.get && - this.props.queryHits.get(this.controlID) - ) { + const { value, field, forID, queryHits, onChange } = this.props; + + if (queryHits !== prevProps.queryHits && queryHits.get(forID)) { this.didInitialSearch = true; - const suggestion = this.props.queryHits.get(this.controlID); - if (suggestion && suggestion.length === 1) { - const val = this.getSuggestionValue(suggestion[0]); - this.props.onChange(val, { - [this.props.field.get('name')]: { - [this.props.field.get('collection')]: { [val]: suggestion[0].data }, - }, + const valueField = field.get('valueField'); + const hits = queryHits.get(forID); + if (value) { + const listValue = List.isList(value) ? value : List([value]); + listValue.forEach(val => { + const hit = hits.find(i => i.data[valueField] === val); + if (hit) { + onChange(value, { + [field.get('name')]: { + [field.get('collection')]: { [val]: hit.data }, + }, + }); + } }); } } } - onChange = (event, { newValue }) => { - this.props.onChange(newValue); + handleChange = selectedOption => { + const { onChange, field } = this.props; + let value; + + if (Array.isArray(selectedOption)) { + value = selectedOption.map(optionToString); + onChange(fromJS(value), { + [field.get('name')]: { + [field.get('collection')]: { + [last(value)]: !isEmpty(selectedOption) && last(selectedOption).data, + }, + }, + }); + } else { + value = optionToString(selectedOption); + onChange(value, { + [field.get('name')]: { + [field.get('collection')]: { [value]: selectedOption.data }, + }, + }); + } }; - onSuggestionSelected = (event, { suggestion }) => { - const value = this.getSuggestionValue(suggestion); - this.props.onChange(value, { - [this.props.field.get('name')]: { - [this.props.field.get('collection')]: { [value]: suggestion.data }, - }, + parseHitOptions = hits => { + const { field } = this.props; + const valueField = field.get('valueField'); + const displayField = field.get('displayFields') || field.get('valueField'); + + return hits.map(hit => { + return { + data: hit.data, + value: hit.data[valueField], + label: List.isList(displayField) + ? displayField + .toJS() + .map(key => hit.data[key]) + .join(' ') + : hit.data[displayField], + }; }); }; - onSuggestionsFetchRequested = debounce(({ value }) => { - if (value.length < 2) return; - const { field } = this.props; + loadOptions = debounce((term, callback) => { + const { field, query, forID } = this.props; const collection = field.get('collection'); const searchFields = field.get('searchFields').toJS(); - this.props.query(this.controlID, collection, searchFields, value); + + query(forID, collection, searchFields, term).then(({ payload }) => { + let options = this.parseHitOptions(payload.response.hits); + + if (!this.allOptions && !term) { + this.allOptions = options; + } + + if (!term) { + options = options.slice(0, 20); + } + + callback(options); + }); }, 500); - onSuggestionsClearRequested = () => { - this.props.clearSearch(); - }; - - getSuggestionValue = suggestion => { - const { field } = this.props; - const valueField = field.get('valueField'); - return suggestion.data[valueField]; - }; - - renderSuggestion = suggestion => { - const { field } = this.props; - const valueField = field.get('displayFields') || field.get('valueField'); - if (List.isList(valueField)) { - return ( - - {valueField.toJS().map(key => ( - {new String(suggestion.data[key])} - ))} - - ); - } - return {new String(suggestion.data[valueField])}; - }; - render() { const { value, - isFetching, - fetchID, + field, forID, - queryHits, classNameWrapper, setActiveStyle, setInactiveStyle, + queryHits, } = this.props; + const isMultiple = field.get('multiple', false); + const isClearable = !field.get('required', true) || isMultiple; - const inputProps = { - placeholder: '', - value: value || '', - onChange: this.onChange, - id: forID, - className: classNameWrapper, - onFocus: setActiveStyle, - onBlur: setInactiveStyle, - }; - - const suggestions = queryHits.get ? queryHits.get(this.controlID, []) : []; + const hits = queryHits.get(forID, []); + const options = this.allOptions || this.parseHitOptions(hits); + const selectedValue = getSelectedValue({ + options, + value, + isMultiple, + }); return ( -
- - -
+ ); } } diff --git a/packages/netlify-cms-widget-relation/src/__tests__/relation.spec.js b/packages/netlify-cms-widget-relation/src/__tests__/relation.spec.js new file mode 100644 index 00000000..764915df --- /dev/null +++ b/packages/netlify-cms-widget-relation/src/__tests__/relation.spec.js @@ -0,0 +1,207 @@ +import React from 'react'; +import { fromJS, Map } from 'immutable'; +import { last } from 'lodash'; +import { render, fireEvent, wait } from 'react-testing-library'; +import 'react-testing-library/cleanup-after-each'; +import 'jest-dom/extend-expect'; +import { RelationControl } from '../'; + +const fieldConfig = { + name: 'post', + collection: 'posts', + displayFields: ['title', 'slug'], + searchFields: ['title', 'body'], + valueField: 'title', +}; + +const generateHits = length => { + const hits = Array.from({ length }, (val, idx) => { + const title = `Post # ${idx + 1}`; + const slug = `post-number-${idx + 1}`; + return { collection: 'posts', data: { title, slug } }; + }); + + return [ + ...hits, + { + collection: 'posts', + data: { title: 'YAML post', slug: 'post-yaml', body: 'Body yaml' }, + }, + ]; +}; + +class RelationController extends React.Component { + state = { + value: this.props.value, + queryHits: Map(), + }; + + handleOnChange = jest.fn(value => { + this.setState({ ...this.state, value }); + }); + + setQueryHits = jest.fn(hits => { + const queryHits = Map().set('relation-field', hits); + this.setState({ ...this.state, queryHits }); + }); + + query = jest.fn((...args) => { + const queryHits = generateHits(25); + if (last(args) === 'YAML') { + return Promise.resolve({ payload: { response: { hits: [last(queryHits)] } } }); + } + return Promise.resolve({ payload: { response: { hits: queryHits } } }); + }); + + render() { + return this.props.children({ + value: this.state.value, + handleOnChange: this.handleOnChange, + query: this.query, + queryHits: this.state.queryHits, + setQueryHits: this.setQueryHits, + }); + } +} + +function setup({ field, value }) { + let renderArgs; + const setActiveSpy = jest.fn(); + const setInactiveSpy = jest.fn(); + + const helpers = render( + + {({ handleOnChange, value, query, queryHits, setQueryHits }) => { + renderArgs = { value, onChangeSpy: handleOnChange, setQueryHitsSpy: setQueryHits }; + return ( + + ); + }} + , + ); + + const input = helpers.container.querySelector('input'); + + return { + ...helpers, + ...renderArgs, + setActiveSpy, + setInactiveSpy, + input, + }; +} + +describe('Relation widget', () => { + it('should list the first 20 option hits on initial load', async () => { + const field = fromJS(fieldConfig); + const { getAllByRole, input } = setup({ field }); + + await wait(() => { + fireEvent.keyDown(input, { key: 'ArrowDown' }); + expect(getAllByRole('option')).toHaveLength(20); + }); + }); + + it('should update option list based on search term', async () => { + const field = fromJS(fieldConfig); + const { getAllByRole, getByText, input } = setup({ field }); + + await wait(() => { + fireEvent.change(input, { target: { value: 'YAML' } }); + expect(getAllByRole('option')).toHaveLength(1); + expect(getByText('YAML post post-yaml')).toBeInTheDocument(); + }); + }); + + it('should call onChange with correct selectedItem value and metadata', async () => { + const field = fromJS(fieldConfig); + const { getByText, input, onChangeSpy } = setup({ field }); + const value = 'Post # 1'; + const label = 'Post # 1 post-number-1'; + const metadata = { + post: { posts: { 'Post # 1': { title: 'Post # 1', slug: 'post-number-1' } } }, + }; + + await wait(() => { + fireEvent.keyDown(input, { key: 'ArrowDown' }); + fireEvent.click(getByText(label)); + expect(onChangeSpy).toHaveBeenCalledTimes(1); + expect(onChangeSpy).toHaveBeenCalledWith(value, metadata); + }); + }); + + it('should update metadata for initial preview', async () => { + const field = fromJS(fieldConfig); + const value = 'Post # 1'; + const { getByText, onChangeSpy, setQueryHitsSpy } = setup({ field, value }); + const label = 'Post # 1 post-number-1'; + const metadata = { + post: { posts: { 'Post # 1': { title: 'Post # 1', slug: 'post-number-1' } } }, + }; + + setQueryHitsSpy(generateHits(1)); + + await wait(() => { + expect(getByText(label)).toBeInTheDocument(); + expect(onChangeSpy).toHaveBeenCalledTimes(1); + expect(onChangeSpy).toHaveBeenCalledWith(value, metadata); + }); + }); + + describe('with multiple', () => { + it('should call onChange with correct selectedItem value and metadata', async () => { + const field = fromJS({ ...fieldConfig, multiple: true }); + const { getByText, input, onChangeSpy } = setup({ field }); + const metadata1 = { + post: { posts: { 'Post # 1': { title: 'Post # 1', slug: 'post-number-1' } } }, + }; + const metadata2 = { + post: { posts: { 'Post # 2': { title: 'Post # 2', slug: 'post-number-2' } } }, + }; + + await wait(() => { + fireEvent.keyDown(input, { key: 'ArrowDown' }); + fireEvent.click(getByText('Post # 1 post-number-1')); + fireEvent.keyDown(input, { key: 'ArrowDown' }); + fireEvent.click(getByText('Post # 2 post-number-2')); + + expect(onChangeSpy).toHaveBeenCalledTimes(2); + expect(onChangeSpy).toHaveBeenCalledWith(fromJS(['Post # 1']), metadata1); + expect(onChangeSpy).toHaveBeenCalledWith(fromJS(['Post # 1', 'Post # 2']), metadata2); + }); + }); + + it('should update metadata for initial preview', async () => { + const field = fromJS({ ...fieldConfig, multiple: true }); + const value = fromJS(['Post # 1', 'Post # 2']); + const { getByText, onChangeSpy, setQueryHitsSpy } = setup({ field, value }); + const metadata1 = { + post: { posts: { 'Post # 1': { title: 'Post # 1', slug: 'post-number-1' } } }, + }; + const metadata2 = { + post: { posts: { 'Post # 2': { title: 'Post # 2', slug: 'post-number-2' } } }, + }; + + setQueryHitsSpy(generateHits(2)); + + await wait(() => { + expect(getByText('Post # 1 post-number-1')).toBeInTheDocument(); + expect(getByText('Post # 2 post-number-2')).toBeInTheDocument(); + + expect(onChangeSpy).toHaveBeenCalledTimes(2); + expect(onChangeSpy).toHaveBeenCalledWith(value, metadata1); + expect(onChangeSpy).toHaveBeenCalledWith(value, metadata2); + }); + }); + }); +}); diff --git a/packages/netlify-cms-widget-select/src/SelectControl.js b/packages/netlify-cms-widget-select/src/SelectControl.js index f94a5824..9cd42eeb 100644 --- a/packages/netlify-cms-widget-select/src/SelectControl.js +++ b/packages/netlify-cms-widget-select/src/SelectControl.js @@ -4,50 +4,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes'; import { Map, List, fromJS } from 'immutable'; import { find } from 'lodash'; import Select from 'react-select'; -import { colors } from 'netlify-cms-ui-default'; - -const styles = { - control: styles => ({ - ...styles, - border: 0, - boxShadow: 'none', - 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, - }), - multiValueLabel: styles => ({ - ...styles, - color: colors.textLead, - fontWeight: 500, - }), - multiValueRemove: styles => ({ - ...styles, - color: colors.controlLabel, - ':hover': { - color: colors.errorText, - backgroundColor: colors.errorBackground, - }, - }), -}; +import { reactSelectStyles } from 'netlify-cms-ui-default'; function optionToString(option) { return option && option.value ? option.value : null; @@ -60,6 +17,23 @@ function convertToOption(raw) { return Map.isMap(raw) ? raw.toJS() : raw; } +function getSelectedValue({ value, options, isMultiple }) { + if (isMultiple) { + const selectedOptions = List.isList(value) ? value.toJS() : value; + + if (!selectedOptions || !Array.isArray(selectedOptions)) { + return null; + } + + return selectedOptions + .map(i => options.find(o => o.value === (i.value || i))) + .filter(Boolean) + .map(convertToOption); + } else { + return find(options, ['value', value]) || null; + } +} + export default class SelectControl extends React.Component { static propTypes = { onChange: PropTypes.func.isRequired, @@ -96,23 +70,6 @@ export default class SelectControl extends React.Component { } }; - getSelectedValue = ({ value, options, isMultiple }) => { - if (isMultiple) { - const selectedOptions = List.isList(value) ? value.toJS() : value; - - if (!selectedOptions || !Array.isArray(selectedOptions)) { - return null; - } - - return selectedOptions - .map(i => options.find(o => o.value === (i.value || i))) - .filter(Boolean) - .map(convertToOption); - } else { - return find(options, ['value', value]) || null; - } - }; - render() { const { field, value, forID, classNameWrapper, setActiveStyle, setInactiveStyle } = this.props; const fieldOptions = field.get('options'); @@ -124,7 +81,7 @@ export default class SelectControl extends React.Component { } const options = [...fieldOptions.map(convertToOption)]; - const selectedValue = this.getSelectedValue({ + const selectedValue = getSelectedValue({ options, value, isMultiple, @@ -139,7 +96,7 @@ export default class SelectControl extends React.Component { onFocus={setActiveStyle} onBlur={setInactiveStyle} options={options} - styles={styles} + styles={reactSelectStyles} isMulti={isMultiple} isClearable={isClearable} placeholder="" diff --git a/yarn.lock b/yarn.lock index 4daa1faa..72c6681a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7168,6 +7168,7 @@ joi@^9.2.0: jquery@>=1.10.2: version "3.3.1" resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.3.1.tgz#958ce29e81c9790f31be7792df5d4d95fc57fbca" + integrity sha512-Ubldcmxp5np52/ENotGxlLe6aGMvmF4R8S6tZjsP6Knsaxd/xp3Zrh50cG93lR6nPXyUFwzN3ZSOQI0wRJNdGg== js-base64@^2.1.9, js-base64@^2.4.8: version "2.4.8" @@ -9705,24 +9706,6 @@ react-aria-menubutton@^5.1.0: prop-types "^15.6.0" teeny-tap "^0.2.0" -react-autosuggest@^9.3.2: - version "9.3.4" - resolved "https://registry.yarnpkg.com/react-autosuggest/-/react-autosuggest-9.3.4.tgz#e47ff800081b2f7c678165bfb7cc84b07f462336" - integrity sha512-vcAsZw+6zkjimni4aun1tvuzVCGilmFihAgF8yCeVm/p82ssGgtQb0pnNCcEBcPzPTLJjQc2O8dLJidoOyjlcA== - dependencies: - prop-types "^15.5.10" - react-autowhatever "^10.1.0" - shallow-equal "^1.0.0" - -react-autowhatever@^10.1.0: - version "10.1.2" - resolved "https://registry.yarnpkg.com/react-autowhatever/-/react-autowhatever-10.1.2.tgz#200ffc41373b2189e3f6140ac7bdb82363a79fd3" - integrity sha512-+0XgELT1LF7hHEJv5H5Zwkfb4Q1rqmMZZ5U/XJ2J+UcSPRKnG6CqEjXUJ+hYLXDHgvDqwEN5PBdxczD5rHvOuA== - dependencies: - prop-types "^15.5.8" - react-themeable "^1.1.0" - section-iterator "^2.0.0" - react-datetime@^2.11.0: version "2.15.0" resolved "https://registry.yarnpkg.com/react-datetime/-/react-datetime-2.15.0.tgz#a8f7da6c58b6b45dbeea32d4e8485db17614e12c" @@ -9926,6 +9909,19 @@ react-select@^2.1.1: react-input-autosize "^2.2.1" react-transition-group "^2.2.1" +react-select@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/react-select/-/react-select-2.3.0.tgz#990429622445eb2b4a6e985b8069fe7d498cae91" + integrity sha512-CD3jyZs5lwy/CHW3SdYU1d1FtmJxgvVPdKMwEE8dD6MyNANFtvW95P/V20StPsPppFY7oePv/i2Mun2C2+WgTA== + dependencies: + classnames "^2.2.5" + emotion "^9.1.2" + memoize-one "^4.0.0" + prop-types "^15.6.0" + raf "^3.4.0" + react-input-autosize "^2.2.1" + react-transition-group "^2.2.1" + react-sortable-hoc@^0.6.8: version "0.6.8" resolved "https://registry.yarnpkg.com/react-sortable-hoc/-/react-sortable-hoc-0.6.8.tgz#b08562f570d7c41f6e393fca52879d2ebb9118e9" @@ -9977,13 +9973,6 @@ react-textarea-autosize@^7.0.0: dependencies: prop-types "^15.6.0" -react-themeable@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/react-themeable/-/react-themeable-1.1.0.tgz#7d4466dd9b2b5fa75058727825e9f152ba379a0e" - integrity sha1-fURm3ZsrX6dQWHJ4JenxUro3mg4= - dependencies: - object-assign "^3.0.0" - react-toggled@^1.1.2: version "1.2.7" resolved "https://registry.yarnpkg.com/react-toggled/-/react-toggled-1.2.7.tgz#be1b72058358dd1ffe11811e4427e5c9cf140c10" @@ -10779,11 +10768,6 @@ schema-utils@^1.0.0: ajv-errors "^1.0.0" ajv-keywords "^3.1.0" -section-iterator@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/section-iterator/-/section-iterator-2.0.0.tgz#bf444d7afeeb94ad43c39ad2fb26151627ccba2a" - integrity sha1-v0RNev7rlK1Dw5rS+yYVFifMuio= - section-matter@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/section-matter/-/section-matter-1.0.0.tgz#e9041953506780ec01d59f292a19c7b850b84167" @@ -10929,11 +10913,6 @@ sha.js@^2.4.0, sha.js@^2.4.8: inherits "^2.0.1" safe-buffer "^5.0.1" -shallow-equal@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/shallow-equal/-/shallow-equal-1.0.0.tgz#508d1838b3de590ab8757b011b25e430900945f7" - integrity sha1-UI0YOLPeWQq4dXsBGyXkMJAJRfc= - shallowequal@^1.0.2, shallowequal@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/shallowequal/-/shallowequal-1.1.0.tgz#188d521de95b9087404fd4dcb68b13df0ae4e7f8" @@ -12374,10 +12353,12 @@ upath@^1.0.5: uploadcare-widget-tab-effects@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/uploadcare-widget-tab-effects/-/uploadcare-widget-tab-effects-1.3.0.tgz#13a7d58af0a38dca34dac03f90670d5103d17be7" + integrity sha512-6ZJm96K8/fKySU7NU8YNG0ZxyEeSToeTu4+2Iz7YFf6qZgLw3sECn/XudGxN8jJIsHpilhy040pqDzu6A78Mig== uploadcare-widget@^3.6.2: version "3.6.2" resolved "https://registry.yarnpkg.com/uploadcare-widget/-/uploadcare-widget-3.6.2.tgz#621bbef6d9a527a7fdcd6939e574819e6683e717" + integrity sha512-M/nIb88eGgvD5ua28TA9v8pI7922HucOIkLlu8kDcepbG9Psw0EEBQnOgn0eW4lHAMNH1Hs8vg0T33zgsiN4bg== dependencies: jquery ">=1.10.2"