From 8aa8f5ce39eda3bb3ff6c3f0ca64487d337b7844 Mon Sep 17 00:00:00 2001 From: Mathias Biilmann Christensen Date: Thu, 27 Oct 2016 11:34:22 +0200 Subject: [PATCH 01/18] Add shortcut for italics --- .../Widgets/MarkdownControlElements/RawEditor/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/Widgets/MarkdownControlElements/RawEditor/index.js b/src/components/Widgets/MarkdownControlElements/RawEditor/index.js index bcc7c673..5f68e04a 100644 --- a/src/components/Widgets/MarkdownControlElements/RawEditor/index.js +++ b/src/components/Widgets/MarkdownControlElements/RawEditor/index.js @@ -98,6 +98,7 @@ export default class RawEditor extends React.Component { this.shortcuts = { meta: { b: this.handleBold, + i: this.handleItalic, }, }; } From 2b99e2d176cf4144ab5521406d959a56adc94e5f Mon Sep 17 00:00:00 2001 From: Mathias Biilmann Christensen Date: Thu, 27 Oct 2016 11:47:19 +0200 Subject: [PATCH 02/18] Support entriesByFiles in test-repo backend --- src/backends/test-repo/implementation.js | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/backends/test-repo/implementation.js b/src/backends/test-repo/implementation.js index d2e8ec84..cc094e10 100644 --- a/src/backends/test-repo/implementation.js +++ b/src/backends/test-repo/implementation.js @@ -6,6 +6,15 @@ function getSlug(path) { return m && m[1]; } +function getFile(path) { + const segments = path.split('/'); + let obj = window.repoFiles; + while (obj && segments.length) { + obj = obj[segments.shift()]; + } + return obj; +} + export default class TestRepo { constructor(config) { this.config = config; @@ -42,7 +51,10 @@ export default class TestRepo { } entriesByFiles(collection, files) { - throw new Error('Not implemented yet'); + return Promise.all(files.map(file => ({ + file, + data: getFile(file.path).content, + }))); } lookupEntry(collection, slug) { From bbce1c30deca72cb2d077298c52af71aec281eac Mon Sep 17 00:00:00 2001 From: Mathias Biilmann Christensen Date: Thu, 27 Oct 2016 14:23:36 +0200 Subject: [PATCH 03/18] 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(); + } +} From 7713c4c6f8002ddc44b23fc9777509b5cb9d211a Mon Sep 17 00:00:00 2001 From: Mathias Biilmann Christensen Date: Thu, 27 Oct 2016 14:45:32 +0200 Subject: [PATCH 04/18] Fix some linting errors --- src/backends/backend.js | 30 ++++++++++++------------------ 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/src/backends/backend.js b/src/backends/backend.js index 9deececb..82b8ba66 100644 --- a/src/backends/backend.js +++ b/src/backends/backend.js @@ -4,7 +4,6 @@ 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 { storageKey = 'nf-cms-user'; @@ -43,7 +42,7 @@ class Backend { this.implementation = implementation; this.authStore = authStore; if (this.implementation === null) { - throw 'Cannot instantiate a Backend with no implementation'; + throw new Error('Cannot instantiate a Backend with no implementation'); } } @@ -94,13 +93,12 @@ class Backend { lookupEntry(collection, slug) { const collectionModel = new Collection(collection); return this.implementation.getEntry(collection, slug, collectionModel.entryPath(slug)) - .then(loadedEntry => { - return this.entryWithFormat(collection)(createEntry( + .then(loadedEntry => this.entryWithFormat(collection)(createEntry( collection.get('name'), slug, loadedEntry.file.path, { raw: loadedEntry.data, label: loadedEntry.file.label } - ))} + )) ); } @@ -112,21 +110,17 @@ class Backend { return (entry) => { const format = resolveFormat(collectionOrEntity, entry); if (entry && entry.raw) { - entry.data = format && format.fromFile(entry.raw); - return entry; - } else { - return format.fromFile(entry); + return Object.assign(entry, { data: format && format.fromFile(entry.raw) }); } + return format.fromFile(entry); }; } unpublishedEntries(page, perPage) { - return this.implementation.unpublishedEntries(page, perPage).then((response) => { - return { - pagination: response.pagination, - entries: response.entries.map(this.entryWithFormat('editorialWorkflow')), - }; - }); + return this.implementation.unpublishedEntries(page, perPage).then(response => ({ + pagination: response.pagination, + entries: response.entries.map(this.entryWithFormat('editorialWorkflow')), + })); } unpublishedEntry(collection, slug) { @@ -146,7 +140,7 @@ class Backend { let entryObj; if (newEntry) { if (!collectionModel.allowNewEntries()) { - throw ('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'])); entryObj = { @@ -197,7 +191,7 @@ class Backend { export function resolveBackend(config) { const name = config.getIn(['backend', 'name']); if (name == null) { - throw 'No backend defined in configuration'; + throw new Error('No backend defined in configuration'); } const authStore = new LocalStorageAuthStore(); @@ -210,7 +204,7 @@ export function resolveBackend(config) { case 'netlify-git': return new Backend(new NetlifyGitBackend(config, slugFormatter), authStore); default: - throw `Backend not found: ${ name }`; + throw new Error(`Backend not found: ${ name }`); } } From 80a2cefbf0081416890466864c2b941a9c70a529 Mon Sep 17 00:00:00 2001 From: Mathias Biilmann Christensen Date: Thu, 27 Oct 2016 15:33:15 +0200 Subject: [PATCH 05/18] Add json format support --- example/config.yml | 2 +- src/backends/backend.js | 13 ++++++------- src/formats/formats.js | 20 ++++++++++++-------- src/formats/json.js | 9 +++++++++ 4 files changed, 28 insertions(+), 16 deletions(-) create mode 100644 src/formats/json.js diff --git a/example/config.yml b/example/config.yml index 3d5f55bf..c53282ca 100644 --- a/example/config.yml +++ b/example/config.yml @@ -36,7 +36,7 @@ collections: # A list of collections the CMS should be able to edit file: "_data/settings.json" description: "General Site Settings" fields: - - {label: "Global title", name: site_title, widget: "string"} + - {label: "Global title", name: "site_title", widget: "string"} - name: "authors" label: "Authors" diff --git a/src/backends/backend.js b/src/backends/backend.js index 82b8ba66..17dd7ff0 100644 --- a/src/backends/backend.js +++ b/src/backends/backend.js @@ -87,13 +87,8 @@ class Backend { // We have the file path. Fetch and parse the file. getEntry(collection, slug, path) { - return this.implementation.getEntry(collection, slug, path).then(this.entryWithFormat(collection)); - } - - lookupEntry(collection, slug) { - const collectionModel = new Collection(collection); - return this.implementation.getEntry(collection, slug, collectionModel.entryPath(slug)) - .then(loadedEntry => this.entryWithFormat(collection)(createEntry( + return this.implementation.getEntry(collection, slug, path) + .then(loadedEntry => this.entryWithFormat(collection, slug)(createEntry( collection.get('name'), slug, loadedEntry.file.path, @@ -102,6 +97,10 @@ class Backend { ); } + lookupEntry(collection, slug) { + return this.getEntry(collection, slug, new Collection(collection).entryPath(slug)); + } + newEntry(collection) { return createEntry(collection.get('name')); } diff --git a/src/formats/formats.js b/src/formats/formats.js index fc8f91b1..48fa5367 100644 --- a/src/formats/formats.js +++ b/src/formats/formats.js @@ -1,7 +1,9 @@ import YAML from './yaml'; +import JSONFormatter from './json'; import YAMLFrontmatter from './yaml-frontmatter'; const yamlFormatter = new YAML(); +const jsonFormatter = new JSONFormatter(); const YamlFrontmatterFormatter = new YAMLFrontmatter(); function formatByType(type) { @@ -12,17 +14,18 @@ function formatByType(type) { function formatByExtension(extension) { return { - 'yml': yamlFormatter, - 'md': YamlFrontmatterFormatter, - 'markdown': YamlFrontmatterFormatter, - 'html': YamlFrontmatterFormatter + yml: yamlFormatter, + json: jsonFormatter, + md: YamlFrontmatterFormatter, + markdown: YamlFrontmatterFormatter, + html: YamlFrontmatterFormatter, }[extension] || YamlFrontmatterFormatter; } function formatByName(name) { return { - 'yaml': yamlFormatter, - 'frontmatter': YamlFrontmatterFormatter + yaml: yamlFormatter, + frontmatter: YamlFrontmatterFormatter, }[name] || YamlFrontmatterFormatter; } @@ -30,8 +33,9 @@ export function resolveFormat(collectionOrEntity, entry) { if (typeof collectionOrEntity === 'string') { return formatByType(collectionOrEntity); } - if (entry && entry.path) { - return formatByExtension(entry.path.split('.').pop()); + const path = entry && entry.path; + if (path) { + return formatByExtension(path.split('.').pop()); } return formatByName(collectionOrEntity.get('format')); } diff --git a/src/formats/json.js b/src/formats/json.js new file mode 100644 index 00000000..8cfd87dd --- /dev/null +++ b/src/formats/json.js @@ -0,0 +1,9 @@ +export default class JSONFormatter { + fromFile(content) { + return JSON.parse(content); + } + + toFile(data) { + return JSON.generate(data); + } +} From dabf6a1be103fb6a838f60c8d54d4fdb70b06241 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A1ssio=20Zen?= Date: Thu, 27 Oct 2016 13:10:44 -0200 Subject: [PATCH 06/18] typo --- src/containers/EntryPage.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/containers/EntryPage.js b/src/containers/EntryPage.js index c9a35474..b6fda5b1 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 Collection from '../valueObjects/collection'; +import Collection from '../valueObjects/Collection'; import EntryEditor from '../components/EntryEditor/EntryEditor'; import entryPageHOC from './editorialWorkflow/EntryPageHOC'; import { Loader } from '../components/UI'; From baafe0b32fcb76f84452b630d57380ea9569aad6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A1ssio=20Zen?= Date: Thu, 27 Oct 2016 13:12:18 -0200 Subject: [PATCH 07/18] refactor on slugformatter --- src/actions/entries.js | 14 ++++---------- src/backends/backend.js | 14 +++++--------- src/backends/github/implementation.js | 24 ++++++++++++------------ src/reducers/entries.js | 1 - src/valueObjects/Collection.js | 22 +++++++++++++++++++++- 5 files changed, 42 insertions(+), 33 deletions(-) diff --git a/src/actions/entries.js b/src/actions/entries.js index 899356e2..c8d3914a 100644 --- a/src/actions/entries.js +++ b/src/actions/entries.js @@ -181,16 +181,10 @@ export function loadEntry(entry, collection, slug) { const state = getState(); const backend = currentBackend(state.config); dispatch(entryLoading(collection, slug)); - let getPromise; - if (entry && entry.get('path') && entry.get('partial')) { - getPromise = backend.getEntry(entry.get('collection'), entry.get('slug'), entry.get('path')); - } else { - getPromise = backend.lookupEntry(collection, slug); - } - return getPromise - .then((loadedEntry) => { - return dispatch(entryLoaded(collection, loadedEntry)); - }); + return backend.getEntry(collection, slug) + .then(loadedEntry => ( + dispatch(entryLoaded(collection, loadedEntry)) + )); }; } diff --git a/src/backends/backend.js b/src/backends/backend.js index 17dd7ff0..edfe254f 100644 --- a/src/backends/backend.js +++ b/src/backends/backend.js @@ -86,8 +86,8 @@ class Backend { } // We have the file path. Fetch and parse the file. - getEntry(collection, slug, path) { - return this.implementation.getEntry(collection, slug, path) + getEntry(collection, slug) { + return this.implementation.getEntry(collection, slug, new Collection(collection).entryPath(slug)) .then(loadedEntry => this.entryWithFormat(collection, slug)(createEntry( collection.get('name'), slug, @@ -97,10 +97,6 @@ class Backend { ); } - lookupEntry(collection, slug) { - return this.getEntry(collection, slug, new Collection(collection).entryPath(slug)); - } - newEntry(collection) { return createEntry(collection.get('name')); } @@ -197,11 +193,11 @@ export function resolveBackend(config) { switch (name) { case 'test-repo': - return new Backend(new TestRepoBackend(config, slugFormatter), authStore); + return new Backend(new TestRepoBackend(config), authStore); case 'github': - return new Backend(new GitHubBackend(config, slugFormatter), authStore); + return new Backend(new GitHubBackend(config), authStore); case 'netlify-git': - return new Backend(new NetlifyGitBackend(config, slugFormatter), authStore); + return new Backend(new NetlifyGitBackend(config), authStore); default: throw new Error(`Backend not found: ${ name }`); } diff --git a/src/backends/github/implementation.js b/src/backends/github/implementation.js index 609ebf87..860a4c3e 100644 --- a/src/backends/github/implementation.js +++ b/src/backends/github/implementation.js @@ -32,7 +32,8 @@ export default class GitHub { } entriesByFolder(collection) { - return this.api.listFiles(collection.get('folder')).then(files => this.entriesByFiles(collection, files)); + return this.api.listFiles(collection.get('folder')) + .then(this.fetchFiles); } entriesByFiles(collection) { @@ -40,26 +41,25 @@ export default class GitHub { path: collectionFile.get('file'), label: collectionFile.get('label'), })); + return this.fetchFiles(files); + } + + fetchFiles = (files) => { const sem = semaphore(MAX_CONCURRENT_DOWNLOADS); const promises = []; files.forEach((file) => { - promises.push(new Promise((resolve, reject) => { - return sem.take(() => this.api.readFile(file.path, file.sha).then((data) => { - resolve( - { - file, - data, - } - ); + promises.push(new Promise((resolve, reject) => ( + sem.take(() => this.api.readFile(file.path, file.sha).then((data) => { + resolve({ file, data }); sem.leave(); }).catch((err) => { sem.leave(); reject(err); - })); - })); + })) + ))); }); return Promise.all(promises); - } + }; // Fetches a single entry. getEntry(collection, slug, path) { diff --git a/src/reducers/entries.js b/src/reducers/entries.js index 8e5b36ca..f29c0d90 100644 --- a/src/reducers/entries.js +++ b/src/reducers/entries.js @@ -31,7 +31,6 @@ const entries = (state = Map({ entities: Map(), pages: Map() }), action) => { collection = action.payload.collection; loadedEntries = action.payload.entries; page = action.payload.page; - return state.withMutations((map) => { loadedEntries.forEach(entry => ( map.setIn(['entities', `${ collection }.${ entry.slug }`], fromJS(entry).set('isFetching', false)) diff --git a/src/valueObjects/Collection.js b/src/valueObjects/Collection.js index 39f71025..58950a82 100644 --- a/src/valueObjects/Collection.js +++ b/src/valueObjects/Collection.js @@ -9,6 +9,26 @@ function formatToExtension(format) { }[format]; } +function slugFormatter(template, entryData) { + const date = new Date(); + const entry = (typeof entryData === 'string') ? entryData : entryData.get('title', entryData.get('path')); + const identifier = entry.match(/([^:\\/]*?)(?:\.([^ :\\/.]*))?$/)[1]; + return template.replace(/\{\{([^\}]+)\}\}/g, (_, name) => { + switch (name) { + case 'year': + return date.getFullYear(); + case 'month': + return (`0${ date.getMonth() + 1 }`).slice(-2); + case 'day': + return (`0${ date.getDate() }`).slice(-2); + case 'slug': + return identifier.trim().toLowerCase().replace(/[^a-z0-9\.\-_]+/gi, '-'); + default: + return identifier.trim().toLowerCase().replace(/[^a-z0-9\.\-_]+/gi, '-'); + } + }); +} + class FolderCollection { constructor(collection) { this.collection = collection; @@ -23,7 +43,7 @@ class FolderCollection { } entrySlug(path) { - return path.split('/').pop().replace(/\.[^\.]+$/, ''); + return slugFormatter(this.collection.get('slug'), path); } listMethod() { From 2e6e5d1bec014409b712cabbb0f0566aa53c7f41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A1ssio=20Zen?= Date: Thu, 27 Oct 2016 13:59:21 -0200 Subject: [PATCH 08/18] removed unused function --- src/backends/test-repo/implementation.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/backends/test-repo/implementation.js b/src/backends/test-repo/implementation.js index 11db83f6..3e9b79f7 100644 --- a/src/backends/test-repo/implementation.js +++ b/src/backends/test-repo/implementation.js @@ -1,10 +1,4 @@ import AuthenticationPage from './AuthenticationPage'; -import { createEntry } from '../../valueObjects/Entry'; - -function getSlug(path) { - const m = path.match(/([^\/]+?)(\.[^\/\.]+)?$/); - return m && m[1]; -} function getFile(path) { const segments = path.split('/'); From 1566e247f3e8a73bae04b86a81920af61f838560 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A1ssio=20Zen?= Date: Thu, 27 Oct 2016 14:59:41 -0200 Subject: [PATCH 09/18] Reverted Collection VO slug generation --- src/valueObjects/Collection.js | 22 +--------------------- 1 file changed, 1 insertion(+), 21 deletions(-) diff --git a/src/valueObjects/Collection.js b/src/valueObjects/Collection.js index 58950a82..39f71025 100644 --- a/src/valueObjects/Collection.js +++ b/src/valueObjects/Collection.js @@ -9,26 +9,6 @@ function formatToExtension(format) { }[format]; } -function slugFormatter(template, entryData) { - const date = new Date(); - const entry = (typeof entryData === 'string') ? entryData : entryData.get('title', entryData.get('path')); - const identifier = entry.match(/([^:\\/]*?)(?:\.([^ :\\/.]*))?$/)[1]; - return template.replace(/\{\{([^\}]+)\}\}/g, (_, name) => { - switch (name) { - case 'year': - return date.getFullYear(); - case 'month': - return (`0${ date.getMonth() + 1 }`).slice(-2); - case 'day': - return (`0${ date.getDate() }`).slice(-2); - case 'slug': - return identifier.trim().toLowerCase().replace(/[^a-z0-9\.\-_]+/gi, '-'); - default: - return identifier.trim().toLowerCase().replace(/[^a-z0-9\.\-_]+/gi, '-'); - } - }); -} - class FolderCollection { constructor(collection) { this.collection = collection; @@ -43,7 +23,7 @@ class FolderCollection { } entrySlug(path) { - return slugFormatter(this.collection.get('slug'), path); + return path.split('/').pop().replace(/\.[^\.]+$/, ''); } listMethod() { From a33aa13d0f066101f9f695f79fce5f06b2777fbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A1ssio=20Zen?= Date: Thu, 27 Oct 2016 15:27:39 -0200 Subject: [PATCH 10/18] moved Entry VO away from implementations --- src/backends/backend.js | 28 +++++++++++++++++++++------ src/backends/github/API.js | 2 +- src/backends/github/implementation.js | 25 +++++++++++------------- 3 files changed, 34 insertions(+), 21 deletions(-) diff --git a/src/backends/backend.js b/src/backends/backend.js index edfe254f..47db23e8 100644 --- a/src/backends/backend.js +++ b/src/backends/backend.js @@ -85,7 +85,6 @@ class Backend { )); } - // We have the file path. Fetch and parse the file. getEntry(collection, slug) { return this.implementation.getEntry(collection, slug, new Collection(collection).entryPath(slug)) .then(loadedEntry => this.entryWithFormat(collection, slug)(createEntry( @@ -112,14 +111,31 @@ class Backend { } unpublishedEntries(page, perPage) { - return this.implementation.unpublishedEntries(page, perPage).then(response => ({ - pagination: response.pagination, - entries: response.entries.map(this.entryWithFormat('editorialWorkflow')), - })); + return this.implementation.unpublishedEntries(page, perPage) + .then(loadedEntries => ( + loadedEntries.map((loadedEntry) => { + const entry = createEntry('draft', loadedEntry.slug, loadedEntry.file.path, { raw: loadedEntry.data }) + entry.metaData = loadedEntry.metaData; + return entry; + }) + )) + .then((entries) => { + const filteredEntries = entries.filter(entry => entry !== null); + return { + pagination: 0, + entries: filteredEntries.map(this.entryWithFormat('editorialWorkflow')), + }; + }); } unpublishedEntry(collection, slug) { - return this.implementation.unpublishedEntry(collection, slug).then(this.entryWithFormat(collection)); + return this.implementation.unpublishedEntry(collection, slug) + .then(loadedEntry => this.entryWithFormat(collection, slug)(createEntry( + collection.get('name'), + slug, + loadedEntry.file.path, + { raw: loadedEntry.data } + ))); } persistEntry(config, collection, entryDraft, MediaFiles, options) { diff --git a/src/backends/github/API.js b/src/backends/github/API.js index 606debbd..83c01043 100644 --- a/src/backends/github/API.js +++ b/src/backends/github/API.js @@ -154,7 +154,7 @@ export default class API { metaData = data; return this.readFile(data.objects.entry, null, data.branch); }) - .then(file => ({ metaData, file })) + .then(fileData => ({ metaData, fileData })) .catch((error) => { return null; }); diff --git a/src/backends/github/implementation.js b/src/backends/github/implementation.js index 860a4c3e..4c6e5c2f 100644 --- a/src/backends/github/implementation.js +++ b/src/backends/github/implementation.js @@ -79,16 +79,19 @@ export default class GitHub { const promises = []; branches.map((branch) => { promises.push(new Promise((resolve, reject) => { - const contentKey = branch.ref.split('refs/heads/cms/').pop(); - return sem.take(() => this.api.readUnpublishedBranchFile(contentKey).then((data) => { + const slug = branch.ref.split('refs/heads/cms/').pop(); + return sem.take(() => this.api.readUnpublishedBranchFile(slug).then((data) => { if (data === null || data === undefined) { resolve(null); sem.leave(); } else { - const entryPath = data.metaData.objects.entry; - const entry = createEntry('draft', contentKey, entryPath, { raw: data.file }); - entry.metaData = data.metaData; - resolve(entry); + const path = data.metaData.objects.entry; + resolve({ + slug, + file: { path }, + data: data.fileData, + metaData: data.metaData, + }); sem.leave(); } }).catch((err) => { @@ -98,18 +101,12 @@ export default class GitHub { })); }); return Promise.all(promises); - }).then((entries) => { - const filteredEntries = entries.filter(entry => entry !== null); - return { - pagination: 0, - entries: filteredEntries, - }; - }); + }) } unpublishedEntry(collection, slug) { return this.unpublishedEntries().then(response => ( - response.entries.filter((entry) => { + response.filter((entry) => { return entry.metaData && entry.slug === slug; })[0] )); From 442a02f0090386a2f3b77d4971805fa132b25c01 Mon Sep 17 00:00:00 2001 From: Mathias Biilmann Christensen Date: Thu, 27 Oct 2016 19:56:50 +0200 Subject: [PATCH 11/18] Fix slugs for search result entries --- src/integrations/providers/algolia/implementation.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/integrations/providers/algolia/implementation.js b/src/integrations/providers/algolia/implementation.js index 4eb90d2c..034c5e49 100644 --- a/src/integrations/providers/algolia/implementation.js +++ b/src/integrations/providers/algolia/implementation.js @@ -1,5 +1,6 @@ import { createEntry } from '../../../valueObjects/Entry'; import _ from 'lodash'; +import Collection from '../../../valueObjects/Collection'; function getSlug(path) { const m = path.match(/([^\/]+?)(\.[^\/\.]+)?$/); @@ -101,11 +102,12 @@ export default class Algolia { if (this.entriesCache.collection === collection && this.entriesCache.page === page) { return Promise.resolve({ page: this.entriesCache.page, entries: this.entriesCache.entries }); } else { + const collectionModel = new Collection(collection); return this.request(`${ this.searchURL }/indexes/${ collection.get('name') }`, { params: { page }, }).then((response) => { const entries = response.hits.map((hit) => { - const slug = hit.slug || getSlug(hit.path); + const slug = collectionModel.entrySlug(hit.path); return createEntry(collection.get('name'), slug, hit.path, { data: hit.data, partial: true }); }); this.entriesCache = { collection, pagination: response.page, entries }; From 6b73c39ba844f1f3f072aafa773eaf552c7ae6a4 Mon Sep 17 00:00:00 2001 From: Mathias Biilmann Christensen Date: Fri, 28 Oct 2016 04:51:37 +0200 Subject: [PATCH 12/18] Support for object widget --- example/config.yml | 7 ++++ src/backends/backend.js | 10 +++-- src/components/Widgets.js | 6 +++ src/components/Widgets/NumberControl.js | 16 ++++++++ src/components/Widgets/NumberPreview.js | 9 +++++ src/components/Widgets/ObjectControl.js | 51 +++++++++++++++++++++++++ src/components/Widgets/ObjectPreview.js | 28 ++++++++++++++ src/formats/formats.js | 1 + src/formats/json.js | 2 +- 9 files changed, 125 insertions(+), 5 deletions(-) create mode 100644 src/components/Widgets/NumberControl.js create mode 100644 src/components/Widgets/NumberPreview.js create mode 100644 src/components/Widgets/ObjectControl.js create mode 100644 src/components/Widgets/ObjectPreview.js diff --git a/example/config.yml b/example/config.yml index c53282ca..15f01c74 100644 --- a/example/config.yml +++ b/example/config.yml @@ -37,6 +37,13 @@ collections: # A list of collections the CMS should be able to edit description: "General Site Settings" fields: - {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" label: "Authors" diff --git a/src/backends/backend.js b/src/backends/backend.js index 47db23e8..68bd3857 100644 --- a/src/backends/backend.js +++ b/src/backends/backend.js @@ -154,16 +154,18 @@ class Backend { throw (new Error('Not allowed to create new entries in this collection')); } const slug = slugFormatter(collection.get('slug'), entryDraft.getIn(['entry', 'data'])); + const path = collectionModel.entryPath(slug); entryObj = { - path: collectionModel.entryPath(slug), + path, slug, - raw: this.entryToRaw(collection, entryData), + raw: this.entryToRaw(collection, Object.assign({ path }, entryData)), }; } else { + const path = entryDraft.getIn(['entry', 'path']); entryObj = { - path: entryDraft.getIn(['entry', 'path']), + path, slug: entryDraft.getIn(['entry', 'slug']), - raw: this.entryToRaw(collection, entryData), + raw: this.entryToRaw(collection, Object.assign({ path }, entryData)), }; } diff --git a/src/components/Widgets.js b/src/components/Widgets.js index 74bd97ec..bdecd996 100644 --- a/src/components/Widgets.js +++ b/src/components/Widgets.js @@ -3,6 +3,8 @@ import UnknownControl from './Widgets/UnknownControl'; import UnknownPreview from './Widgets/UnknownPreview'; import StringControl from './Widgets/StringControl'; import StringPreview from './Widgets/StringPreview'; +import NumberControl from './Widgets/NumberControl'; +import NumberPreview from './Widgets/NumberPreview'; import ListControl from './Widgets/ListControl'; import ListPreview from './Widgets/ListPreview'; import TextControl from './Widgets/TextControl'; @@ -13,13 +15,17 @@ import ImageControl from './Widgets/ImageControl'; import ImagePreview from './Widgets/ImagePreview'; import DateTimeControl from './Widgets/DateTimeControl'; import DateTimePreview from './Widgets/DateTimePreview'; +import ObjectControl from './Widgets/ObjectControl'; +import ObjectPreview from './Widgets/ObjectPreview'; registry.registerWidget('string', StringControl, StringPreview); registry.registerWidget('text', TextControl, TextPreview); +registry.registerWidget('number', NumberControl, NumberPreview); registry.registerWidget('list', ListControl, ListPreview); registry.registerWidget('markdown', MarkdownControl, MarkdownPreview); registry.registerWidget('image', ImageControl, ImagePreview); registry.registerWidget('datetime', DateTimeControl, DateTimePreview); +registry.registerWidget('object', ObjectControl, ObjectPreview); registry.registerWidget('unknown', UnknownControl, UnknownPreview); export function resolveWidget(name) { diff --git a/src/components/Widgets/NumberControl.js b/src/components/Widgets/NumberControl.js new file mode 100644 index 00000000..fe92b768 --- /dev/null +++ b/src/components/Widgets/NumberControl.js @@ -0,0 +1,16 @@ +import React, { PropTypes } from 'react'; + +export default class StringControl extends React.Component { + handleChange = e => { + this.props.onChange(e.target.value); + }; + + render() { + return ; + } +} + +StringControl.propTypes = { + onChange: PropTypes.func.isRequired, + value: PropTypes.node, +}; diff --git a/src/components/Widgets/NumberPreview.js b/src/components/Widgets/NumberPreview.js new file mode 100644 index 00000000..972e068c --- /dev/null +++ b/src/components/Widgets/NumberPreview.js @@ -0,0 +1,9 @@ +import React, { PropTypes } from 'react'; + +export default function StringPreview({ value }) { + return {value}; +} + +StringPreview.propTypes = { + value: PropTypes.node, +}; diff --git a/src/components/Widgets/ObjectControl.js b/src/components/Widgets/ObjectControl.js new file mode 100644 index 00000000..d2dfaf37 --- /dev/null +++ b/src/components/Widgets/ObjectControl.js @@ -0,0 +1,51 @@ +import React, { Component, PropTypes } from 'react'; +import { Map } from 'immutable'; +import { resolveWidget } from '../Widgets'; +import styles from '../ControlPanel/ControlPane.css'; + +export default class ObjectControl extends Component { + static propTypes = { + onChange: PropTypes.func.isRequired, + onAddMedia: PropTypes.func.isRequired, + getMedia: PropTypes.func.isRequired, + value: PropTypes.node, + field: PropTypes.node, + }; + + controlFor(field) { + const { onAddMedia, onRemoveMedia, getMedia, value, onChange } = this.props; + const widget = resolveWidget(field.get('widget') || 'string'); + const fieldValue = value && value.get(field.get('name')); + + return ( +
+ + { + React.createElement(widget.control, { + field, + value: fieldValue, + onChange: (val) => { + onChange((value || Map()).set(field.get('name'), val)); + }, + onAddMedia, + onRemoveMedia, + getMedia, + }) + } +
+ ); + } + + render() { + const { field } = this.props; + const fields = field.get('fields'); + + if (!fields) { + return

No fields defined for this widget

; + } + + return (
+ {field.get('fields').map(field => this.controlFor(field))} +
); + } +} diff --git a/src/components/Widgets/ObjectPreview.js b/src/components/Widgets/ObjectPreview.js new file mode 100644 index 00000000..3c541ef6 --- /dev/null +++ b/src/components/Widgets/ObjectPreview.js @@ -0,0 +1,28 @@ +import React, { PropTypes, Component } from 'react'; +import { resolveWidget } from '../Widgets'; + +export default class ObjectPreview extends Component { + widgetFor = (field) => { + const { value, getMedia } = this.props; + const widget = resolveWidget(field.get('widget')); + return (
{React.createElement(widget.preview, { + key: field.get('name'), + value: value && value.get(field.get('name')), + field, + getMedia, + })}
); + }; + + render() { + const { field } = this.props; + const fields = field && field.get('fields'); + + return
{fields && fields.map(f => this.widgetFor(f))}
; + } +} + +ObjectPreview.propTypes = { + value: PropTypes.node, + field: PropTypes.node, + getMedia: PropTypes.func.isRequired, +}; diff --git a/src/formats/formats.js b/src/formats/formats.js index 48fa5367..2b7d9648 100644 --- a/src/formats/formats.js +++ b/src/formats/formats.js @@ -33,6 +33,7 @@ export function resolveFormat(collectionOrEntity, entry) { if (typeof collectionOrEntity === 'string') { return formatByType(collectionOrEntity); } + console.log('entry: %o', entry); const path = entry && entry.path; if (path) { return formatByExtension(path.split('.').pop()); diff --git a/src/formats/json.js b/src/formats/json.js index 8cfd87dd..e2e5467e 100644 --- a/src/formats/json.js +++ b/src/formats/json.js @@ -4,6 +4,6 @@ export default class JSONFormatter { } toFile(data) { - return JSON.generate(data); + return JSON.stringify(data); } } From 86e3aed065785b4a1ecb91a751f465f8b867a885 Mon Sep 17 00:00:00 2001 From: Mathias Biilmann Christensen Date: Fri, 28 Oct 2016 10:21:13 +0200 Subject: [PATCH 13/18] Better styling for object controls --- example/index.html | 24 +++++++++ src/components/ControlPanel/ControlPane.css | 56 ++------------------- src/components/PreviewPane/PreviewPane.js | 5 +- src/components/Widgets/ObjectControl.css | 6 +++ src/components/Widgets/ObjectControl.js | 13 ++--- src/valueObjects/Collection.js | 12 +++++ 6 files changed, 57 insertions(+), 59 deletions(-) create mode 100644 src/components/Widgets/ObjectControl.css diff --git a/example/index.html b/example/index.html index 3edb2778..86e3541c 100644 --- a/example/index.html +++ b/example/index.html @@ -87,7 +87,31 @@ } }); + var GeneralPreview = createClass({ + render: function() { + var entry = this.props.entry; + var title = entry.getIn(['data', 'site_title']); + var posts = entry.getIn(['data', 'posts']); + var thumb = posts && posts.get('thumb'); + + return h('div', {}, + h('h1', {}, title), + h('dl', {}, + h('dt', {}, 'Posts on Frontpage'), + h('dd', {}, posts && posts.get('front_limit') || '0'), + + h('dt', {}, 'Default Author'), + h('dd', {}, posts && posts.get('author') || 'None'), + + h('dt', {}, 'Default Thumbnail'), + h('dd', {}, thumb && h('img', {src: this.props.getMedia(thumb).toString()})) + ) + ); + } + }); + CMS.registerPreviewTemplate("posts", PostPreview); + CMS.registerPreviewTemplate("general", GeneralPreview); CMS.registerPreviewStyle("/example.css"); CMS.registerEditorComponent({ id: "youtube", diff --git a/src/components/ControlPanel/ControlPane.css b/src/components/ControlPanel/ControlPane.css index 25973c88..1f614ecd 100644 --- a/src/components/ControlPanel/ControlPane.css +++ b/src/components/ControlPanel/ControlPane.css @@ -1,54 +1,6 @@ -.control { - color: #7c8382; +.root { position: relative; - padding: 20px 0; - - & input, - & textarea, - & select { - font-family: monospace; - display: block; - width: 100%; - padding: 0; - margin: 0; - border: none; - outline: 0; - box-shadow: none; - background: 0 0; - font-size: 18px; - color: #7c8382; - } -} -.label { - display: block; - color: #AAB0AF; - font-size: 12px; - margin-bottom: 18px; -} -.widget { - border-bottom: 1px solid #e8eae8; - position: relative; - - &:after { - content: ''; - position: absolute; - left: 42px; - bottom: -7px; - width: 12px; - height: 12px; - background-color: #f2f5f4; - -webkit-transform: rotate(45deg); - transform: rotate(45deg); - z-index: 1; - border-right: 1px solid #e8eae8; - border-bottom: 1px solid #e8eae8; - } - - &:last-child { - border-bottom: none; - } - - &:last-child:after { - display: none; - } + margin-bottom: 20px; + padding: 20px; + border: 1px solid #e8eae8; } diff --git a/src/components/PreviewPane/PreviewPane.js b/src/components/PreviewPane/PreviewPane.js index f3c99de2..1a297bef 100644 --- a/src/components/PreviewPane/PreviewPane.js +++ b/src/components/PreviewPane/PreviewPane.js @@ -3,6 +3,7 @@ import ReactDOM from 'react-dom'; import ImmutablePropTypes from 'react-immutable-proptypes'; import { ScrollSyncPane } from '../ScrollSync'; import registry from '../../lib/registry'; +import Collection from '../../valueObjects/Collection'; import { resolveWidget } from '../Widgets'; import Preview from './Preview'; import styles from './PreviewPane.css'; @@ -26,7 +27,9 @@ export default class PreviewPane extends React.Component { }; renderPreview() { - const component = registry.getPreviewTemplate(this.props.collection.get('name')) || Preview; + const { entry, collection } = this.props; + const collectionModel = new Collection(collection); + const component = registry.getPreviewTemplate(collectionModel.templateName(entry.get('slug'))) || Preview; const previewProps = { ...this.props, widgetFor: this.widgetFor, diff --git a/src/components/Widgets/ObjectControl.css b/src/components/Widgets/ObjectControl.css new file mode 100644 index 00000000..f9c6a947 --- /dev/null +++ b/src/components/Widgets/ObjectControl.css @@ -0,0 +1,6 @@ +.root { + position: relative; + border: 1px solid #e8eae8; + margin-bottom: 20px; + padding: 20px; +} diff --git a/src/components/Widgets/ObjectControl.js b/src/components/Widgets/ObjectControl.js index d2dfaf37..953fc913 100644 --- a/src/components/Widgets/ObjectControl.js +++ b/src/components/Widgets/ObjectControl.js @@ -1,7 +1,8 @@ import React, { Component, PropTypes } from 'react'; import { Map } from 'immutable'; import { resolveWidget } from '../Widgets'; -import styles from '../ControlPanel/ControlPane.css'; +import controlStyles from '../ControlPanel/ControlPane.css'; +import styles from './ObjectControl.css'; export default class ObjectControl extends Component { static propTypes = { @@ -17,9 +18,9 @@ export default class ObjectControl extends Component { const widget = resolveWidget(field.get('widget') || 'string'); const fieldValue = value && value.get(field.get('name')); - return ( -
- + return (
+
+ { React.createElement(widget.control, { field, @@ -33,7 +34,7 @@ export default class ObjectControl extends Component { }) }
- ); +
); } render() { @@ -44,7 +45,7 @@ export default class ObjectControl extends Component { return

No fields defined for this widget

; } - return (
+ return (
{field.get('fields').map(field => this.controlFor(field))}
); } diff --git a/src/valueObjects/Collection.js b/src/valueObjects/Collection.js index 39f71025..a8b5c220 100644 --- a/src/valueObjects/Collection.js +++ b/src/valueObjects/Collection.js @@ -37,6 +37,10 @@ class FolderCollection { allowNewEntries() { return this.collection.get('create'); } + + templateName() { + return this.props.collection.get('name'); + } } class FilesCollection { @@ -71,6 +75,10 @@ class FilesCollection { allowNewEntries() { return false; } + + templateName(slug) { + return slug; + } } export default class Collection { @@ -106,4 +114,8 @@ export default class Collection { allowNewEntries() { return this.collection.allowNewEntries(); } + + templateName(slug) { + return this.collection.templateName(slug); + } } From d7f1b25c1b236b79e165414586a5da911fdf31f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A1ssio=20Zen?= Date: Fri, 28 Oct 2016 11:42:31 -0200 Subject: [PATCH 14/18] Bugfixes due to new slug signature --- src/backends/backend.js | 43 +++++++++++++-------------- src/backends/github/implementation.js | 17 ++++++----- 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/src/backends/backend.js b/src/backends/backend.js index 47db23e8..ab469875 100644 --- a/src/backends/backend.js +++ b/src/backends/backend.js @@ -20,8 +20,9 @@ class LocalStorageAuthStore { const slugFormatter = (template, entryData) => { const date = new Date(); - return template.replace(/\{\{([^\}]+)\}\}/g, (_, name) => { - switch (name) { + const identifier = entryData.get('title', entryData.get('path')); + return template.replace(/\{\{([^\}]+)\}\}/g, (_, field) => { + switch (field) { case 'year': return date.getFullYear(); case 'month': @@ -29,10 +30,9 @@ const slugFormatter = (template, entryData) => { case 'day': return (`0${ date.getDate() }`).slice(-2); case 'slug': - 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, '-'); default: - return entryData.get(name); + return entryData.get(field); } }); }; @@ -53,6 +53,7 @@ class Backend { this.implementation.setUser(stored); return stored; } + return null; } authComponent() { @@ -112,30 +113,28 @@ class Backend { unpublishedEntries(page, perPage) { return this.implementation.unpublishedEntries(page, perPage) - .then(loadedEntries => ( - loadedEntries.map((loadedEntry) => { - const entry = createEntry('draft', loadedEntry.slug, loadedEntry.file.path, { raw: loadedEntry.data }) + .then(loadedEntries => loadedEntries.filter(entry => entry !== null)) + .then(entries => ( + entries.map((loadedEntry) => { + const entry = createEntry('draft', loadedEntry.slug, loadedEntry.file.path, { raw: loadedEntry.data }); entry.metaData = loadedEntry.metaData; return entry; }) )) - .then((entries) => { - const filteredEntries = entries.filter(entry => entry !== null); - return { - pagination: 0, - entries: filteredEntries.map(this.entryWithFormat('editorialWorkflow')), - }; - }); + .then(entries => ({ + pagination: 0, + entries: entries.map(this.entryWithFormat('editorialWorkflow')), + })); } unpublishedEntry(collection, slug) { return this.implementation.unpublishedEntry(collection, slug) - .then(loadedEntry => this.entryWithFormat(collection, slug)(createEntry( - collection.get('name'), - slug, - loadedEntry.file.path, - { raw: loadedEntry.data } - ))); + .then((loadedEntry) => { + const entry = createEntry('draft', loadedEntry.slug, loadedEntry.file.path, { raw: loadedEntry.data }); + entry.metaData = loadedEntry.metaData; + return entry; + }) + .then(this.entryWithFormat(collection, slug)); } persistEntry(config, collection, entryDraft, MediaFiles, options) { @@ -144,7 +143,7 @@ class Backend { const parsedData = { title: entryDraft.getIn(['entry', 'data', 'title'], 'No Title'), - description: entryDraft.getIn(['entry', 'data', 'description'], 'No Description'), + description: entryDraft.getIn(['entry', 'data', 'description'], 'No Description!'), }; const entryData = entryDraft.getIn(['entry', 'data']).toJS(); diff --git a/src/backends/github/implementation.js b/src/backends/github/implementation.js index 4c6e5c2f..eb7dddf1 100644 --- a/src/backends/github/implementation.js +++ b/src/backends/github/implementation.js @@ -1,5 +1,4 @@ import semaphore from 'semaphore'; -import { createEntry } from '../../valueObjects/Entry'; import AuthenticationPage from './AuthenticationPage'; import API from './API'; @@ -9,7 +8,7 @@ export default class GitHub { constructor(config) { this.config = config; if (config.getIn(['backend', 'repo']) == null) { - throw 'The GitHub backend needs a "repo" in the backend configuration.'; + throw new Error('The GitHub backend needs a "repo" in the backend configuration.'); } this.repo = config.getIn(['backend', 'repo']); this.branch = config.getIn(['backend', 'branch']) || 'master'; @@ -101,15 +100,17 @@ export default class GitHub { })); }); return Promise.all(promises); - }) + }); } unpublishedEntry(collection, slug) { - return this.unpublishedEntries().then(response => ( - response.filter((entry) => { - return entry.metaData && entry.slug === slug; - })[0] - )); + return this.api.readUnpublishedBranchFile(slug) + .then(data => ({ + slug, + file: { path: data.metaData.objects.entry }, + data: data.fileData, + metaData: data.metaData, + })); } updateUnpublishedEntryStatus(collection, slug, newStatus) { From 13cbf2115921b64dbd8ec4bd5574949917a1bc0b Mon Sep 17 00:00:00 2001 From: Mathias Biilmann Christensen Date: Fri, 28 Oct 2016 19:13:26 +0200 Subject: [PATCH 15/18] Implement list control with fields --- src/components/ControlPanel/ControlPane.css | 56 ++++++++++++- src/components/Widgets/ListControl.css | 68 +++++++++++++++ src/components/Widgets/ListControl.js | 91 ++++++++++++++++++++- src/valueObjects/Collection.js | 2 +- 4 files changed, 210 insertions(+), 7 deletions(-) create mode 100644 src/components/Widgets/ListControl.css diff --git a/src/components/ControlPanel/ControlPane.css b/src/components/ControlPanel/ControlPane.css index 1f614ecd..25973c88 100644 --- a/src/components/ControlPanel/ControlPane.css +++ b/src/components/ControlPanel/ControlPane.css @@ -1,6 +1,54 @@ -.root { +.control { + color: #7c8382; position: relative; - margin-bottom: 20px; - padding: 20px; - border: 1px solid #e8eae8; + padding: 20px 0; + + & input, + & textarea, + & select { + font-family: monospace; + display: block; + width: 100%; + padding: 0; + margin: 0; + border: none; + outline: 0; + box-shadow: none; + background: 0 0; + font-size: 18px; + color: #7c8382; + } +} +.label { + display: block; + color: #AAB0AF; + font-size: 12px; + margin-bottom: 18px; +} +.widget { + border-bottom: 1px solid #e8eae8; + position: relative; + + &:after { + content: ''; + position: absolute; + left: 42px; + bottom: -7px; + width: 12px; + height: 12px; + background-color: #f2f5f4; + -webkit-transform: rotate(45deg); + transform: rotate(45deg); + z-index: 1; + border-right: 1px solid #e8eae8; + border-bottom: 1px solid #e8eae8; + } + + &:last-child { + border-bottom: none; + } + + &:last-child:after { + display: none; + } } diff --git a/src/components/Widgets/ListControl.css b/src/components/Widgets/ListControl.css new file mode 100644 index 00000000..fedcd1dc --- /dev/null +++ b/src/components/Widgets/ListControl.css @@ -0,0 +1,68 @@ +.addButton { + display: block; + cursor: pointer; + margin: 20px 0; + border: none; + background: transparent; + &::before { + content: "+"; + display: inline-block; + margin-right: 5px; + width: 15px; + height: 15px; + border: 1px solid #444; + border-radius: 100%; + background: transparent; + line-height: 13px; + } +} + +.removeButton { + position: absolute; + top: 5px; + right: 5px; + display: inline-block; + cursor: pointer; + border: none; + background: transparent; + width: 15px; + height: 15px; + border: 1px solid #444; + border-radius: 100%; + line-height: 13px; +} + +.toggleButton { + position: absolute; + top: 5px; + left: 5px; +} + +.item { + position: relative; + padding-left: 20px; + cursor: move; +} + +.objectLabel { + border: 1px solid #e8eae8; + margin-bottom: 20px; + padding: 20px; + display: none; +} + +.objectControl { + display: block; +} + +.expanded { +} + +.collapsed { + & .objectLabel { + display: block; + } + & .objectControl { + display: none; + } +} diff --git a/src/components/Widgets/ListControl.js b/src/components/Widgets/ListControl.js index 7d58de07..50a9ec35 100644 --- a/src/components/Widgets/ListControl.js +++ b/src/components/Widgets/ListControl.js @@ -1,17 +1,104 @@ import React, { Component, PropTypes } from 'react'; +import { List, Map } from 'immutable'; +import ObjectControl from './ObjectControl'; +import styles from './ListControl.css'; export default class ListControl extends Component { - static propTypes = { onChange: PropTypes.func.isRequired, value: PropTypes.node, }; + + constructor(props) { + super(props); + this.state = {itemStates: Map()}; + } + handleChange = (e) => { this.props.onChange(e.target.value.split(',').map(item => item.trim())); }; - render() { + handleAdd = (e) => { + e.preventDefault(); + const { value, onChange } = this.props; + + onChange((value || List()).push(Map())); + }; + + handleChangeFor(index) { + return (newValue) => { + const { value, onChange } = this.props; + onChange(value.set(index, newValue)); + }; + } + + handleRemove(index) { + return (e) => { + e.preventDefault(); + const { value, onChange } = this.props; + onChange(value.remove(index)); + }; + } + + handleToggle(index) { + return (e) => { + e.preventDefault(); + const { itemStates } = this.state; + this.setState({ + itemStates: itemStates.setIn([index, 'collapsed'], !itemStates.getIn([index, 'collapsed'])), + }); + }; + } + + objectLabel(item) { + const { field } = this.props; + const fields = field.get('fields'); + const first = fields.first(); + const value = item.get(first.get('name')); + return value || `No ${first.get('name')}`; + } + + renderItem(item, index) { + const { field, getMedia, onAddMedia, onRemoveMedia } = this.props; + const { itemStates } = this.state; + const collapsed = itemStates.getIn([index, 'collapsed']); + const classNames = [styles.item, collapsed ? styles.collapsed : styles.expanded]; + + return (
+
{this.objectLabel(item)}
+
+ +
+ + +
); + } + + renderListControl() { const { value } = this.props; + return (
+ {value && value.map((item, index) => this.renderItem(item, index))} +
+
); + } + + render() { + const { value, field } = this.props; + console.log('field: %o', field.toJS()); + + if (field.get('fields')) { + return this.renderListControl(); + } + return ; } } diff --git a/src/valueObjects/Collection.js b/src/valueObjects/Collection.js index a8b5c220..d4daaa64 100644 --- a/src/valueObjects/Collection.js +++ b/src/valueObjects/Collection.js @@ -39,7 +39,7 @@ class FolderCollection { } templateName() { - return this.props.collection.get('name'); + return this.collection.get('name'); } } From c23b2fb5310d8fc24cbd84b3bb698e9237af4dbc Mon Sep 17 00:00:00 2001 From: Mathias Biilmann Christensen Date: Sun, 30 Oct 2016 16:01:10 -0700 Subject: [PATCH 16/18] Make list widget sortable --- example/index.html | 2 +- package.json | 3 +- src/components/Widgets/ListControl.css | 4 ++ src/components/Widgets/ListControl.js | 74 +++++++++++++++++-------- src/components/Widgets/ListPreview.js | 34 ++++++++++-- src/components/Widgets/ObjectControl.js | 2 +- src/formats/formats.js | 1 - 7 files changed, 87 insertions(+), 33 deletions(-) diff --git a/example/index.html b/example/index.html index 86e3541c..ffd26667 100644 --- a/example/index.html +++ b/example/index.html @@ -33,7 +33,7 @@ content: '{"site_title": "CMS Demo"}' }, "authors.yml": { - content: 'authors:\n - name: Mathias\n description: Co-founder @ Netlify\n' + content: 'authors:\n - name: Mathias\n description: Co-founder @ Netlify\n - name: Chris\n description: Co-founder @ Netlify\n' } } } diff --git a/package.json b/package.json index 9f123f68..4da9aac2 100644 --- a/package.json +++ b/package.json @@ -91,7 +91,6 @@ "@kadira/storybook": "^1.36.0", "autoprefixer": "^6.3.3", "bricks.js": "^1.7.0", - "textarea-caret-position": "^0.1.1", "dateformat": "^1.0.12", "fuzzy": "^0.1.1", "immutability-helper": "^2.0.0", @@ -120,6 +119,7 @@ "react-router": "^2.5.1", "react-router-redux": "^4.0.5", "react-simple-dnd": "^0.1.2", + "react-sortable": "^1.2.0", "react-toolbox": "^1.2.1", "react-topbar-progress-indicator": "^1.0.0", "react-waypoint": "^3.1.3", @@ -131,6 +131,7 @@ "semaphore": "^1.0.5", "slate": "^0.14.14", "slate-drop-or-paste-images": "^0.2.0", + "textarea-caret-position": "^0.1.1", "uuid": "^2.0.3", "whatwg-fetch": "^1.0.0" }, diff --git a/src/components/Widgets/ListControl.css b/src/components/Widgets/ListControl.css index fedcd1dc..8fab091e 100644 --- a/src/components/Widgets/ListControl.css +++ b/src/components/Widgets/ListControl.css @@ -1,3 +1,7 @@ +:global(.list-item-dragging) { + opacity: 0.5; +} + .addButton { display: block; cursor: pointer; diff --git a/src/components/Widgets/ListControl.js b/src/components/Widgets/ListControl.js index 50a9ec35..62c634b5 100644 --- a/src/components/Widgets/ListControl.js +++ b/src/components/Widgets/ListControl.js @@ -1,17 +1,30 @@ import React, { Component, PropTypes } from 'react'; -import { List, Map } from 'immutable'; +import { List, Map, fromJS } from 'immutable'; +import { sortable } from 'react-sortable'; import ObjectControl from './ObjectControl'; import styles from './ListControl.css'; +function ListItem(props) { + return
{props.children}
; +} +ListItem.propTypes = { + className: PropTypes.string, + children: PropTypes.node, +}; +ListItem.displayName = 'list-item'; + +const SortableListItem = sortable(ListItem); + export default class ListControl extends Component { static propTypes = { onChange: PropTypes.func.isRequired, value: PropTypes.node, + field: PropTypes.node, }; constructor(props) { super(props); - this.state = {itemStates: Map()}; + this.state = { itemStates: Map() }; } handleChange = (e) => { @@ -55,36 +68,52 @@ export default class ListControl extends Component { const fields = field.get('fields'); const first = fields.first(); const value = item.get(first.get('name')); - return value || `No ${first.get('name')}`; + return value || `No ${ first.get('name') }`; } + handleSort = (obj) => { + this.setState({ draggingIndex: obj.draggingIndex }); + if ('items' in obj) { + this.props.onChange(fromJS(obj.items)); + } + }; + renderItem(item, index) { - const { field, getMedia, onAddMedia, onRemoveMedia } = this.props; - const { itemStates } = this.state; + const { value, field, getMedia, onAddMedia, onRemoveMedia } = this.props; + const { itemStates, draggedItem } = this.state; const collapsed = itemStates.getIn([index, 'collapsed']); const classNames = [styles.item, collapsed ? styles.collapsed : styles.expanded]; - return (
-
{this.objectLabel(item)}
-
- + return ( +
+
{this.objectLabel(item)}
+
+ +
+ +
- - -
); + ); } renderListControl() { - const { value } = this.props; + const { value, field } = this.props; return (
{value && value.map((item, index) => this.renderItem(item, index))}
@@ -93,7 +122,6 @@ export default class ListControl extends Component { render() { const { value, field } = this.props; - console.log('field: %o', field.toJS()); if (field.get('fields')) { return this.renderListControl(); diff --git a/src/components/Widgets/ListPreview.js b/src/components/Widgets/ListPreview.js index 1b68a1ec..ed9542ca 100644 --- a/src/components/Widgets/ListPreview.js +++ b/src/components/Widgets/ListPreview.js @@ -1,11 +1,33 @@ -import React, { PropTypes } from 'react'; +import React, { PropTypes, Component } from 'react'; +import { resolveWidget } from '../Widgets'; -export default function ListPreview({ value }) { - return (
    - { value && value.map(item =>
  • {item}
  • ) } -
); +export default class ObjectPreview extends Component { + widgetFor = (field, value) => { + const { getMedia } = this.props; + const widget = resolveWidget(field.get('widget')); + return (
{React.createElement(widget.preview, { + key: field.get('name'), + value: value && value.get(field.get('name')), + field, + getMedia, + })}
); + }; + + render() { + const { field, value } = this.props; + const fields = field && field.get('fields'); + if (fields) { + return value ? (
{value.map((val, index) =>
+ {fields && fields.map(f => this.widgetFor(f, val))} +
)}
) : null; + } + + return value ? value.join(', ') : null; + } } -ListPreview.propTypes = { +ObjectPreview.propTypes = { value: PropTypes.node, + field: PropTypes.node, + getMedia: PropTypes.func.isRequired, }; diff --git a/src/components/Widgets/ObjectControl.js b/src/components/Widgets/ObjectControl.js index 953fc913..4c7ed706 100644 --- a/src/components/Widgets/ObjectControl.js +++ b/src/components/Widgets/ObjectControl.js @@ -18,7 +18,7 @@ export default class ObjectControl extends Component { const widget = resolveWidget(field.get('widget') || 'string'); const fieldValue = value && value.get(field.get('name')); - return (
+ return (
{ diff --git a/src/formats/formats.js b/src/formats/formats.js index 2b7d9648..48fa5367 100644 --- a/src/formats/formats.js +++ b/src/formats/formats.js @@ -33,7 +33,6 @@ export function resolveFormat(collectionOrEntity, entry) { if (typeof collectionOrEntity === 'string') { return formatByType(collectionOrEntity); } - console.log('entry: %o', entry); const path = entry && entry.path; if (path) { return formatByExtension(path.split('.').pop()); From aca88ef441a57a429aa03182bb976486e883b0ee Mon Sep 17 00:00:00 2001 From: Mathias Biilmann Christensen Date: Sun, 30 Oct 2016 23:38:12 -0700 Subject: [PATCH 17/18] Make editor plugins work in preview --- src/components/MarkupItReactRenderer/index.js | 79 +++++++++++-------- .../RawEditor/BlockMenu.css | 1 + .../RawEditor/index.js | 26 +++--- 3 files changed, 61 insertions(+), 45 deletions(-) diff --git a/src/components/MarkupItReactRenderer/index.js b/src/components/MarkupItReactRenderer/index.js index f5ede88b..06ca60ee 100644 --- a/src/components/MarkupItReactRenderer/index.js +++ b/src/components/MarkupItReactRenderer/index.js @@ -1,6 +1,7 @@ import React, { PropTypes } from 'react'; import MarkupIt, { Syntax, BLOCKS, STYLES, ENTITIES } from 'markup-it'; import { omit } from 'lodash'; +import registry from '../../lib/registry'; const defaultSchema = { [BLOCKS.DOCUMENT]: 'article', @@ -44,43 +45,16 @@ function sanitizeProps(props) { return omit(props, notAllowedAttributes); } -function renderToken(schema, token, index = 0, key = '0') { - const type = token.get('type'); - const data = token.get('data'); - const text = token.get('text'); - const tokens = token.get('tokens'); - const nodeType = schema[type]; - key = `${ key }.${ index }`; - - // Only render if type is registered as renderer - if (typeof nodeType !== 'undefined') { - let children = null; - if (tokens.size) { - children = tokens.map((token, idx) => renderToken(schema, token, idx, key)); - } else if (type === 'text') { - children = text; - } - if (nodeType !== null) { - let props = { key, token }; - if (typeof nodeType !== 'function') { - props = { key, ...sanitizeProps(data.toJS()) }; - } - // If this is a react element - return React.createElement(nodeType, props, children); - } else { - // If this is a text node - return children; - } - } - return null; -} - export default class MarkupItReactRenderer extends React.Component { constructor(props) { super(props); const { syntax } = props; this.parser = new MarkupIt(syntax); + this.plugins = {}; + registry.getEditorComponents().forEach((component) => { + this.plugins[component.get('id')] = component; + }); } componentWillReceiveProps(nextProps) { @@ -89,10 +63,51 @@ export default class MarkupItReactRenderer extends React.Component { } } + renderToken(schema, token, index = 0, key = '0') { + const type = token.get('type'); + const data = token.get('data'); + const text = token.get('text'); + const tokens = token.get('tokens'); + const nodeType = schema[type]; + key = `${ key }.${ index }`; + + // Only render if type is registered as renderer + if (typeof nodeType !== 'undefined') { + let children = null; + if (tokens.size) { + children = tokens.map((token, idx) => this.renderToken(schema, token, idx, key)); + } else if (type === 'text') { + children = text; + } + if (nodeType !== null) { + let props = { key, token }; + if (typeof nodeType !== 'function') { + props = { key, ...sanitizeProps(data.toJS()) }; + } + // If this is a react element + return React.createElement(nodeType, props, children); + } else { + // If this is a text node + return children; + } + } + + const plugin = this.plugins[token.get('type')]; + if (plugin) { + const output = plugin.toPreview(token.get('data').toJS()); + return output instanceof React.Component ? + output : + ; + } + + return null; + } + + render() { const { value, schema } = this.props; const content = this.parser.toContent(value); - return renderToken({ ...defaultSchema, ...schema }, content.get('token')); + return this.renderToken({ ...defaultSchema, ...schema }, content.get('token')); } } diff --git a/src/components/Widgets/MarkdownControlElements/RawEditor/BlockMenu.css b/src/components/Widgets/MarkdownControlElements/RawEditor/BlockMenu.css index 5183a9e7..6f602ca7 100644 --- a/src/components/Widgets/MarkdownControlElements/RawEditor/BlockMenu.css +++ b/src/components/Widgets/MarkdownControlElements/RawEditor/BlockMenu.css @@ -3,6 +3,7 @@ left: -18px; display: none; width: 100%; + z-index: 1000; } .visible { diff --git a/src/components/Widgets/MarkdownControlElements/RawEditor/index.js b/src/components/Widgets/MarkdownControlElements/RawEditor/index.js index 5f68e04a..d8ad8eb3 100644 --- a/src/components/Widgets/MarkdownControlElements/RawEditor/index.js +++ b/src/components/Widgets/MarkdownControlElements/RawEditor/index.js @@ -65,18 +65,17 @@ function getCleanPaste(e) { }); } -const buildtInPlugins = fromJS([{ +const buildtInPlugins = [{ label: 'Image', id: 'image', - fromBlock: (data) => { - const m = data.match(/^!\[([^\]]+)\]\(([^\)]+)\)$/); - return m && { - image: m[2], - alt: m[1], - }; + fromBlock: match => match && { + image: match[2], + alt: match[1], }, toBlock: data => `![${ data.alt }](${ data.image })`, - toPreview: data => `${ data.alt }`, + toPreview: (data) => { + return {data.alt}; + }, pattern: /^!\[([^\]]+)\]\(([^\)]+)\)$/, fields: [{ label: 'Image', @@ -86,14 +85,15 @@ const buildtInPlugins = fromJS([{ label: 'Alt Text', name: 'alt', }], -}]); +}]; +buildtInPlugins.forEach(plugin => registry.registerEditorComponent(plugin)); export default class RawEditor extends React.Component { constructor(props) { super(props); const plugins = registry.getEditorComponents(); this.state = { - plugins: buildtInPlugins.concat(plugins), + plugins: plugins, }; this.shortcuts = { meta: { @@ -161,7 +161,7 @@ export default class RawEditor extends React.Component { } replaceSelection(chars) { - const { value } = this.props; + const value = this.props.value || ''; const selection = this.getSelection(); const newSelection = Object.assign({}, selection); const beforeSelection = value.substr(0, selection.start); @@ -172,7 +172,7 @@ export default class RawEditor extends React.Component { } toggleHeader(header) { - const { value } = this.props; + const value = this.props.value || ''; const selection = this.getSelection(); const newSelection = Object.assign({}, selection); const lastNewline = value.lastIndexOf('\n', selection.start); @@ -234,7 +234,7 @@ export default class RawEditor extends React.Component { }; handleSelection = () => { - const { value } = this.props; + const value = this.props.value || ''; const selection = this.getSelection(); if (selection.start !== selection.end && !HAS_LINE_BREAK.test(selection.selected)) { try { From 67a00e5b65d9785e72ef3fd7710825388dd303b4 Mon Sep 17 00:00:00 2001 From: Mathias Biilmann Christensen Date: Sun, 30 Oct 2016 23:46:34 -0700 Subject: [PATCH 18/18] Fix rendering of plugins returining react components --- src/components/MarkupItReactRenderer/index.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/MarkupItReactRenderer/index.js b/src/components/MarkupItReactRenderer/index.js index 06ca60ee..c87e8c1b 100644 --- a/src/components/MarkupItReactRenderer/index.js +++ b/src/components/MarkupItReactRenderer/index.js @@ -95,9 +95,9 @@ export default class MarkupItReactRenderer extends React.Component { const plugin = this.plugins[token.get('type')]; if (plugin) { const output = plugin.toPreview(token.get('data').toJS()); - return output instanceof React.Component ? - output : - ; + return typeof output === 'string' ? + : + output; } return null;