Merge pull request #447 from netlify/kitchen-sink
Add kitchen sink example, restore object/list previews
This commit is contained in:
commit
0adb3df4ae
@ -56,3 +56,89 @@ collections: # A list of collections the CMS should be able to edit
|
||||
fields:
|
||||
- {label: "Name", name: "name", widget: "string"}
|
||||
- {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
BIN
example/moby-dick.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 10 KiB |
@ -147,7 +147,7 @@
|
||||
"react-simple-dnd": "^0.1.2",
|
||||
"react-sortable": "^1.2.0",
|
||||
"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-topbar-progress-indicator": "^1.0.0",
|
||||
"react-waypoint": "^3.1.3",
|
||||
|
@ -124,6 +124,7 @@ class Backend {
|
||||
const format = resolveFormat(collectionOrEntity, entry);
|
||||
if (entry && entry.raw !== undefined) {
|
||||
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 format.fromFile(entry);
|
||||
|
@ -15,7 +15,8 @@ export default class PreviewPane extends React.Component {
|
||||
getWidget = (field, value, props) => {
|
||||
const { fieldsMetaData, getAsset } = props;
|
||||
const widget = resolveWidget(field.get('widget'));
|
||||
return React.createElement(widget.preview, {
|
||||
|
||||
return !widget.preview ? null : React.createElement(widget.preview, {
|
||||
field,
|
||||
key: field.get('name'),
|
||||
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;
|
||||
}
|
||||
|
||||
widgetFor = (name) => {
|
||||
const { fields, entry } = this.props;
|
||||
const field = fields.find(f => f.get('name') === name);
|
||||
let value = entry.getIn(['data', field.get('name')]);
|
||||
/**
|
||||
* Returns the widget component for a named field, and makes recursive calls
|
||||
* to retrieve components for nested and deeply nested fields, which occur in
|
||||
* 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'];
|
||||
if (Object.keys(this.inferedFields).indexOf(name) !== -1) {
|
||||
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;
|
||||
};
|
||||
|
||||
/**
|
||||
* 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) => {
|
||||
const { fields, entry } = this.props;
|
||||
const field = fields.find(f => f.get('name') === name);
|
||||
|
3
src/components/Widgets/BooleanControl.css
Normal file
3
src/components/Widgets/BooleanControl.css
Normal file
@ -0,0 +1,3 @@
|
||||
.switch {
|
||||
display: inline-block;
|
||||
}
|
@ -1,6 +1,8 @@
|
||||
import React, { PropTypes } from 'react';
|
||||
import ImmutablePropTypes from "react-immutable-proptypes";
|
||||
import Switch from 'react-toolbox/lib/switch';
|
||||
import { isBoolean } from 'lodash';
|
||||
import styles from './BooleanControl.css';
|
||||
|
||||
export default class BooleanControl extends React.Component {
|
||||
render() {
|
||||
@ -8,7 +10,8 @@ export default class BooleanControl extends React.Component {
|
||||
return (
|
||||
<Switch
|
||||
id={forID}
|
||||
checked={value === undefined ? field.get('defaultValue', false) : value}
|
||||
className={styles.switch}
|
||||
checked={isBoolean(value) ? value : field.get('defaultValue', false)}
|
||||
onChange={onChange}
|
||||
/>
|
||||
);
|
||||
|
@ -9,8 +9,10 @@ class ControlHOC extends Component {
|
||||
controlComponent: PropTypes.func.isRequired,
|
||||
field: ImmutablePropTypes.map.isRequired,
|
||||
value: PropTypes.oneOfType([
|
||||
PropTypes.node,
|
||||
PropTypes.object,
|
||||
PropTypes.string,
|
||||
PropTypes.bool,
|
||||
]),
|
||||
metadata: ImmutablePropTypes.map,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
|
@ -6,5 +6,5 @@ export default function DatePreview({ value }) {
|
||||
}
|
||||
|
||||
DatePreview.propTypes = {
|
||||
value: PropTypes.node,
|
||||
value: PropTypes.object,
|
||||
};
|
||||
|
@ -1,10 +1,10 @@
|
||||
import React, { PropTypes } from 'react';
|
||||
import previewStyle from './defaultPreviewStyle';
|
||||
|
||||
export default function DatePreview({ value }) {
|
||||
export default function DateTimePreview({ value }) {
|
||||
return <div style={previewStyle}>{value ? value.toString() : null}</div>;
|
||||
}
|
||||
|
||||
DatePreview.propTypes = {
|
||||
value: PropTypes.node,
|
||||
DateTimePreview.propTypes = {
|
||||
value: PropTypes.object,
|
||||
};
|
||||
|
@ -2,6 +2,7 @@ import React, { PropTypes } from 'react';
|
||||
import { truncateMiddle } from '../../lib/textHelper';
|
||||
import { Loader } from '../UI';
|
||||
import AssetProxy, { createAssetProxy } from '../../valueObjects/AssetProxy';
|
||||
import styles from './FileControl.css';
|
||||
|
||||
const MAX_DISPLAY_LENGTH = 50;
|
||||
|
||||
@ -77,8 +78,8 @@ export default class FileControl extends React.Component {
|
||||
const fileName = this.renderFileName();
|
||||
if (processing) {
|
||||
return (
|
||||
<div style={styles.imageUpload}>
|
||||
<span style={styles.message}>
|
||||
<div className={styles.imageUpload}>
|
||||
<span className={styles.message}>
|
||||
<Loader active />
|
||||
</span>
|
||||
</div>
|
||||
@ -86,18 +87,18 @@ export default class FileControl extends React.Component {
|
||||
}
|
||||
return (
|
||||
<div
|
||||
style={styles.imageUpload}
|
||||
className={styles.imageUpload}
|
||||
onDragEnter={this.handleDragEnter}
|
||||
onDragOver={this.handleDragOver}
|
||||
onDrop={this.handleChange}
|
||||
>
|
||||
<span style={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'}
|
||||
<span className={styles.message} onClick={this.handleClick}>
|
||||
{fileName ? fileName : 'Click here to upload a file from your computer, or drag and drop a file directly into this box'}
|
||||
</span>
|
||||
<input
|
||||
type="file"
|
||||
onChange={this.handleChange}
|
||||
style={styles.input}
|
||||
className={styles.input}
|
||||
ref={this.handleFileInputRef}
|
||||
/>
|
||||
</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 = {
|
||||
onAddAsset: PropTypes.func.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
|
@ -2,7 +2,7 @@ import React, { PropTypes } from 'react';
|
||||
import { truncateMiddle } from '../../lib/textHelper';
|
||||
import { Loader } from '../UI';
|
||||
import AssetProxy, { createAssetProxy } from '../../valueObjects/AssetProxy';
|
||||
import styles from './ImageControl.css';
|
||||
import styles from './FileControl.css';
|
||||
|
||||
const MAX_DISPLAY_LENGTH = 50;
|
||||
|
||||
|
@ -1,36 +1,12 @@
|
||||
import React, { PropTypes, Component } from 'react';
|
||||
import { resolveWidget } from '../Widgets';
|
||||
import previewStyle from './defaultPreviewStyle';
|
||||
import ObjectPreview from './ObjectPreview';
|
||||
|
||||
export default class ListPreview extends Component {
|
||||
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>;
|
||||
}
|
||||
}
|
||||
const ListPreview = ObjectPreview;
|
||||
|
||||
ListPreview.propTypes = {
|
||||
value: PropTypes.node,
|
||||
field: PropTypes.node,
|
||||
getAsset: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default ListPreview;
|
||||
|
@ -3,7 +3,7 @@ import MarkupIt from 'markup-it';
|
||||
import markdownSyntax from 'markup-it/syntaxes/markdown';
|
||||
import htmlSyntax from 'markup-it/syntaxes/html';
|
||||
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 { createAssetProxy } from '../../../../valueObjects/AssetProxy';
|
||||
import Toolbar from '../Toolbar/Toolbar';
|
||||
|
@ -10,7 +10,11 @@ export default class ObjectControl extends Component {
|
||||
onAddAsset: PropTypes.func.isRequired,
|
||||
onRemoveAsset: PropTypes.func.isRequired,
|
||||
getAsset: PropTypes.func.isRequired,
|
||||
value: PropTypes.node,
|
||||
value: PropTypes.oneOfType([
|
||||
PropTypes.node,
|
||||
PropTypes.object,
|
||||
PropTypes.bool,
|
||||
]),
|
||||
field: PropTypes.object,
|
||||
forID: PropTypes.string,
|
||||
className: PropTypes.string,
|
||||
@ -35,6 +39,7 @@ export default class ObjectControl extends Component {
|
||||
onAddAsset,
|
||||
onRemoveAsset,
|
||||
getAsset,
|
||||
forID: field.get('name'),
|
||||
})
|
||||
}
|
||||
</div>
|
||||
|
@ -2,32 +2,12 @@ import React, { PropTypes, Component } from 'react';
|
||||
import { resolveWidget } from '../Widgets';
|
||||
import previewStyle from './defaultPreviewStyle';
|
||||
|
||||
export default class ObjectPreview extends Component {
|
||||
widgetFor = (field) => {
|
||||
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>;
|
||||
}
|
||||
}
|
||||
const ObjectPreview = ({ field }) => (
|
||||
<div style={previewStyle}>{(field && field.get('fields')) || null}</div>
|
||||
);
|
||||
|
||||
ObjectPreview.propTypes = {
|
||||
value: PropTypes.node,
|
||||
field: PropTypes.node,
|
||||
getAsset: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default ObjectPreview;
|
||||
|
@ -34,7 +34,7 @@ SelectControl.propTypes = {
|
||||
value: PropTypes.node,
|
||||
forID: PropTypes.string.isRequired,
|
||||
field: ImmutablePropTypes.contains({
|
||||
options: ImmutablePropTypes.listOf(PropTypes.oneOf([
|
||||
options: ImmutablePropTypes.listOf(PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
ImmutablePropTypes.contains({
|
||||
label: PropTypes.string.isRequired,
|
||||
|
@ -2,7 +2,7 @@ import React, { PropTypes } from 'react';
|
||||
import previewStyle from './defaultPreviewStyle';
|
||||
|
||||
export default function TextPreview({ value }) {
|
||||
return <div style={previewStyle}>{value ? value.toString() : null}</div>;
|
||||
return <div style={previewStyle}>{value}</div>;
|
||||
}
|
||||
|
||||
TextPreview.propTypes = {
|
||||
|
@ -7090,9 +7090,11 @@ react-style-proptype@^3.0.0:
|
||||
dependencies:
|
||||
prop-types "^15.5.4"
|
||||
|
||||
react-textarea-autosize-inputref@^4.1.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/react-textarea-autosize-inputref/-/react-textarea-autosize-inputref-4.1.0.tgz#4a12921f9c992a8e2c6ce569ab46982a96ca48f6"
|
||||
react-textarea-autosize@^4.3.2:
|
||||
version "4.3.2"
|
||||
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:
|
||||
version "1.1.0"
|
||||
|
Loading…
x
Reference in New Issue
Block a user