UI updates (#151)
* infer card title * Infer entry body & image * infer image * Better terminology: EntryListing accept a single Collection * remove log * Refactored Collections VO into selectors * use selectors when showning card * fixed size cards * Added 'bio' and 'biography' to collection description inference synonyms * Removed unused card file * throw error instance * bugfix for file based collections * lint * moved components with css to own folder * Search Bugfix: More than one collection might be returned * Changed sidebar implementation. Closes #104 & #152 * Show spinning loading for unpublished entries * Refactored Sidebar into a separate container * Make preview widgets more robust
This commit is contained in:
parent
3420273691
commit
2a2497072d
@ -117,6 +117,7 @@
|
||||
"react-redux": "^4.4.0",
|
||||
"react-router": "^2.5.1",
|
||||
"react-router-redux": "^4.0.5",
|
||||
"react-sidebar": "^2.2.1",
|
||||
"react-simple-dnd": "^0.1.2",
|
||||
"react-sortable": "^1.2.0",
|
||||
"react-toolbox": "^1.2.1",
|
||||
|
13
src/actions/globalUI.js
Normal file
13
src/actions/globalUI.js
Normal file
@ -0,0 +1,13 @@
|
||||
export const TOGGLE_SIDEBAR = 'TOGGLE_SIDEBAR';
|
||||
export const OPEN_SIDEBAR = 'OPEN_SIDEBAR';
|
||||
|
||||
export function toggleSidebar() {
|
||||
return { type: TOGGLE_SIDEBAR };
|
||||
}
|
||||
|
||||
export function openSidebar(open = false) {
|
||||
return {
|
||||
type: OPEN_SIDEBAR,
|
||||
payload: { open },
|
||||
};
|
||||
}
|
@ -2,8 +2,8 @@ import TestRepoBackend from './test-repo/implementation';
|
||||
import GitHubBackend from './github/implementation';
|
||||
import NetlifyGitBackend from './netlify-git/implementation';
|
||||
import { resolveFormat } from '../formats/formats';
|
||||
import { selectListMethod, selectEntrySlug, selectEntryPath, selectAllowNewEntries } from '../reducers/collections';
|
||||
import { createEntry } from '../valueObjects/Entry';
|
||||
import Collection from '../valueObjects/Collection';
|
||||
|
||||
class LocalStorageAuthStore {
|
||||
storageKey = 'nf-cms-user';
|
||||
@ -80,13 +80,12 @@ class Backend {
|
||||
}
|
||||
|
||||
listEntries(collection) {
|
||||
const collectionModel = new Collection(collection);
|
||||
const listMethod = this.implementation[collectionModel.listMethod()];
|
||||
const listMethod = this.implementation[selectListMethod(collection)];
|
||||
return listMethod.call(this.implementation, collection)
|
||||
.then(loadedEntries => (
|
||||
loadedEntries.map(loadedEntry => createEntry(
|
||||
collection.get('name'),
|
||||
collectionModel.entrySlug(loadedEntry.file.path),
|
||||
selectEntrySlug(collection, loadedEntry.file.path),
|
||||
loadedEntry.file.path,
|
||||
{ raw: loadedEntry.data, label: loadedEntry.file.label }
|
||||
))
|
||||
@ -99,7 +98,7 @@ class Backend {
|
||||
}
|
||||
|
||||
getEntry(collection, slug) {
|
||||
return this.implementation.getEntry(collection, slug, new Collection(collection).entryPath(slug))
|
||||
return this.implementation.getEntry(collection, slug, selectEntryPath(collection, slug))
|
||||
.then(loadedEntry => this.entryWithFormat(collection, slug)(createEntry(
|
||||
collection.get('name'),
|
||||
slug,
|
||||
@ -150,7 +149,6 @@ class Backend {
|
||||
}
|
||||
|
||||
persistEntry(config, collection, entryDraft, MediaFiles, options) {
|
||||
const collectionModel = new Collection(collection);
|
||||
const newEntry = entryDraft.getIn(['entry', 'newRecord']) || false;
|
||||
|
||||
const parsedData = {
|
||||
@ -161,11 +159,11 @@ class Backend {
|
||||
const entryData = entryDraft.getIn(['entry', 'data']).toJS();
|
||||
let entryObj;
|
||||
if (newEntry) {
|
||||
if (!collectionModel.allowNewEntries()) {
|
||||
if (!selectAllowNewEntries(collection)) {
|
||||
throw (new Error('Not allowed to create new entries in this collection'));
|
||||
}
|
||||
const slug = slugFormatter(collection.get('slug'), entryDraft.getIn(['entry', 'data']));
|
||||
const path = collectionModel.entryPath(slug);
|
||||
const path = selectEntryPath(collection, slug);
|
||||
entryObj = {
|
||||
path,
|
||||
slug,
|
||||
|
@ -17,7 +17,7 @@ export default class AppHeader extends React.Component {
|
||||
commands: PropTypes.array.isRequired, // eslint-disable-line
|
||||
defaultCommands: PropTypes.array.isRequired, // eslint-disable-line
|
||||
runCommand: PropTypes.func.isRequired,
|
||||
toggleNavDrawer: PropTypes.func.isRequired,
|
||||
toggleDrawer: PropTypes.func.isRequired,
|
||||
onCreateEntryClick: PropTypes.func.isRequired,
|
||||
onLogoutClick: PropTypes.func.isRequired,
|
||||
};
|
||||
@ -59,7 +59,7 @@ export default class AppHeader extends React.Component {
|
||||
commands,
|
||||
defaultCommands,
|
||||
runCommand,
|
||||
toggleNavDrawer,
|
||||
toggleDrawer,
|
||||
onLogoutClick,
|
||||
} = this.props;
|
||||
|
||||
@ -88,7 +88,7 @@ export default class AppHeader extends React.Component {
|
||||
</Menu>
|
||||
</div>
|
||||
}
|
||||
onLeftIconClick={toggleNavDrawer}
|
||||
onLeftIconClick={toggleDrawer}
|
||||
onRightIconClick={this.handleRightIconClick}
|
||||
>
|
||||
<IndexLink to="/">
|
||||
|
@ -1,11 +0,0 @@
|
||||
import UnknownCard from './Cards/UnknownCard';
|
||||
import ImageCard from './Cards/ImageCard';
|
||||
import AlltypeCard from './Cards/AlltypeCard';
|
||||
|
||||
const Cards = {
|
||||
unknown: UnknownCard,
|
||||
image: ImageCard,
|
||||
alltype: AlltypeCard,
|
||||
};
|
||||
|
||||
export default Cards;
|
@ -1,5 +0,0 @@
|
||||
.cardContent {
|
||||
white-space: nowrap;
|
||||
text-align: center;
|
||||
font-weight: 500;
|
||||
}
|
@ -1,81 +0,0 @@
|
||||
import React, { PropTypes } from 'react';
|
||||
import { Card } from '../UI';
|
||||
import ScaledLine from './ScaledLine';
|
||||
import styles from './AlltypeCard.css';
|
||||
|
||||
export default class AlltypeCard extends React.Component {
|
||||
|
||||
// Based on the Slabtype Algorithm by Erik Loyer
|
||||
// http://erikloyer.com/index.php/blog/the_slabtype_algorithm_part_1_background/
|
||||
renderInscription(inscription) {
|
||||
|
||||
const idealCharPerLine = 22;
|
||||
|
||||
// segment the text into lines
|
||||
const words = inscription.split(' ');
|
||||
let preText, postText, finalText;
|
||||
let preDiff, postDiff;
|
||||
let wordIndex = 0;
|
||||
const lineText = [];
|
||||
|
||||
// while we still have words left, build the next line
|
||||
while (wordIndex < words.length) {
|
||||
postText = '';
|
||||
|
||||
// build two strings (preText and postText) word by word, with one
|
||||
// string always one word behind the other, until
|
||||
// the length of one string is less than the ideal number of characters
|
||||
// per line, while the length of the other is greater than that ideal
|
||||
while (postText.length < idealCharPerLine) {
|
||||
preText = postText;
|
||||
postText += words[wordIndex] + ' ';
|
||||
wordIndex++;
|
||||
if (wordIndex >= words.length) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// calculate the character difference between the two strings and the
|
||||
// ideal number of characters per line
|
||||
preDiff = idealCharPerLine - preText.length;
|
||||
postDiff = postText.length - idealCharPerLine;
|
||||
|
||||
// if the smaller string is closer to the length of the ideal than
|
||||
// the longer string, and doesn’t contain just a single space, then
|
||||
// use that one for the line
|
||||
if ((preDiff < postDiff) && (preText.length > 2)) {
|
||||
finalText = preText;
|
||||
wordIndex--;
|
||||
|
||||
// otherwise, use the longer string for the line
|
||||
} else {
|
||||
finalText = postText;
|
||||
}
|
||||
|
||||
lineText.push(finalText.substr(0, finalText.length - 1));
|
||||
}
|
||||
return lineText.map(text => (
|
||||
<ScaledLine key={text.trim().replace(/[^a-z0-9]+/gi, '-')} toWidth={216}>
|
||||
{text}
|
||||
</ScaledLine>
|
||||
));
|
||||
}
|
||||
|
||||
render() {
|
||||
const { onClick, text } = this.props;
|
||||
return (
|
||||
<Card onClick={onClick}>
|
||||
<div className={styles.cardContent}>{this.renderInscription(text)}</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AlltypeCard.propTypes = {
|
||||
onClick: PropTypes.func,
|
||||
text: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
AlltypeCard.defaultProps = {
|
||||
onClick: function() {},
|
||||
};
|
@ -1,4 +0,0 @@
|
||||
.root {
|
||||
width: 240px;
|
||||
cursor: pointer;
|
||||
}
|
@ -1,49 +0,0 @@
|
||||
import React, { PropTypes } from 'react';
|
||||
import { Card, CardMedia, CardTitle, CardText } from 'react-toolbox/lib/card';
|
||||
import styles from './ImageCard.css';
|
||||
|
||||
const ImageCard = (
|
||||
{
|
||||
author,
|
||||
description,
|
||||
image,
|
||||
text,
|
||||
onClick,
|
||||
onImageLoaded,
|
||||
}) => (
|
||||
<Card
|
||||
onClick={onClick}
|
||||
className={styles.root}
|
||||
>
|
||||
<CardTitle
|
||||
title={text}
|
||||
/>
|
||||
{
|
||||
image && <CardMedia aspectRatio="wide">
|
||||
<img
|
||||
src={image}
|
||||
alt={text}
|
||||
onLoad={onImageLoaded}
|
||||
/>
|
||||
</CardMedia>
|
||||
}
|
||||
{ description && <CardText>{ description }</CardText> }
|
||||
</Card>
|
||||
);
|
||||
|
||||
ImageCard.propTypes = {
|
||||
image: PropTypes.string,
|
||||
onClick: PropTypes.func,
|
||||
onImageLoaded: PropTypes.func,
|
||||
text: PropTypes.string.isRequired,
|
||||
description: PropTypes.string,
|
||||
};
|
||||
|
||||
ImageCard.defaultProps = {
|
||||
onClick: () => {
|
||||
},
|
||||
onImageLoaded: () => {
|
||||
},
|
||||
};
|
||||
|
||||
export default ImageCard;
|
@ -1,39 +0,0 @@
|
||||
import React, { PropTypes } from 'react';
|
||||
|
||||
export default class ScaledLine extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this._content = null;
|
||||
this.state = {
|
||||
ratio: 1,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const actualContent = this._content.children[0];
|
||||
|
||||
this.setState({
|
||||
ratio: this.props.toWidth / actualContent.offsetWidth,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { ratio } = this.state;
|
||||
const { children } = this.props;
|
||||
|
||||
const styles = {
|
||||
fontSize: ratio.toFixed(3) + 'em'
|
||||
};
|
||||
|
||||
return (
|
||||
<div ref={(c) => this._content = c} style={styles}>
|
||||
<span>{children}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ScaledLine.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
toWidth: PropTypes.number.isRequired
|
||||
};
|
@ -1,15 +0,0 @@
|
||||
import React from 'react';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { Card } from '../UI';
|
||||
|
||||
export default function UnknownCard({ collection }) {
|
||||
return (
|
||||
<Card>
|
||||
<p>No card of type “{collection.getIn(['card', 'type'])}”.</p>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
UnknownCard.propTypes = {
|
||||
collection: ImmutablePropTypes.map,
|
||||
};
|
@ -3,6 +3,10 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { resolveWidget } from '../Widgets';
|
||||
import styles from './ControlPane.css';
|
||||
|
||||
function isHidden(field) {
|
||||
return field.get('widget') === 'hidden';
|
||||
}
|
||||
|
||||
export default class ControlPane extends Component {
|
||||
|
||||
controlFor(field) {
|
||||
@ -38,7 +42,7 @@ export default class ControlPane extends Component {
|
||||
<div>
|
||||
{
|
||||
fields.map(field =>
|
||||
<div
|
||||
isHidden(field) ? null : <div
|
||||
key={field.get('name')}
|
||||
className={styles.widget}
|
||||
>
|
||||
|
@ -1,127 +0,0 @@
|
||||
import React, { PropTypes } from 'react';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { Map } from 'immutable';
|
||||
import { throttle } from 'lodash';
|
||||
import bricks from 'bricks.js';
|
||||
import Waypoint from 'react-waypoint';
|
||||
import history from '../routing/history';
|
||||
import Cards from './Cards';
|
||||
|
||||
export default class EntryListing extends React.Component {
|
||||
static propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
collections: PropTypes.oneOfType([
|
||||
ImmutablePropTypes.map,
|
||||
ImmutablePropTypes.iterable,
|
||||
]).isRequired,
|
||||
entries: ImmutablePropTypes.list,
|
||||
onPaginate: PropTypes.func.isRequired,
|
||||
page: PropTypes.number,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.bricksInstance = null;
|
||||
|
||||
this.bricksConfig = {
|
||||
packed: 'data-packed',
|
||||
sizes: [
|
||||
{ columns: 1, gutter: 15 },
|
||||
{ mq: '495px', columns: 2, gutter: 15 },
|
||||
{ mq: '750px', columns: 3, gutter: 15 },
|
||||
{ mq: '1005px', columns: 4, gutter: 15 },
|
||||
{ mq: '1515px', columns: 5, gutter: 15 },
|
||||
{ mq: '1770px', columns: 6, gutter: 15 },
|
||||
],
|
||||
};
|
||||
|
||||
this.updateBricks = throttle(this.updateBricks.bind(this), 30);
|
||||
this.handleLoadMore = this.handleLoadMore.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.bricksInstance = bricks({
|
||||
container: this.containerNode,
|
||||
packed: this.bricksConfig.packed,
|
||||
sizes: this.bricksConfig.sizes,
|
||||
});
|
||||
|
||||
this.bricksInstance.resize(true);
|
||||
|
||||
if (this.props.entries && this.props.entries.size > 0) {
|
||||
setTimeout(() => {
|
||||
this.bricksInstance.pack();
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if ((prevProps.entries === undefined || prevProps.entries.size === 0)
|
||||
&& this.props.entries.size === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.bricksInstance.pack();
|
||||
}
|
||||
|
||||
componengWillUnmount() {
|
||||
this.bricksInstance.resize(false);
|
||||
}
|
||||
|
||||
updateBricks() {
|
||||
this.bricksInstance.pack();
|
||||
}
|
||||
|
||||
cardFor(collection, entry, link) {
|
||||
const cardType = collection.getIn(['card', 'type']) || 'alltype';
|
||||
const card = Cards[cardType] || Cards.unknown;
|
||||
return React.createElement(card, {
|
||||
key: entry.get('slug'),
|
||||
collection,
|
||||
description: entry.getIn(['data', collection.getIn(['card', 'description'])]),
|
||||
image: entry.getIn(['data', collection.getIn(['card', 'image'])]),
|
||||
link,
|
||||
text: entry.get('label') ? entry.get('label') : entry.getIn(['data', collection.getIn(['card', 'text'])]),
|
||||
onClick: history.push.bind(this, link),
|
||||
onImageLoaded: this.updateBricks,
|
||||
});
|
||||
}
|
||||
|
||||
handleLoadMore() {
|
||||
this.props.onPaginate(this.props.page + 1);
|
||||
}
|
||||
|
||||
handleRef = (node) => {
|
||||
this.containerNode = node;
|
||||
};
|
||||
|
||||
renderCards = () => {
|
||||
const { collections, entries } = this.props;
|
||||
if (Map.isMap(collections)) {
|
||||
const collectionName = collections.get('name');
|
||||
return entries.map((entry) => {
|
||||
const path = `/collections/${ collectionName }/entries/${ entry.get('slug') }`;
|
||||
return this.cardFor(collections, entry, path);
|
||||
});
|
||||
}
|
||||
return entries.map((entry) => {
|
||||
const collection = collections
|
||||
.filter(collection => collection.get('name') === entry.get('collection')).first();
|
||||
const path = `/collections/${ collection.get('name') }/entries/${ entry.get('slug') }`;
|
||||
return this.cardFor(collection, entry, path);
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const { children } = this.props;
|
||||
return (
|
||||
<div>
|
||||
<h1>{children}</h1>
|
||||
<div ref={this.handleRef}>
|
||||
{ this.renderCards() }
|
||||
<Waypoint onEnter={this.handleLoadMore} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
21
src/components/EntryListing/EntryListing.css
Normal file
21
src/components/EntryListing/EntryListing.css
Normal file
@ -0,0 +1,21 @@
|
||||
.card {
|
||||
overflow: hidden;
|
||||
margin-bottom: 10px;
|
||||
max-height: 290px;
|
||||
width: 240px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.cardImage {
|
||||
width: 240px;
|
||||
height: 135px;
|
||||
background-position: center center;
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.cardsGrid {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
justify-content: space-around;
|
||||
}
|
89
src/components/EntryListing/EntryListing.js
Normal file
89
src/components/EntryListing/EntryListing.js
Normal file
@ -0,0 +1,89 @@
|
||||
import React, { PropTypes } from 'react';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import Waypoint from 'react-waypoint';
|
||||
import { Map } from 'immutable';
|
||||
import history from '../../routing/history';
|
||||
import { selectFields, selectInferedField } from '../../reducers/collections';
|
||||
import { Card } from '../UI';
|
||||
import styles from './EntryListing.css';
|
||||
|
||||
export default class EntryListing extends React.Component {
|
||||
static propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
collections: PropTypes.oneOfType([
|
||||
ImmutablePropTypes.map,
|
||||
ImmutablePropTypes.iterable,
|
||||
]).isRequired,
|
||||
entries: ImmutablePropTypes.list,
|
||||
onPaginate: PropTypes.func.isRequired,
|
||||
page: PropTypes.number,
|
||||
};
|
||||
|
||||
handleLoadMore = () => {
|
||||
this.props.onPaginate(this.props.page + 1);
|
||||
};
|
||||
|
||||
inferFields(collection) {
|
||||
const titleField = selectInferedField(collection, 'title');
|
||||
const descriptionField = selectInferedField(collection, 'description');
|
||||
const imageField = selectInferedField(collection, 'image');
|
||||
const fields = selectFields(collection);
|
||||
const inferedFields = [titleField, descriptionField, imageField];
|
||||
const remainingFields = fields && fields.filter(f => inferedFields.indexOf(f.get('name')) === -1);
|
||||
return { titleField, descriptionField, imageField, remainingFields };
|
||||
}
|
||||
|
||||
renderCard(collection, entry, inferedFields) {
|
||||
const path = `/collections/${ collection.get('name') }/entries/${ entry.get('slug') }`;
|
||||
const label = entry.get('label');
|
||||
const title = label || entry.getIn(['data', inferedFields.titleField]);
|
||||
const image = entry.getIn(['data', inferedFields.imageField]);
|
||||
return (
|
||||
<Card
|
||||
key={entry.get('slug')}
|
||||
onClick={history.push.bind(this, path)} // eslint-disable-line
|
||||
className={styles.card}
|
||||
>
|
||||
{ image &&
|
||||
<header className={styles.cardImage} style={{ backgroundImage: `url(${ image })` }} />
|
||||
}
|
||||
<h1>{title}</h1>
|
||||
{inferedFields.descriptionField ?
|
||||
<p>{entry.getIn(['data', inferedFields.descriptionField])}</p>
|
||||
: inferedFields.remainingFields && inferedFields.remainingFields.map(f => (
|
||||
<p key={f.get('name')}>
|
||||
<strong>{f.get('label')}:</strong> {entry.getIn(['data', f.get('name')])}
|
||||
</p>
|
||||
))
|
||||
}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
renderCards = () => {
|
||||
const { collections, entries } = this.props;
|
||||
|
||||
if (Map.isMap(collections)) {
|
||||
const inferedFields = this.inferFields(collections);
|
||||
return entries.map(entry => this.renderCard(collections, entry, inferedFields));
|
||||
}
|
||||
return entries.map((entry) => {
|
||||
const collection = collections
|
||||
.filter(collection => collection.get('name') === entry.get('collection')).first();
|
||||
const inferedFields = this.inferFields(collection);
|
||||
return this.renderCard(collection, entry, inferedFields);
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const { children } = this.props;
|
||||
return (
|
||||
<div>
|
||||
<h1>{children}</h1>
|
||||
<div className={styles.cardsGrid}>
|
||||
{ this.renderCards() }
|
||||
<Waypoint onEnter={this.handleLoadMore} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
@ -1,13 +1,17 @@
|
||||
import React, { PropTypes } from 'react';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
|
||||
function isVisible(field) {
|
||||
return field.get('widget') !== 'hidden';
|
||||
}
|
||||
|
||||
export default function Preview({ collection, fields, widgetFor }) {
|
||||
if (!collection || !fields) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
{fields.map(field => widgetFor(field.get('name')))}
|
||||
{fields.filter(isVisible).map(field => widgetFor(field.get('name')))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -3,13 +3,12 @@ import ReactDOM from 'react-dom';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { ScrollSyncPane } from '../ScrollSync';
|
||||
import registry from '../../lib/registry';
|
||||
import Collection from '../../valueObjects/Collection';
|
||||
import { resolveWidget } from '../Widgets';
|
||||
import { selectTemplateName } from '../../reducers/collections';
|
||||
import Preview from './Preview';
|
||||
import styles from './PreviewPane.css';
|
||||
|
||||
export default class PreviewPane extends React.Component {
|
||||
|
||||
componentDidUpdate() {
|
||||
this.renderPreview();
|
||||
}
|
||||
@ -28,8 +27,8 @@ export default class PreviewPane extends React.Component {
|
||||
|
||||
renderPreview() {
|
||||
const { entry, collection } = this.props;
|
||||
const collectionModel = new Collection(collection);
|
||||
const component = registry.getPreviewTemplate(collectionModel.templateName(entry.get('slug'))) || Preview;
|
||||
const component = registry.getPreviewTemplate(selectTemplateName(collection, entry.get('slug'))) || Preview;
|
||||
|
||||
const previewProps = {
|
||||
...this.props,
|
||||
widgetFor: this.widgetFor,
|
||||
|
@ -1,4 +1,4 @@
|
||||
@import "../theme.css";
|
||||
@import '../theme.css';
|
||||
|
||||
.card {
|
||||
composes: base container rounded depth;
|
||||
@ -7,8 +7,8 @@
|
||||
}
|
||||
|
||||
.card > *:not(iframe, video, img, header, footer) {
|
||||
margin-left: 10px;
|
||||
margin-right: 10px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.card > *:not(iframe, video, img, header, footer):first-child {
|
||||
@ -19,6 +19,16 @@
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.card > iframe, .card > video, .card > img {
|
||||
.card > iframe,
|
||||
.card > video,
|
||||
.card > img {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.card h1 {
|
||||
border: none;
|
||||
color: var(--defaultColor);
|
||||
font-size: 18px;
|
||||
margin: 15px 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ import { Link } from 'react-router';
|
||||
import moment from 'moment';
|
||||
import { Card, CardTitle, CardText, CardActions } from 'react-toolbox/lib/card';
|
||||
import Button from 'react-toolbox/lib/button';
|
||||
import { status, statusDescriptions } from '../constants/publishModes';
|
||||
import { status, statusDescriptions } from '../../constants/publishModes';
|
||||
import styles from './UnpublishedListing.css';
|
||||
|
||||
class UnpublishedListing extends React.Component {
|
@ -1,9 +1,9 @@
|
||||
import React, { PropTypes } from 'react';
|
||||
|
||||
export default function StringPreview({ value }) {
|
||||
return <span>{value}</span>;
|
||||
export default function DatePreview({ value }) {
|
||||
return <span>{value ? value.toString() : null}</span>;
|
||||
}
|
||||
|
||||
StringPreview.propTypes = {
|
||||
DatePreview.propTypes = {
|
||||
value: PropTypes.node,
|
||||
};
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React, { PropTypes, Component } from 'react';
|
||||
import { resolveWidget } from '../Widgets';
|
||||
|
||||
export default class ObjectPreview extends Component {
|
||||
export default class ListPreview extends Component {
|
||||
widgetFor = (field, value) => {
|
||||
const { getMedia } = this.props;
|
||||
const widget = resolveWidget(field.get('widget'));
|
||||
@ -22,11 +22,11 @@ export default class ObjectPreview extends Component {
|
||||
</div>)}</div>) : null;
|
||||
}
|
||||
|
||||
return value ? value.join(', ') : null;
|
||||
return <span>{value ? value.join(', ') : null}</span>;
|
||||
}
|
||||
}
|
||||
|
||||
ObjectPreview.propTypes = {
|
||||
ListPreview.propTypes = {
|
||||
value: PropTypes.node,
|
||||
field: PropTypes.node,
|
||||
getMedia: PropTypes.func.isRequired,
|
||||
|
@ -1,9 +1,9 @@
|
||||
import React, { PropTypes } from 'react';
|
||||
|
||||
export default function StringPreview({ value }) {
|
||||
return <span>{value}</span>;
|
||||
export default function NumberPreview({ value }) {
|
||||
return <span>{value ? value.toString() : null}</span>;
|
||||
}
|
||||
|
||||
StringPreview.propTypes = {
|
||||
NumberPreview.propTypes = {
|
||||
value: PropTypes.node,
|
||||
};
|
||||
|
@ -17,7 +17,7 @@ export default class ObjectPreview extends Component {
|
||||
const { field } = this.props;
|
||||
const fields = field && field.get('fields');
|
||||
|
||||
return <div>{fields && fields.map(f => this.widgetFor(f))}</div>;
|
||||
return <div>{fields ? fields.map(f => this.widgetFor(f)) : null}</div>;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React, { PropTypes } from 'react';
|
||||
|
||||
export default function StringPreview({ value }) {
|
||||
return <span>{value}</span>;
|
||||
return <span>{value ? value.toString() : null}</span>;
|
||||
}
|
||||
|
||||
StringPreview.propTypes = {
|
||||
|
@ -1,4 +1,9 @@
|
||||
import StringPreview from './StringPreview';
|
||||
import React, { PropTypes } from 'react';
|
||||
|
||||
export default class TextPreview extends StringPreview {
|
||||
export default function TextPreview({ value }) {
|
||||
return <span>{value ? value.toString() : null}</span>;
|
||||
}
|
||||
|
||||
TextPreview.propTypes = {
|
||||
value: PropTypes.node,
|
||||
};
|
||||
|
@ -1,28 +1,8 @@
|
||||
@import '../components/UI/theme.css';
|
||||
|
||||
.nav {
|
||||
display: block;
|
||||
padding: 1rem;
|
||||
|
||||
& .heading {
|
||||
border: none;
|
||||
}
|
||||
.root {
|
||||
margin-top: 64px;
|
||||
}
|
||||
|
||||
.navDrawer {
|
||||
max-width: 240px !important;
|
||||
|
||||
& .drawerContent {
|
||||
max-width: 240px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.notifsContainer {
|
||||
position: fixed;
|
||||
top: 60px;
|
||||
right: 0;
|
||||
bottom: 60px;
|
||||
z-index: var(--topmostZindex);
|
||||
width: 360px;
|
||||
pointer-events: none;
|
||||
.sidebar {
|
||||
width: 200px;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
@ -2,13 +2,15 @@ import React, { PropTypes } from 'react';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import pluralize from 'pluralize';
|
||||
import { connect } from 'react-redux';
|
||||
import { Layout, Panel, NavDrawer } from 'react-toolbox/lib/layout';
|
||||
import { Layout, Panel } from 'react-toolbox/lib/layout';
|
||||
import { Navigation } from 'react-toolbox/lib/navigation';
|
||||
import { Link } from 'react-toolbox/lib/link';
|
||||
import { Notifs } from 'redux-notifications';
|
||||
import TopBarProgress from 'react-topbar-progress-indicator';
|
||||
import Sidebar from './Sidebar';
|
||||
import { loadConfig } from '../actions/config';
|
||||
import { loginUser, logoutUser } from '../actions/auth';
|
||||
import { toggleSidebar } from '../actions/globalUI';
|
||||
import { currentBackend } from '../backends/backend';
|
||||
import {
|
||||
SHOW_COLLECTION,
|
||||
@ -42,6 +44,7 @@ class App extends React.Component {
|
||||
createNewEntryInCollection: PropTypes.func.isRequired,
|
||||
logoutUser: PropTypes.func.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
toggleSidebar: PropTypes.func.isRequired,
|
||||
navigateToCollection: PropTypes.func.isRequired,
|
||||
user: ImmutablePropTypes.map,
|
||||
runCommand: PropTypes.func.isRequired,
|
||||
@ -59,10 +62,6 @@ class App extends React.Component {
|
||||
</div>);
|
||||
}
|
||||
|
||||
state = {
|
||||
navDrawerIsVisible: true,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.props.dispatch(loadConfig());
|
||||
}
|
||||
@ -123,19 +122,13 @@ class App extends React.Component {
|
||||
return { commands, defaultCommands };
|
||||
}
|
||||
|
||||
toggleNavDrawer = () => {
|
||||
this.setState({
|
||||
navDrawerIsVisible: !this.state.navDrawerIsVisible,
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const { navDrawerIsVisible } = this.state;
|
||||
const {
|
||||
user,
|
||||
config,
|
||||
children,
|
||||
collections,
|
||||
toggleSidebar,
|
||||
runCommand,
|
||||
navigateToCollection,
|
||||
createNewEntryInCollection,
|
||||
@ -160,20 +153,7 @@ class App extends React.Component {
|
||||
}
|
||||
|
||||
const { commands, defaultCommands } = this.generateFindBarCommands();
|
||||
|
||||
return (
|
||||
<Layout theme={styles}>
|
||||
<Notifs
|
||||
className={styles.notifsContainer}
|
||||
CustomComponent={Toast}
|
||||
/>
|
||||
<NavDrawer
|
||||
active={navDrawerIsVisible}
|
||||
scrollY
|
||||
permanentAt={navDrawerIsVisible ? 'lg' : null}
|
||||
onOverlayClick={this.toggleNavDrawer} // eslint-disable-line
|
||||
theme={styles}
|
||||
>
|
||||
const sidebarContent = (
|
||||
<nav className={styles.nav}>
|
||||
<h1 className={styles.heading}>Collections</h1>
|
||||
<Navigation type="vertical">
|
||||
@ -189,7 +169,15 @@ class App extends React.Component {
|
||||
}
|
||||
</Navigation>
|
||||
</nav>
|
||||
</NavDrawer>
|
||||
);
|
||||
|
||||
return (
|
||||
<Sidebar content={sidebarContent}>
|
||||
<Layout theme={styles}>
|
||||
<Notifs
|
||||
className={styles.notifsContainer}
|
||||
CustomComponent={Toast}
|
||||
/>
|
||||
<AppHeader
|
||||
user={user}
|
||||
collections={collections}
|
||||
@ -198,7 +186,7 @@ class App extends React.Component {
|
||||
runCommand={runCommand}
|
||||
onCreateEntryClick={createNewEntryInCollection}
|
||||
onLogoutClick={logoutUser}
|
||||
toggleNavDrawer={this.toggleNavDrawer}
|
||||
toggleDrawer={toggleSidebar}
|
||||
/>
|
||||
<Panel scrollY>
|
||||
{ isFetching && <TopBarProgress /> }
|
||||
@ -206,21 +194,24 @@ class App extends React.Component {
|
||||
{children}
|
||||
</div>
|
||||
</Panel>
|
||||
|
||||
</Layout>
|
||||
</Sidebar>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps(state) {
|
||||
const { auth, config, collections, global } = state;
|
||||
const { auth, config, collections, globalUI } = state;
|
||||
const user = auth && auth.get('user');
|
||||
const { isFetching } = global;
|
||||
const isFetching = globalUI.get('isFetching');
|
||||
return { auth, config, collections, user, isFetching };
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
dispatch,
|
||||
toggleSidebar: () => dispatch(toggleSidebar()),
|
||||
runCommand: (type, payload) => {
|
||||
dispatch(runCommand(type, payload));
|
||||
},
|
||||
|
@ -4,7 +4,7 @@ import { connect } from 'react-redux';
|
||||
import { loadEntries } from '../actions/entries';
|
||||
import { selectEntries } from '../reducers';
|
||||
import { Loader } from '../components/UI';
|
||||
import EntryListing from '../components/EntryListing';
|
||||
import EntryListing from '../components/EntryListing/EntryListing';
|
||||
import styles from './breakpoints.css';
|
||||
|
||||
class CollectionPage extends React.Component {
|
||||
|
@ -11,8 +11,9 @@ import {
|
||||
} from '../actions/entries';
|
||||
import { cancelEdit } from '../actions/editor';
|
||||
import { addMedia, removeMedia } from '../actions/media';
|
||||
import { openSidebar } from '../actions/globalUI';
|
||||
import { selectEntry, getMedia } from '../reducers';
|
||||
import Collection from '../valueObjects/Collection';
|
||||
import { selectFields } from '../reducers/collections';
|
||||
import EntryEditor from '../components/EntryEditor/EntryEditor';
|
||||
import entryPageHOC from './editorialWorkflow/EntryPageHOC';
|
||||
import { Loader } from '../components/UI';
|
||||
@ -32,6 +33,7 @@ class EntryPage extends React.Component {
|
||||
persistEntry: PropTypes.func.isRequired,
|
||||
removeMedia: PropTypes.func.isRequired,
|
||||
cancelEdit: PropTypes.func.isRequired,
|
||||
openSidebar: PropTypes.func.isRequired,
|
||||
fields: ImmutablePropTypes.list.isRequired,
|
||||
slug: PropTypes.string,
|
||||
newEntry: PropTypes.bool.isRequired,
|
||||
@ -39,6 +41,7 @@ class EntryPage extends React.Component {
|
||||
|
||||
componentDidMount() {
|
||||
const { entry, newEntry, collection, slug, loadEntry } = this.props;
|
||||
this.props.openSidebar();
|
||||
if (newEntry) {
|
||||
createEmptyDraft(collection);
|
||||
} else {
|
||||
@ -108,9 +111,8 @@ function mapStateToProps(state, ownProps) {
|
||||
const { collections, entryDraft } = state;
|
||||
const slug = ownProps.params.slug;
|
||||
const collection = collections.get(ownProps.params.name);
|
||||
const collectionModel = new Collection(collection);
|
||||
const newEntry = ownProps.route && ownProps.route.newRecord === true;
|
||||
|
||||
const fields = selectFields(collection, slug);
|
||||
const entry = newEntry ? null : selectEntry(state, collection.get('name'), slug);
|
||||
const boundGetMedia = getMedia.bind(null, state);
|
||||
return {
|
||||
@ -119,7 +121,7 @@ function mapStateToProps(state, ownProps) {
|
||||
newEntry,
|
||||
entryDraft,
|
||||
boundGetMedia,
|
||||
fields: collectionModel.entryFields(slug),
|
||||
fields,
|
||||
slug,
|
||||
entry,
|
||||
};
|
||||
@ -137,5 +139,6 @@ export default connect(
|
||||
discardDraft,
|
||||
persistEntry,
|
||||
cancelEdit,
|
||||
openSidebar,
|
||||
}
|
||||
)(entryPageHOC(EntryPage));
|
||||
|
@ -4,7 +4,7 @@ import { connect } from 'react-redux';
|
||||
import { selectSearchedEntries } from '../reducers';
|
||||
import { searchEntries } from '../actions/entries';
|
||||
import { Loader } from '../components/UI';
|
||||
import EntryListing from '../components/EntryListing';
|
||||
import EntryListing from '../components/EntryListing/EntryListing';
|
||||
import styles from './breakpoints.css';
|
||||
|
||||
class SearchPage extends React.Component {
|
||||
|
8
src/containers/Sidebar.css
Normal file
8
src/containers/Sidebar.css
Normal file
@ -0,0 +1,8 @@
|
||||
.root {
|
||||
margin-top: 64px;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
width: 200px;
|
||||
background-color: #fff
|
||||
}
|
63
src/containers/Sidebar.js
Normal file
63
src/containers/Sidebar.js
Normal file
@ -0,0 +1,63 @@
|
||||
import React, { PropTypes } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import ReactSidebar from 'react-sidebar';
|
||||
import _ from 'lodash';
|
||||
import { openSidebar } from '../actions/globalUI';
|
||||
import styles from './Sidebar.css';
|
||||
|
||||
class Sidebar extends React.Component {
|
||||
|
||||
static propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
content: PropTypes.node.isRequired,
|
||||
sidebarIsOpen: PropTypes.bool.isRequired,
|
||||
openSidebar: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
state = { sidebarDocked: false };
|
||||
|
||||
componentWillMount() {
|
||||
this.mql = window.matchMedia('(min-width: 1200px)');
|
||||
this.mql.addListener(this.mediaQueryChanged);
|
||||
this.setState({ sidebarDocked: this.mql.matches });
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.mql.removeListener(this.mediaQueryChanged);
|
||||
}
|
||||
|
||||
mediaQueryChanged = _.throttle(() => {
|
||||
this.setState({ sidebarDocked: this.mql.matches });
|
||||
}, 500);
|
||||
|
||||
|
||||
render() {
|
||||
const {
|
||||
children,
|
||||
content,
|
||||
sidebarIsOpen,
|
||||
openSidebar,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<ReactSidebar
|
||||
sidebar={content}
|
||||
rootClassName={styles.root}
|
||||
sidebarClassName={styles.sidebar}
|
||||
docked={sidebarIsOpen && this.state.sidebarDocked} // ALWAYS can hide sidebar
|
||||
open={sidebarIsOpen}
|
||||
onSetOpen={openSidebar}
|
||||
>
|
||||
{children}
|
||||
</ReactSidebar>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps(state) {
|
||||
const { globalUI } = state;
|
||||
const sidebarIsOpen = globalUI.get('sidebarIsOpen');
|
||||
return { sidebarIsOpen };
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, { openSidebar })(Sidebar);
|
@ -5,11 +5,13 @@ import { connect } from 'react-redux';
|
||||
import { loadUnpublishedEntries, updateUnpublishedEntryStatus, publishUnpublishedEntry } from '../../actions/editorialWorkflow';
|
||||
import { selectUnpublishedEntries } from '../../reducers';
|
||||
import { EDITORIAL_WORKFLOW, status } from '../../constants/publishModes';
|
||||
import UnpublishedListing from '../../components/UnpublishedListing';
|
||||
import UnpublishedListing from '../../components/UnpublishedListing/UnpublishedListing';
|
||||
import { Loader } from '../../components/UI';
|
||||
|
||||
class unpublishedEntriesPanel extends Component {
|
||||
static propTypes = {
|
||||
isEditorialWorkflow: PropTypes.bool.isRequired,
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
unpublishedEntries: ImmutablePropTypes.map,
|
||||
loadUnpublishedEntries: PropTypes.func.isRequired,
|
||||
updateUnpublishedEntryStatus: PropTypes.func.isRequired,
|
||||
@ -24,9 +26,9 @@ class unpublishedEntriesPanel extends Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { isEditorialWorkflow, unpublishedEntries, updateUnpublishedEntryStatus, publishUnpublishedEntry } = this.props;
|
||||
const { isEditorialWorkflow, isFetching, unpublishedEntries, updateUnpublishedEntryStatus, publishUnpublishedEntry } = this.props;
|
||||
if (!isEditorialWorkflow) return null;
|
||||
|
||||
if (isFetching) return <Loader active>Loading Editorial Workflow Entries</Loader>;
|
||||
return (
|
||||
<UnpublishedListing
|
||||
entries={unpublishedEntries}
|
||||
@ -42,6 +44,8 @@ function mapStateToProps(state) {
|
||||
const returnObj = { isEditorialWorkflow };
|
||||
|
||||
if (isEditorialWorkflow) {
|
||||
returnObj.isFetching = state.editorialWorkflow.getIn(['pages', 'isFetching'], false);
|
||||
|
||||
/*
|
||||
* Generates an ordered Map of the available status as keys.
|
||||
* Each key containing a List of available unpubhlished entries
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { createEntry } from '../../../valueObjects/Entry';
|
||||
import _ from 'lodash';
|
||||
import Collection from '../../../valueObjects/Collection';
|
||||
import { createEntry } from '../../../valueObjects/Entry';
|
||||
import { selectEntrySlug } from '../../../reducers/collections';
|
||||
|
||||
function getSlug(path) {
|
||||
const m = path.match(/([^\/]+?)(\.[^\/\.]+)?$/);
|
||||
@ -102,12 +102,11 @@ export default class Algolia {
|
||||
if (this.entriesCache.collection === collection && this.entriesCache.page === page) {
|
||||
return Promise.resolve({ page: this.entriesCache.page, entries: this.entriesCache.entries });
|
||||
} else {
|
||||
const collectionModel = new Collection(collection);
|
||||
return this.request(`${ this.searchURL }/indexes/${ collection.get('name') }`, {
|
||||
params: { page },
|
||||
}).then((response) => {
|
||||
const entries = response.hits.map((hit) => {
|
||||
const slug = collectionModel.entrySlug(hit.path);
|
||||
const slug = selectEntrySlug(collection, hit.path);
|
||||
return createEntry(collection.get('name'), slug, hit.path, { data: hit.data, partial: true });
|
||||
});
|
||||
this.entriesCache = { collection, pagination: response.page, entries };
|
||||
|
8
src/lib/consoleError.js
Normal file
8
src/lib/consoleError.js
Normal file
@ -0,0 +1,8 @@
|
||||
|
||||
export default function consoleError(title, description) {
|
||||
console.error(
|
||||
`%c ⛔ ${ title }\n` + `%c${ description }\n\n`,
|
||||
'color: black; font-weight: bold; font-size: 16px; line-height: 50px;',
|
||||
'color: black;'
|
||||
);
|
||||
}
|
@ -1,19 +1,22 @@
|
||||
import { OrderedMap, fromJS } from 'immutable';
|
||||
import consoleError from '../lib/consoleError';
|
||||
import { CONFIG_SUCCESS } from '../actions/config';
|
||||
import { FILES, FOLDER } from '../constants/collectionTypes';
|
||||
|
||||
const hasProperty = (config, property) => ({}.hasOwnProperty.call(config, property));
|
||||
|
||||
const collections = (state = null, action) => {
|
||||
const configCollections = action.payload && action.payload.collections;
|
||||
switch (action.type) {
|
||||
case CONFIG_SUCCESS:
|
||||
const configCollections = action.payload && action.payload.collections;
|
||||
return OrderedMap().withMutations((map) => {
|
||||
(configCollections || []).forEach((configCollection) => {
|
||||
if (hasProperty(configCollection, 'folder')) {
|
||||
configCollection.type = FOLDER; // eslint-disable-line no-param-reassign
|
||||
} else if (hasProperty(configCollection, 'files')) {
|
||||
configCollection.type = FILES; // eslint-disable-line no-param-reassign
|
||||
} else {
|
||||
throw new Error('Unknown collection type. Collections can be either Folder based or File based. Please verify your site configuration');
|
||||
}
|
||||
map.set(configCollection.name, fromJS(configCollection));
|
||||
});
|
||||
@ -23,4 +26,126 @@ const collections = (state = null, action) => {
|
||||
}
|
||||
};
|
||||
|
||||
const formatToExtension = format => ({
|
||||
markdown: 'md',
|
||||
yaml: 'yml',
|
||||
json: 'json',
|
||||
html: 'html',
|
||||
}[format]);
|
||||
|
||||
const inferables = {
|
||||
title: {
|
||||
type: 'string',
|
||||
secondaryTypes: [],
|
||||
synonyms: ['title', 'name', 'label', 'headline'],
|
||||
fallbackToFirstField: true,
|
||||
showError: true,
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
secondaryTypes: ['text', 'markdown'],
|
||||
synonyms: ['shortDescription', 'short_description', 'shortdescription', 'description', 'brief', 'body', 'content', 'biography', 'bio'],
|
||||
fallbackToFirstField: false,
|
||||
showError: false,
|
||||
},
|
||||
image: {
|
||||
type: 'image',
|
||||
secondaryTypes: [],
|
||||
synonyms: ['image', 'thumbnail', 'thumb', 'picture', 'avatar'],
|
||||
fallbackToFirstField: false,
|
||||
showError: false,
|
||||
},
|
||||
};
|
||||
|
||||
const selectors = {
|
||||
[FOLDER]: {
|
||||
entryExtension(collection) {
|
||||
return collection.get('extension') || formatToExtension(collection.get('format') || 'markdown');
|
||||
},
|
||||
fields(collection) {
|
||||
return collection.get('fields');
|
||||
},
|
||||
entryPath(collection, slug) {
|
||||
return `${ collection.get('folder') }/${ slug }.${ this.entryExtension(collection) }`;
|
||||
},
|
||||
entrySlug(collection, path) {
|
||||
return path.split('/').pop().replace(/\.[^\.]+$/, '');
|
||||
},
|
||||
listMethod() {
|
||||
return 'entriesByFolder';
|
||||
},
|
||||
allowNewEntries(collection) {
|
||||
return collection.get('create');
|
||||
},
|
||||
templateName(collection) {
|
||||
return collection.get('name');
|
||||
},
|
||||
},
|
||||
[FILES]: {
|
||||
fileForEntry(collection, slug) {
|
||||
const files = collection.get('files');
|
||||
return files.filter(f => f.get('name') === slug).get(0);
|
||||
},
|
||||
fields(collection, slug) {
|
||||
const file = this.fileForEntry(collection, slug);
|
||||
return file && file.get('fields');
|
||||
},
|
||||
entryPath(collection, slug) {
|
||||
const file = this.fileForEntry(collection, slug);
|
||||
return file && file.get('file');
|
||||
},
|
||||
entrySlug(collection, path) {
|
||||
const file = collection.get('files').filter(f => f.get('file') === path).get(0);
|
||||
return file && file.get('name');
|
||||
},
|
||||
listMethod() {
|
||||
return 'entriesByFiles';
|
||||
},
|
||||
allowNewEntries() {
|
||||
return false;
|
||||
},
|
||||
templateName(collection, slug) {
|
||||
return slug;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const selectFields = (collection, slug) => selectors[collection.get('type')].fields(collection, slug);
|
||||
export const selectEntryPath = (collection, slug) => selectors[collection.get('type')].entryPath(collection, slug);
|
||||
export const selectEntrySlug = (collection, path) => selectors[collection.get('type')].entrySlug(collection, path);
|
||||
export const selectListMethod = collection => selectors[collection.get('type')].listMethod();
|
||||
export const selectAllowNewEntries = collection => selectors[collection.get('type')].allowNewEntries(collection);
|
||||
export const selectTemplateName = (collection, slug) => selectors[collection.get('type')].templateName(collection, slug);
|
||||
export const selectInferedField = (collection, fieldName) => {
|
||||
const inferableField = inferables[fieldName];
|
||||
const fields = collection.get('fields');
|
||||
let field;
|
||||
|
||||
// If colllection has no fields or fieldName is not defined within inferables list, return null
|
||||
if (!fields || !inferableField) return null;
|
||||
|
||||
// Try to return a field of the specified type with one of the synonyms
|
||||
const mainTypeFields = fields.filter(f => f.get('widget') === inferableField.type).map(f => f.get('name'));
|
||||
field = mainTypeFields.filter(f => inferableField.synonyms.indexOf(f) !== -1);
|
||||
if (field && field.size > 0) return field.first();
|
||||
|
||||
// Try to return a field for each of the specified secondary types
|
||||
const secondaryTypeFields = fields.filter(f => inferableField.secondaryTypes.indexOf(f.get('widget')) !== -1).map(f => f.get('name'));
|
||||
field = secondaryTypeFields.filter(f => inferableField.synonyms.indexOf(f) !== -1);
|
||||
if (field && field.size > 0) return field.first();
|
||||
|
||||
// Try to return the first field of the specified type
|
||||
if (inferableField.fallbackToFirstField && mainTypeFields.size > 0) return mainTypeFields.first();
|
||||
|
||||
// Coundn't infer the field. Show error and return null.
|
||||
if (inferableField.showError) {
|
||||
consoleError(
|
||||
`The Field ${ fieldName } is missing for the collection “${ collection.get('name') }”`,
|
||||
`Netlify CMS tries to infer the entry ${ fieldName } automatically, but one couldn\'t be found for entries of the collection “${ collection.get('name') }”. Please check your site configuration.`
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default collections;
|
||||
|
@ -1,17 +0,0 @@
|
||||
/* Reducer for some global UI state that we want to share between components
|
||||
* Now being used for isFetching state to display global loading indicator
|
||||
* */
|
||||
|
||||
const globalReducer = (state = { isFetching: false }, action) => {
|
||||
if ((action.type.indexOf('REQUEST') > -1)) {
|
||||
return { isFetching: true };
|
||||
} else if (
|
||||
(action.type.indexOf('SUCCESS') > -1) ||
|
||||
(action.type.indexOf('FAILURE') > -1)
|
||||
) {
|
||||
return { isFetching: false };
|
||||
}
|
||||
return state;
|
||||
};
|
||||
|
||||
export default globalReducer;
|
27
src/reducers/globalUI.js
Normal file
27
src/reducers/globalUI.js
Normal file
@ -0,0 +1,27 @@
|
||||
import { Map } from 'immutable';
|
||||
import { TOGGLE_SIDEBAR, OPEN_SIDEBAR } from '../actions/globalUI';
|
||||
/*
|
||||
* Reducer for some global UI state that we want to share between components
|
||||
* */
|
||||
const globalUI = (state = Map({ isFetching: false, sidebarIsOpen: true }), action) => {
|
||||
// Generic, global loading indicator
|
||||
if ((action.type.indexOf('REQUEST') > -1)) {
|
||||
return state.set('isFetching', true);
|
||||
} else if (
|
||||
(action.type.indexOf('SUCCESS') > -1) ||
|
||||
(action.type.indexOf('FAILURE') > -1)
|
||||
) {
|
||||
return state.set('isFetching', false);
|
||||
}
|
||||
|
||||
switch (action.type) {
|
||||
case TOGGLE_SIDEBAR:
|
||||
return state.set('sidebarIsOpen', !state.get('sidebarIsOpen'));
|
||||
case OPEN_SIDEBAR:
|
||||
return state.set('sidebarIsOpen', action.payload.open);
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default globalUI;
|
@ -7,7 +7,7 @@ import editorialWorkflow, * as fromEditorialWorkflow from './editorialWorkflow';
|
||||
import entryDraft from './entryDraft';
|
||||
import collections from './collections';
|
||||
import medias, * as fromMedias from './medias';
|
||||
import global from './global';
|
||||
import globalUI from './globalUI';
|
||||
|
||||
const reducers = {
|
||||
auth,
|
||||
@ -19,7 +19,7 @@ const reducers = {
|
||||
editorialWorkflow,
|
||||
entryDraft,
|
||||
medias,
|
||||
global,
|
||||
globalUI,
|
||||
};
|
||||
|
||||
export default reducers;
|
||||
|
@ -1,121 +0,0 @@
|
||||
import { FOLDER, FILES } from '../constants/collectionTypes';
|
||||
|
||||
function formatToExtension(format) {
|
||||
return {
|
||||
markdown: 'md',
|
||||
yaml: 'yml',
|
||||
json: 'json',
|
||||
html: 'html',
|
||||
}[format];
|
||||
}
|
||||
|
||||
class FolderCollection {
|
||||
constructor(collection) {
|
||||
this.collection = collection;
|
||||
}
|
||||
|
||||
entryFields() {
|
||||
return this.collection.get('fields');
|
||||
}
|
||||
|
||||
entryPath(slug) {
|
||||
return `${ this.collection.get('folder') }/${ slug }.${ this.entryExtension() }`;
|
||||
}
|
||||
|
||||
entrySlug(path) {
|
||||
return path.split('/').pop().replace(/\.[^\.]+$/, '');
|
||||
}
|
||||
|
||||
listMethod() {
|
||||
return 'entriesByFolder';
|
||||
}
|
||||
|
||||
entryExtension() {
|
||||
return this.collection.get('extension') || formatToExtension(this.collection.get('format') || 'markdown');
|
||||
}
|
||||
|
||||
allowNewEntries() {
|
||||
return this.collection.get('create');
|
||||
}
|
||||
|
||||
templateName() {
|
||||
return this.collection.get('name');
|
||||
}
|
||||
}
|
||||
|
||||
class FilesCollection {
|
||||
constructor(collection) {
|
||||
this.collection = collection;
|
||||
}
|
||||
|
||||
entryFields(slug) {
|
||||
const file = this.fileForEntry(slug);
|
||||
return file && file.get('fields');
|
||||
}
|
||||
|
||||
entryPath(slug) {
|
||||
const file = this.fileForEntry(slug);
|
||||
return file && file.get('file');
|
||||
}
|
||||
|
||||
entrySlug(path) {
|
||||
const file = this.collection.get('files').filter(f => f.get('file') === path).get(0);
|
||||
return file && file.get('name');
|
||||
}
|
||||
|
||||
fileForEntry(slug) {
|
||||
const files = this.collection.get('files');
|
||||
return files.filter(f => f.get('name') === slug).get(0);
|
||||
}
|
||||
|
||||
listMethod() {
|
||||
return 'entriesByFiles';
|
||||
}
|
||||
|
||||
allowNewEntries() {
|
||||
return false;
|
||||
}
|
||||
|
||||
templateName(slug) {
|
||||
return slug;
|
||||
}
|
||||
}
|
||||
|
||||
export default class Collection {
|
||||
constructor(collection) {
|
||||
switch (collection.get('type')) {
|
||||
case FOLDER:
|
||||
this.collection = new FolderCollection(collection);
|
||||
break;
|
||||
case FILES:
|
||||
this.collection = new FilesCollection(collection);
|
||||
break;
|
||||
default:
|
||||
throw ('Unknown collection type: %o', collection.get('type'));
|
||||
}
|
||||
}
|
||||
|
||||
entryFields(slug) {
|
||||
return this.collection.entryFields(slug);
|
||||
}
|
||||
|
||||
entryPath(slug) {
|
||||
return this.collection.entryPath(slug);
|
||||
}
|
||||
|
||||
entrySlug(path) {
|
||||
return this.collection.entrySlug(path);
|
||||
}
|
||||
|
||||
listMethod() {
|
||||
return this.collection.listMethod();
|
||||
}
|
||||
|
||||
allowNewEntries() {
|
||||
return this.collection.allowNewEntries();
|
||||
}
|
||||
|
||||
templateName(slug) {
|
||||
return this.collection.templateName(slug);
|
||||
}
|
||||
}
|
@ -6961,6 +6961,10 @@ react-router@^2.5.1:
|
||||
loose-envify "^1.2.0"
|
||||
warning "^3.0.0"
|
||||
|
||||
react-sidebar:
|
||||
version "2.2.1"
|
||||
resolved "https://registry.yarnpkg.com/react-sidebar/-/react-sidebar-2.2.1.tgz#a8faf6a3c62ddc562c70680d5d016fe9741b585f"
|
||||
|
||||
react-simple-di@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/react-simple-di/-/react-simple-di-1.2.0.tgz#dde0e5bf689f391ef2ab02c9043b213fe239c6d0"
|
||||
|
Loading…
x
Reference in New Issue
Block a user