From bbce1c30deca72cb2d077298c52af71aec281eac Mon Sep 17 00:00:00 2001 From: Mathias Biilmann Christensen Date: Thu, 27 Oct 2016 14:23:36 +0200 Subject: [PATCH] Make loading single file work without scanning whole collection --- src/actions/entries.js | 5 +- src/backends/backend.js | 54 ++++++----- src/backends/github/implementation.js | 11 ++- src/backends/test-repo/implementation.js | 15 ++-- src/containers/EntryPage.js | 17 +--- src/index.js | 1 - src/valueObjects/Collection.js | 109 +++++++++++++++++++++++ 7 files changed, 161 insertions(+), 51 deletions(-) create mode 100644 src/valueObjects/Collection.js diff --git a/src/actions/entries.js b/src/actions/entries.js index c023b3b3..899356e2 100644 --- a/src/actions/entries.js +++ b/src/actions/entries.js @@ -187,7 +187,10 @@ export function loadEntry(entry, collection, slug) { } else { getPromise = backend.lookupEntry(collection, slug); } - return getPromise.then(loadedEntry => dispatch(entryLoaded(collection, loadedEntry))); + return getPromise + .then((loadedEntry) => { + return dispatch(entryLoaded(collection, loadedEntry)); + }); }; } diff --git a/src/backends/backend.js b/src/backends/backend.js index 5d566cba..9deececb 100644 --- a/src/backends/backend.js +++ b/src/backends/backend.js @@ -3,6 +3,7 @@ import GitHubBackend from './github/implementation'; import NetlifyGitBackend from './netlify-git/implementation'; import { resolveFormat } from '../formats/formats'; import { createEntry } from '../valueObjects/Entry'; +import Collection from '../valueObjects/Collection'; import { FILES, FOLDER } from '../constants/collectionTypes'; class LocalStorageAuthStore { @@ -67,30 +68,22 @@ class Backend { } listEntries(collection) { - const type = collection.get('type'); - if (type === FOLDER) { - return this.implementation.entriesByFolder(collection) + const collectionModel = new Collection(collection); + const listMethod = this.implementation[collectionModel.listMethod()]; + return listMethod.call(this.implementation, collection) .then(loadedEntries => ( - loadedEntries.map(loadedEntry => createEntry(collection.get('name'), loadedEntry.file.path.split('/').pop().replace(/\.[^\.]+$/, ''), loadedEntry.file.path, { raw: loadedEntry.data })) + loadedEntries.map(loadedEntry => createEntry( + collection.get('name'), + collectionModel.entrySlug(loadedEntry.file.path), + loadedEntry.file.path, + { raw: loadedEntry.data, label: loadedEntry.file.label } + )) )) .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. @@ -98,18 +91,17 @@ class Backend { return this.implementation.getEntry(collection, slug, path).then(this.entryWithFormat(collection)); } - // 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). lookupEntry(collection, slug) { - 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)); - } + const collectionModel = new Collection(collection); + return this.implementation.getEntry(collection, slug, collectionModel.entryPath(slug)) + .then(loadedEntry => { + return this.entryWithFormat(collection)(createEntry( + collection.get('name'), + slug, + loadedEntry.file.path, + { raw: loadedEntry.data, label: loadedEntry.file.label } + ))} + ); } newEntry(collection) { @@ -142,6 +134,7 @@ class Backend { } persistEntry(config, collection, entryDraft, MediaFiles, options) { + const collectionModel = new Collection(collection); const newEntry = entryDraft.getIn(['entry', 'newRecord']) || false; const parsedData = { @@ -152,9 +145,12 @@ class Backend { const entryData = entryDraft.getIn(['entry', 'data']).toJS(); let entryObj; if (newEntry) { + if (!collectionModel.allowNewEntries()) { + throw ('Not allowed to create new entries in this collection'); + } const slug = slugFormatter(collection.get('slug'), entryDraft.getIn(['entry', 'data'])); entryObj = { - path: `${ collection.get('folder') }/${ slug }.md`, + path: collectionModel.entryPath(slug), slug, raw: this.entryToRaw(collection, entryData), }; diff --git a/src/backends/github/implementation.js b/src/backends/github/implementation.js index 5015ea25..609ebf87 100644 --- a/src/backends/github/implementation.js +++ b/src/backends/github/implementation.js @@ -35,7 +35,11 @@ export default class GitHub { return this.api.listFiles(collection.get('folder')).then(files => this.entriesByFiles(collection, files)); } - entriesByFiles(collection, files) { + entriesByFiles(collection) { + const files = collection.get('files').map(collectionFile => ({ + path: collectionFile.get('file'), + label: collectionFile.get('label'), + })); const sem = semaphore(MAX_CONCURRENT_DOWNLOADS); const promises = []; files.forEach((file) => { @@ -59,7 +63,10 @@ export default class GitHub { // Fetches a single entry. getEntry(collection, slug, path) { - return this.api.readFile(path).then(data => createEntry(collection, slug, path, { raw: data })); + return this.api.readFile(path).then(data => ({ + file: { path }, + data, + })); } persistEntry(entry, mediaFiles = [], options = {}) { diff --git a/src/backends/test-repo/implementation.js b/src/backends/test-repo/implementation.js index cc094e10..11db83f6 100644 --- a/src/backends/test-repo/implementation.js +++ b/src/backends/test-repo/implementation.js @@ -50,17 +50,22 @@ export default class TestRepo { return Promise.resolve(entries); } - entriesByFiles(collection, files) { + entriesByFiles(collection) { + const files = collection.get('files').map(collectionFile => ({ + path: collectionFile.get('file'), + label: collectionFile.get('label'), + })); return Promise.all(files.map(file => ({ file, data: getFile(file.path).content, }))); } - lookupEntry(collection, slug) { - return this.entries(collection).then(response => ( - response.entries.filter(entry => entry.slug === slug)[0] - )); + getEntry(collection, slug, path) { + return Promise.resolve({ + file: { path }, + data: getFile(path).content + }); } persistEntry(entry, mediaFiles = [], options) { diff --git a/src/containers/EntryPage.js b/src/containers/EntryPage.js index 20c7289c..c9a35474 100644 --- a/src/containers/EntryPage.js +++ b/src/containers/EntryPage.js @@ -12,7 +12,7 @@ import { import { cancelEdit } from '../actions/editor'; import { addMedia, removeMedia } from '../actions/media'; import { selectEntry, getMedia } from '../reducers'; -import { FOLDER, FILES } from '../constants/collectionTypes'; +import Collection from '../valueObjects/collection'; import EntryEditor from '../components/EntryEditor/EntryEditor'; import entryPageHOC from './editorialWorkflow/EntryPageHOC'; import { Loader } from '../components/UI'; @@ -39,11 +39,10 @@ class EntryPage extends React.Component { componentDidMount() { const { entry, newEntry, collection, slug, createEmptyDraft, loadEntry } = this.props; - if (newEntry) { createEmptyDraft(collection); } else { - if (collection.get('type') === FOLDER) loadEntry(entry, collection, slug); + loadEntry(entry, collection, slug); this.createDraft(entry); } } @@ -108,15 +107,7 @@ function mapStateToProps(state, ownProps) { const { collections, entryDraft } = state; 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 collectionModel = new Collection(collection); const newEntry = ownProps.route && ownProps.route.newRecord === true; const entry = newEntry ? null : selectEntry(state, collection.get('name'), slug); @@ -127,7 +118,7 @@ function mapStateToProps(state, ownProps) { newEntry, entryDraft, boundGetMedia, - fields, + fields: collectionModel.entryFields(slug), slug, entry, }; diff --git a/src/index.js b/src/index.js index a9dc35ad..daa0530f 100644 --- a/src/index.js +++ b/src/index.js @@ -34,7 +34,6 @@ if (process.env.NODE_ENV !== 'production' && module.hot) { } window.CMS = {}; -console.log('reg: ', registry); for (const method in registry) { window.CMS[method] = registry[method]; } diff --git a/src/valueObjects/Collection.js b/src/valueObjects/Collection.js new file mode 100644 index 00000000..39f71025 --- /dev/null +++ b/src/valueObjects/Collection.js @@ -0,0 +1,109 @@ +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'); + } +} + +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; + } +} + +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(); + } +}