Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added "types" option to list widget #1857

Merged
merged 8 commits into from
Dec 27, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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