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-redux": "^4.4.0",
|
||||||
"react-router": "^2.5.1",
|
"react-router": "^2.5.1",
|
||||||
"react-router-redux": "^4.0.5",
|
"react-router-redux": "^4.0.5",
|
||||||
|
"react-sidebar": "^2.2.1",
|
||||||
"react-simple-dnd": "^0.1.2",
|
"react-simple-dnd": "^0.1.2",
|
||||||
"react-sortable": "^1.2.0",
|
"react-sortable": "^1.2.0",
|
||||||
"react-toolbox": "^1.2.1",
|
"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 GitHubBackend from './github/implementation';
|
||||||
import NetlifyGitBackend from './netlify-git/implementation';
|
import NetlifyGitBackend from './netlify-git/implementation';
|
||||||
import { resolveFormat } from '../formats/formats';
|
import { resolveFormat } from '../formats/formats';
|
||||||
|
import { selectListMethod, selectEntrySlug, selectEntryPath, selectAllowNewEntries } from '../reducers/collections';
|
||||||
import { createEntry } from '../valueObjects/Entry';
|
import { createEntry } from '../valueObjects/Entry';
|
||||||
import Collection from '../valueObjects/Collection';
|
|
||||||
|
|
||||||
class LocalStorageAuthStore {
|
class LocalStorageAuthStore {
|
||||||
storageKey = 'nf-cms-user';
|
storageKey = 'nf-cms-user';
|
||||||
@ -80,13 +80,12 @@ class Backend {
|
|||||||
}
|
}
|
||||||
|
|
||||||
listEntries(collection) {
|
listEntries(collection) {
|
||||||
const collectionModel = new Collection(collection);
|
const listMethod = this.implementation[selectListMethod(collection)];
|
||||||
const listMethod = this.implementation[collectionModel.listMethod()];
|
|
||||||
return listMethod.call(this.implementation, collection)
|
return listMethod.call(this.implementation, collection)
|
||||||
.then(loadedEntries => (
|
.then(loadedEntries => (
|
||||||
loadedEntries.map(loadedEntry => createEntry(
|
loadedEntries.map(loadedEntry => createEntry(
|
||||||
collection.get('name'),
|
collection.get('name'),
|
||||||
collectionModel.entrySlug(loadedEntry.file.path),
|
selectEntrySlug(collection, loadedEntry.file.path),
|
||||||
loadedEntry.file.path,
|
loadedEntry.file.path,
|
||||||
{ raw: loadedEntry.data, label: loadedEntry.file.label }
|
{ raw: loadedEntry.data, label: loadedEntry.file.label }
|
||||||
))
|
))
|
||||||
@ -99,7 +98,7 @@ class Backend {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getEntry(collection, slug) {
|
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(
|
.then(loadedEntry => this.entryWithFormat(collection, slug)(createEntry(
|
||||||
collection.get('name'),
|
collection.get('name'),
|
||||||
slug,
|
slug,
|
||||||
@ -150,7 +149,6 @@ class Backend {
|
|||||||
}
|
}
|
||||||
|
|
||||||
persistEntry(config, collection, entryDraft, MediaFiles, options) {
|
persistEntry(config, collection, entryDraft, MediaFiles, options) {
|
||||||
const collectionModel = new Collection(collection);
|
|
||||||
const newEntry = entryDraft.getIn(['entry', 'newRecord']) || false;
|
const newEntry = entryDraft.getIn(['entry', 'newRecord']) || false;
|
||||||
|
|
||||||
const parsedData = {
|
const parsedData = {
|
||||||
@ -161,11 +159,11 @@ class Backend {
|
|||||||
const entryData = entryDraft.getIn(['entry', 'data']).toJS();
|
const entryData = entryDraft.getIn(['entry', 'data']).toJS();
|
||||||
let entryObj;
|
let entryObj;
|
||||||
if (newEntry) {
|
if (newEntry) {
|
||||||
if (!collectionModel.allowNewEntries()) {
|
if (!selectAllowNewEntries(collection)) {
|
||||||
throw (new Error('Not allowed to create new entries in this collection'));
|
throw (new Error('Not allowed to create new entries in this collection'));
|
||||||
}
|
}
|
||||||
const slug = slugFormatter(collection.get('slug'), entryDraft.getIn(['entry', 'data']));
|
const slug = slugFormatter(collection.get('slug'), entryDraft.getIn(['entry', 'data']));
|
||||||
const path = collectionModel.entryPath(slug);
|
const path = selectEntryPath(collection, slug);
|
||||||
entryObj = {
|
entryObj = {
|
||||||
path,
|
path,
|
||||||
slug,
|
slug,
|
||||||
|
@ -17,7 +17,7 @@ export default class AppHeader extends React.Component {
|
|||||||
commands: PropTypes.array.isRequired, // eslint-disable-line
|
commands: PropTypes.array.isRequired, // eslint-disable-line
|
||||||
defaultCommands: PropTypes.array.isRequired, // eslint-disable-line
|
defaultCommands: PropTypes.array.isRequired, // eslint-disable-line
|
||||||
runCommand: PropTypes.func.isRequired,
|
runCommand: PropTypes.func.isRequired,
|
||||||
toggleNavDrawer: PropTypes.func.isRequired,
|
toggleDrawer: PropTypes.func.isRequired,
|
||||||
onCreateEntryClick: PropTypes.func.isRequired,
|
onCreateEntryClick: PropTypes.func.isRequired,
|
||||||
onLogoutClick: PropTypes.func.isRequired,
|
onLogoutClick: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
@ -59,7 +59,7 @@ export default class AppHeader extends React.Component {
|
|||||||
commands,
|
commands,
|
||||||
defaultCommands,
|
defaultCommands,
|
||||||
runCommand,
|
runCommand,
|
||||||
toggleNavDrawer,
|
toggleDrawer,
|
||||||
onLogoutClick,
|
onLogoutClick,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
@ -88,7 +88,7 @@ export default class AppHeader extends React.Component {
|
|||||||
</Menu>
|
</Menu>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
onLeftIconClick={toggleNavDrawer}
|
onLeftIconClick={toggleDrawer}
|
||||||
onRightIconClick={this.handleRightIconClick}
|
onRightIconClick={this.handleRightIconClick}
|
||||||
>
|
>
|
||||||
<IndexLink to="/">
|
<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 { resolveWidget } from '../Widgets';
|
||||||
import styles from './ControlPane.css';
|
import styles from './ControlPane.css';
|
||||||
|
|
||||||
|
function isHidden(field) {
|
||||||
|
return field.get('widget') === 'hidden';
|
||||||
|
}
|
||||||
|
|
||||||
export default class ControlPane extends Component {
|
export default class ControlPane extends Component {
|
||||||
|
|
||||||
controlFor(field) {
|
controlFor(field) {
|
||||||
@ -38,7 +42,7 @@ export default class ControlPane extends Component {
|
|||||||
<div>
|
<div>
|
||||||
{
|
{
|
||||||
fields.map(field =>
|
fields.map(field =>
|
||||||
<div
|
isHidden(field) ? null : <div
|
||||||
key={field.get('name')}
|
key={field.get('name')}
|
||||||
className={styles.widget}
|
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 React, { PropTypes } from 'react';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
|
||||||
|
function isVisible(field) {
|
||||||
|
return field.get('widget') !== 'hidden';
|
||||||
|
}
|
||||||
|
|
||||||
export default function Preview({ collection, fields, widgetFor }) {
|
export default function Preview({ collection, fields, widgetFor }) {
|
||||||
if (!collection || !fields) {
|
if (!collection || !fields) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{fields.map(field => widgetFor(field.get('name')))}
|
{fields.filter(isVisible).map(field => widgetFor(field.get('name')))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -3,13 +3,12 @@ import ReactDOM from 'react-dom';
|
|||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import { ScrollSyncPane } from '../ScrollSync';
|
import { ScrollSyncPane } from '../ScrollSync';
|
||||||
import registry from '../../lib/registry';
|
import registry from '../../lib/registry';
|
||||||
import Collection from '../../valueObjects/Collection';
|
|
||||||
import { resolveWidget } from '../Widgets';
|
import { resolveWidget } from '../Widgets';
|
||||||
|
import { selectTemplateName } from '../../reducers/collections';
|
||||||
import Preview from './Preview';
|
import Preview from './Preview';
|
||||||
import styles from './PreviewPane.css';
|
import styles from './PreviewPane.css';
|
||||||
|
|
||||||
export default class PreviewPane extends React.Component {
|
export default class PreviewPane extends React.Component {
|
||||||
|
|
||||||
componentDidUpdate() {
|
componentDidUpdate() {
|
||||||
this.renderPreview();
|
this.renderPreview();
|
||||||
}
|
}
|
||||||
@ -28,8 +27,8 @@ export default class PreviewPane extends React.Component {
|
|||||||
|
|
||||||
renderPreview() {
|
renderPreview() {
|
||||||
const { entry, collection } = this.props;
|
const { entry, collection } = this.props;
|
||||||
const collectionModel = new Collection(collection);
|
const component = registry.getPreviewTemplate(selectTemplateName(collection, entry.get('slug'))) || Preview;
|
||||||
const component = registry.getPreviewTemplate(collectionModel.templateName(entry.get('slug'))) || Preview;
|
|
||||||
const previewProps = {
|
const previewProps = {
|
||||||
...this.props,
|
...this.props,
|
||||||
widgetFor: this.widgetFor,
|
widgetFor: this.widgetFor,
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
@import "../theme.css";
|
@import '../theme.css';
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
composes: base container rounded depth;
|
composes: base container rounded depth;
|
||||||
@ -7,8 +7,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.card > *:not(iframe, video, img, header, footer) {
|
.card > *:not(iframe, video, img, header, footer) {
|
||||||
margin-left: 10px;
|
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
|
margin-left: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card > *:not(iframe, video, img, header, footer):first-child {
|
.card > *:not(iframe, video, img, header, footer):first-child {
|
||||||
@ -19,6 +19,16 @@
|
|||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card > iframe, .card > video, .card > img {
|
.card > iframe,
|
||||||
|
.card > video,
|
||||||
|
.card > img {
|
||||||
max-width: 100%;
|
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 moment from 'moment';
|
||||||
import { Card, CardTitle, CardText, CardActions } from 'react-toolbox/lib/card';
|
import { Card, CardTitle, CardText, CardActions } from 'react-toolbox/lib/card';
|
||||||
import Button from 'react-toolbox/lib/button';
|
import Button from 'react-toolbox/lib/button';
|
||||||
import { status, statusDescriptions } from '../constants/publishModes';
|
import { status, statusDescriptions } from '../../constants/publishModes';
|
||||||
import styles from './UnpublishedListing.css';
|
import styles from './UnpublishedListing.css';
|
||||||
|
|
||||||
class UnpublishedListing extends React.Component {
|
class UnpublishedListing extends React.Component {
|
@ -1,9 +1,9 @@
|
|||||||
import React, { PropTypes } from 'react';
|
import React, { PropTypes } from 'react';
|
||||||
|
|
||||||
export default function StringPreview({ value }) {
|
export default function DatePreview({ value }) {
|
||||||
return <span>{value}</span>;
|
return <span>{value ? value.toString() : null}</span>;
|
||||||
}
|
}
|
||||||
|
|
||||||
StringPreview.propTypes = {
|
DatePreview.propTypes = {
|
||||||
value: PropTypes.node,
|
value: PropTypes.node,
|
||||||
};
|
};
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import React, { PropTypes, Component } from 'react';
|
import React, { PropTypes, Component } from 'react';
|
||||||
import { resolveWidget } from '../Widgets';
|
import { resolveWidget } from '../Widgets';
|
||||||
|
|
||||||
export default class ObjectPreview extends Component {
|
export default class ListPreview extends Component {
|
||||||
widgetFor = (field, value) => {
|
widgetFor = (field, value) => {
|
||||||
const { getMedia } = this.props;
|
const { getMedia } = this.props;
|
||||||
const widget = resolveWidget(field.get('widget'));
|
const widget = resolveWidget(field.get('widget'));
|
||||||
@ -22,11 +22,11 @@ export default class ObjectPreview extends Component {
|
|||||||
</div>)}</div>) : null;
|
</div>)}</div>) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return value ? value.join(', ') : null;
|
return <span>{value ? value.join(', ') : null}</span>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ObjectPreview.propTypes = {
|
ListPreview.propTypes = {
|
||||||
value: PropTypes.node,
|
value: PropTypes.node,
|
||||||
field: PropTypes.node,
|
field: PropTypes.node,
|
||||||
getMedia: PropTypes.func.isRequired,
|
getMedia: PropTypes.func.isRequired,
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import React, { PropTypes } from 'react';
|
import React, { PropTypes } from 'react';
|
||||||
|
|
||||||
export default function StringPreview({ value }) {
|
export default function NumberPreview({ value }) {
|
||||||
return <span>{value}</span>;
|
return <span>{value ? value.toString() : null}</span>;
|
||||||
}
|
}
|
||||||
|
|
||||||
StringPreview.propTypes = {
|
NumberPreview.propTypes = {
|
||||||
value: PropTypes.node,
|
value: PropTypes.node,
|
||||||
};
|
};
|
||||||
|
@ -17,7 +17,7 @@ export default class ObjectPreview extends Component {
|
|||||||
const { field } = this.props;
|
const { field } = this.props;
|
||||||
const fields = field && field.get('fields');
|
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';
|
import React, { PropTypes } from 'react';
|
||||||
|
|
||||||
export default function StringPreview({ value }) {
|
export default function StringPreview({ value }) {
|
||||||
return <span>{value}</span>;
|
return <span>{value ? value.toString() : null}</span>;
|
||||||
}
|
}
|
||||||
|
|
||||||
StringPreview.propTypes = {
|
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';
|
.root {
|
||||||
|
margin-top: 64px;
|
||||||
.nav {
|
|
||||||
display: block;
|
|
||||||
padding: 1rem;
|
|
||||||
|
|
||||||
& .heading {
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.navDrawer {
|
.sidebar {
|
||||||
max-width: 240px !important;
|
width: 200px;
|
||||||
|
background-color: #fff;
|
||||||
& .drawerContent {
|
|
||||||
max-width: 240px !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.notifsContainer {
|
|
||||||
position: fixed;
|
|
||||||
top: 60px;
|
|
||||||
right: 0;
|
|
||||||
bottom: 60px;
|
|
||||||
z-index: var(--topmostZindex);
|
|
||||||
width: 360px;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
}
|
||||||
|
@ -2,13 +2,15 @@ import React, { PropTypes } from 'react';
|
|||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import pluralize from 'pluralize';
|
import pluralize from 'pluralize';
|
||||||
import { connect } from 'react-redux';
|
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 { Navigation } from 'react-toolbox/lib/navigation';
|
||||||
import { Link } from 'react-toolbox/lib/link';
|
import { Link } from 'react-toolbox/lib/link';
|
||||||
import { Notifs } from 'redux-notifications';
|
import { Notifs } from 'redux-notifications';
|
||||||
import TopBarProgress from 'react-topbar-progress-indicator';
|
import TopBarProgress from 'react-topbar-progress-indicator';
|
||||||
|
import Sidebar from './Sidebar';
|
||||||
import { loadConfig } from '../actions/config';
|
import { loadConfig } from '../actions/config';
|
||||||
import { loginUser, logoutUser } from '../actions/auth';
|
import { loginUser, logoutUser } from '../actions/auth';
|
||||||
|
import { toggleSidebar } from '../actions/globalUI';
|
||||||
import { currentBackend } from '../backends/backend';
|
import { currentBackend } from '../backends/backend';
|
||||||
import {
|
import {
|
||||||
SHOW_COLLECTION,
|
SHOW_COLLECTION,
|
||||||
@ -42,6 +44,7 @@ class App extends React.Component {
|
|||||||
createNewEntryInCollection: PropTypes.func.isRequired,
|
createNewEntryInCollection: PropTypes.func.isRequired,
|
||||||
logoutUser: PropTypes.func.isRequired,
|
logoutUser: PropTypes.func.isRequired,
|
||||||
dispatch: PropTypes.func.isRequired,
|
dispatch: PropTypes.func.isRequired,
|
||||||
|
toggleSidebar: PropTypes.func.isRequired,
|
||||||
navigateToCollection: PropTypes.func.isRequired,
|
navigateToCollection: PropTypes.func.isRequired,
|
||||||
user: ImmutablePropTypes.map,
|
user: ImmutablePropTypes.map,
|
||||||
runCommand: PropTypes.func.isRequired,
|
runCommand: PropTypes.func.isRequired,
|
||||||
@ -59,10 +62,6 @@ class App extends React.Component {
|
|||||||
</div>);
|
</div>);
|
||||||
}
|
}
|
||||||
|
|
||||||
state = {
|
|
||||||
navDrawerIsVisible: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.props.dispatch(loadConfig());
|
this.props.dispatch(loadConfig());
|
||||||
}
|
}
|
||||||
@ -123,19 +122,13 @@ class App extends React.Component {
|
|||||||
return { commands, defaultCommands };
|
return { commands, defaultCommands };
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleNavDrawer = () => {
|
|
||||||
this.setState({
|
|
||||||
navDrawerIsVisible: !this.state.navDrawerIsVisible,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { navDrawerIsVisible } = this.state;
|
|
||||||
const {
|
const {
|
||||||
user,
|
user,
|
||||||
config,
|
config,
|
||||||
children,
|
children,
|
||||||
collections,
|
collections,
|
||||||
|
toggleSidebar,
|
||||||
runCommand,
|
runCommand,
|
||||||
navigateToCollection,
|
navigateToCollection,
|
||||||
createNewEntryInCollection,
|
createNewEntryInCollection,
|
||||||
@ -160,20 +153,7 @@ class App extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { commands, defaultCommands } = this.generateFindBarCommands();
|
const { commands, defaultCommands } = this.generateFindBarCommands();
|
||||||
|
const sidebarContent = (
|
||||||
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}
|
|
||||||
>
|
|
||||||
<nav className={styles.nav}>
|
<nav className={styles.nav}>
|
||||||
<h1 className={styles.heading}>Collections</h1>
|
<h1 className={styles.heading}>Collections</h1>
|
||||||
<Navigation type="vertical">
|
<Navigation type="vertical">
|
||||||
@ -189,7 +169,15 @@ class App extends React.Component {
|
|||||||
}
|
}
|
||||||
</Navigation>
|
</Navigation>
|
||||||
</nav>
|
</nav>
|
||||||
</NavDrawer>
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Sidebar content={sidebarContent}>
|
||||||
|
<Layout theme={styles}>
|
||||||
|
<Notifs
|
||||||
|
className={styles.notifsContainer}
|
||||||
|
CustomComponent={Toast}
|
||||||
|
/>
|
||||||
<AppHeader
|
<AppHeader
|
||||||
user={user}
|
user={user}
|
||||||
collections={collections}
|
collections={collections}
|
||||||
@ -198,7 +186,7 @@ class App extends React.Component {
|
|||||||
runCommand={runCommand}
|
runCommand={runCommand}
|
||||||
onCreateEntryClick={createNewEntryInCollection}
|
onCreateEntryClick={createNewEntryInCollection}
|
||||||
onLogoutClick={logoutUser}
|
onLogoutClick={logoutUser}
|
||||||
toggleNavDrawer={this.toggleNavDrawer}
|
toggleDrawer={toggleSidebar}
|
||||||
/>
|
/>
|
||||||
<Panel scrollY>
|
<Panel scrollY>
|
||||||
{ isFetching && <TopBarProgress /> }
|
{ isFetching && <TopBarProgress /> }
|
||||||
@ -206,21 +194,24 @@ class App extends React.Component {
|
|||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</Panel>
|
</Panel>
|
||||||
|
|
||||||
</Layout>
|
</Layout>
|
||||||
|
</Sidebar>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapStateToProps(state) {
|
function mapStateToProps(state) {
|
||||||
const { auth, config, collections, global } = state;
|
const { auth, config, collections, globalUI } = state;
|
||||||
const user = auth && auth.get('user');
|
const user = auth && auth.get('user');
|
||||||
const { isFetching } = global;
|
const isFetching = globalUI.get('isFetching');
|
||||||
return { auth, config, collections, user, isFetching };
|
return { auth, config, collections, user, isFetching };
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapDispatchToProps(dispatch) {
|
function mapDispatchToProps(dispatch) {
|
||||||
return {
|
return {
|
||||||
dispatch,
|
dispatch,
|
||||||
|
toggleSidebar: () => dispatch(toggleSidebar()),
|
||||||
runCommand: (type, payload) => {
|
runCommand: (type, payload) => {
|
||||||
dispatch(runCommand(type, payload));
|
dispatch(runCommand(type, payload));
|
||||||
},
|
},
|
||||||
|
@ -4,7 +4,7 @@ import { connect } from 'react-redux';
|
|||||||
import { loadEntries } from '../actions/entries';
|
import { loadEntries } from '../actions/entries';
|
||||||
import { selectEntries } from '../reducers';
|
import { selectEntries } from '../reducers';
|
||||||
import { Loader } from '../components/UI';
|
import { Loader } from '../components/UI';
|
||||||
import EntryListing from '../components/EntryListing';
|
import EntryListing from '../components/EntryListing/EntryListing';
|
||||||
import styles from './breakpoints.css';
|
import styles from './breakpoints.css';
|
||||||
|
|
||||||
class CollectionPage extends React.Component {
|
class CollectionPage extends React.Component {
|
||||||
|
@ -11,8 +11,9 @@ import {
|
|||||||
} from '../actions/entries';
|
} from '../actions/entries';
|
||||||
import { cancelEdit } from '../actions/editor';
|
import { cancelEdit } from '../actions/editor';
|
||||||
import { addMedia, removeMedia } from '../actions/media';
|
import { addMedia, removeMedia } from '../actions/media';
|
||||||
|
import { openSidebar } from '../actions/globalUI';
|
||||||
import { selectEntry, getMedia } from '../reducers';
|
import { selectEntry, getMedia } from '../reducers';
|
||||||
import Collection from '../valueObjects/Collection';
|
import { selectFields } from '../reducers/collections';
|
||||||
import EntryEditor from '../components/EntryEditor/EntryEditor';
|
import EntryEditor from '../components/EntryEditor/EntryEditor';
|
||||||
import entryPageHOC from './editorialWorkflow/EntryPageHOC';
|
import entryPageHOC from './editorialWorkflow/EntryPageHOC';
|
||||||
import { Loader } from '../components/UI';
|
import { Loader } from '../components/UI';
|
||||||
@ -32,6 +33,7 @@ class EntryPage extends React.Component {
|
|||||||
persistEntry: PropTypes.func.isRequired,
|
persistEntry: PropTypes.func.isRequired,
|
||||||
removeMedia: PropTypes.func.isRequired,
|
removeMedia: PropTypes.func.isRequired,
|
||||||
cancelEdit: PropTypes.func.isRequired,
|
cancelEdit: PropTypes.func.isRequired,
|
||||||
|
openSidebar: PropTypes.func.isRequired,
|
||||||
fields: ImmutablePropTypes.list.isRequired,
|
fields: ImmutablePropTypes.list.isRequired,
|
||||||
slug: PropTypes.string,
|
slug: PropTypes.string,
|
||||||
newEntry: PropTypes.bool.isRequired,
|
newEntry: PropTypes.bool.isRequired,
|
||||||
@ -39,6 +41,7 @@ class EntryPage extends React.Component {
|
|||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
const { entry, newEntry, collection, slug, loadEntry } = this.props;
|
const { entry, newEntry, collection, slug, loadEntry } = this.props;
|
||||||
|
this.props.openSidebar();
|
||||||
if (newEntry) {
|
if (newEntry) {
|
||||||
createEmptyDraft(collection);
|
createEmptyDraft(collection);
|
||||||
} else {
|
} else {
|
||||||
@ -108,9 +111,8 @@ function mapStateToProps(state, ownProps) {
|
|||||||
const { collections, entryDraft } = state;
|
const { collections, entryDraft } = state;
|
||||||
const slug = ownProps.params.slug;
|
const slug = ownProps.params.slug;
|
||||||
const collection = collections.get(ownProps.params.name);
|
const collection = collections.get(ownProps.params.name);
|
||||||
const collectionModel = new Collection(collection);
|
|
||||||
const newEntry = ownProps.route && ownProps.route.newRecord === true;
|
const newEntry = ownProps.route && ownProps.route.newRecord === true;
|
||||||
|
const fields = selectFields(collection, slug);
|
||||||
const entry = newEntry ? null : selectEntry(state, collection.get('name'), slug);
|
const entry = newEntry ? null : selectEntry(state, collection.get('name'), slug);
|
||||||
const boundGetMedia = getMedia.bind(null, state);
|
const boundGetMedia = getMedia.bind(null, state);
|
||||||
return {
|
return {
|
||||||
@ -119,7 +121,7 @@ function mapStateToProps(state, ownProps) {
|
|||||||
newEntry,
|
newEntry,
|
||||||
entryDraft,
|
entryDraft,
|
||||||
boundGetMedia,
|
boundGetMedia,
|
||||||
fields: collectionModel.entryFields(slug),
|
fields,
|
||||||
slug,
|
slug,
|
||||||
entry,
|
entry,
|
||||||
};
|
};
|
||||||
@ -137,5 +139,6 @@ export default connect(
|
|||||||
discardDraft,
|
discardDraft,
|
||||||
persistEntry,
|
persistEntry,
|
||||||
cancelEdit,
|
cancelEdit,
|
||||||
|
openSidebar,
|
||||||
}
|
}
|
||||||
)(entryPageHOC(EntryPage));
|
)(entryPageHOC(EntryPage));
|
||||||
|
@ -4,7 +4,7 @@ import { connect } from 'react-redux';
|
|||||||
import { selectSearchedEntries } from '../reducers';
|
import { selectSearchedEntries } from '../reducers';
|
||||||
import { searchEntries } from '../actions/entries';
|
import { searchEntries } from '../actions/entries';
|
||||||
import { Loader } from '../components/UI';
|
import { Loader } from '../components/UI';
|
||||||
import EntryListing from '../components/EntryListing';
|
import EntryListing from '../components/EntryListing/EntryListing';
|
||||||
import styles from './breakpoints.css';
|
import styles from './breakpoints.css';
|
||||||
|
|
||||||
class SearchPage extends React.Component {
|
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 { loadUnpublishedEntries, updateUnpublishedEntryStatus, publishUnpublishedEntry } from '../../actions/editorialWorkflow';
|
||||||
import { selectUnpublishedEntries } from '../../reducers';
|
import { selectUnpublishedEntries } from '../../reducers';
|
||||||
import { EDITORIAL_WORKFLOW, status } from '../../constants/publishModes';
|
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 {
|
class unpublishedEntriesPanel extends Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
isEditorialWorkflow: PropTypes.bool.isRequired,
|
isEditorialWorkflow: PropTypes.bool.isRequired,
|
||||||
|
isFetching: PropTypes.bool.isRequired,
|
||||||
unpublishedEntries: ImmutablePropTypes.map,
|
unpublishedEntries: ImmutablePropTypes.map,
|
||||||
loadUnpublishedEntries: PropTypes.func.isRequired,
|
loadUnpublishedEntries: PropTypes.func.isRequired,
|
||||||
updateUnpublishedEntryStatus: PropTypes.func.isRequired,
|
updateUnpublishedEntryStatus: PropTypes.func.isRequired,
|
||||||
@ -24,9 +26,9 @@ class unpublishedEntriesPanel extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { isEditorialWorkflow, unpublishedEntries, updateUnpublishedEntryStatus, publishUnpublishedEntry } = this.props;
|
const { isEditorialWorkflow, isFetching, unpublishedEntries, updateUnpublishedEntryStatus, publishUnpublishedEntry } = this.props;
|
||||||
if (!isEditorialWorkflow) return null;
|
if (!isEditorialWorkflow) return null;
|
||||||
|
if (isFetching) return <Loader active>Loading Editorial Workflow Entries</Loader>;
|
||||||
return (
|
return (
|
||||||
<UnpublishedListing
|
<UnpublishedListing
|
||||||
entries={unpublishedEntries}
|
entries={unpublishedEntries}
|
||||||
@ -42,6 +44,8 @@ function mapStateToProps(state) {
|
|||||||
const returnObj = { isEditorialWorkflow };
|
const returnObj = { isEditorialWorkflow };
|
||||||
|
|
||||||
if (isEditorialWorkflow) {
|
if (isEditorialWorkflow) {
|
||||||
|
returnObj.isFetching = state.editorialWorkflow.getIn(['pages', 'isFetching'], false);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Generates an ordered Map of the available status as keys.
|
* Generates an ordered Map of the available status as keys.
|
||||||
* Each key containing a List of available unpubhlished entries
|
* Each key containing a List of available unpubhlished entries
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { createEntry } from '../../../valueObjects/Entry';
|
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import Collection from '../../../valueObjects/Collection';
|
import { createEntry } from '../../../valueObjects/Entry';
|
||||||
|
import { selectEntrySlug } from '../../../reducers/collections';
|
||||||
|
|
||||||
function getSlug(path) {
|
function getSlug(path) {
|
||||||
const m = path.match(/([^\/]+?)(\.[^\/\.]+)?$/);
|
const m = path.match(/([^\/]+?)(\.[^\/\.]+)?$/);
|
||||||
@ -102,12 +102,11 @@ export default class Algolia {
|
|||||||
if (this.entriesCache.collection === collection && this.entriesCache.page === page) {
|
if (this.entriesCache.collection === collection && this.entriesCache.page === page) {
|
||||||
return Promise.resolve({ page: this.entriesCache.page, entries: this.entriesCache.entries });
|
return Promise.resolve({ page: this.entriesCache.page, entries: this.entriesCache.entries });
|
||||||
} else {
|
} else {
|
||||||
const collectionModel = new Collection(collection);
|
|
||||||
return this.request(`${ this.searchURL }/indexes/${ collection.get('name') }`, {
|
return this.request(`${ this.searchURL }/indexes/${ collection.get('name') }`, {
|
||||||
params: { page },
|
params: { page },
|
||||||
}).then((response) => {
|
}).then((response) => {
|
||||||
const entries = response.hits.map((hit) => {
|
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 });
|
return createEntry(collection.get('name'), slug, hit.path, { data: hit.data, partial: true });
|
||||||
});
|
});
|
||||||
this.entriesCache = { collection, pagination: response.page, entries };
|
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 { OrderedMap, fromJS } from 'immutable';
|
||||||
|
import consoleError from '../lib/consoleError';
|
||||||
import { CONFIG_SUCCESS } from '../actions/config';
|
import { CONFIG_SUCCESS } from '../actions/config';
|
||||||
import { FILES, FOLDER } from '../constants/collectionTypes';
|
import { FILES, FOLDER } from '../constants/collectionTypes';
|
||||||
|
|
||||||
const hasProperty = (config, property) => ({}.hasOwnProperty.call(config, property));
|
const hasProperty = (config, property) => ({}.hasOwnProperty.call(config, property));
|
||||||
|
|
||||||
const collections = (state = null, action) => {
|
const collections = (state = null, action) => {
|
||||||
|
const configCollections = action.payload && action.payload.collections;
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case CONFIG_SUCCESS:
|
case CONFIG_SUCCESS:
|
||||||
const configCollections = action.payload && action.payload.collections;
|
|
||||||
return OrderedMap().withMutations((map) => {
|
return OrderedMap().withMutations((map) => {
|
||||||
(configCollections || []).forEach((configCollection) => {
|
(configCollections || []).forEach((configCollection) => {
|
||||||
if (hasProperty(configCollection, 'folder')) {
|
if (hasProperty(configCollection, 'folder')) {
|
||||||
configCollection.type = FOLDER; // eslint-disable-line no-param-reassign
|
configCollection.type = FOLDER; // eslint-disable-line no-param-reassign
|
||||||
} else if (hasProperty(configCollection, 'files')) {
|
} else if (hasProperty(configCollection, 'files')) {
|
||||||
configCollection.type = FILES; // eslint-disable-line no-param-reassign
|
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));
|
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;
|
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 entryDraft from './entryDraft';
|
||||||
import collections from './collections';
|
import collections from './collections';
|
||||||
import medias, * as fromMedias from './medias';
|
import medias, * as fromMedias from './medias';
|
||||||
import global from './global';
|
import globalUI from './globalUI';
|
||||||
|
|
||||||
const reducers = {
|
const reducers = {
|
||||||
auth,
|
auth,
|
||||||
@ -19,7 +19,7 @@ const reducers = {
|
|||||||
editorialWorkflow,
|
editorialWorkflow,
|
||||||
entryDraft,
|
entryDraft,
|
||||||
medias,
|
medias,
|
||||||
global,
|
globalUI,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default reducers;
|
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"
|
loose-envify "^1.2.0"
|
||||||
warning "^3.0.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:
|
react-simple-di@^1.2.0:
|
||||||
version "1.2.0"
|
version "1.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/react-simple-di/-/react-simple-di-1.2.0.tgz#dde0e5bf689f391ef2ab02c9043b213fe239c6d0"
|
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