migrate object and list widgets

This commit is contained in:
Shawn Erquhart 2018-07-24 17:13:48 -04:00
parent 2efd09ba94
commit 3f47fe6dbf
29 changed files with 289 additions and 422 deletions

View File

@ -8,13 +8,13 @@ import { DateControl, DatePreview } from 'netlify-cms-widget-date';
import { DateTimeControl, DateTimePreview } from 'netlify-cms-widget-datetime'; import { DateTimeControl, DateTimePreview } from 'netlify-cms-widget-datetime';
import { FileControl, FilePreview } from 'netlify-cms-widget-file'; import { FileControl, FilePreview } from 'netlify-cms-widget-file';
import { ImageControl, ImagePreview } from 'netlify-cms-widget-image'; import { ImageControl, ImagePreview } from 'netlify-cms-widget-image';
import { ListControl, ListPreview } from 'netlify-cms-widget-list';
import { ObjectControl, ObjectPreview } from 'netlify-cms-widget-object';
import { StringControl, StringPreview } from 'netlify-cms-widget-string'; import { StringControl, StringPreview } from 'netlify-cms-widget-string';
// import { NumberControl, NumberPreview } from 'netlify-cms-widget-number'; // import { NumberControl, NumberPreview } from 'netlify-cms-widget-number';
// import { TextControl, TextPreview } from 'netlify-cms-widget-text'; // import { TextControl, TextPreview } from 'netlify-cms-widget-text';
// import { SelectControl, SelectPreview } from 'netlify-cms-widget-select'; // import { SelectControl, SelectPreview } from 'netlify-cms-widget-select';
// import { MarkdownControl, MarkdownPreview } from 'netlify-cms-widget-markdown'; // import { MarkdownControl, MarkdownPreview } from 'netlify-cms-widget-markdown';
// import { ListControl, ListPreview } from 'netlify-cms-widget-list';
// import { ObjectControl, ObjectPreview } from 'netlify-cms-widget-object';
// import { RelationControl, RelationPreview } from 'netlify-cms-widget-relation'; // import { RelationControl, RelationPreview } from 'netlify-cms-widget-relation';
import image from 'netlify-cms-editor-component-image'; import image from 'netlify-cms-editor-component-image';
@ -27,12 +27,12 @@ registerWidget('date', DateControl, DatePreview);
registerWidget('datetime', DateTimeControl, DateTimePreview); registerWidget('datetime', DateTimeControl, DateTimePreview);
registerWidget('file', FileControl, FilePreview); registerWidget('file', FileControl, FilePreview);
registerWidget('image', ImageControl, ImagePreview); registerWidget('image', ImageControl, ImagePreview);
registerWidget('list', ListControl, ListPreview);
registerWidget('object', ObjectControl, ObjectPreview);
registerWidget('string', StringControl, StringPreview); registerWidget('string', StringControl, StringPreview);
// registerWidget('text', TextControl, TextPreview); // registerWidget('text', TextControl, TextPreview);
// registerWidget('number', NumberControl, NumberPreview); // registerWidget('number', NumberControl, NumberPreview);
// registerWidget('list', ListControl, ListPreview);
// registerWidget('markdown', MarkdownControl, MarkdownPreview); // registerWidget('markdown', MarkdownControl, MarkdownPreview);
// registerWidget('select', SelectControl, SelectPreview); // registerWidget('select', SelectControl, SelectPreview);
// registerWidget('object', ObjectControl, ObjectPreview);
// registerWidget('relation', RelationControl, RelationPreview); // registerWidget('relation', RelationControl, RelationPreview);
registerEditorComponent(image); registerEditorComponent(image);

View File

@ -172,6 +172,7 @@ export default class EditorControl extends React.Component {
hasActiveStyle={this.state.styleActive} hasActiveStyle={this.state.styleActive}
setActiveStyle={() => this.setState({ styleActive: true })} setActiveStyle={() => this.setState({ styleActive: true })}
setInactiveStyle={() => this.setState({ styleActive: false })} setInactiveStyle={() => this.setState({ styleActive: false })}
resolveWidget={resolveWidget}
ref={processControlRef && partial(processControlRef, fieldName)} ref={processControlRef && partial(processControlRef, fieldName)}
editorControl={EditorControl} editorControl={EditorControl}
/> />

View File

@ -39,6 +39,7 @@ export default class Widget extends Component {
onAddAsset: PropTypes.func.isRequired, onAddAsset: PropTypes.func.isRequired,
onRemoveInsertedMedia: PropTypes.func.isRequired, onRemoveInsertedMedia: PropTypes.func.isRequired,
getAsset: PropTypes.func.isRequired, getAsset: PropTypes.func.isRequired,
resolveWidget: PropTypes.func.isRequired,
}; };
shouldComponentUpdate(nextProps) { shouldComponentUpdate(nextProps) {
@ -189,7 +190,8 @@ export default class Widget extends Component {
setInactiveStyle, setInactiveStyle,
hasActiveStyle, hasActiveStyle,
editorControl, editorControl,
uniqueFieldId uniqueFieldId,
resolveWidget,
} = this.props; } = this.props;
return React.createElement(controlComponent, { return React.createElement(controlComponent, {
field, field,
@ -213,6 +215,7 @@ export default class Widget extends Component {
setInactiveStyle, setInactiveStyle,
hasActiveStyle, hasActiveStyle,
editorControl, editorControl,
resolveWidget,
}); });
} }
} }

View File

@ -1,7 +0,0 @@
.nc-fileControl-input {
display: none !important;
}
.nc-fileControl-imageUpload {
cursor: pointer;
}

View File

@ -1,5 +0,0 @@
import withMediaControl from 'EditorWidgets/withMedia/withMediaControl';
const FileControl = withMediaControl();
export default FileControl;

View File

@ -1,15 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
export default function FilePreview({ value, getAsset }) {
return (<div className="nc-widgetPreview">
{ value ?
<a href={getAsset(value)}>{ value }</a>
: null}
</div>);
}
FilePreview.propTypes = {
getAsset: PropTypes.func.isRequired,
value: PropTypes.node,
};

View File

@ -1,4 +0,0 @@
.nc-imagePreview-image {
max-width: 100%;
height: auto;
}

View File

@ -1,5 +0,0 @@
import withMediaControl from 'EditorWidgets/withMedia/withMediaControl';
const ImageControl = withMediaControl(true);
export default ImageControl;

View File

@ -1,19 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
export default function ImagePreview({ value, getAsset }) {
return (<div className='nc-widgetPreview'>
{ value ?
<img
src={getAsset(value)}
className='nc-imageWidget-image'
role="presentation"
/>
: null}
</div>);
}
ImagePreview.propTypes = {
getAsset: PropTypes.func.isRequired,
value: PropTypes.node,
};

View File

@ -1,88 +0,0 @@
.nc-listControl {
padding: 0 14px 14px;
&.nc-listControl-collapsed {
padding-bottom: 0;
}
}
.list-item-dragging {
opacity: 0.5;
}
.nc-listControl-topBar {
display: flex;
justify-content: space-between;
align-items: center;
margin: 0 -14px;
background-color: var(--textFieldBorderColor);
padding: 13px;
}
.nc-listControl-addButton {
display: flex;
justify-content: center;
align-items: center;
padding: 2px 12px;
font-size: 12px;
font-weight: bold;
border-radius: 3px;
& .nc-icon {
padding-left: 6px;
}
}
.nc-listControl-listCollapseToggle {
display: flex;
align-items: center;
font-size: 14px;
font-weight: 500;
line-height: 1;
}
.nc-listControl-listCollapseToggleButton{
padding: 4px;
background-color: transparent;
color: inherit;
&:last-of-type {
margin-right: 4px;
}
}
.nc-listControl-item {
margin-top: 18px;
&:first-of-type {
margin-top: 26px;
}
}
.nc-listControl-itemTopBar {
background-color: var(--textFieldBorderColor);
}
.nc-listControl-objectLabel {
display: none;
border-top: 0;
background-color: var(--textFieldBorderColor);
padding: 13px;
border-radius: 0 0 var(--borderRadius) var(--borderRadius);
}
.nc-listControl-objectControl {
padding: 6px 14px 14px;
border-top: 0;
border-top-left-radius: 0;
border-top-right-radius: 0;
}
.nc-listControl-collapsed {
& .nc-listControl-objectLabel {
display: block;
}
& .nc-listControl-objectControl {
display: none;
}
}

View File

@ -1,11 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import ObjectPreview from 'EditorWidgets/Object/ObjectPreview';
const ListPreview = ObjectPreview;
ListPreview.propTypes = {
field: PropTypes.node,
};
export default ListPreview;

View File

@ -1,12 +0,0 @@
.nc-objectControl-root {
padding: 0 14px 14px;
}
.nc-objectControl-topBar {
align-items: center;
background-color: #dfdfe3;
display: flex;
justify-content: space-between;
margin: 0 -14px;
padding: 13px;
}

View File

@ -1,12 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
const ObjectPreview = ({ field }) => (
<div className="nc-widgetPreview">{(field && field.get('fields')) || null}</div>
);
ObjectPreview.propTypes = {
field: PropTypes.node,
};
export default ObjectPreview;

View File

@ -9,18 +9,12 @@ import SelectControl from './Select/SelectControl';
import SelectPreview from './Select/SelectPreview'; import SelectPreview from './Select/SelectPreview';
import MarkdownControl from './Markdown/MarkdownControl'; import MarkdownControl from './Markdown/MarkdownControl';
import MarkdownPreview from './Markdown/MarkdownPreview'; import MarkdownPreview from './Markdown/MarkdownPreview';
import ListControl from './List/ListControl';
import ListPreview from './List/ListPreview';
import ObjectControl from './Object/ObjectControl';
import ObjectPreview from './Object/ObjectPreview';
import RelationControl from './Relation/RelationControl'; import RelationControl from './Relation/RelationControl';
import RelationPreview from './Relation/RelationPreview'; import RelationPreview from './Relation/RelationPreview';
registerWidget('text', TextControl, TextPreview); registerWidget('text', TextControl, TextPreview);
registerWidget('number', NumberControl, NumberPreview); registerWidget('number', NumberControl, NumberPreview);
registerWidget('list', ListControl, ListPreview);
registerWidget('markdown', MarkdownControl, MarkdownPreview); registerWidget('markdown', MarkdownControl, MarkdownPreview);
registerWidget('select', SelectControl, SelectPreview); registerWidget('select', SelectControl, SelectPreview);
registerWidget('object', ObjectControl, ObjectPreview);
registerWidget('relation', RelationControl, RelationPreview); registerWidget('relation', RelationControl, RelationPreview);
registerWidget('unknown', UnknownControl, UnknownPreview); registerWidget('unknown', UnknownControl, UnknownPreview);

View File

@ -1,34 +0,0 @@
.nc-imageControl-content {
display: flex;
}
.nc-imageControl-imageWrapper {
width: 155px;
height: 100px;
margin-right: 20px;
& img {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: var(--borderRadius);
}
}
.nc-imageControl-filename {
display: block;
font-size: 16px;
margin-bottom: 20px;
}
.nc-imageControl-chooseButton,
.nc-imageControl-changeButton {
@apply(--textBadge);
display: block;
}
.nc-imageControl-removeButton {
@apply(--textBadgeDanger);
display: block;
margin-top: 12px;
}

View File

@ -1,120 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import ImmutablePropTypes from "react-immutable-proptypes";
import uuid from 'uuid/v4';
import { truncateMiddle } from 'Lib/textHelper';
const MAX_DISPLAY_LENGTH = 50;
export default function withMediaControl(forImage) {
return class extends React.Component {
static propTypes = {
field: PropTypes.object.isRequired,
getAsset: PropTypes.func.isRequired,
mediaPaths: ImmutablePropTypes.map.isRequired,
onAddAsset: PropTypes.func.isRequired,
onChange: PropTypes.func.isRequired,
onRemoveInsertedMedia: PropTypes.func.isRequired,
onOpenMediaLibrary: PropTypes.func.isRequired,
classNameWrapper: PropTypes.string.isRequired,
value: PropTypes.node,
};
static defaultProps = {
value: '',
};
constructor(props) {
super(props);
this.controlID = uuid();
}
shouldComponentUpdate(nextProps) {
/**
* Always update if the value changes.
*/
if (this.props.value !== nextProps.value) {
return true;
}
/**
* If there is a media path for this control in the state object, and that
* path is different than the value in `nextProps`, update.
*/
const mediaPath = nextProps.mediaPaths.get(this.controlID);
if (mediaPath && (nextProps.value !== mediaPath)) {
return true;
}
return false;
}
componentWillReceiveProps(nextProps) {
const { mediaPaths, value, onRemoveInsertedMedia, onChange } = nextProps;
const mediaPath = mediaPaths.get(this.controlID);
if (mediaPath && mediaPath !== value) {
onChange(mediaPath);
} else if (mediaPath && mediaPath === value) {
onRemoveInsertedMedia(this.controlID);
}
}
handleChange = e => {
const { field, onOpenMediaLibrary} = this.props;
e.preventDefault();
return onOpenMediaLibrary({
controlID: this.controlID,
forImage,
privateUpload: field.get('private'),
});
};
handleRemove = e => {
e.preventDefault();
return this.props.onChange('');
};
renderFileName = () => {
const { value, classNameWrapper } = this.props;
return value ? truncateMiddle(value, MAX_DISPLAY_LENGTH) : null;
};
render() {
const { value, getAsset, onRemoveAsset, classNameWrapper } = this.props;
const fileName = this.renderFileName();
const subject = forImage ? 'image' : 'file';
const article = forImage ? 'an' : 'a';
return (
<div className={`${classNameWrapper} nc-imageControl-imageUpload`}>
<span className="nc-imageControl-message">
{
fileName
? <div className="nc-imageControl-content">
{
forImage
? <div className="nc-imageControl-imageWrapper">
<img src={getAsset(value)}/>
</div>
: null
}
<div>
<span className="nc-imageControl-filename">{fileName}</span>
<button className="nc-imageControl-changeButton" onClick={this.handleChange}>
Choose different {subject}
</button>
<button className="nc-imageControl-removeButton" onClick={this.handleRemove}>
Remove {subject}
</button>
</div>
</div>
: <button className="nc-imageControl-chooseButton" onClick={this.handleChange}>
Choose {article} {subject}
</button>
}
</span>
</div>
);
}
};
};

View File

@ -1,9 +1,18 @@
import React from 'react'; import React from 'react';
import styled from 'react-emotion'; import styled from 'react-emotion';
import Icon from './Icon'; import Icon from './Icon';
import { colors, lengths } from './styles'; import { colors, lengths, buttons } from './styles';
const TopBar = styled.div`
display: flex;
justify-content: space-between;
height: 26px;
border-radius: ${lengths.borderRadius} ${lengths.borderRadius} 0 0;
position: relative;
`
const TopBarButton = styled.button` const TopBarButton = styled.button`
${buttons.button};
color: ${colors.controlLabel}; color: ${colors.controlLabel};
background: transparent; background: transparent;
font-size: 16px; font-size: 16px;
@ -24,8 +33,8 @@ const DragIcon = styled(TopBarButtonSpan)`
cursor: move; cursor: move;
` `
const ListItemTopBar = ({ collapsed, onCollapseToggle, onRemove, dragHandleHOC }) => ( const ListItemTopBar = ({ className, collapsed, onCollapseToggle, onRemove, dragHandleHOC }) => (
<div> <TopBar className={className}>
{ {
onCollapseToggle onCollapseToggle
? <TopBarButton onClick={onCollapseToggle}> ? <TopBarButton onClick={onCollapseToggle}>
@ -47,7 +56,7 @@ const ListItemTopBar = ({ collapsed, onCollapseToggle, onRemove, dragHandleHOC }
</TopBarButton> </TopBarButton>
: null : null
} }
</div> </TopBar>
); );
const StyledListItemTopBar = styled(ListItemTopBar)` const StyledListItemTopBar = styled(ListItemTopBar)`

View File

@ -0,0 +1,67 @@
import React from 'react';
import styled, { css }from 'react-emotion';
import Icon from './Icon';
import { colors, buttons } from './styles';
const TopBarContainer = styled.div`
align-items: center;
background-color: ${colors.textFieldBorder};
display: flex;
justify-content: space-between;
margin: 0 -14px;
padding: 13px;
`
const ExpandButtonContainer = styled.div`
${props => props.hasHeading && css`
display: flex;
align-items: center;
font-size: 14px;
font-weight: 500;
line-height: 1;
`}
`
const ExpandButton = styled.button`
${buttons.button};
padding: 4px;
background-color: transparent;
color: inherit;
&:last-of-type {
margin-right: 4px;
}
`
const AddButton = styled.button`
${buttons.button};
display: flex;
justify-content: center;
align-items: center;
padding: 2px 12px;
font-size: 12px;
font-weight: bold;
border-radius: 3px;
${Icon} {
margin-left: 6px;
}
`
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>
);
export default ObjectWidgetTopBar;

View File

@ -5,6 +5,7 @@ export Loader from './Loader';
export Toggle, { ToggleContainer, ToggleBackground, ToggleHandle } from './Toggle'; export Toggle, { ToggleContainer, ToggleBackground, ToggleHandle } from './Toggle';
export AuthenticationPage from './AuthenticationPage'; export AuthenticationPage from './AuthenticationPage';
export WidgetPreviewContainer from './WidgetPreviewContainer'; export WidgetPreviewContainer from './WidgetPreviewContainer';
export ObjectWidgetTopBar from './ObjectWidgetTopBar';
export { export {
fonts, fonts,
colorsRaw, colorsRaw,

View File

@ -253,6 +253,9 @@ const components = {
font-size: 14px; font-size: 14px;
margin-top: 8px; margin-top: 8px;
`, `,
objectWidgetTopBarContainer: css`
padding: 0 14px 14px;
`,
} }
injectGlobal` injectGlobal`

View File

@ -0,0 +1,39 @@
{
"name": "netlify-cms-widget-list",
"description": "Widget for editing lists in Netlify CMS.",
"version": "2.0.0-alpha.0",
"main": "dist/netlify-cms-widget-list.js",
"license": "MIT",
"keywords": [
"netlify",
"netlify-cms",
"widget",
"list",
"object"
],
"sideEffects": false,
"scripts": {
"watch": "webpack -w",
"build": "webpack"
},
"dependencies": {
"netlify-cms-widget-object": "^2.0.0-alpha.0",
"react-sortable-hoc": "^0.6.8"
},
"devDependencies": {
"webpack": "^4.16.1",
"webpack-cli": "^3.1.0"
},
"peerDependencies": {
"immutable": "^3.7.6",
"lodash": "^4.17.10",
"netlify-cms-ui-default": "^2.0.0-alpha.0",
"prop-types": "^15.5.10",
"react": "^16.4.1",
"react-emotion": "^9.2.6",
"react-immutable-proptypes": "^2.1.0"
},
"localExternals": [
"netlify-cms-widget-object"
]
}

View File

@ -1,47 +1,55 @@
import React, { Component } from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import styled, { cx, css } from 'react-emotion';
import { List, Map } from 'immutable'; import { List, Map } from 'immutable';
import { partial } from 'lodash'; import { partial } from 'lodash';
import c from 'classnames';
import { SortableContainer, SortableElement, SortableHandle } from 'react-sortable-hoc'; import { SortableContainer, SortableElement, SortableHandle } from 'react-sortable-hoc';
import { Icon, ListItemTopBar } from 'netlify-cms-ui-default'; import { ObjectControl } from 'netlify-cms-widget-object';
import ObjectControl from 'EditorWidgets/Object/ObjectControl'; import {
Icon,
function ListItem(props) { ListItemTopBar,
return <div {...props} className={`list-item ${ props.className || '' }`}>{props.children}</div>; ObjectWidgetTopBar,
} colors,
ListItem.propTypes = { lengths,
className: PropTypes.string, components,
children: PropTypes.node, } from 'netlify-cms-ui-default';
};
ListItem.displayName = 'list-item';
function valueToString(value) { function valueToString(value) {
return value ? value.join(',').replace(/,([^\s]|$)/g, ', $1') : ''; return value ? value.join(',').replace(/,([^\s]|$)/g, ', $1') : '';
} }
const ListItem = styled.div();
const SortableListItem = SortableElement(ListItem); const SortableListItem = SortableElement(ListItem);
const TopBar = ({ allowAdd, onAdd, listLabel, onCollapseAllToggle, allItemsCollapsed, itemsCount }) => ( const StyledListItemTopBar = styled(ListItemTopBar)`
<div className="nc-listControl-topBar"> background-color: ${colors.textFieldBorder};
<div className="nc-listControl-listCollapseToggle"> `
<button className="nc-listControl-listCollapseToggleButton" onClick={onCollapseAllToggle}>
<Icon type="chevron" direction={allItemsCollapsed ? 'right' : 'down'} size="small" />
</button>
{itemsCount} {listLabel}
</div>
{ const NestedObjectLabel = styled.div`
allowAdd ? display: ${props => props.collapsed ? 'block' : 'none'};
<button className="nc-listControl-addButton" onClick={onAdd}> border-top: 0;
Add {listLabel} <Icon type="add" size="xsmall" /> background-color: ${colors.textFieldBorder};
</button> padding: 13px;
: border-radius: 0 0 ${lengths.borderRadius} ${lengths.borderRadius};
null `
const styles = {
collapsedObjectControl: css`
display: none;
`,
listControlItem: css`
margin-top: 18px;
&:first-of-type {
margin-top: 26px;
} }
</div> `,
); listControlItemCollapsed: css`
padding-bottom: 0;
`,
};
const SortableList = SortableContainer(({ items, renderItem }) => { const SortableList = SortableContainer(({ items, renderItem }) => {
return <div>{items.map(renderItem)}</div>; return <div>{items.map(renderItem)}</div>;
@ -52,7 +60,7 @@ const valueTypes = {
MULTIPLE: 'MULTIPLE', MULTIPLE: 'MULTIPLE',
}; };
export default class ListControl extends Component { export default class ListControl extends React.Component {
static propTypes = { static propTypes = {
onChange: PropTypes.func.isRequired, onChange: PropTypes.func.isRequired,
onChangeObject: PropTypes.func.isRequired, onChangeObject: PropTypes.func.isRequired,
@ -229,33 +237,44 @@ export default class ListControl extends Component {
onAddAsset, onAddAsset,
onRemoveInsertedMedia, onRemoveInsertedMedia,
classNameWrapper, classNameWrapper,
editorControl,
resolveWidget,
} = this.props; } = this.props;
const { itemsCollapsed } = this.state; const { itemsCollapsed } = this.state;
const collapsed = itemsCollapsed.get(index); const collapsed = itemsCollapsed.get(index);
const classNames = ['nc-listControl-item', collapsed ? 'nc-listControl-collapsed' : ''];
return (<SortableListItem className={classNames.join(' ')} index={index} key={`item-${ index }`}> return (
<ListItemTopBar <SortableListItem
className="nc-listControl-itemTopBar" className={cx(styles.listControlItem, { [styles.listControlItemCollapsed]: collapsed })}
collapsed={collapsed} index={index}
onCollapseToggle={partial(this.handleItemCollapseToggle, index)} key={`item-${ index }`}
onRemove={partial(this.handleRemove, index)} >
dragHandleHOC={SortableHandle} <StyledListItemTopBar
/> collapsed={collapsed}
<div className="nc-listControl-objectLabel">{this.objectLabel(item)}</div> onCollapseToggle={partial(this.handleItemCollapseToggle, index)}
<ObjectControl onRemove={partial(this.handleRemove, index)}
value={item} dragHandleHOC={SortableHandle}
field={field} />
onChangeObject={this.handleChangeFor(index)} <NestedObjectLabel collapsed={collapsed}>{this.objectLabel(item)}</NestedObjectLabel>
getAsset={getAsset} <ObjectControl
onOpenMediaLibrary={onOpenMediaLibrary} value={item}
mediaPaths={mediaPaths} field={field}
onAddAsset={onAddAsset} onChangeObject={this.handleChangeFor(index)}
onRemoveInsertedMedia={onRemoveInsertedMedia} getAsset={getAsset}
classNameWrapper={`${ classNameWrapper } nc-listControl-objectControl`} onOpenMediaLibrary={onOpenMediaLibrary}
forList mediaPaths={mediaPaths}
/> onAddAsset={onAddAsset}
</SortableListItem>); onRemoveInsertedMedia={onRemoveInsertedMedia}
classNameWrapper={cx(
classNameWrapper,
{ [styles.collapsedObjectControl]: collapsed },
)}
editorControl={editorControl}
resolveWidget={resolveWidget}
forList
/>
</SortableListItem>
);
}; };
renderListControl() { renderListControl() {
@ -264,16 +283,17 @@ export default class ListControl extends Component {
const items = value || List(); const items = value || List();
const label = field.get('label'); const label = field.get('label');
const labelSingular = field.get('label_singular') || field.get('label'); const labelSingular = field.get('label_singular') || field.get('label');
const listLabel = items.size === 1 ? labelSingular.toLowerCase() : label.toLowerCase();
return ( return (
<div id={forID} className={c(classNameWrapper, 'nc-listControl')}> <div id={forID} className={cx(classNameWrapper, components.objectWidgetTopBarContainer)}>
<TopBar <ObjectWidgetTopBar
allowAdd={field.get('allow_add', true)} allowAdd={field.get('allow_add', true)}
onAdd={this.handleAdd} onAdd={this.handleAdd}
listLabel={items.size === 1 ? labelSingular.toLowerCase() : label.toLowerCase()} heading={`${items.size} ${listLabel}`}
onCollapseAllToggle={this.handleCollapseAllToggle} label={listLabel}
allItemsCollapsed={itemsCollapsed.every(val => val === true)} onCollapseToggle={this.handleCollapseAllToggle}
itemsCount={items.size} collapsed={itemsCollapsed.every(val => val === true)}
/> />
<SortableList <SortableList
items={items} items={items}

View File

@ -0,0 +1,2 @@
export ListControl from './ListControl';
export { ObjectPreview as ListPreview } from 'netlify-cms-widget-object';

View File

@ -0,0 +1,3 @@
const { getConfig } = require('../../scripts/webpack.js');
module.exports = getConfig();

View File

@ -0,0 +1,33 @@
{
"name": "netlify-cms-widget-object",
"description": "Widget for displaying an object of fields for Netlify CMS.",
"version": "2.0.0-alpha.0",
"main": "dist/netlify-cms-widget-object.js",
"license": "MIT",
"keywords": [
"netlify",
"netlify-cms",
"widget",
"fields",
"object",
"nested"
],
"sideEffects": false,
"scripts": {
"watch": "webpack -w",
"build": "webpack"
},
"devDependencies": {
"webpack": "^4.16.1",
"webpack-cli": "^3.1.0"
},
"peerDependencies": {
"immutable": "^3.7.6",
"lodash": "^4.17.10",
"netlify-cms-ui-default": "^2.0.0-alpha.0",
"prop-types": "^15.5.10",
"react": "^16.4.1",
"react-emotion": "^9.2.6",
"react-immutable-proptypes": "^2.1.0"
}
}

View File

@ -1,21 +1,19 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import styled, { css, cx } from 'react-emotion';
import { Map } from 'immutable'; import { Map } from 'immutable';
import { partial } from 'lodash'; import { partial } from 'lodash';
import c from 'classnames'; import { ObjectWidgetTopBar, Icon, colors, components } from 'netlify-cms-ui-default';
import { Icon } from 'netlify-cms-ui-default';
import { resolveWidget } from 'Lib/registry';
const TopBar = ({ collapsed, onCollapseToggle }) => ( const styles = {
<div className="nc-objectControl-topBar"> nestedObjectControl: css`
<div className="nc-objectControl-objectCollapseToggle"> padding: 6px 14px 14px;
<button className="nc-listControl-listCollapseToggleButton" onClick={onCollapseToggle}> border-top: 0;
<Icon type="chevron" direction={collapsed ? 'right' : 'down'} size="small" /> border-top-left-radius: 0;
</button> border-top-right-radius: 0;
</div> `,
</div> };
);
export default class ObjectControl extends Component { export default class ObjectControl extends Component {
static propTypes = { static propTypes = {
@ -35,6 +33,7 @@ export default class ObjectControl extends Component {
classNameWrapper: PropTypes.string.isRequired, classNameWrapper: PropTypes.string.isRequired,
forList: PropTypes.bool, forList: PropTypes.bool,
editorControl: PropTypes.func.isRequired, editorControl: PropTypes.func.isRequired,
resolveWidget: PropTypes.func.isRequired,
}; };
static defaultProps = { static defaultProps = {
@ -68,6 +67,7 @@ export default class ObjectControl extends Component {
value, value,
onChangeObject, onChangeObject,
editorControl: EditorControl, editorControl: EditorControl,
resolveWidget,
} = this.props; } = this.props;
if (field.get('widget') === 'hidden') { if (field.get('widget') === 'hidden') {
@ -105,9 +105,15 @@ export default class ObjectControl extends Component {
if (multiFields) { if (multiFields) {
return ( return (
<div id={forID} className={c(classNameWrapper, 'nc-objectControl-root')}> <div id={forID} className={cx(
{ forList ? null : <TopBar collapsed={collapsed} onCollapseToggle={this.handleCollapseToggle} /> } classNameWrapper,
{ collapsed ? null : multiFields.map((f, idx) => this.controlFor(f, idx)) } components.objectWidgetTopBarContainer,
{ [styles.nestedObjectControl]: forList },
)}>
{forList ? null :
<ObjectWidgetTopBar collapsed={collapsed} onCollapseToggle={this.handleCollapseToggle} />
}
{collapsed ? null : multiFields.map((f, idx) => this.controlFor(f, idx))}
</div> </div>
); );
} else if (singleField) { } else if (singleField) {

View File

@ -0,0 +1,13 @@
import React from 'react';
import PropTypes from 'prop-types';
import { WidgetPreviewContainer } from 'netlify-cms-ui-default';
const ObjectPreview = ({ field }) => (
<WidgetPreviewContainer>{(field && field.get('fields')) || null}</WidgetPreviewContainer>
);
ObjectPreview.propTypes = {
field: PropTypes.node,
};
export default ObjectPreview;

View File

@ -0,0 +1,2 @@
export ObjectControl from './ObjectControl';
export ObjectPreview from './ObjectPreview';

View File

@ -0,0 +1,3 @@
const { getConfig } = require('../../scripts/webpack.js');
module.exports = getConfig();