Single file collections (#132)
* Files based collections skeleton * listing file based cards * create new entry with collection * moved lookupEntry to main backend * Editing single page Collections file * List widget basic implementation * Adjustments for test-repo * check if value exists before trying to iterate over
This commit is contained in:
parent
cca338df79
commit
2496ec09a4
@ -37,13 +37,6 @@ collections: # A list of collections the CMS should be able to edit
|
|||||||
description: "General Site Settings"
|
description: "General Site Settings"
|
||||||
fields:
|
fields:
|
||||||
- {label: "Global title", name: site_title, widget: "string"}
|
- {label: "Global title", name: site_title, widget: "string"}
|
||||||
- label: "Post Settings"
|
|
||||||
name: posts
|
|
||||||
widget: "object"
|
|
||||||
fields:
|
|
||||||
- {label: "Number of posts on frontpage", name: front_limit, widget: number}
|
|
||||||
- {label: "Default Author", name: author, widget: string}
|
|
||||||
- {label: "Default Thumbnail", name: thumb, widget: image, class: "thumb"}
|
|
||||||
|
|
||||||
- name: "authors"
|
- name: "authors"
|
||||||
label: "Authors"
|
label: "Authors"
|
||||||
|
@ -30,7 +30,7 @@
|
|||||||
},
|
},
|
||||||
_data: {
|
_data: {
|
||||||
"settings.json": {
|
"settings.json": {
|
||||||
content: '{"site_title": "CMS Demo", "posts": {"front_limit": 5, "author": "Matt Biilmann"}}'
|
content: '{"site_title": "CMS Demo"}'
|
||||||
},
|
},
|
||||||
"authors.yml": {
|
"authors.yml": {
|
||||||
content: 'authors:\n - name: Mathias\n description: Co-founder @ Netlify\n'
|
content: 'authors:\n - name: Mathias\n description: Co-founder @ Netlify\n'
|
||||||
|
@ -3,6 +3,7 @@ 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 { createEntry } from '../valueObjects/Entry';
|
import { createEntry } from '../valueObjects/Entry';
|
||||||
|
import { FILES, FOLDER } from '../constants/collectionTypes';
|
||||||
|
|
||||||
class LocalStorageAuthStore {
|
class LocalStorageAuthStore {
|
||||||
storageKey = 'nf-cms-user';
|
storageKey = 'nf-cms-user';
|
||||||
@ -18,15 +19,15 @@ class LocalStorageAuthStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const slugFormatter = (template, entryData) => {
|
const slugFormatter = (template, entryData) => {
|
||||||
var date = new Date();
|
const date = new Date();
|
||||||
return template.replace(/\{\{([^\}]+)\}\}/g, function(_, name) {
|
return template.replace(/\{\{([^\}]+)\}\}/g, (_, name) => {
|
||||||
switch (name) {
|
switch (name) {
|
||||||
case 'year':
|
case 'year':
|
||||||
return date.getFullYear();
|
return date.getFullYear();
|
||||||
case 'month':
|
case 'month':
|
||||||
return ('0' + (date.getMonth() + 1)).slice(-2);
|
return (`0${ date.getMonth() + 1 }`).slice(-2);
|
||||||
case 'day':
|
case 'day':
|
||||||
return ('0' + date.getDate()).slice(-2);
|
return (`0${ date.getDate() }`).slice(-2);
|
||||||
case 'slug':
|
case 'slug':
|
||||||
const identifier = entryData.get('title', entryData.get('path'));
|
const identifier = entryData.get('title', entryData.get('path'));
|
||||||
return identifier.trim().toLowerCase().replace(/[^a-z0-9\.\-\_]+/gi, '-');
|
return identifier.trim().toLowerCase().replace(/[^a-z0-9\.\-\_]+/gi, '-');
|
||||||
@ -65,13 +66,31 @@ class Backend {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
listEntries(collection, page, perPage) {
|
listEntries(collection) {
|
||||||
return this.implementation.entries(collection, page, perPage).then((response) => {
|
const type = collection.get('type');
|
||||||
return {
|
if (type === FOLDER) {
|
||||||
pagination: response.pagination,
|
return this.implementation.entriesByFolder(collection)
|
||||||
entries: response.entries.map(this.entryWithFormat(collection))
|
.then(loadedEntries => (
|
||||||
};
|
loadedEntries.map(loadedEntry => createEntry(collection.get('name'), loadedEntry.file.path.split('/').pop().replace(/\.[^\.]+$/, ''), loadedEntry.file.path, { raw: loadedEntry.data }))
|
||||||
});
|
))
|
||||||
|
.then(entries => (
|
||||||
|
{
|
||||||
|
entries: entries.map(this.entryWithFormat(collection)),
|
||||||
|
}
|
||||||
|
));
|
||||||
|
} else if (type === FILES) {
|
||||||
|
const collectionFiles = collection.get('files').map(collectionFile => ({ path: collectionFile.get('file'), label: collectionFile.get('label') }));
|
||||||
|
return this.implementation.entriesByFiles(collection, collectionFiles)
|
||||||
|
.then(loadedEntries => (
|
||||||
|
loadedEntries.map(loadedEntry => createEntry(collection.get('name'), loadedEntry.file.path.split('/').pop().replace(/\.[^\.]+$/, ''), loadedEntry.file.path, { raw: loadedEntry.data, label: loadedEntry.file.label }))
|
||||||
|
))
|
||||||
|
.then(entries => (
|
||||||
|
{
|
||||||
|
entries: entries.map(this.entryWithFormat(collection)),
|
||||||
|
}
|
||||||
|
));
|
||||||
|
}
|
||||||
|
return Promise.reject(`Couldn't process collection type ${ type }`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// We have the file path. Fetch and parse the file.
|
// We have the file path. Fetch and parse the file.
|
||||||
@ -82,12 +101,19 @@ class Backend {
|
|||||||
// Will fetch the whole list of files from GitHub and load each file, then looks up for entry.
|
// Will fetch the whole list of files from GitHub and load each file, then looks up for entry.
|
||||||
// (Files are persisted in local storage - only expensive on the first run for each file).
|
// (Files are persisted in local storage - only expensive on the first run for each file).
|
||||||
lookupEntry(collection, slug) {
|
lookupEntry(collection, slug) {
|
||||||
return this.implementation.lookupEntry(collection, slug).then(this.entryWithFormat(collection));
|
const type = collection.get('type');
|
||||||
|
if (type === FOLDER) {
|
||||||
|
return this.implementation.entriesByFolder(collection)
|
||||||
|
.then(loadedEntries => (
|
||||||
|
loadedEntries.map(loadedEntry => createEntry(collection.get('name'), loadedEntry.file.path.split('/').pop().replace(/\.[^\.]+$/, ''), loadedEntry.file.path, { raw: loadedEntry.data }))
|
||||||
|
))
|
||||||
|
.then(response => response.filter(entry => entry.slug === slug)[0])
|
||||||
|
.then(this.entryWithFormat(collection));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
newEntry(collection) {
|
newEntry(collection) {
|
||||||
const newEntry = createEntry();
|
return createEntry(collection.get('name'));
|
||||||
return this.entryWithFormat(collection)(newEntry);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
entryWithFormat(collectionOrEntity) {
|
entryWithFormat(collectionOrEntity) {
|
||||||
@ -95,8 +121,10 @@ class Backend {
|
|||||||
const format = resolveFormat(collectionOrEntity, entry);
|
const format = resolveFormat(collectionOrEntity, entry);
|
||||||
if (entry && entry.raw) {
|
if (entry && entry.raw) {
|
||||||
entry.data = format && format.fromFile(entry.raw);
|
entry.data = format && format.fromFile(entry.raw);
|
||||||
}
|
|
||||||
return entry;
|
return entry;
|
||||||
|
} else {
|
||||||
|
return format.fromFile(entry);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -104,7 +132,7 @@ class Backend {
|
|||||||
return this.implementation.unpublishedEntries(page, perPage).then((response) => {
|
return this.implementation.unpublishedEntries(page, perPage).then((response) => {
|
||||||
return {
|
return {
|
||||||
pagination: response.pagination,
|
pagination: response.pagination,
|
||||||
entries: response.entries.map(this.entryWithFormat('editorialWorkflow'))
|
entries: response.entries.map(this.entryWithFormat('editorialWorkflow')),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -126,28 +154,28 @@ class Backend {
|
|||||||
if (newEntry) {
|
if (newEntry) {
|
||||||
const slug = slugFormatter(collection.get('slug'), entryDraft.getIn(['entry', 'data']));
|
const slug = slugFormatter(collection.get('slug'), entryDraft.getIn(['entry', 'data']));
|
||||||
entryObj = {
|
entryObj = {
|
||||||
path: `${collection.get('folder')}/${slug}.md`,
|
path: `${ collection.get('folder') }/${ slug }.md`,
|
||||||
slug: slug,
|
slug,
|
||||||
raw: this.entryToRaw(collection, entryData)
|
raw: this.entryToRaw(collection, entryData),
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
entryObj = {
|
entryObj = {
|
||||||
path: entryDraft.getIn(['entry', 'path']),
|
path: entryDraft.getIn(['entry', 'path']),
|
||||||
slug: entryDraft.getIn(['entry', 'slug']),
|
slug: entryDraft.getIn(['entry', 'slug']),
|
||||||
raw: this.entryToRaw(collection, entryData)
|
raw: this.entryToRaw(collection, entryData),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const commitMessage = (newEntry ? 'Created ' : 'Updated ') +
|
const commitMessage = `${ (newEntry ? 'Created ' : 'Updated ') +
|
||||||
collection.get('label') + ' “' +
|
collection.get('label') } “${
|
||||||
entryDraft.getIn(['entry', 'data', 'title']) + '”';
|
entryDraft.getIn(['entry', 'data', 'title']) }”`;
|
||||||
|
|
||||||
const mode = config.get('publish_mode');
|
const mode = config.get('publish_mode');
|
||||||
|
|
||||||
const collectionName = collection.get('name');
|
const collectionName = collection.get('name');
|
||||||
|
|
||||||
return this.implementation.persistEntry(entryObj, MediaFiles, {
|
return this.implementation.persistEntry(entryObj, MediaFiles, {
|
||||||
newEntry, parsedData, commitMessage, collectionName, mode, ...options
|
newEntry, parsedData, commitMessage, collectionName, mode, ...options,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -186,11 +214,11 @@ export function resolveBackend(config) {
|
|||||||
case 'netlify-git':
|
case 'netlify-git':
|
||||||
return new Backend(new NetlifyGitBackend(config, slugFormatter), authStore);
|
return new Backend(new NetlifyGitBackend(config, slugFormatter), authStore);
|
||||||
default:
|
default:
|
||||||
throw `Backend not found: ${name}`;
|
throw `Backend not found: ${ name }`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const currentBackend = (function() {
|
export const currentBackend = (function () {
|
||||||
let backend = null;
|
let backend = null;
|
||||||
|
|
||||||
return (config) => {
|
return (config) => {
|
||||||
@ -199,4 +227,4 @@ export const currentBackend = (function() {
|
|||||||
return backend = resolveBackend(config);
|
return backend = resolveBackend(config);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
})();
|
}());
|
||||||
|
@ -112,13 +112,14 @@ export default class API {
|
|||||||
const cache = LocalForage.getItem(`gh.meta.${ key }`);
|
const cache = LocalForage.getItem(`gh.meta.${ key }`);
|
||||||
return cache.then((cached) => {
|
return cache.then((cached) => {
|
||||||
if (cached && cached.expires > Date.now()) { return cached.data; }
|
if (cached && cached.expires > Date.now()) { return cached.data; }
|
||||||
|
console.log("%c Checking for MetaData files", "line-height: 30px;text-align: center;font-weight: bold"); // eslint-disable-line
|
||||||
return this.request(`${ this.repoURL }/contents/${ key }.json`, {
|
return this.request(`${ this.repoURL }/contents/${ key }.json`, {
|
||||||
params: { ref: 'refs/meta/_netlify_cms' },
|
params: { ref: 'refs/meta/_netlify_cms' },
|
||||||
headers: { Accept: 'application/vnd.github.VERSION.raw' },
|
headers: { Accept: 'application/vnd.github.VERSION.raw' },
|
||||||
cache: 'no-store',
|
cache: 'no-store',
|
||||||
})
|
})
|
||||||
.then(response => JSON.parse(response));
|
.then(response => JSON.parse(response))
|
||||||
|
.catch(error => console.log("%c %s does not have metadata", "line-height: 30px;text-align: center;font-weight: bold", key)); // eslint-disable-line
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -31,14 +31,22 @@ export default class GitHub {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
entries(collection) {
|
entriesByFolder(collection) {
|
||||||
return this.api.listFiles(collection.get('folder')).then((files) => {
|
return this.api.listFiles(collection.get('folder')).then(files => this.entriesByFiles(collection, files));
|
||||||
|
}
|
||||||
|
|
||||||
|
entriesByFiles(collection, files) {
|
||||||
const sem = semaphore(MAX_CONCURRENT_DOWNLOADS);
|
const sem = semaphore(MAX_CONCURRENT_DOWNLOADS);
|
||||||
const promises = [];
|
const promises = [];
|
||||||
files.map((file) => {
|
files.forEach((file) => {
|
||||||
promises.push(new Promise((resolve, reject) => {
|
promises.push(new Promise((resolve, reject) => {
|
||||||
return sem.take(() => this.api.readFile(file.path, file.sha).then((data) => {
|
return sem.take(() => this.api.readFile(file.path, file.sha).then((data) => {
|
||||||
resolve(createEntry(collection.get('name'), file.path.split('/').pop().replace(/\.[^\.]+$/, ''), file.path, { raw: data }));
|
resolve(
|
||||||
|
{
|
||||||
|
file,
|
||||||
|
data,
|
||||||
|
}
|
||||||
|
);
|
||||||
sem.leave();
|
sem.leave();
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
sem.leave();
|
sem.leave();
|
||||||
@ -47,17 +55,6 @@ export default class GitHub {
|
|||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
return Promise.all(promises);
|
return Promise.all(promises);
|
||||||
}).then(entries => ({
|
|
||||||
entries,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Will fetch the entire list of entries from github.
|
|
||||||
lookupEntry(collection, slug) {
|
|
||||||
return this.entries(collection).then(response => (
|
|
||||||
response.entries.filter(entry => entry.slug === slug)[0]
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetches a single entry.
|
// Fetches a single entry.
|
||||||
|
@ -24,18 +24,25 @@ export default class TestRepo {
|
|||||||
return Promise.resolve({ email: state.email });
|
return Promise.resolve({ email: state.email });
|
||||||
}
|
}
|
||||||
|
|
||||||
entries(collection) {
|
entriesByFolder(collection) {
|
||||||
const entries = [];
|
const entries = [];
|
||||||
const folder = collection.get('folder');
|
const folder = collection.get('folder');
|
||||||
if (folder) {
|
if (folder) {
|
||||||
for (const path in window.repoFiles[folder]) {
|
for (const path in window.repoFiles[folder]) {
|
||||||
entries.push(createEntry(collection.get('name'), getSlug(path), `${ folder }/${ path }`, { raw: window.repoFiles[folder][path].content }));
|
const file = { path: `${ folder }/${ path }` };
|
||||||
|
entries.push(
|
||||||
|
{
|
||||||
|
file,
|
||||||
|
data: window.repoFiles[folder][path].content,
|
||||||
}
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Promise.resolve(entries);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Promise.resolve({
|
entriesByFiles(collection, files) {
|
||||||
entries,
|
throw new Error('Not implemented yet');
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
lookupEntry(collection, slug) {
|
lookupEntry(collection, slug) {
|
||||||
|
@ -6,7 +6,7 @@ import styles from './ControlPane.css';
|
|||||||
export default class ControlPane extends Component {
|
export default class ControlPane extends Component {
|
||||||
|
|
||||||
controlFor(field) {
|
controlFor(field) {
|
||||||
const { entry, getMedia, onChange, onAddMedia, onRemoveMedia } = this.props;
|
const { entry, fields, getMedia, onChange, onAddMedia, onRemoveMedia } = this.props;
|
||||||
const widget = resolveWidget(field.get('widget'));
|
const widget = resolveWidget(field.get('widget'));
|
||||||
const fieldName = field.get('name');
|
const fieldName = field.get('name');
|
||||||
const value = entry.getIn(['data', fieldName]);
|
const value = entry.getIn(['data', fieldName]);
|
||||||
@ -29,16 +29,15 @@ export default class ControlPane extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { collection } = this.props;
|
const { collection, fields } = this.props;
|
||||||
if (!collection) {
|
if (!collection || !fields) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{
|
{
|
||||||
collection
|
fields.map(field =>
|
||||||
.get('fields')
|
|
||||||
.map(field =>
|
|
||||||
<div
|
<div
|
||||||
key={field.get('name')}
|
key={field.get('name')}
|
||||||
className={styles.widget}
|
className={styles.widget}
|
||||||
@ -55,6 +54,7 @@ export default class ControlPane extends Component {
|
|||||||
ControlPane.propTypes = {
|
ControlPane.propTypes = {
|
||||||
collection: ImmutablePropTypes.map.isRequired,
|
collection: ImmutablePropTypes.map.isRequired,
|
||||||
entry: ImmutablePropTypes.map.isRequired,
|
entry: ImmutablePropTypes.map.isRequired,
|
||||||
|
fields: ImmutablePropTypes.list.isRequired,
|
||||||
getMedia: PropTypes.func.isRequired,
|
getMedia: PropTypes.func.isRequired,
|
||||||
onAddMedia: PropTypes.func.isRequired,
|
onAddMedia: PropTypes.func.isRequired,
|
||||||
onChange: PropTypes.func.isRequired,
|
onChange: PropTypes.func.isRequired,
|
||||||
|
@ -10,6 +10,7 @@ export default function EntryEditor(
|
|||||||
{
|
{
|
||||||
collection,
|
collection,
|
||||||
entry,
|
entry,
|
||||||
|
fields,
|
||||||
getMedia,
|
getMedia,
|
||||||
onChange,
|
onChange,
|
||||||
onAddMedia,
|
onAddMedia,
|
||||||
@ -26,6 +27,7 @@ export default function EntryEditor(
|
|||||||
<ControlPane
|
<ControlPane
|
||||||
collection={collection}
|
collection={collection}
|
||||||
entry={entry}
|
entry={entry}
|
||||||
|
fields={fields}
|
||||||
getMedia={getMedia}
|
getMedia={getMedia}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
onAddMedia={onAddMedia}
|
onAddMedia={onAddMedia}
|
||||||
@ -37,6 +39,7 @@ export default function EntryEditor(
|
|||||||
<PreviewPane
|
<PreviewPane
|
||||||
collection={collection}
|
collection={collection}
|
||||||
entry={entry}
|
entry={entry}
|
||||||
|
fields={fields}
|
||||||
getMedia={getMedia}
|
getMedia={getMedia}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -56,6 +59,7 @@ export default function EntryEditor(
|
|||||||
EntryEditor.propTypes = {
|
EntryEditor.propTypes = {
|
||||||
collection: ImmutablePropTypes.map.isRequired,
|
collection: ImmutablePropTypes.map.isRequired,
|
||||||
entry: ImmutablePropTypes.map.isRequired,
|
entry: ImmutablePropTypes.map.isRequired,
|
||||||
|
fields: ImmutablePropTypes.list.isRequired,
|
||||||
getMedia: PropTypes.func.isRequired,
|
getMedia: PropTypes.func.isRequired,
|
||||||
onAddMedia: PropTypes.func.isRequired,
|
onAddMedia: PropTypes.func.isRequired,
|
||||||
onChange: PropTypes.func.isRequired,
|
onChange: PropTypes.func.isRequired,
|
||||||
|
@ -21,7 +21,7 @@ export default class EntryListing extends React.Component {
|
|||||||
{ mq: '1005px', columns: 4, gutter: 15 },
|
{ mq: '1005px', columns: 4, gutter: 15 },
|
||||||
{ mq: '1515px', columns: 5, gutter: 15 },
|
{ mq: '1515px', columns: 5, gutter: 15 },
|
||||||
{ mq: '1770px', columns: 6, gutter: 15 },
|
{ mq: '1770px', columns: 6, gutter: 15 },
|
||||||
]
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
this.updateBricks = _.throttle(this.updateBricks.bind(this), 30);
|
this.updateBricks = _.throttle(this.updateBricks.bind(this), 30);
|
||||||
@ -32,7 +32,7 @@ export default class EntryListing extends React.Component {
|
|||||||
this.bricksInstance = Bricks({
|
this.bricksInstance = Bricks({
|
||||||
container: this._entries,
|
container: this._entries,
|
||||||
packed: this.bricksConfig.packed,
|
packed: this.bricksConfig.packed,
|
||||||
sizes: this.bricksConfig.sizes
|
sizes: this.bricksConfig.sizes,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.bricksInstance.resize(true);
|
this.bricksInstance.resize(true);
|
||||||
@ -65,10 +65,10 @@ export default class EntryListing extends React.Component {
|
|||||||
const card = Cards[cartType] || Cards._unknown;
|
const card = Cards[cartType] || Cards._unknown;
|
||||||
return React.createElement(card, {
|
return React.createElement(card, {
|
||||||
key: entry.get('slug'),
|
key: entry.get('slug'),
|
||||||
collection: collection,
|
collection,
|
||||||
onClick: history.push.bind(this, link),
|
onClick: history.push.bind(this, link),
|
||||||
onImageLoaded: this.updateBricks,
|
onImageLoaded: this.updateBricks,
|
||||||
text: entry.getIn(['data', collection.getIn(['card', 'text'])]),
|
text: entry.get('label') ? entry.get('label') : entry.getIn(['data', collection.getIn(['card', 'text'])]),
|
||||||
description: entry.getIn(['data', collection.getIn(['card', 'description'])]),
|
description: entry.getIn(['data', collection.getIn(['card', 'description'])]),
|
||||||
image: entry.getIn(['data', collection.getIn(['card', 'image'])]),
|
image: entry.getIn(['data', collection.getIn(['card', 'image'])]),
|
||||||
});
|
});
|
||||||
@ -83,13 +83,13 @@ export default class EntryListing extends React.Component {
|
|||||||
if (Map.isMap(collections)) {
|
if (Map.isMap(collections)) {
|
||||||
const collectionName = collections.get('name');
|
const collectionName = collections.get('name');
|
||||||
return entries.map((entry) => {
|
return entries.map((entry) => {
|
||||||
const path = `/collections/${collectionName}/entries/${entry.get('slug')}`;
|
const path = `/collections/${ collectionName }/entries/${ entry.get('slug') }`;
|
||||||
return this.cardFor(collections, entry, path);
|
return this.cardFor(collections, entry, path);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
return entries.map((entry) => {
|
return entries.map((entry) => {
|
||||||
const collection = collections.filter(collection => collection.get('name') === entry.get('collection')).first();
|
const collection = collections.filter(collection => collection.get('name') === entry.get('collection')).first();
|
||||||
const path = `/collections/${collection.get('name')}/entries/${entry.get('slug')}`;
|
const path = `/collections/${ collection.get('name') }/entries/${ entry.get('slug') }`;
|
||||||
return this.cardFor(collection, entry, path);
|
return this.cardFor(collection, entry, path);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -98,13 +98,13 @@ export default class EntryListing extends React.Component {
|
|||||||
render() {
|
render() {
|
||||||
const { children } = this.props;
|
const { children } = this.props;
|
||||||
const cards = this.renderCards();
|
const cards = this.renderCards();
|
||||||
return <div>
|
return (<div>
|
||||||
<h1>{children}</h1>
|
<h1>{children}</h1>
|
||||||
<div ref={(c) => this._entries = c}>
|
<div ref={c => this._entries = c}>
|
||||||
{cards}
|
{cards}
|
||||||
<Waypoint onEnter={this.handleLoadMore} />
|
<Waypoint onEnter={this.handleLoadMore} />
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -112,7 +112,7 @@ EntryListing.propTypes = {
|
|||||||
children: PropTypes.node.isRequired,
|
children: PropTypes.node.isRequired,
|
||||||
collections: PropTypes.oneOfType([
|
collections: PropTypes.oneOfType([
|
||||||
ImmutablePropTypes.map,
|
ImmutablePropTypes.map,
|
||||||
ImmutablePropTypes.iterable
|
ImmutablePropTypes.iterable,
|
||||||
]).isRequired,
|
]).isRequired,
|
||||||
entries: ImmutablePropTypes.list,
|
entries: ImmutablePropTypes.list,
|
||||||
onPaginate: PropTypes.func.isRequired,
|
onPaginate: PropTypes.func.isRequired,
|
||||||
|
@ -1,14 +1,13 @@
|
|||||||
import React, { PropTypes } from 'react';
|
import React, { PropTypes } from 'react';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
|
||||||
export default function Preview({ collection, widgetFor }) {
|
export default function Preview({ collection, fields, widgetFor }) {
|
||||||
if (!collection) {
|
if (!collection || !fields) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{collection.get('fields').map(field => widgetFor(field.get('name')))}
|
{fields.map(field => widgetFor(field.get('name')))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -16,6 +15,7 @@ export default function Preview({ collection, widgetFor }) {
|
|||||||
Preview.propTypes = {
|
Preview.propTypes = {
|
||||||
collection: ImmutablePropTypes.map.isRequired,
|
collection: ImmutablePropTypes.map.isRequired,
|
||||||
entry: ImmutablePropTypes.map.isRequired,
|
entry: ImmutablePropTypes.map.isRequired,
|
||||||
|
fields: ImmutablePropTypes.list.isRequired,
|
||||||
getMedia: PropTypes.func.isRequired,
|
getMedia: PropTypes.func.isRequired,
|
||||||
widgetFor: PropTypes.func.isRequired,
|
widgetFor: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
@ -14,8 +14,8 @@ export default class PreviewPane extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
widgetFor = (name) => {
|
widgetFor = (name) => {
|
||||||
const { collection, entry, getMedia } = this.props;
|
const { fields, entry, getMedia } = this.props;
|
||||||
const field = collection.get('fields').find(field => field.get('name') === name);
|
const field = fields.find(field => field.get('name') === name);
|
||||||
const widget = resolveWidget(field.get('widget'));
|
const widget = resolveWidget(field.get('widget'));
|
||||||
return React.createElement(widget.preview, {
|
return React.createElement(widget.preview, {
|
||||||
key: field.get('name'),
|
key: field.get('name'),
|
||||||
@ -67,6 +67,7 @@ export default class PreviewPane extends React.Component {
|
|||||||
|
|
||||||
PreviewPane.propTypes = {
|
PreviewPane.propTypes = {
|
||||||
collection: ImmutablePropTypes.map.isRequired,
|
collection: ImmutablePropTypes.map.isRequired,
|
||||||
|
fields: ImmutablePropTypes.list.isRequired,
|
||||||
entry: ImmutablePropTypes.map.isRequired,
|
entry: ImmutablePropTypes.map.isRequired,
|
||||||
getMedia: PropTypes.func.isRequired,
|
getMedia: PropTypes.func.isRequired,
|
||||||
scrollTop: PropTypes.number,
|
scrollTop: PropTypes.number,
|
||||||
|
@ -3,6 +3,8 @@ import UnknownControl from './Widgets/UnknownControl';
|
|||||||
import UnknownPreview from './Widgets/UnknownPreview';
|
import UnknownPreview from './Widgets/UnknownPreview';
|
||||||
import StringControl from './Widgets/StringControl';
|
import StringControl from './Widgets/StringControl';
|
||||||
import StringPreview from './Widgets/StringPreview';
|
import StringPreview from './Widgets/StringPreview';
|
||||||
|
import ListControl from './Widgets/ListControl';
|
||||||
|
import ListPreview from './Widgets/ListPreview';
|
||||||
import TextControl from './Widgets/TextControl';
|
import TextControl from './Widgets/TextControl';
|
||||||
import TextPreview from './Widgets/TextPreview';
|
import TextPreview from './Widgets/TextPreview';
|
||||||
import MarkdownControl from './Widgets/MarkdownControl';
|
import MarkdownControl from './Widgets/MarkdownControl';
|
||||||
@ -14,6 +16,7 @@ import DateTimePreview from './Widgets/DateTimePreview';
|
|||||||
|
|
||||||
registry.registerWidget('string', StringControl, StringPreview);
|
registry.registerWidget('string', StringControl, StringPreview);
|
||||||
registry.registerWidget('text', TextControl, TextPreview);
|
registry.registerWidget('text', TextControl, TextPreview);
|
||||||
|
registry.registerWidget('list', ListControl, ListPreview);
|
||||||
registry.registerWidget('markdown', MarkdownControl, MarkdownPreview);
|
registry.registerWidget('markdown', MarkdownControl, MarkdownPreview);
|
||||||
registry.registerWidget('image', ImageControl, ImagePreview);
|
registry.registerWidget('image', ImageControl, ImagePreview);
|
||||||
registry.registerWidget('datetime', DateTimeControl, DateTimePreview);
|
registry.registerWidget('datetime', DateTimeControl, DateTimePreview);
|
||||||
|
17
src/components/Widgets/ListControl.js
Normal file
17
src/components/Widgets/ListControl.js
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import React, { Component, PropTypes } from 'react';
|
||||||
|
|
||||||
|
export default class ListControl extends Component {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
onChange: PropTypes.func.isRequired,
|
||||||
|
value: PropTypes.node,
|
||||||
|
};
|
||||||
|
handleChange = (e) => {
|
||||||
|
this.props.onChange(e.target.value.split(',').map(item => item.trim()));
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { value } = this.props;
|
||||||
|
return <input type="text" value={value ? value.join(', ') : ''} onChange={this.handleChange} />;
|
||||||
|
}
|
||||||
|
}
|
11
src/components/Widgets/ListPreview.js
Normal file
11
src/components/Widgets/ListPreview.js
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import React, { PropTypes } from 'react';
|
||||||
|
|
||||||
|
export default function ListPreview({ value }) {
|
||||||
|
return (<ul>
|
||||||
|
{ value && value.map(item => <li key={item}>{item}</li>) }
|
||||||
|
</ul>);
|
||||||
|
}
|
||||||
|
|
||||||
|
ListPreview.propTypes = {
|
||||||
|
value: PropTypes.node,
|
||||||
|
};
|
2
src/constants/collectionTypes.js
Normal file
2
src/constants/collectionTypes.js
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export const FILES = 'file_based_collection';
|
||||||
|
export const FOLDER = 'folder_based_collection';
|
@ -12,6 +12,7 @@ import {
|
|||||||
import { cancelEdit } from '../actions/editor';
|
import { cancelEdit } from '../actions/editor';
|
||||||
import { addMedia, removeMedia } from '../actions/media';
|
import { addMedia, removeMedia } from '../actions/media';
|
||||||
import { selectEntry, getMedia } from '../reducers';
|
import { selectEntry, getMedia } from '../reducers';
|
||||||
|
import { FOLDER, FILES } from '../constants/collectionTypes';
|
||||||
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';
|
||||||
@ -31,6 +32,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,
|
||||||
|
fields: ImmutablePropTypes.list.isRequired,
|
||||||
slug: PropTypes.string,
|
slug: PropTypes.string,
|
||||||
newEntry: PropTypes.bool.isRequired,
|
newEntry: PropTypes.bool.isRequired,
|
||||||
};
|
};
|
||||||
@ -41,7 +43,7 @@ class EntryPage extends React.Component {
|
|||||||
if (newEntry) {
|
if (newEntry) {
|
||||||
createEmptyDraft(collection);
|
createEmptyDraft(collection);
|
||||||
} else {
|
} else {
|
||||||
loadEntry(entry, collection, slug);
|
if (collection.get('type') === FOLDER) loadEntry(entry, collection, slug);
|
||||||
this.createDraft(entry);
|
this.createDraft(entry);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -72,6 +74,7 @@ class EntryPage extends React.Component {
|
|||||||
const {
|
const {
|
||||||
entry,
|
entry,
|
||||||
entryDraft,
|
entryDraft,
|
||||||
|
fields,
|
||||||
boundGetMedia,
|
boundGetMedia,
|
||||||
collection,
|
collection,
|
||||||
changeDraft,
|
changeDraft,
|
||||||
@ -90,6 +93,7 @@ class EntryPage extends React.Component {
|
|||||||
entry={entryDraft.get('entry')}
|
entry={entryDraft.get('entry')}
|
||||||
getMedia={boundGetMedia}
|
getMedia={boundGetMedia}
|
||||||
collection={collection}
|
collection={collection}
|
||||||
|
fields={fields}
|
||||||
onChange={changeDraft}
|
onChange={changeDraft}
|
||||||
onAddMedia={addMedia}
|
onAddMedia={addMedia}
|
||||||
onRemoveMedia={removeMedia}
|
onRemoveMedia={removeMedia}
|
||||||
@ -102,9 +106,19 @@ class EntryPage extends React.Component {
|
|||||||
|
|
||||||
function mapStateToProps(state, ownProps) {
|
function mapStateToProps(state, ownProps) {
|
||||||
const { collections, entryDraft } = state;
|
const { collections, entryDraft } = state;
|
||||||
const collection = collections.get(ownProps.params.name);
|
|
||||||
const newEntry = ownProps.route && ownProps.route.newRecord === true;
|
|
||||||
const slug = ownProps.params.slug;
|
const slug = ownProps.params.slug;
|
||||||
|
const collection = collections.get(ownProps.params.name);
|
||||||
|
|
||||||
|
let fields;
|
||||||
|
if (collection.get('type') === FOLDER) {
|
||||||
|
fields = collection.get('fields');
|
||||||
|
} else {
|
||||||
|
const files = collection.get('files');
|
||||||
|
const file = files.filter(f => f.get('name') === slug);
|
||||||
|
fields = file.getIn([0, 'fields']);
|
||||||
|
}
|
||||||
|
const newEntry = ownProps.route && ownProps.route.newRecord === true;
|
||||||
|
|
||||||
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 {
|
||||||
@ -113,6 +127,7 @@ function mapStateToProps(state, ownProps) {
|
|||||||
newEntry,
|
newEntry,
|
||||||
entryDraft,
|
entryDraft,
|
||||||
boundGetMedia,
|
boundGetMedia,
|
||||||
|
fields,
|
||||||
slug,
|
slug,
|
||||||
entry,
|
entry,
|
||||||
};
|
};
|
||||||
|
@ -1,13 +1,21 @@
|
|||||||
import { OrderedMap, fromJS } from 'immutable';
|
import { OrderedMap, fromJS } from 'immutable';
|
||||||
import { CONFIG_SUCCESS } from '../actions/config';
|
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 collections = (state = null, action) => {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case CONFIG_SUCCESS:
|
case CONFIG_SUCCESS:
|
||||||
const collections = action.payload && action.payload.collections;
|
const configCollections = action.payload && action.payload.collections;
|
||||||
return OrderedMap().withMutations((map) => {
|
return OrderedMap().withMutations((map) => {
|
||||||
(collections || []).forEach(function(collection) {
|
(configCollections || []).forEach((configCollection) => {
|
||||||
map.set(collection.name, fromJS(collection));
|
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
|
||||||
|
}
|
||||||
|
map.set(configCollection.name, fromJS(configCollection));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
default:
|
default:
|
||||||
|
@ -6,6 +6,7 @@ export function createEntry(collection, slug = '', path = '', options = {}) {
|
|||||||
returnObj.partial = options.partial || false;
|
returnObj.partial = options.partial || false;
|
||||||
returnObj.raw = options.raw || '';
|
returnObj.raw = options.raw || '';
|
||||||
returnObj.data = options.data || {};
|
returnObj.data = options.data || {};
|
||||||
|
returnObj.label = options.label || null;
|
||||||
returnObj.metaData = options.metaData || null;
|
returnObj.metaData = options.metaData || null;
|
||||||
return returnObj;
|
return returnObj;
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user