Merge pull request #447 from netlify/kitchen-sink

Add kitchen sink example, restore object/list previews
This commit is contained in:
Shawn Erquhart 2017-06-09 15:35:34 -04:00 committed by GitHub
commit 0adb3df4ae
21 changed files with 181 additions and 96 deletions

View File

@ -56,3 +56,89 @@ collections: # A list of collections the CMS should be able to edit
fields: fields:
- {label: "Name", name: "name", widget: "string"} - {label: "Name", name: "name", widget: "string"}
- {label: "Description", name: "description", widget: "markdown"} - {label: "Description", name: "description", widget: "markdown"}
- name: "kitchenSink" # all the things in one entry, for documentation and quick testing
label: "Kitchen Sink"
folder: "_sink"
create: true
fields:
- {label: "Title", name: "title", widget: "string"}
- {label: "Boolean", name: "boolean", widget: "boolean", default: true}
- {label: "Text", name: "text", widget: "text"}
- {label: "Number", name: "number", widget: "number"}
- {label: "Markdown", name: "markdown", widget: "markdown"}
- {label: "Datetime", name: "datetime", widget: "datetime"}
- {label: "Date", name: "date", widget: "date"}
- {label: "Image", name: "image", widget: "image"}
- {label: "File", name: "file", widget: "file"}
- {label: "Select", name: "select", widget: "select", options: ["a", "b", "c"]}
- label: "Object"
name: "object"
widget: "object"
fields:
- {label: "String", name: "string", widget: "string"}
- {label: "Boolean", name: "boolean", widget: "boolean", default: false}
- {label: "Text", name: "text", widget: "text"}
- {label: "Number", name: "number", widget: "number"}
- {label: "Markdown", name: "markdown", widget: "markdown"}
- {label: "Datetime", name: "datetime", widget: "datetime"}
- {label: "Date", name: "date", widget: "date"}
- {label: "Image", name: "image", widget: "image"}
- {label: "File", name: "file", widget: "file"}
- {label: "Select", name: "select", widget: "select", options: ["a", "b", "c"]}
- label: "List"
name: "list"
widget: "list"
fields:
- {label: "String", name: "string", widget: "string"}
- {label: "Boolean", name: "boolean", widget: "boolean"}
- {label: "Text", name: "text", widget: "text"}
- {label: "Number", name: "number", widget: "number"}
- {label: "Markdown", name: "markdown", widget: "markdown"}
- {label: "Datetime", name: "datetime", widget: "datetime"}
- {label: "Date", name: "date", widget: "date"}
- {label: "Image", name: "image", widget: "image"}
- {label: "File", name: "file", widget: "file"}
- {label: "Select", name: "select", widget: "select", options: ["a", "b", "c"]}
- label: "Object"
name: "object"
widget: "object"
fields:
- {label: "String", name: "string", widget: "string"}
- {label: "Boolean", name: "boolean", widget: "boolean"}
- {label: "Text", name: "text", widget: "text"}
- {label: "Number", name: "number", widget: "number"}
- {label: "Markdown", name: "markdown", widget: "markdown"}
- {label: "Datetime", name: "datetime", widget: "datetime"}
- {label: "Date", name: "date", widget: "date"}
- {label: "Image", name: "image", widget: "image"}
- {label: "File", name: "file", widget: "file"}
- {label: "Select", name: "select", widget: "select", options: ["a", "b", "c"]}
- label: "List"
name: "list"
widget: "list"
fields:
- {label: "String", name: "string", widget: "string"}
- {label: "Boolean", name: "boolean", widget: "boolean"}
- {label: "Text", name: "text", widget: "text"}
- {label: "Number", name: "number", widget: "number"}
- {label: "Markdown", name: "markdown", widget: "markdown"}
- {label: "Datetime", name: "datetime", widget: "datetime"}
- {label: "Date", name: "date", widget: "date"}
- {label: "Image", name: "image", widget: "image"}
- {label: "File", name: "file", widget: "file"}
- {label: "Select", name: "select", widget: "select", options: ["a", "b", "c"]}
- label: "Object"
name: "object"
widget: "object"
fields:
- {label: "String", name: "string", widget: "string"}
- {label: "Boolean", name: "boolean", widget: "boolean"}
- {label: "Text", name: "text", widget: "text"}
- {label: "Number", name: "number", widget: "number"}
- {label: "Markdown", name: "markdown", widget: "markdown"}
- {label: "Datetime", name: "datetime", widget: "datetime"}
- {label: "Date", name: "date", widget: "date"}
- {label: "Image", name: "image", widget: "image"}
- {label: "File", name: "file", widget: "file"}
- {label: "Select", name: "select", widget: "select", options: ["a", "b", "c"]}

File diff suppressed because one or more lines are too long

BIN
example/moby-dick.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -147,7 +147,7 @@
"react-simple-dnd": "^0.1.2", "react-simple-dnd": "^0.1.2",
"react-sortable": "^1.2.0", "react-sortable": "^1.2.0",
"react-split-pane": "^0.1.57", "react-split-pane": "^0.1.57",
"react-textarea-autosize-inputref": "^4.1.0", "react-textarea-autosize": "^4.3.2",
"react-toolbox": "^1.2.1", "react-toolbox": "^1.2.1",
"react-topbar-progress-indicator": "^1.0.0", "react-topbar-progress-indicator": "^1.0.0",
"react-waypoint": "^3.1.3", "react-waypoint": "^3.1.3",

View File

@ -124,6 +124,7 @@ class Backend {
const format = resolveFormat(collectionOrEntity, entry); const format = resolveFormat(collectionOrEntity, entry);
if (entry && entry.raw !== undefined) { if (entry && entry.raw !== undefined) {
const data = (format && attempt(format.fromFile.bind(null, entry.raw))) || {}; const data = (format && attempt(format.fromFile.bind(null, entry.raw))) || {};
if (isError(data)) console.error(data);
return Object.assign(entry, { data: isError(data) ? {} : data }); return Object.assign(entry, { data: isError(data) ? {} : data });
} }
return format.fromFile(entry); return format.fromFile(entry);

View File

@ -15,7 +15,8 @@ export default class PreviewPane extends React.Component {
getWidget = (field, value, props) => { getWidget = (field, value, props) => {
const { fieldsMetaData, getAsset } = props; const { fieldsMetaData, getAsset } = props;
const widget = resolveWidget(field.get('widget')); const widget = resolveWidget(field.get('widget'));
return React.createElement(widget.preview, {
return !widget.preview ? null : React.createElement(widget.preview, {
field, field,
key: field.get('name'), key: field.get('name'),
value: value && Map.isMap(value) ? value.get(field.get('name')) : value, value: value && Map.isMap(value) ? value.get(field.get('name')) : value,
@ -37,10 +38,23 @@ export default class PreviewPane extends React.Component {
if (authorField) this.inferedFields[authorField] = INFERABLE_FIELDS.author; if (authorField) this.inferedFields[authorField] = INFERABLE_FIELDS.author;
} }
widgetFor = (name) => { /**
const { fields, entry } = this.props; * Returns the widget component for a named field, and makes recursive calls
const field = fields.find(f => f.get('name') === name); * to retrieve components for nested and deeply nested fields, which occur in
let value = entry.getIn(['data', field.get('name')]); * object and list type fields. Used internally to retrieve widgets, and also
* exposed for use in custom preview templates.
*/
widgetFor = (name, fields = this.props.fields, values = this.props.entry.get('data')) => {
// We retrieve the field by name so that this function can also be used in
// custom preview templates, where the field object can't be passed in.
let field = fields && fields.find(f => f.get('name') === name);
let value = values && values.get(field.get('name'));
let nestedFields = field.get('fields');
if (nestedFields) {
field = field.set('fields', this.getNestedWidgets(nestedFields, value));
}
const labelledWidgets = ['string', 'text', 'number']; const labelledWidgets = ['string', 'text', 'number'];
if (Object.keys(this.inferedFields).indexOf(name) !== -1) { if (Object.keys(this.inferedFields).indexOf(name) !== -1) {
value = this.inferedFields[name].defaultPreview(value); value = this.inferedFields[name].defaultPreview(value);
@ -51,6 +65,31 @@ export default class PreviewPane extends React.Component {
return value ? this.getWidget(field, value, this.props) : null; return value ? this.getWidget(field, value, this.props) : null;
}; };
/**
* Retrieves widgets for nested fields (children of object/list fields)
*/
getNestedWidgets = (fields, values) => {
// Fields nested within a list field will be paired with a List of value Maps.
if (List.isList(values)) {
return values.map(value => this.widgetsForNestedFields(fields, value));
}
// Fields nested within an object field will be paired with a single Map of values.
return this.widgetsForNestedFields(fields, values);
};
/**
* Use widgetFor as a mapping function for recursive widget retrieval
*/
widgetsForNestedFields = (fields, values) => {
return fields.map(field => this.widgetFor(field.get('name'), fields, values));
};
/**
* This function exists entirely to expose nested widgets for object and list
* fields to custom preview templates.
*
* TODO: see if widgetFor can now provide this functionality for preview templates
*/
widgetsFor = (name) => { widgetsFor = (name) => {
const { fields, entry } = this.props; const { fields, entry } = this.props;
const field = fields.find(f => f.get('name') === name); const field = fields.find(f => f.get('name') === name);

View File

@ -0,0 +1,3 @@
.switch {
display: inline-block;
}

View File

@ -1,6 +1,8 @@
import React, { PropTypes } from 'react'; import React, { PropTypes } from 'react';
import ImmutablePropTypes from "react-immutable-proptypes"; import ImmutablePropTypes from "react-immutable-proptypes";
import Switch from 'react-toolbox/lib/switch'; import Switch from 'react-toolbox/lib/switch';
import { isBoolean } from 'lodash';
import styles from './BooleanControl.css';
export default class BooleanControl extends React.Component { export default class BooleanControl extends React.Component {
render() { render() {
@ -8,7 +10,8 @@ export default class BooleanControl extends React.Component {
return ( return (
<Switch <Switch
id={forID} id={forID}
checked={value === undefined ? field.get('defaultValue', false) : value} className={styles.switch}
checked={isBoolean(value) ? value : field.get('defaultValue', false)}
onChange={onChange} onChange={onChange}
/> />
); );

View File

@ -9,8 +9,10 @@ class ControlHOC extends Component {
controlComponent: PropTypes.func.isRequired, controlComponent: PropTypes.func.isRequired,
field: ImmutablePropTypes.map.isRequired, field: ImmutablePropTypes.map.isRequired,
value: PropTypes.oneOfType([ value: PropTypes.oneOfType([
PropTypes.node,
PropTypes.object, PropTypes.object,
PropTypes.string, PropTypes.string,
PropTypes.bool,
]), ]),
metadata: ImmutablePropTypes.map, metadata: ImmutablePropTypes.map,
onChange: PropTypes.func.isRequired, onChange: PropTypes.func.isRequired,

View File

@ -6,5 +6,5 @@ export default function DatePreview({ value }) {
} }
DatePreview.propTypes = { DatePreview.propTypes = {
value: PropTypes.node, value: PropTypes.object,
}; };

View File

@ -1,10 +1,10 @@
import React, { PropTypes } from 'react'; import React, { PropTypes } from 'react';
import previewStyle from './defaultPreviewStyle'; import previewStyle from './defaultPreviewStyle';
export default function DatePreview({ value }) { export default function DateTimePreview({ value }) {
return <div style={previewStyle}>{value ? value.toString() : null}</div>; return <div style={previewStyle}>{value ? value.toString() : null}</div>;
} }
DatePreview.propTypes = { DateTimePreview.propTypes = {
value: PropTypes.node, value: PropTypes.object,
}; };

View File

@ -2,6 +2,7 @@ import React, { PropTypes } from 'react';
import { truncateMiddle } from '../../lib/textHelper'; import { truncateMiddle } from '../../lib/textHelper';
import { Loader } from '../UI'; import { Loader } from '../UI';
import AssetProxy, { createAssetProxy } from '../../valueObjects/AssetProxy'; import AssetProxy, { createAssetProxy } from '../../valueObjects/AssetProxy';
import styles from './FileControl.css';
const MAX_DISPLAY_LENGTH = 50; const MAX_DISPLAY_LENGTH = 50;
@ -77,8 +78,8 @@ export default class FileControl extends React.Component {
const fileName = this.renderFileName(); const fileName = this.renderFileName();
if (processing) { if (processing) {
return ( return (
<div style={styles.imageUpload}> <div className={styles.imageUpload}>
<span style={styles.message}> <span className={styles.message}>
<Loader active /> <Loader active />
</span> </span>
</div> </div>
@ -86,18 +87,18 @@ export default class FileControl extends React.Component {
} }
return ( return (
<div <div
style={styles.imageUpload} className={styles.imageUpload}
onDragEnter={this.handleDragEnter} onDragEnter={this.handleDragEnter}
onDragOver={this.handleDragOver} onDragOver={this.handleDragOver}
onDrop={this.handleChange} onDrop={this.handleChange}
> >
<span style={styles.message} onClick={this.handleClick}> <span className={styles.message} onClick={this.handleClick}>
{fileName ? fileName : 'Tip: Click here to select a file to upload, or drag an image directly into this box from your desktop'} {fileName ? fileName : 'Click here to upload a file from your computer, or drag and drop a file directly into this box'}
</span> </span>
<input <input
type="file" type="file"
onChange={this.handleChange} onChange={this.handleChange}
style={styles.input} className={styles.input}
ref={this.handleFileInputRef} ref={this.handleFileInputRef}
/> />
</div> </div>
@ -105,24 +106,6 @@ export default class FileControl extends React.Component {
} }
} }
const styles = {
input: {
display: 'none',
},
message: {
padding: '20px',
display: 'block',
fontSize: '12px',
},
imageUpload: {
backgroundColor: '#fff',
textAlign: 'center',
color: '#999',
border: '1px dashed #eee',
cursor: 'pointer',
},
};
FileControl.propTypes = { FileControl.propTypes = {
onAddAsset: PropTypes.func.isRequired, onAddAsset: PropTypes.func.isRequired,
onChange: PropTypes.func.isRequired, onChange: PropTypes.func.isRequired,

View File

@ -2,7 +2,7 @@ import React, { PropTypes } from 'react';
import { truncateMiddle } from '../../lib/textHelper'; import { truncateMiddle } from '../../lib/textHelper';
import { Loader } from '../UI'; import { Loader } from '../UI';
import AssetProxy, { createAssetProxy } from '../../valueObjects/AssetProxy'; import AssetProxy, { createAssetProxy } from '../../valueObjects/AssetProxy';
import styles from './ImageControl.css'; import styles from './FileControl.css';
const MAX_DISPLAY_LENGTH = 50; const MAX_DISPLAY_LENGTH = 50;

View File

@ -1,36 +1,12 @@
import React, { PropTypes, Component } from 'react'; import React, { PropTypes, Component } from 'react';
import { resolveWidget } from '../Widgets'; import { resolveWidget } from '../Widgets';
import previewStyle from './defaultPreviewStyle'; import previewStyle from './defaultPreviewStyle';
import ObjectPreview from './ObjectPreview';
export default class ListPreview extends Component { const ListPreview = ObjectPreview;
widgetFor = (field, value) => {
const { getAsset } = this.props;
const widget = resolveWidget(field.get('widget'));
return (<div key={field.get('name')}>{React.createElement(widget.preview, {
key: field.get('name'),
value: value && value.get(field.get('name')),
field,
getAsset,
})}</div>);
};
render() {
const { field, value } = this.props;
const fields = field && field.get('fields');
if (fields) {
return value ? (<div style={previewStyle}>
{value.map((val, index) => <div key={index}>
{fields && fields.map(f => this.widgetFor(f, val))}
</div>)}
</div>) : null;
}
return <div style={previewStyle}>{value ? value.join(', ') : null}</div>;
}
}
ListPreview.propTypes = { ListPreview.propTypes = {
value: PropTypes.node,
field: PropTypes.node, field: PropTypes.node,
getAsset: PropTypes.func.isRequired,
}; };
export default ListPreview;

View File

@ -3,7 +3,7 @@ import MarkupIt from 'markup-it';
import markdownSyntax from 'markup-it/syntaxes/markdown'; import markdownSyntax from 'markup-it/syntaxes/markdown';
import htmlSyntax from 'markup-it/syntaxes/html'; import htmlSyntax from 'markup-it/syntaxes/html';
import CaretPosition from 'textarea-caret-position'; import CaretPosition from 'textarea-caret-position';
import TextareaAutosize from 'react-textarea-autosize-inputref'; import TextareaAutosize from 'react-textarea-autosize';
import registry from '../../../../lib/registry'; import registry from '../../../../lib/registry';
import { createAssetProxy } from '../../../../valueObjects/AssetProxy'; import { createAssetProxy } from '../../../../valueObjects/AssetProxy';
import Toolbar from '../Toolbar/Toolbar'; import Toolbar from '../Toolbar/Toolbar';

View File

@ -10,7 +10,11 @@ export default class ObjectControl extends Component {
onAddAsset: PropTypes.func.isRequired, onAddAsset: PropTypes.func.isRequired,
onRemoveAsset: PropTypes.func.isRequired, onRemoveAsset: PropTypes.func.isRequired,
getAsset: PropTypes.func.isRequired, getAsset: PropTypes.func.isRequired,
value: PropTypes.node, value: PropTypes.oneOfType([
PropTypes.node,
PropTypes.object,
PropTypes.bool,
]),
field: PropTypes.object, field: PropTypes.object,
forID: PropTypes.string, forID: PropTypes.string,
className: PropTypes.string, className: PropTypes.string,
@ -35,6 +39,7 @@ export default class ObjectControl extends Component {
onAddAsset, onAddAsset,
onRemoveAsset, onRemoveAsset,
getAsset, getAsset,
forID: field.get('name'),
}) })
} }
</div> </div>

View File

@ -2,32 +2,12 @@ import React, { PropTypes, Component } from 'react';
import { resolveWidget } from '../Widgets'; import { resolveWidget } from '../Widgets';
import previewStyle from './defaultPreviewStyle'; import previewStyle from './defaultPreviewStyle';
export default class ObjectPreview extends Component { const ObjectPreview = ({ field }) => (
widgetFor = (field) => { <div style={previewStyle}>{(field && field.get('fields')) || null}</div>
const { value, getAsset } = this.props; );
const widget = resolveWidget(field.get('widget'));
return (
<div key={field.get('name')}>
{React.createElement(widget.preview, {
key: field.get('name'),
value: value && value.get(field.get('name')),
field,
getAsset,
})}
</div>
);
};
render() {
const { field } = this.props;
const fields = field && field.get('fields');
return <div style={previewStyle}>{fields ? fields.map(f => this.widgetFor(f)) : null}</div>;
}
}
ObjectPreview.propTypes = { ObjectPreview.propTypes = {
value: PropTypes.node,
field: PropTypes.node, field: PropTypes.node,
getAsset: PropTypes.func.isRequired,
}; };
export default ObjectPreview;

View File

@ -34,7 +34,7 @@ SelectControl.propTypes = {
value: PropTypes.node, value: PropTypes.node,
forID: PropTypes.string.isRequired, forID: PropTypes.string.isRequired,
field: ImmutablePropTypes.contains({ field: ImmutablePropTypes.contains({
options: ImmutablePropTypes.listOf(PropTypes.oneOf([ options: ImmutablePropTypes.listOf(PropTypes.oneOfType([
PropTypes.string, PropTypes.string,
ImmutablePropTypes.contains({ ImmutablePropTypes.contains({
label: PropTypes.string.isRequired, label: PropTypes.string.isRequired,

View File

@ -2,7 +2,7 @@ import React, { PropTypes } from 'react';
import previewStyle from './defaultPreviewStyle'; import previewStyle from './defaultPreviewStyle';
export default function TextPreview({ value }) { export default function TextPreview({ value }) {
return <div style={previewStyle}>{value ? value.toString() : null}</div>; return <div style={previewStyle}>{value}</div>;
} }
TextPreview.propTypes = { TextPreview.propTypes = {

View File

@ -7090,9 +7090,11 @@ react-style-proptype@^3.0.0:
dependencies: dependencies:
prop-types "^15.5.4" prop-types "^15.5.4"
react-textarea-autosize-inputref@^4.1.0: react-textarea-autosize@^4.3.2:
version "4.1.0" version "4.3.2"
resolved "https://registry.yarnpkg.com/react-textarea-autosize-inputref/-/react-textarea-autosize-inputref-4.1.0.tgz#4a12921f9c992a8e2c6ce569ab46982a96ca48f6" resolved "https://registry.yarnpkg.com/react-textarea-autosize/-/react-textarea-autosize-4.3.2.tgz#962a52c68caceae408c18acecec29049b81e42fa"
dependencies:
prop-types "^15.5.8"
react-themeable@^1.1.0: react-themeable@^1.1.0:
version "1.1.0" version "1.1.0"