Skip to content

Commit

Permalink
feat(netlify-cms-widget-list): add variable type definitions to list …
Browse files Browse the repository at this point in the history
…widget (#1857)
  • Loading branch information
smnh authored and erquhart committed Dec 27, 2018
1 parent 44b7cdf commit 8ddc168
Show file tree
Hide file tree
Showing 6 changed files with 268 additions and 38 deletions.
26 changes: 26 additions & 0 deletions dev-test/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -195,3 +195,29 @@ collections: # A list of collections the CMS should be able to edit
widget: 'select',
options: ['a', 'b', 'c'],
}
- label: 'Typed List'
name: 'typed_list'
widget: 'list'
types:
- label: 'Type 1 Object'
name: 'type_1_object'
widget: 'object'
fields:
- { label: 'String', name: 'string', widget: 'string' }
- { label: 'Boolean', name: 'boolean', widget: 'boolean' }
- { label: 'Text', name: 'text', widget: 'text' }
- label: 'Type 2 Object'
name: 'type_2_object'
widget: 'object'
fields:
- { label: 'Number', name: 'number', widget: 'number' }
- { label: 'Select', name: 'select', widget: 'select', options: ['a', 'b', 'c'] }
- { label: 'Datetime', name: 'datetime', widget: 'datetime' }
- { label: 'Markdown', name: 'markdown', widget: 'markdown' }
- label: 'Type 3 Object'
name: 'type_3_object'
widget: 'object'
fields:
- { label: 'Date', name: 'date', widget: 'date' }
- { label: 'Image', name: 'image', widget: 'image' }
- { label: 'File', name: 'file', widget: 'file' }
2 changes: 1 addition & 1 deletion dev-test/index.html

Large diffs are not rendered by default.

94 changes: 65 additions & 29 deletions packages/netlify-cms-ui-default/src/ObjectWidgetTopBar.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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" />
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,
};

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>
)}
</TopBarContainer>
);
);
}

ObjectWidgetTopBar.propTypes = {
allowAdd: PropTypes.bool,
onAdd: PropTypes.func,
onCollapseToggle: PropTypes.func,
collapsed: PropTypes.bool,
heading: PropTypes.node,
label: PropTypes.string,
};
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;
71 changes: 63 additions & 8 deletions packages/netlify-cms-widget-list/src/ListControl.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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};
Expand Down Expand Up @@ -57,6 +64,7 @@ const SortableList = SortableContainer(({ items, renderItem }) => {
const valueTypes = {
SINGLE: 'SINGLE',
MULTIPLE: 'MULTIPLE',
MIXED: 'MIXED',
};

export default class ListControl extends React.Component {
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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
Expand All @@ -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 = {
Expand Down Expand Up @@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -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;
Expand All @@ -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}
Expand All @@ -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"
Expand All @@ -312,4 +359,12 @@ export default class ListControl extends React.Component {
/>
);
}

render() {
if (this.getValueType() !== null) {
return this.renderListControl();
} else {
return this.renderInput();
}
}
}
35 changes: 35 additions & 0 deletions packages/netlify-cms-widget-list/src/typedListHelpers.js
Original file line number Diff line number Diff line change
@@ -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;
}
Loading

0 comments on commit 8ddc168

Please sign in to comment.