diff --git a/dev-test/config.yml b/dev-test/config.yml index 7a2f4ccc..6eb9615b 100644 --- a/dev-test/config.yml +++ b/dev-test/config.yml @@ -18,7 +18,14 @@ collections: # A list of collections the CMS should be able to edit create: true # Allow users to create new documents in this collection fields: # The fields each document in this collection have - { label: 'Title', name: 'title', widget: 'string', tagname: 'h1' } - - { label: 'Publish Date', name: 'date', widget: 'datetime', dateFormat: 'YYYY-MM-DD', timeFormat: 'HH:mm', format: 'YYYY-MM-DD HH:mm' } + - { + label: 'Publish Date', + name: 'date', + widget: 'datetime', + dateFormat: 'YYYY-MM-DD', + timeFormat: 'HH:mm', + format: 'YYYY-MM-DD HH:mm', + } - label: 'Cover Image' name: 'image' widget: 'image' @@ -92,6 +99,13 @@ collections: # A list of collections the CMS should be able to edit - { label: 'Image', name: 'image', widget: 'image' } - { label: 'File', name: 'file', widget: 'file' } - { label: 'Select', name: 'select', widget: 'select', options: ['a', 'b', 'c'] } + - { + label: 'Select multiple', + name: 'select_multiple', + widget: 'select', + options: ['a', 'b', 'c'], + multiple: true, + } - { label: 'Hidden', name: 'hidden', widget: 'hidden', default: 'hidden' } - label: 'Object' name: 'object' 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 e9740a35..002e0ad0 100644 --- a/packages/netlify-cms-core/src/components/Editor/EditorControlPane/Widget.js +++ b/packages/netlify-cms-core/src/components/Editor/EditorControlPane/Widget.js @@ -1,7 +1,7 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; -import { Map } from 'immutable'; +import { Map, List } from 'immutable'; import ValidationErrorTypes from 'Constants/validationErrorTypes'; const truthy = () => ({ error: false }); @@ -10,7 +10,8 @@ const isEmpty = value => value === null || value === undefined || (value.hasOwnProperty('length') && value.length === 0) || - (value.constructor === Object && Object.keys(value).length === 0); + (value.constructor === Object && Object.keys(value).length === 0) || + (List.isList(value) && value.size === 0); export default class Widget extends Component { static propTypes = { diff --git a/packages/netlify-cms-widget-select/package.json b/packages/netlify-cms-widget-select/package.json index dd657f51..b91bb888 100644 --- a/packages/netlify-cms-widget-select/package.json +++ b/packages/netlify-cms-widget-select/package.json @@ -23,6 +23,8 @@ }, "devDependencies": { "cross-env": "^5.2.0", + "jest-dom": "^2.1.1", + "react-testing-library": "^5.3.0", "webpack": "^4.16.1", "webpack-cli": "^3.1.0" }, diff --git a/packages/netlify-cms-widget-select/src/SelectControl.js b/packages/netlify-cms-widget-select/src/SelectControl.js index 4a3b2e31..3a53dabf 100644 --- a/packages/netlify-cms-widget-select/src/SelectControl.js +++ b/packages/netlify-cms-widget-select/src/SelectControl.js @@ -1,20 +1,20 @@ import React from 'react'; import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; -import { Map } from 'immutable'; +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: provided => ({ - ...provided, + control: styles => ({ + ...styles, border: 0, boxShadow: 'none', padding: '9px 0 9px 12px', }), - option: (provided, state) => ({ - ...provided, + option: (styles, state) => ({ + ...styles, backgroundColor: state.isSelected ? `${colors.active}` : state.isFocused @@ -22,12 +22,44 @@ const styles = { : 'transparent', paddingLeft: '22px', }), - menu: provided => ({ ...provided, right: 0 }), - container: provided => ({ ...provided, padding: '0 !important' }), - indicatorSeparator: () => ({ display: 'none' }), - dropdownIndicator: provided => ({ ...provided, color: `${colors.controlLabel}` }), + 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, + }, + }), }; +function optionToString(option) { + return option && option.value ? option.value : ''; +} + +function convertToOption(raw) { + if (typeof raw === 'string') { + return { label: raw, value: raw }; + } + return Map.isMap(raw) ? raw.toJS() : raw; +} + export default class SelectControl extends React.Component { static propTypes = { onChange: PropTypes.func.isRequired, @@ -49,33 +81,48 @@ export default class SelectControl extends React.Component { }), }; - static defaultProps = { - value: '', + handleChange = selectedOption => { + const { onChange } = this.props; + + if (Array.isArray(selectedOption)) { + onChange(fromJS(selectedOption.map(optionToString))); + } else { + onChange(optionToString(selectedOption)); + } }; - handleChange = selectedOption => { - this.props.onChange(selectedOption['value']); + getSelectedValue = ({ value, options, isMultiple }) => { + if (isMultiple) { + const selectedOptions = List.isList(value) ? value.toJS() : value; + + if (!selectedOptions || !Array.isArray(selectedOptions)) { + return null; + } + + return selectedOptions + .filter(i => options.find(o => o.value === (i.value || i))) + .map(convertToOption); + } else { + return find(options, ['value', value]) || null; + } }; render() { const { field, value, forID, classNameWrapper, setActiveStyle, setInactiveStyle } = this.props; const fieldOptions = field.get('options'); + const isMultiple = field.get('multiple', false); + const isClearable = !field.get('required', true) || isMultiple; if (!fieldOptions) { return
Error rendering select control for {field.get('name')}: No options
; } - const options = [ - ...(field.get('default', false) ? [] : [{ label: '', value: '' }]), - ...fieldOptions.map(option => { - if (typeof option === 'string') { - return { label: option, value: option }; - } - return Map.isMap(option) ? option.toJS() : option; - }), - ]; - - const selectedValue = find(options, ['value', value]); + const options = [...fieldOptions.map(convertToOption)]; + const selectedValue = this.getSelectedValue({ + options, + value, + isMultiple, + }); return (