feat(netlify-cms-widget-list): add variable type definitions to list widget (#1857)
This commit is contained in:
committed by
Shawn Erquhart
parent
44b7cdf9f8
commit
8ddc168197
@ -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;
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
35
packages/netlify-cms-widget-list/src/typedListHelpers.js
Normal file
35
packages/netlify-cms-widget-list/src/typedListHelpers.js
Normal 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;
|
||||
}
|
Reference in New Issue
Block a user