feat(netlify-cms-widget-list): add variable type definitions to list widget (#1857)

This commit is contained in:
Simon Hanukaev
2018-12-27 06:51:35 +02:00
committed by Shawn Erquhart
parent 44b7cdf9f8
commit 8ddc168197
6 changed files with 269 additions and 39 deletions

View File

@ -3,6 +3,8 @@ import PropTypes from 'prop-types';
import styled, { css } from 'react-emotion';
import Icon from './Icon';
import { colors, buttons } from './styles';
import Dropdown, { StyledDropdownButton, DropdownItem } from './Dropdown';
import ImmutablePropTypes from 'react-immutable-proptypes';
const TopBarContainer = styled.div`
align-items: center;
@ -51,36 +53,70 @@ const AddButton = styled.button`
}
`;
const ObjectWidgetTopBar = ({
allowAdd,
onAdd,
onCollapseToggle,
collapsed,
heading = null,
label,
}) => (
<TopBarContainer>
<ExpandButtonContainer hasHeading={!!heading}>
<ExpandButton onClick={onCollapseToggle}>
<Icon type="chevron" direction={collapsed ? 'right' : 'down'} size="small" />
</ExpandButton>
{heading}
</ExpandButtonContainer>
{!allowAdd ? null : (
<AddButton onClick={onAdd}>
Add {label} <Icon type="add" size="xsmall" />
</AddButton>
)}
</TopBarContainer>
);
class ObjectWidgetTopBar extends React.Component {
static propTypes = {
allowAdd: PropTypes.bool,
types: ImmutablePropTypes.list,
onAdd: PropTypes.func,
onAddType: PropTypes.func,
onCollapseToggle: PropTypes.func,
collapsed: PropTypes.bool,
heading: PropTypes.node,
label: PropTypes.string,
};
ObjectWidgetTopBar.propTypes = {
allowAdd: PropTypes.bool,
onAdd: PropTypes.func,
onCollapseToggle: PropTypes.func,
collapsed: PropTypes.bool,
heading: PropTypes.node,
label: PropTypes.string,
};
renderAddUI() {
if (!this.props.allowAdd) {
return null;
}
if (this.props.types && this.props.types.size > 0) {
return this.renderTypesDropdown(this.props.types);
} else {
return this.renderAddButton();
}
}
renderTypesDropdown(types) {
return (
<Dropdown
renderButton={() => (
<StyledDropdownButton>Add {this.props.label} item</StyledDropdownButton>
)}
>
{types.map((type, idx) => (
<DropdownItem
key={idx}
label={type.get('label', type.get('name'))}
onClick={() => this.props.onAddType(type.get('name'))}
/>
))}
</Dropdown>
);
}
renderAddButton() {
return (
<AddButton onClick={this.props.onAdd}>
Add {this.props.label} <Icon type="add" size="xsmall" />
</AddButton>
);
}
render() {
const { onCollapseToggle, collapsed, heading = null } = this.props;
return (
<TopBarContainer>
<ExpandButtonContainer hasHeading={!!heading}>
<ExpandButton onClick={onCollapseToggle}>
<Icon type="chevron" direction={collapsed ? 'right' : 'down'} size="small" />
</ExpandButton>
{heading}
</ExpandButtonContainer>
{this.renderAddUI()}
</TopBarContainer>
);
}
}
export default ObjectWidgetTopBar;

View File

@ -6,6 +6,12 @@ import { List, Map } from 'immutable';
import { partial } from 'lodash';
import { SortableContainer, SortableElement, SortableHandle } from 'react-sortable-hoc';
import { ObjectControl } from 'netlify-cms-widget-object';
import {
TYPES_KEY,
getTypedFieldForValue,
resolveFieldKeyType,
getErrorMessageForTypedFieldAndValue,
} from './typedListHelpers';
import {
ListItemTopBar,
ObjectWidgetTopBar,
@ -29,6 +35,7 @@ const StyledListItemTopBar = styled(ListItemTopBar)`
const NestedObjectLabel = styled.div`
display: ${props => (props.collapsed ? 'block' : 'none')};
border-top: 0;
color: ${props => (props.error ? colors.errorText : 'inherit')};
background-color: ${colors.textFieldBorder};
padding: 13px;
border-radius: 0 0 ${lengths.borderRadius} ${lengths.borderRadius};
@ -57,6 +64,7 @@ const SortableList = SortableContainer(({ items, renderItem }) => {
const valueTypes = {
SINGLE: 'SINGLE',
MULTIPLE: 'MULTIPLE',
MIXED: 'MIXED',
};
export default class ListControl extends React.Component {
@ -101,6 +109,8 @@ export default class ListControl extends React.Component {
return valueTypes.MULTIPLE;
} else if (field.get('field')) {
return valueTypes.SINGLE;
} else if (field.get(TYPES_KEY)) {
return valueTypes.MIXED;
} else {
return null;
}
@ -151,6 +161,13 @@ export default class ListControl extends React.Component {
onChange((value || List()).push(parsedValue));
};
handleAddType = (type, typeKey) => {
const { value, onChange } = this.props;
let parsedValue = Map().set(typeKey, type);
this.setState({ itemsCollapsed: this.state.itemsCollapsed.push(false) });
onChange((value || List()).push(parsedValue));
};
/**
* In case the `onChangeObject` function is frozen by a child widget implementation,
* e.g. when debounced, always get the latest object value instead of using
@ -163,7 +180,7 @@ export default class ListControl extends React.Component {
const { value, metadata, onChange, field } = this.props;
const collectionName = field.get('name');
const newObjectValue =
this.getValueType() === valueTypes.MULTIPLE
this.getValueType() !== valueTypes.SINGLE
? this.getObjectValue(index).set(fieldName, newValue)
: newValue;
const parsedMetadata = {
@ -208,6 +225,9 @@ export default class ListControl extends React.Component {
objectLabel(item) {
const { field } = this.props;
if (this.getValueType() === valueTypes.MIXED) {
return getTypedFieldForValue(field, item).get('label', field.get('name'));
}
const multiFields = field.get('fields');
const singleField = field.get('field');
const labelField = (multiFields && multiFields.first()) || singleField;
@ -233,9 +253,17 @@ export default class ListControl extends React.Component {
};
renderItem = (item, index) => {
const { field, classNameWrapper, editorControl, resolveWidget } = this.props;
const { classNameWrapper, editorControl, resolveWidget } = this.props;
const { itemsCollapsed } = this.state;
const collapsed = itemsCollapsed.get(index);
let field = this.props.field;
if (this.getValueType() === valueTypes.MIXED) {
field = getTypedFieldForValue(field, item);
if (!field) {
return this.renderErroneousTypedItem(index, item);
}
}
return (
<SortableListItem
@ -263,6 +291,27 @@ export default class ListControl extends React.Component {
);
};
renderErroneousTypedItem(index, item) {
const field = this.props.field;
const errorMessage = getErrorMessageForTypedFieldAndValue(field, item);
return (
<SortableListItem
className={cx(styles.listControlItem, styles.listControlItemCollapsed)}
index={index}
key={`item-${index}`}
>
<StyledListItemTopBar
onCollapseToggle={null}
onRemove={partial(this.handleRemove, index)}
dragHandleHOC={SortableHandle}
/>
<NestedObjectLabel collapsed={true} error={true}>
{errorMessage}
</NestedObjectLabel>
</SortableListItem>
);
}
renderListControl() {
const { value, forID, field, classNameWrapper } = this.props;
const { itemsCollapsed } = this.state;
@ -276,6 +325,8 @@ export default class ListControl extends React.Component {
<ObjectWidgetTopBar
allowAdd={field.get('allow_add', true)}
onAdd={this.handleAdd}
types={field.get(TYPES_KEY, null)}
onAddType={type => this.handleAddType(type, resolveFieldKeyType(field))}
heading={`${items.size} ${listLabel}`}
label={labelSingular.toLowerCase()}
onCollapseToggle={this.handleCollapseAllToggle}
@ -292,14 +343,10 @@ export default class ListControl extends React.Component {
);
}
render() {
const { field, forID, classNameWrapper } = this.props;
renderInput() {
const { forID, classNameWrapper } = this.props;
const { value } = this.state;
if (field.get('field') || field.get('fields')) {
return this.renderListControl();
}
return (
<input
type="text"
@ -312,4 +359,12 @@ export default class ListControl extends React.Component {
/>
);
}
render() {
if (this.getValueType() !== null) {
return this.renderListControl();
} else {
return this.renderInput();
}
}
}

View File

@ -0,0 +1,35 @@
export const TYPES_KEY = 'types';
export const TYPE_KEY = 'typeKey';
export const DEFAULT_TYPE_KEY = 'type';
export function getTypedFieldForValue(field, value) {
const typeKey = resolveFieldKeyType(field);
const types = field.get(TYPES_KEY);
const valueType = value.get(typeKey);
return types.find(type => type.get('name') === valueType);
}
export function resolveFunctionForTypedField(field) {
const typeKey = resolveFieldKeyType(field);
const types = field.get(TYPES_KEY);
return value => {
const valueType = value.get(typeKey);
return types.find(type => type.get('name') === valueType);
};
}
export function resolveFieldKeyType(field) {
return field.get(TYPE_KEY, DEFAULT_TYPE_KEY);
}
export function getErrorMessageForTypedFieldAndValue(field, value) {
const keyType = resolveFieldKeyType(field);
const type = value.get(keyType);
let errorMessage;
if (!type) {
errorMessage = `Error: item has no '${keyType}' property`;
} else {
errorMessage = `Error: item has illegal '${keyType}' property: '${type}'`;
}
return errorMessage;
}