From d351f10a9be1e6ac403bf43e13d1b9d356b18658 Mon Sep 17 00:00:00 2001 From: Andrey Okonetchnikov Date: Thu, 22 Sep 2016 10:03:02 +0200 Subject: [PATCH 01/57] Fixed formatting of richText.js --- src/components/Widgets/richText.js | 31 +++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/src/components/Widgets/richText.js b/src/components/Widgets/richText.js index 082631e0..e3ce7bd5 100644 --- a/src/components/Widgets/richText.js +++ b/src/components/Widgets/richText.js @@ -15,7 +15,6 @@ import { Icon } from '../UI'; let processedPlugins = List([]); - const nodes = {}; let augmentedMarkdownSyntax = markdownSyntax; let augmentedHTMLSyntax = htmlSyntax; @@ -27,7 +26,7 @@ function processEditorPlugins(plugins) { plugins.forEach(plugin => { const basicRule = MarkupIt.Rule(plugin.id).regExp(plugin.pattern, (state, match) => ( - { data: plugin.fromBlock(match) } + { data: plugin.fromBlock(match) } )); const markdownRule = basicRule.toText((state, token) => ( @@ -68,8 +67,8 @@ function processMediaProxyPlugins(getMedia) { } var imgData = Map({ - alt: match[1], - src: match[2], + alt: match[1], + src: match[2], title: match[3] }).filter(Boolean); @@ -78,9 +77,9 @@ function processMediaProxyPlugins(getMedia) { }; }); const mediaProxyMarkdownRule = mediaProxyRule.toText((state, token) => { - var data = token.getData(); - var alt = data.get('alt', ''); - var src = data.get('src', ''); + var data = token.getData(); + var alt = data.get('alt', ''); + var src = data.get('src', ''); var title = data.get('title', ''); if (title) { @@ -90,9 +89,9 @@ function processMediaProxyPlugins(getMedia) { } }); const mediaProxyHTMLRule = mediaProxyRule.toText((state, token) => { - var data = token.getData(); - var alt = data.get('alt', ''); - var src = data.get('src', ''); + var data = token.getData(); + var alt = data.get('alt', ''); + var src = data.get('src', ''); return `${alt}`; }); @@ -103,7 +102,7 @@ function processMediaProxyPlugins(getMedia) { const className = isFocused ? 'active' : null; const src = node.data.get('src'); return ( - + ); }; augmentedMarkdownSyntax = augmentedMarkdownSyntax.addInlineRules(mediaProxyMarkdownRule); @@ -111,9 +110,11 @@ function processMediaProxyPlugins(getMedia) { } function getPlugins() { - return processedPlugins.map(plugin => ( - { id: plugin.id, icon: plugin.icon, fields: plugin.fields } - )).toArray(); + return processedPlugins.map(plugin => ({ + id: plugin.id, + icon: plugin.icon, + fields: plugin.fields + })).toArray(); } function getNodes() { @@ -124,7 +125,7 @@ function getSyntaxes(getMedia) { if (getMedia) { processMediaProxyPlugins(getMedia); } - return { markdown: augmentedMarkdownSyntax, html:augmentedHTMLSyntax }; + return { markdown: augmentedMarkdownSyntax, html: augmentedHTMLSyntax }; } export { processEditorPlugins, getNodes, getSyntaxes, getPlugins }; From 862b85e4c3acaf3e2be88729e71614190e32b83d Mon Sep 17 00:00:00 2001 From: Andrey Okonetchnikov Date: Thu, 22 Sep 2016 17:20:05 +0200 Subject: [PATCH 02/57] Added support for testing React components --- package.json | 2 ++ wallaby.config.js | 1 + 2 files changed, 3 insertions(+) diff --git a/package.json b/package.json index 21d378f9..cd457fb0 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "autoprefixer": "^6.3.3", "babel-core": "^6.5.1", "babel-eslint": "^6.1.2", + "babel-jest": "^15.0.0", "babel-loader": "^6.2.2", "babel-plugin-lodash": "^3.2.0", "babel-plugin-transform-class-properties": "^6.5.2", @@ -67,6 +68,7 @@ "react-redux": "^4.4.0", "react-router": "^2.5.1", "react-router-redux": "^4.0.5", + "react-test-renderer": "^15.3.2", "redux": "^3.3.1", "redux-thunk": "^1.0.3", "sass-loader": "^4.0.2", diff --git a/wallaby.config.js b/wallaby.config.js index bf73937d..fa4a6e78 100644 --- a/wallaby.config.js +++ b/wallaby.config.js @@ -3,6 +3,7 @@ process.env.BABEL_ENV = 'test'; module.exports = wallaby => ({ files: [ { pattern: 'src/**/*.js' }, + { pattern: 'src/**/*.js.snap' }, { pattern: 'src/**/*.spec.js', ignore: true } ], From 95008d8607af4c9e9f81ec10574b9837167f7754 Mon Sep 17 00:00:00 2001 From: Andrey Okonetchnikov Date: Thu, 22 Sep 2016 17:36:02 +0200 Subject: [PATCH 03/57] WIP on React renderer for any Markit-up syntax --- .../Widgets/MarkitupReactRenderer.js | 69 ++++++++++++++ .../__tests__/MarkitupReactRenderer.spec.js | 70 +++++++++++++++ .../MarkitupReactRenderer.spec.js.snap | 90 +++++++++++++++++++ 3 files changed, 229 insertions(+) create mode 100644 src/components/Widgets/MarkitupReactRenderer.js create mode 100644 src/components/Widgets/__tests__/MarkitupReactRenderer.spec.js create mode 100644 src/components/Widgets/__tests__/__snapshots__/MarkitupReactRenderer.spec.js.snap diff --git a/src/components/Widgets/MarkitupReactRenderer.js b/src/components/Widgets/MarkitupReactRenderer.js new file mode 100644 index 00000000..26137f7d --- /dev/null +++ b/src/components/Widgets/MarkitupReactRenderer.js @@ -0,0 +1,69 @@ +import React, { PropTypes } from 'react'; +import MarkupIt, { Syntax, JSONUtils } from 'markup-it'; + +const defaultRenderers = { + 'doc': 'article', + 'header_one': 'h1', + 'header_two': 'h2', + 'header_three': 'h3', + 'header_four': 'h4', + 'header_five': 'h5', + 'header_six': 'h6', + 'paragraph': 'p', + 'ordered_list': 'ol', + 'unordered_list': 'ul', + 'list_item': 'li', + 'link': 'a', + 'image': 'img', + 'BOLD': 'strong', + 'ITALIC': 'em', + 'text': null, + 'unstyled': null, +}; + +export default class MarkitupReactRenderer extends React.Component { + + renderToken = (token) => { + const { type, data, text, tokens } = token; + const element = defaultRenderers[type]; + + // Only render if type is registered as renderer + if (typeof element !== 'undefined') { + let children = null; + if (Array.isArray(tokens) && tokens.length) { + children = tokens.map(this.renderToken); + } else if (type === 'text') { + children = text; + } + if (element !== null) { + // If this is a react element + return React.createElement(element, data, children); + } else { + // If this is a text node + return children; + } + } + return null; + } + + render() { + const { value, syntax } = this.props; + + if (typeof this.parser === 'undefined') { + this.parser = new MarkupIt(syntax); + } + + const content = this.parser.toContent(value); + const json = JSONUtils.encode(content); + // console.log(JSON.stringify(json, null, 2)); + + return ( +
{this.renderToken(json.token)}
+ ); + } +} + +MarkitupReactRenderer.propTypes = { + value: PropTypes.string, + syntax: PropTypes.instanceOf(Syntax).isRequired +}; diff --git a/src/components/Widgets/__tests__/MarkitupReactRenderer.spec.js b/src/components/Widgets/__tests__/MarkitupReactRenderer.spec.js new file mode 100644 index 00000000..65ede560 --- /dev/null +++ b/src/components/Widgets/__tests__/MarkitupReactRenderer.spec.js @@ -0,0 +1,70 @@ +import React from 'react'; +import renderer from 'react-test-renderer'; +import markdownSyntax from 'markup-it/syntaxes/markdown'; +import htmlSyntax from 'markup-it/syntaxes/html'; +import MarkitupReactRenderer from '../MarkitupReactRenderer'; + +describe('MarkitupReactRenderer', () => { + it('should render markdown', () => { + const value = ` +# H1 + +Text with **bold** & _em_ elements + +## H2 + +* ul item 1 +* ul item 2 + +### H3 + +1. ol item 1 +1. ol item 2 +1. ol item 3 + +#### H4 + +[link title](http://google.com) + +##### H5 + +![alt text](https://pbs.twimg.com/profile_images/678903331176214528/TQTdqGwD.jpg) + +###### H6 + +`; + const component = renderer.create( + + ); + const tree = component.toJSON(); + expect(tree).toMatchSnapshot(); + }); + + it('should support custom syntax', () => { + const value = ` +`; + const component = renderer.create( + + ); + const tree = component.toJSON(); + expect(tree).toMatchSnapshot(); + }); + + it('should render HTML', () => { + const value = '

Paragraph with inline element

'; + const component = renderer.create( + + ); + const tree = component.toJSON(); + expect(tree).toMatchSnapshot(); + }); +}); diff --git a/src/components/Widgets/__tests__/__snapshots__/MarkitupReactRenderer.spec.js.snap b/src/components/Widgets/__tests__/__snapshots__/MarkitupReactRenderer.spec.js.snap new file mode 100644 index 00000000..2f959a8f --- /dev/null +++ b/src/components/Widgets/__tests__/__snapshots__/MarkitupReactRenderer.spec.js.snap @@ -0,0 +1,90 @@ +exports[`MarkitupReactRenderer should render HTML 1`] = ` +
+
+

+ Paragraph with + + inline + + element +

+
+
+`; + +exports[`MarkitupReactRenderer should render markdown 1`] = ` +
+
+

+ H1 +

+

+ Text with + + bold + + & + + em + + elements +

+

+ H2 +

+
    +
  • + ul item 1 +
  • +
  • + ul item 2 +
  • +
+

+ H3 +

+
    +
  1. + ol item 1 +
  2. +
  3. + ol item 2 +
  4. +
  5. + ol item 3 +
  6. +
+

+ H4 +

+

+ + link title + +

+
+ H5 +
+

+ alt text +

+
+ H6 +
+
+
+`; From 78e7a44aaa3280158e69a637977acfc30fa28447 Mon Sep 17 00:00:00 2001 From: Andrey Okonetchnikov Date: Thu, 22 Sep 2016 17:37:06 +0200 Subject: [PATCH 04/57] Added jest env to eslintrc --- .eslintrc | 1 + 1 file changed, 1 insertion(+) diff --git a/.eslintrc b/.eslintrc index 8f6cd62a..1d2c26bc 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,6 +1,7 @@ env: browser: true es6: true + jest: true parser: babel-eslint plugins: [ "react" ] From 2c483220b511d34aa82ce6d9e2e1eaf1d8963964 Mon Sep 17 00:00:00 2001 From: Andrey Okonetchnikov Date: Thu, 22 Sep 2016 17:37:18 +0200 Subject: [PATCH 05/57] Updated lint-staged --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index cd457fb0..f74fb62d 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,7 @@ "imports-loader": "^0.6.5", "jest-cli": "^15.1.1", "js-yaml": "^3.5.3", - "lint-staged": "^3.0.2", + "lint-staged": "^3.0.3", "moment": "^2.11.2", "node-sass": "^3.10.0", "normalizr": "^2.0.0", From 7fe1a6f8b68b6512bad900786c91bb54aa68db04 Mon Sep 17 00:00:00 2001 From: Andrey Okonetchnikov Date: Thu, 22 Sep 2016 17:44:25 +0200 Subject: [PATCH 06/57] Extracted renderToken function from class. Do not render additional `
`. --- .../Widgets/MarkitupReactRenderer.js | 46 +++-- .../__tests__/MarkitupReactRenderer.spec.js | 3 +- .../MarkitupReactRenderer.spec.js.snap | 168 +++++++++--------- 3 files changed, 106 insertions(+), 111 deletions(-) diff --git a/src/components/Widgets/MarkitupReactRenderer.js b/src/components/Widgets/MarkitupReactRenderer.js index 26137f7d..4f291cf2 100644 --- a/src/components/Widgets/MarkitupReactRenderer.js +++ b/src/components/Widgets/MarkitupReactRenderer.js @@ -21,30 +21,30 @@ const defaultRenderers = { 'unstyled': null, }; -export default class MarkitupReactRenderer extends React.Component { +function renderToken(token) { + const { type, data, text, tokens } = token; + const element = defaultRenderers[type]; - renderToken = (token) => { - const { type, data, text, tokens } = token; - const element = defaultRenderers[type]; - - // Only render if type is registered as renderer - if (typeof element !== 'undefined') { - let children = null; - if (Array.isArray(tokens) && tokens.length) { - children = tokens.map(this.renderToken); - } else if (type === 'text') { - children = text; - } - if (element !== null) { - // If this is a react element - return React.createElement(element, data, children); - } else { - // If this is a text node - return children; - } + // Only render if type is registered as renderer + if (typeof element !== 'undefined') { + let children = null; + if (Array.isArray(tokens) && tokens.length) { + children = tokens.map(renderToken); + } else if (type === 'text') { + children = text; + } + if (element !== null) { + // If this is a react element + return React.createElement(element, data, children); + } else { + // If this is a text node + return children; } - return null; } + return null; +} + +export default class MarkitupReactRenderer extends React.Component { render() { const { value, syntax } = this.props; @@ -57,9 +57,7 @@ export default class MarkitupReactRenderer extends React.Component { const json = JSONUtils.encode(content); // console.log(JSON.stringify(json, null, 2)); - return ( -
{this.renderToken(json.token)}
- ); + return renderToken(json.token); } } diff --git a/src/components/Widgets/__tests__/MarkitupReactRenderer.spec.js b/src/components/Widgets/__tests__/MarkitupReactRenderer.spec.js index 65ede560..c8b0e396 100644 --- a/src/components/Widgets/__tests__/MarkitupReactRenderer.spec.js +++ b/src/components/Widgets/__tests__/MarkitupReactRenderer.spec.js @@ -44,8 +44,7 @@ Text with **bold** & _em_ elements }); it('should support custom syntax', () => { - const value = ` -`; + const value = ''; const component = renderer.create( -
-

- Paragraph with - - inline - - element -

-
-
+
+

+ Paragraph with + + inline + + element +

+
`; exports[`MarkitupReactRenderer should render markdown 1`] = ` -
-
-

- H1 -

-

- Text with - - bold - - & - - em - - elements -

-

- H2 -

-
    -
  • - ul item 1 -
  • -
  • - ul item 2 -
  • -
-

- H3 -

-
    -
  1. - ol item 1 -
  2. -
  3. - ol item 2 -
  4. -
  5. - ol item 3 -
  6. -
-

- H4 -

-

- - link title - -

-
- H5 -
-

- alt text -

-
- H6 -
-
-
+
+

+ H1 +

+

+ Text with + + bold + + & + + em + + elements +

+

+ H2 +

+
    +
  • + ul item 1 +
  • +
  • + ul item 2 +
  • +
+

+ H3 +

+
    +
  1. + ol item 1 +
  2. +
  3. + ol item 2 +
  4. +
  5. + ol item 3 +
  6. +
+

+ H4 +

+

+ + link title + +

+
+ H5 +
+

+ alt text +

+
+ H6 +
+
`; + +exports[`MarkitupReactRenderer should support custom syntax 1`] = `
`; From c243a62a32e5a4406e420e63433c30ea666a1c24 Mon Sep 17 00:00:00 2001 From: Andrey Okonetchnikov Date: Thu, 22 Sep 2016 18:23:44 +0200 Subject: [PATCH 07/57] Generate keys for arrays of elements to remove React warnings. Pass only a single child if possible. --- src/components/Widgets/MarkitupReactRenderer.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/components/Widgets/MarkitupReactRenderer.js b/src/components/Widgets/MarkitupReactRenderer.js index 4f291cf2..d8f226a7 100644 --- a/src/components/Widgets/MarkitupReactRenderer.js +++ b/src/components/Widgets/MarkitupReactRenderer.js @@ -21,21 +21,26 @@ const defaultRenderers = { 'unstyled': null, }; -function renderToken(token) { +function renderToken(token, index = 0, key = '0') { const { type, data, text, tokens } = token; const element = defaultRenderers[type]; + key = `${key}.${index}`; // Only render if type is registered as renderer if (typeof element !== 'undefined') { let children = null; if (Array.isArray(tokens) && tokens.length) { - children = tokens.map(renderToken); + children = tokens.map((token, idx) => renderToken(token, idx, key)); } else if (type === 'text') { children = text; } if (element !== null) { // If this is a react element - return React.createElement(element, data, children); + return React.createElement( + element, + { key, ...data }, // Add key as a prop + Array.isArray(children) && children.length === 1 + ? children[0] : children); // Pass single child if possible } else { // If this is a text node return children; From c928fbccaf680e8312eddb374e30424cbf202e4a Mon Sep 17 00:00:00 2001 From: Andrey Okonetchnikov Date: Thu, 22 Sep 2016 21:52:43 +0200 Subject: [PATCH 08/57] Switched to enzyme --- package.json | 3 +- .../__tests__/MarkitupReactRenderer.spec.js | 65 +++++++++++-- .../MarkitupReactRenderer.spec.js.snap | 91 +------------------ 3 files changed, 64 insertions(+), 95 deletions(-) diff --git a/package.json b/package.json index f74fb62d..0ededa44 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "babel-preset-react": "^6.5.0", "babel-runtime": "^6.5.0", "css-loader": "^0.23.1", + "enzyme": "^2.4.1", "eslint": "^3.5.0", "eslint-plugin-react": "^5.1.1", "expect": "^1.20.2", @@ -60,6 +61,7 @@ "postcss-loader": "^0.9.1", "pre-commit": "^1.1.3", "react": "^15.1.0", + "react-addons-test-utils": "^15.3.2", "react-dom": "^15.1.0", "react-hot-loader": "^3.0.0-beta.2", "react-immutable-proptypes": "^1.6.0", @@ -68,7 +70,6 @@ "react-redux": "^4.4.0", "react-router": "^2.5.1", "react-router-redux": "^4.0.5", - "react-test-renderer": "^15.3.2", "redux": "^3.3.1", "redux-thunk": "^1.0.3", "sass-loader": "^4.0.2", diff --git a/src/components/Widgets/__tests__/MarkitupReactRenderer.spec.js b/src/components/Widgets/__tests__/MarkitupReactRenderer.spec.js index c8b0e396..4e9858d6 100644 --- a/src/components/Widgets/__tests__/MarkitupReactRenderer.spec.js +++ b/src/components/Widgets/__tests__/MarkitupReactRenderer.spec.js @@ -1,10 +1,41 @@ import React from 'react'; -import renderer from 'react-test-renderer'; +import { shallow } from 'enzyme'; import markdownSyntax from 'markup-it/syntaxes/markdown'; import htmlSyntax from 'markup-it/syntaxes/html'; import MarkitupReactRenderer from '../MarkitupReactRenderer'; describe('MarkitupReactRenderer', () => { + it('should re-render properly after a value and syntax update', () => { + const component = shallow( + + ); + const tree1 = component.html(); + component.setProps({ + value: '

Title

', + syntax: htmlSyntax + }); + const tree2 = component.html(); + expect(tree1).toEqual(tree2); + }); + + it('should not update the parser if syntax didn\'t change', () => { + const component = shallow( + + ); + const syntax1 = component.instance().props.syntax; + component.setProps({ + value: '## Title', + }); + const syntax2 = component.instance().props.syntax; + expect(syntax1).toEqual(syntax2); + }); + it('should render markdown', () => { const value = ` # H1 @@ -31,39 +62,57 @@ Text with **bold** & _em_ elements ![alt text](https://pbs.twimg.com/profile_images/678903331176214528/TQTdqGwD.jpg) ###### H6 - `; - const component = renderer.create( + const component = shallow( ); - const tree = component.toJSON(); + const tree = component.html(); + expect(tree).toMatchSnapshot(); + }); + + it('should render HTML as is using Markdown', () => { + const value = ` +# Title + +
+
Test HTML content
+
Testing HTML in Markdown
+
+`; + const component = shallow( + + ); + const tree = component.html(); expect(tree).toMatchSnapshot(); }); it('should support custom syntax', () => { const value = ''; - const component = renderer.create( + const component = shallow( ); - const tree = component.toJSON(); + const tree = component.html(); expect(tree).toMatchSnapshot(); }); it('should render HTML', () => { const value = '

Paragraph with inline element

'; - const component = renderer.create( + const component = shallow( ); - const tree = component.toJSON(); + const tree = component.html(); expect(tree).toMatchSnapshot(); }); }); diff --git a/src/components/Widgets/__tests__/__snapshots__/MarkitupReactRenderer.spec.js.snap b/src/components/Widgets/__tests__/__snapshots__/MarkitupReactRenderer.spec.js.snap index 5b582db8..e3854e2c 100644 --- a/src/components/Widgets/__tests__/__snapshots__/MarkitupReactRenderer.spec.js.snap +++ b/src/components/Widgets/__tests__/__snapshots__/MarkitupReactRenderer.spec.js.snap @@ -1,88 +1,7 @@ -exports[`MarkitupReactRenderer should render HTML 1`] = ` -
-

- Paragraph with - - inline - - element -

-
-`; +exports[`MarkitupReactRenderer should render HTML 1`] = `"

Paragraph with inline element

"`; -exports[`MarkitupReactRenderer should render markdown 1`] = ` -
-

- H1 -

-

- Text with - - bold - - & - - em - - elements -

-

- H2 -

-
    -
  • - ul item 1 -
  • -
  • - ul item 2 -
  • -
-

- H3 -

-
    -
  1. - ol item 1 -
  2. -
  3. - ol item 2 -
  4. -
  5. - ol item 3 -
  6. -
-

- H4 -

-

- - link title - -

-
- H5 -
-

- alt text -

-
- H6 -
-
-`; +exports[`MarkitupReactRenderer should render HTML as is using Markdown 1`] = `"

Title

"`; -exports[`MarkitupReactRenderer should support custom syntax 1`] = `
`; +exports[`MarkitupReactRenderer should render markdown 1`] = `"

H1

Text with bold & em elements

H2

  • ul item 1
  • ul item 2

H3

  1. ol item 1
  2. ol item 2
  3. ol item 3

H4

link title

H5

\"alt

H6
"`; + +exports[`MarkitupReactRenderer should support custom syntax 1`] = `"
"`; From 04c9780ee10a6f27778586a3ae0eeb98e3210af2 Mon Sep 17 00:00:00 2001 From: Andrey Okonetchnikov Date: Thu, 22 Sep 2016 21:53:11 +0200 Subject: [PATCH 09/57] Better implementation --- src/components/Widgets/MarkitupReactRenderer.js | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/components/Widgets/MarkitupReactRenderer.js b/src/components/Widgets/MarkitupReactRenderer.js index d8f226a7..40459162 100644 --- a/src/components/Widgets/MarkitupReactRenderer.js +++ b/src/components/Widgets/MarkitupReactRenderer.js @@ -51,13 +51,20 @@ function renderToken(token, index = 0, key = '0') { export default class MarkitupReactRenderer extends React.Component { - render() { - const { value, syntax } = this.props; + constructor(props) { + super(props); + const { syntax } = props; + this.parser = new MarkupIt(syntax); + } - if (typeof this.parser === 'undefined') { - this.parser = new MarkupIt(syntax); + componentWillReceiveProps(nextProps) { + if (nextProps.syntax != this.props.syntax) { + this.parser = new MarkupIt(nextProps.syntax); } + } + render() { + const { value } = this.props; const content = this.parser.toContent(value); const json = JSONUtils.encode(content); // console.log(JSON.stringify(json, null, 2)); From 9392fdbe30a29419e6c4a77f3dcfa64b8c0473bd Mon Sep 17 00:00:00 2001 From: Andrey Okonetchnikov Date: Thu, 22 Sep 2016 21:53:57 +0200 Subject: [PATCH 10/57] Integrated MarkitupReactRenderer with Preview pane --- src/components/Widgets/MarkdownPreview.js | 35 ++++++++++------------- 1 file changed, 15 insertions(+), 20 deletions(-) diff --git a/src/components/Widgets/MarkdownPreview.js b/src/components/Widgets/MarkdownPreview.js index f9f0021c..d8ababad 100644 --- a/src/components/Widgets/MarkdownPreview.js +++ b/src/components/Widgets/MarkdownPreview.js @@ -1,28 +1,23 @@ import React, { PropTypes } from 'react'; -import MarkupIt from 'markup-it'; import { getSyntaxes } from './richText'; +import MarkitupReactRenderer from './MarkitupReactRenderer'; -export default class MarkdownPreview extends React.Component { - - constructor(props) { - super(props); - - const { markdown, html } = getSyntaxes(); - this.markdown = new MarkupIt(markdown); - this.html = new MarkupIt(html); +const MarkdownPreview = ({ value }) => { + if (value == null) { + return null; } - render() { - const { value } = this.props; - if (value == null) { return null; } - const content = this.markdown.toContent(value); - const contentHtml = { __html: this.html.toText(content) }; - return ( -
- ); - } -} + const { markdown } = getSyntaxes(); + return ( + + ); +}; MarkdownPreview.propTypes = { - value: PropTypes.node, + value: PropTypes.string, }; + +export default MarkdownPreview; From 57688af42e3fb42fb5239bb91e5ad8f05c169c68 Mon Sep 17 00:00:00 2001 From: Andrey Okonetchnikov Date: Thu, 22 Sep 2016 22:34:43 +0200 Subject: [PATCH 11/57] Support more elements. Import dicitonaries from markup-it. Added more tests. --- .../Widgets/MarkitupReactRenderer.js | 58 +++-- .../__tests__/MarkitupReactRenderer.spec.js | 202 ++++++++++++------ 2 files changed, 169 insertions(+), 91 deletions(-) diff --git a/src/components/Widgets/MarkitupReactRenderer.js b/src/components/Widgets/MarkitupReactRenderer.js index 40459162..a923fb65 100644 --- a/src/components/Widgets/MarkitupReactRenderer.js +++ b/src/components/Widgets/MarkitupReactRenderer.js @@ -1,43 +1,57 @@ import React, { PropTypes } from 'react'; -import MarkupIt, { Syntax, JSONUtils } from 'markup-it'; +import MarkupIt, { Syntax, JSONUtils, BLOCKS, STYLES, ENTITIES } from 'markup-it'; const defaultRenderers = { - 'doc': 'article', - 'header_one': 'h1', - 'header_two': 'h2', - 'header_three': 'h3', - 'header_four': 'h4', - 'header_five': 'h5', - 'header_six': 'h6', - 'paragraph': 'p', - 'ordered_list': 'ol', - 'unordered_list': 'ul', - 'list_item': 'li', - 'link': 'a', - 'image': 'img', - 'BOLD': 'strong', - 'ITALIC': 'em', - 'text': null, - 'unstyled': null, + [BLOCKS.DOCUMENT]: 'article', + [BLOCKS.TEXT]: null, + [BLOCKS.CODE]: 'code', + [BLOCKS.BLOCKQUOTE]: 'blockquote', + [BLOCKS.PARAGRAPH]: 'p', + [BLOCKS.FOOTNOTE]: 'footnote', + [BLOCKS.HTML]: (props) => null, + [BLOCKS.HR]: 'hr', + [BLOCKS.HEADING_1]: 'h1', + [BLOCKS.HEADING_2]: 'h2', + [BLOCKS.HEADING_3]: 'h3', + [BLOCKS.HEADING_4]: 'h4', + [BLOCKS.HEADING_5]: 'h5', + [BLOCKS.HEADING_6]: 'h6', + [BLOCKS.TABLE]: 'table', + [BLOCKS.TABLE_ROW]: 'tr', + [BLOCKS.TABLE_CELL]: 'td', + [BLOCKS.OL_LIST]: 'ol', + [BLOCKS.UL_LIST]: 'ul', + [BLOCKS.LIST_ITEM]: 'li', + + [STYLES.TEXT]: null, + [STYLES.BOLD]: 'strong', + [STYLES.ITALIC]: 'em', + [STYLES.CODE]: 'code', + [STYLES.STRIKETHROUGH]: 'del', + + [ENTITIES.LINK]: 'a', + [ENTITIES.IMAGE]: 'img', + [ENTITIES.FOOTNOTE_REF]: 'sup', + [ENTITIES.HARD_BREAK]: 'br' }; function renderToken(token, index = 0, key = '0') { const { type, data, text, tokens } = token; - const element = defaultRenderers[type]; + const nodeType = defaultRenderers[type]; key = `${key}.${index}`; // Only render if type is registered as renderer - if (typeof element !== 'undefined') { + if (typeof nodeType !== 'undefined') { let children = null; if (Array.isArray(tokens) && tokens.length) { children = tokens.map((token, idx) => renderToken(token, idx, key)); } else if (type === 'text') { children = text; } - if (element !== null) { + if (nodeType !== null) { // If this is a react element return React.createElement( - element, + nodeType, { key, ...data }, // Add key as a prop Array.isArray(children) && children.length === 1 ? children[0] : children); // Pass single child if possible diff --git a/src/components/Widgets/__tests__/MarkitupReactRenderer.spec.js b/src/components/Widgets/__tests__/MarkitupReactRenderer.spec.js index 4e9858d6..d2094e06 100644 --- a/src/components/Widgets/__tests__/MarkitupReactRenderer.spec.js +++ b/src/components/Widgets/__tests__/MarkitupReactRenderer.spec.js @@ -1,3 +1,5 @@ +/* eslint max-len:0 */ + import React from 'react'; import { shallow } from 'enzyme'; import markdownSyntax from 'markup-it/syntaxes/markdown'; @@ -5,39 +7,44 @@ import htmlSyntax from 'markup-it/syntaxes/html'; import MarkitupReactRenderer from '../MarkitupReactRenderer'; describe('MarkitupReactRenderer', () => { - it('should re-render properly after a value and syntax update', () => { - const component = shallow( - - ); - const tree1 = component.html(); - component.setProps({ - value: '

Title

', - syntax: htmlSyntax + + describe('basics', () => { + it('should re-render properly after a value and syntax update', () => { + const component = shallow( + + ); + const tree1 = component.html(); + component.setProps({ + value: '

Title

', + syntax: htmlSyntax + }); + const tree2 = component.html(); + expect(tree1).toEqual(tree2); + }); + + it('should not update the parser if syntax didn\'t change', () => { + const component = shallow( + + ); + const syntax1 = component.instance().props.syntax; + component.setProps({ + value: '## Title', + }); + const syntax2 = component.instance().props.syntax; + expect(syntax1).toEqual(syntax2); }); - const tree2 = component.html(); - expect(tree1).toEqual(tree2); }); - it('should not update the parser if syntax didn\'t change', () => { - const component = shallow( - - ); - const syntax1 = component.instance().props.syntax; - component.setProps({ - value: '## Title', - }); - const syntax2 = component.instance().props.syntax; - expect(syntax1).toEqual(syntax2); - }); - - it('should render markdown', () => { - const value = ` + describe('Markdown rendering', () => { + describe('General', () => { + it('should render markdown', () => { + const value = ` # H1 Text with **bold** & _em_ elements @@ -63,18 +70,69 @@ Text with **bold** & _em_ elements ###### H6 `; - const component = shallow( - - ); - const tree = component.html(); - expect(tree).toMatchSnapshot(); - }); + const component = shallow( + + ); + const tree = component.html(); + expect(tree).toMatchSnapshot(); + }); + }); - it('should render HTML as is using Markdown', () => { - const value = ` + describe('Links', () => { + it('should render links', () => { + const value = ` +I get 10 times more traffic from [Google] [1] than from [Yahoo] [2] or [MSN] [3]. + + [1]: http://google.com/ "Google" + [2]: http://search.yahoo.com/ "Yahoo Search" + [3]: http://search.msn.com/ "MSN Search" +`; + const component = shallow( + + ); + const tree = component.html(); + const expected = ''; + expect(tree).toEqual(expected); + }); + }); + + describe('Code', () => { + it('should render code', () => { + const value = 'Use the `printf()` function.'; + const component = shallow( + + ); + const tree = component.html(); + const expected = '

Use the printf() function.

'; + expect(tree).toEqual(expected); + }); + + it('should render code 2', () => { + const value = '``There is a literal backtick (`) here.``'; + const component = shallow( + + ); + const tree = component.html(); + const expected = '

There is a literal backtick (`) here.

'; + expect(tree).toEqual(expected); + }); + }); + + describe('HTML', () => { + it('should render HTML as is using Markdown', () => { + const value = ` # Title
@@ -82,37 +140,43 @@ Text with **bold** & _em_ elements
Testing HTML in Markdown
`; - const component = shallow( - - ); - const tree = component.html(); - expect(tree).toMatchSnapshot(); + const component = shallow( + + ); + const tree = component.html(); + expect(tree).toMatchSnapshot(); + }); + }); }); - it('should support custom syntax', () => { - const value = ''; - const component = shallow( - - ); - const tree = component.html(); - expect(tree).toMatchSnapshot(); + describe('custom elements', () => { + it('should support custom syntax', () => { + const value = ''; + const component = shallow( + + ); + const tree = component.html(); + expect(tree).toMatchSnapshot(); + }); }); - it('should render HTML', () => { - const value = '

Paragraph with inline element

'; - const component = shallow( - - ); - const tree = component.html(); - expect(tree).toMatchSnapshot(); + describe('HTML rendering', () => { + it('should render HTML', () => { + const value = '

Paragraph with inline element

'; + const component = shallow( + + ); + const tree = component.html(); + expect(tree).toMatchSnapshot(); + }); }); }); From 1860a2389db022d5c1c9ae81b9865477b4a536a4 Mon Sep 17 00:00:00 2001 From: Andrey Okonetchnikov Date: Fri, 23 Sep 2016 19:56:35 +0200 Subject: [PATCH 12/57] Use Immutable data structure without converting to JSON for speed and profit. Added more tests. --- .../Widgets/MarkitupReactRenderer.js | 20 ++++++++-------- .../__tests__/MarkitupReactRenderer.spec.js | 24 +++++++++++++++++++ .../MarkitupReactRenderer.spec.js.snap | 10 ++++---- 3 files changed, 40 insertions(+), 14 deletions(-) diff --git a/src/components/Widgets/MarkitupReactRenderer.js b/src/components/Widgets/MarkitupReactRenderer.js index a923fb65..f6272f43 100644 --- a/src/components/Widgets/MarkitupReactRenderer.js +++ b/src/components/Widgets/MarkitupReactRenderer.js @@ -1,5 +1,5 @@ import React, { PropTypes } from 'react'; -import MarkupIt, { Syntax, JSONUtils, BLOCKS, STYLES, ENTITIES } from 'markup-it'; +import MarkupIt, { Syntax, BLOCKS, STYLES, ENTITIES } from 'markup-it'; const defaultRenderers = { [BLOCKS.DOCUMENT]: 'article', @@ -36,14 +36,17 @@ const defaultRenderers = { }; function renderToken(token, index = 0, key = '0') { - const { type, data, text, tokens } = token; + const type = token.get('type'); + const data = token.get('data'); + const text = token.get('text'); + const tokens = token.get('tokens'); const nodeType = defaultRenderers[type]; key = `${key}.${index}`; // Only render if type is registered as renderer if (typeof nodeType !== 'undefined') { let children = null; - if (Array.isArray(tokens) && tokens.length) { + if (tokens.size) { children = tokens.map((token, idx) => renderToken(token, idx, key)); } else if (type === 'text') { children = text; @@ -52,9 +55,9 @@ function renderToken(token, index = 0, key = '0') { // If this is a react element return React.createElement( nodeType, - { key, ...data }, // Add key as a prop - Array.isArray(children) && children.length === 1 - ? children[0] : children); // Pass single child if possible + { key, ...data.toJS() }, // Add key as a prop + children + ); } else { // If this is a text node return children; @@ -80,10 +83,7 @@ export default class MarkitupReactRenderer extends React.Component { render() { const { value } = this.props; const content = this.parser.toContent(value); - const json = JSONUtils.encode(content); - // console.log(JSON.stringify(json, null, 2)); - - return renderToken(json.token); + return renderToken(content.get('token')); } } diff --git a/src/components/Widgets/__tests__/MarkitupReactRenderer.spec.js b/src/components/Widgets/__tests__/MarkitupReactRenderer.spec.js index d2094e06..85c31a98 100644 --- a/src/components/Widgets/__tests__/MarkitupReactRenderer.spec.js +++ b/src/components/Widgets/__tests__/MarkitupReactRenderer.spec.js @@ -81,6 +81,30 @@ Text with **bold** & _em_ elements }); }); + describe('Lists', () => { + it('should render lists', () => { + const value = ` +1. ol item 1 +1. ol item 2 + * Sublist 1 + * Sublist 2 + * Sublist 3 + 1. Sub-Sublist 1 + 1. Sub-Sublist 2 + 1. Sub-Sublist 3 +1. ol item 3 +`; + const component = shallow( + + ); + const tree = component.html(); + expect(tree).toMatchSnapshot(); + }); + }); + describe('Links', () => { it('should render links', () => { const value = ` diff --git a/src/components/Widgets/__tests__/__snapshots__/MarkitupReactRenderer.spec.js.snap b/src/components/Widgets/__tests__/__snapshots__/MarkitupReactRenderer.spec.js.snap index e3854e2c..df9033e3 100644 --- a/src/components/Widgets/__tests__/__snapshots__/MarkitupReactRenderer.spec.js.snap +++ b/src/components/Widgets/__tests__/__snapshots__/MarkitupReactRenderer.spec.js.snap @@ -1,7 +1,9 @@ -exports[`MarkitupReactRenderer should render HTML 1`] = `"

Paragraph with inline element

"`; +exports[`MarkitupReactRenderer HTML rendering should render HTML 1`] = `"

Paragraph with inline element

"`; -exports[`MarkitupReactRenderer should render HTML as is using Markdown 1`] = `"

Title

"`; +exports[`MarkitupReactRenderer Markdown rendering General should render markdown 1`] = `"

H1

Text with bold & em elements

H2

  • ul item 1
  • ul item 2

H3

  1. ol item 1
  2. ol item 2
  3. ol item 3

H4

link title

H5

\"alt

H6
"`; -exports[`MarkitupReactRenderer should render markdown 1`] = `"

H1

Text with bold & em elements

H2

  • ul item 1
  • ul item 2

H3

  1. ol item 1
  2. ol item 2
  3. ol item 3

H4

link title

H5

\"alt

H6
"`; +exports[`MarkitupReactRenderer Markdown rendering HTML should render HTML as is using Markdown 1`] = `"

Title

"`; -exports[`MarkitupReactRenderer should support custom syntax 1`] = `"
"`; +exports[`MarkitupReactRenderer Markdown rendering Lists should render lists 1`] = `"
  1. ol item 1
  2. ol item 2
    • Sublist 1
    • Sublist 2
    • Sublist 3
      1. Sub-Sublist 1
      2. Sub-Sublist 2
      3. Sub-Sublist 3
  3. ol item 3
"`; + +exports[`MarkitupReactRenderer custom elements should support custom syntax 1`] = `"
"`; From 20e681e7ec410c7a2e480a0154b2488168c1f3e5 Mon Sep 17 00:00:00 2001 From: Andrey Okonetchnikov Date: Mon, 26 Sep 2016 13:18:22 +0200 Subject: [PATCH 13/57] Added support for rendering inlined HTML in MD documents --- .../Widgets/MarkitupReactRenderer.js | 14 +++++++- .../__tests__/MarkitupReactRenderer.spec.js | 35 +++++++++++++------ .../MarkitupReactRenderer.spec.js.snap | 24 ++++++++++++- 3 files changed, 60 insertions(+), 13 deletions(-) diff --git a/src/components/Widgets/MarkitupReactRenderer.js b/src/components/Widgets/MarkitupReactRenderer.js index f6272f43..2f2e2948 100644 --- a/src/components/Widgets/MarkitupReactRenderer.js +++ b/src/components/Widgets/MarkitupReactRenderer.js @@ -1,5 +1,6 @@ import React, { PropTypes } from 'react'; import MarkupIt, { Syntax, BLOCKS, STYLES, ENTITIES } from 'markup-it'; +import htmlSyntax from 'markup-it/syntaxes/html'; const defaultRenderers = { [BLOCKS.DOCUMENT]: 'article', @@ -8,7 +9,12 @@ const defaultRenderers = { [BLOCKS.BLOCKQUOTE]: 'blockquote', [BLOCKS.PARAGRAPH]: 'p', [BLOCKS.FOOTNOTE]: 'footnote', - [BLOCKS.HTML]: (props) => null, + [BLOCKS.HTML]: (token) => { + return ; + }, [BLOCKS.HR]: 'hr', [BLOCKS.HEADING_1]: 'h1', [BLOCKS.HEADING_2]: 'h2', @@ -39,6 +45,7 @@ function renderToken(token, index = 0, key = '0') { const type = token.get('type'); const data = token.get('data'); const text = token.get('text'); + const raw = token.get('raw'); const tokens = token.get('tokens'); const nodeType = defaultRenderers[type]; key = `${key}.${index}`; @@ -52,7 +59,12 @@ function renderToken(token, index = 0, key = '0') { children = text; } if (nodeType !== null) { + + if (typeof nodeType === 'function') { + return nodeType(token); + } // If this is a react element + console.log(data.toJS()); return React.createElement( nodeType, { key, ...data.toJS() }, // Add key as a prop diff --git a/src/components/Widgets/__tests__/MarkitupReactRenderer.spec.js b/src/components/Widgets/__tests__/MarkitupReactRenderer.spec.js index 85c31a98..8705e420 100644 --- a/src/components/Widgets/__tests__/MarkitupReactRenderer.spec.js +++ b/src/components/Widgets/__tests__/MarkitupReactRenderer.spec.js @@ -81,6 +81,22 @@ Text with **bold** & _em_ elements }); }); + describe('Headings', () => { + for (const heading of [...Array(6).keys()]) { + it(`should render Heading ${heading + 1}`, () => { + const value = padStart(' Title', heading + 7, '#') + const component = shallow( + + ); + const tree = component.html(); + expect(tree).toMatchSnapshot() + }) + } + }) + describe('Lists', () => { it('should render lists', () => { const value = ` @@ -121,8 +137,7 @@ I get 10 times more traffic from [Google] [1] than from [Yahoo] [2] or [MSN] [3] /> ); const tree = component.html(); - const expected = ''; - expect(tree).toEqual(expected); + expect(tree).toMatchSnapshot(); }); }); @@ -136,8 +151,7 @@ I get 10 times more traffic from [Google] [1] than from [Yahoo] [2] or [MSN] [3] /> ); const tree = component.html(); - const expected = '

Use the printf() function.

'; - expect(tree).toEqual(expected); + expect(tree).toMatchSnapshot(); }); it('should render code 2', () => { @@ -149,8 +163,7 @@ I get 10 times more traffic from [Google] [1] than from [Yahoo] [2] or [MSN] [3] /> ); const tree = component.html(); - const expected = '

There is a literal backtick (`) here.

'; - expect(tree).toEqual(expected); + expect(tree).toMatchSnapshot(); }); }); @@ -159,10 +172,10 @@ I get 10 times more traffic from [Google] [1] than from [Yahoo] [2] or [MSN] [3] const value = ` # Title -
-
Test HTML content
-
Testing HTML in Markdown
-
+
    +
  • Test HTML content
  • +
  • Testing HTML in Markdown
  • +
`; const component = shallow( { it('should render HTML', () => { - const value = '

Paragraph with inline element

'; + const value = '

Paragraph with inline element

'; const component = shallow(

Paragraph with inline element

"`; +exports[`MarkitupReactRenderer Markdown rendering Code should render code 1`] = `"

Use the printf() function.

"`; + +exports[`MarkitupReactRenderer Markdown rendering Code should render code 2 1`] = `"

There is a literal backtick (\`) here.

"`; + exports[`MarkitupReactRenderer Markdown rendering General should render markdown 1`] = `"

H1

Text with bold & em elements

H2

  • ul item 1
  • ul item 2

H3

  1. ol item 1
  2. ol item 2
  3. ol item 3

H4

link title

H5

\"alt

H6
"`; -exports[`MarkitupReactRenderer Markdown rendering HTML should render HTML as is using Markdown 1`] = `"

Title

"`; +exports[`MarkitupReactRenderer Markdown rendering HTML should render HTML as is using Markdown 1`] = `"

Title

  • Test HTML content
  • Testing HTML in Markdown
"`; + +exports[`MarkitupReactRenderer Markdown rendering Headings should render Heading 1 1`] = `"

Title

"`; + +exports[`MarkitupReactRenderer Markdown rendering Headings should render Heading 2 1`] = `"

Title

"`; + +exports[`MarkitupReactRenderer Markdown rendering Headings should render Heading 3 1`] = `"

Title

"`; + +exports[`MarkitupReactRenderer Markdown rendering Headings should render Heading 4 1`] = `"

Title

"`; + +exports[`MarkitupReactRenderer Markdown rendering Headings should render Heading 5 1`] = `"
Title
"`; + +exports[`MarkitupReactRenderer Markdown rendering Headings should render Heading 6 1`] = `"
Title
"`; + +exports[`MarkitupReactRenderer Markdown rendering Links should render links 1`] = `""`; exports[`MarkitupReactRenderer Markdown rendering Lists should render lists 1`] = `"
  1. ol item 1
  2. ol item 2
    • Sublist 1
    • Sublist 2
    • Sublist 3
      1. Sub-Sublist 1
      2. Sub-Sublist 2
      3. Sub-Sublist 3
  3. ol item 3
"`; +exports[`MarkitupReactRenderer custom elements should extend default renderers with custom ones 1`] = `"

"`; + exports[`MarkitupReactRenderer custom elements should support custom syntax 1`] = `"
"`; + +exports[`MarkitupReactRenderer custom elements should support custom syntaxes 1`] = `"

"`; From af8ea8014264a945d15ce23726424037de99369b Mon Sep 17 00:00:00 2001 From: Andrey Okonetchnikov Date: Mon, 26 Sep 2016 13:31:36 +0200 Subject: [PATCH 14/57] Added support for custom renderers --- .../Widgets/MarkitupReactRenderer.js | 19 +++-- .../__tests__/MarkitupReactRenderer.spec.js | 76 ++++++++++++------- .../MarkitupReactRenderer.spec.js.snap | 6 +- 3 files changed, 60 insertions(+), 41 deletions(-) diff --git a/src/components/Widgets/MarkitupReactRenderer.js b/src/components/Widgets/MarkitupReactRenderer.js index 2f2e2948..364266bb 100644 --- a/src/components/Widgets/MarkitupReactRenderer.js +++ b/src/components/Widgets/MarkitupReactRenderer.js @@ -2,7 +2,7 @@ import React, { PropTypes } from 'react'; import MarkupIt, { Syntax, BLOCKS, STYLES, ENTITIES } from 'markup-it'; import htmlSyntax from 'markup-it/syntaxes/html'; -const defaultRenderers = { +const defaultSchema = { [BLOCKS.DOCUMENT]: 'article', [BLOCKS.TEXT]: null, [BLOCKS.CODE]: 'code', @@ -41,30 +41,28 @@ const defaultRenderers = { [ENTITIES.HARD_BREAK]: 'br' }; -function renderToken(token, index = 0, key = '0') { +function renderToken(schema, token, index = 0, key = '0') { const type = token.get('type'); const data = token.get('data'); const text = token.get('text'); - const raw = token.get('raw'); const tokens = token.get('tokens'); - const nodeType = defaultRenderers[type]; + 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(token, idx, key)); + children = tokens.map((token, idx) => renderToken(schema, token, idx, key)); } else if (type === 'text') { children = text; } if (nodeType !== null) { - + // If this is a function we want to pass the `token` as an argument if (typeof nodeType === 'function') { return nodeType(token); } // If this is a react element - console.log(data.toJS()); return React.createElement( nodeType, { key, ...data.toJS() }, // Add key as a prop @@ -93,13 +91,14 @@ export default class MarkitupReactRenderer extends React.Component { } render() { - const { value } = this.props; + const { value, schema } = this.props; const content = this.parser.toContent(value); - return renderToken(content.get('token')); + return renderToken({ ...defaultSchema, ...schema }, content.get('token')); } } MarkitupReactRenderer.propTypes = { value: PropTypes.string, - syntax: PropTypes.instanceOf(Syntax).isRequired + syntax: PropTypes.instanceOf(Syntax).isRequired, + schema: PropTypes.objectOf(PropTypes.node) }; diff --git a/src/components/Widgets/__tests__/MarkitupReactRenderer.spec.js b/src/components/Widgets/__tests__/MarkitupReactRenderer.spec.js index 8705e420..67fe203e 100644 --- a/src/components/Widgets/__tests__/MarkitupReactRenderer.spec.js +++ b/src/components/Widgets/__tests__/MarkitupReactRenderer.spec.js @@ -2,8 +2,12 @@ import React from 'react'; import { shallow } from 'enzyme'; +import { padStart } from 'lodash'; +import { Map } from 'immutable'; +import MarkupIt from 'markup-it'; import markdownSyntax from 'markup-it/syntaxes/markdown'; import htmlSyntax from 'markup-it/syntaxes/html'; +import reInline from 'markup-it/syntaxes/markdown/re/inline'; import MarkitupReactRenderer from '../MarkitupReactRenderer'; describe('MarkitupReactRenderer', () => { @@ -76,26 +80,24 @@ Text with **bold** & _em_ elements syntax={markdownSyntax} /> ); - const tree = component.html(); - expect(tree).toMatchSnapshot(); + expect(component.html()).toMatchSnapshot(); }); }); describe('Headings', () => { for (const heading of [...Array(6).keys()]) { it(`should render Heading ${heading + 1}`, () => { - const value = padStart(' Title', heading + 7, '#') + const value = padStart(' Title', heading + 7, '#'); const component = shallow( ); - const tree = component.html(); - expect(tree).toMatchSnapshot() - }) + expect(component.html()).toMatchSnapshot(); + }); } - }) + }); describe('Lists', () => { it('should render lists', () => { @@ -116,8 +118,7 @@ Text with **bold** & _em_ elements syntax={markdownSyntax} /> ); - const tree = component.html(); - expect(tree).toMatchSnapshot(); + expect(component.html()).toMatchSnapshot(); }); }); @@ -136,8 +137,7 @@ I get 10 times more traffic from [Google] [1] than from [Yahoo] [2] or [MSN] [3] syntax={markdownSyntax} /> ); - const tree = component.html(); - expect(tree).toMatchSnapshot(); + expect(component.html()).toMatchSnapshot(); }); }); @@ -150,8 +150,7 @@ I get 10 times more traffic from [Google] [1] than from [Yahoo] [2] or [MSN] [3] syntax={markdownSyntax} /> ); - const tree = component.html(); - expect(tree).toMatchSnapshot(); + expect(component.html()).toMatchSnapshot(); }); it('should render code 2', () => { @@ -162,8 +161,7 @@ I get 10 times more traffic from [Google] [1] than from [Yahoo] [2] or [MSN] [3] syntax={markdownSyntax} /> ); - const tree = component.html(); - expect(tree).toMatchSnapshot(); + expect(component.html()).toMatchSnapshot(); }); }); @@ -183,23 +181,50 @@ I get 10 times more traffic from [Google] [1] than from [Yahoo] [2] or [MSN] [3] syntax={markdownSyntax} /> ); - const tree = component.html(); - expect(tree).toMatchSnapshot(); + expect(component.html()).toMatchSnapshot(); }); }); }); describe('custom elements', () => { - it('should support custom syntax', () => { - const value = ''; + it('should extend default renderers with custom ones', () => { + const myRule = MarkupIt.Rule('mediaproxy') + .regExp(reInline.link, (state, match) => { + if (match[0].charAt(0) !== '!') { + return; + } + + return { + data: Map({ + alt: match[1], + src: match[2], + title: match[3] + }).filter(Boolean) + }; + }); + + const myCustomSchema = { + 'mediaproxy': (token) => { + const src = token.getIn(['data', 'src']); + const alt = token.getIn(['data', 'alt']); + return {alt}/; + } + }; + + const myMarkdownSyntax = markdownSyntax.addInlineRules(myRule); + const value = ` +## Title + +![mediaproxy test](http://url.to.image) +`; const component = shallow( ); - const tree = component.html(); - expect(tree).toMatchSnapshot(); + expect(component.html()).toMatchSnapshot(); }); }); @@ -212,8 +237,7 @@ I get 10 times more traffic from [Google] [1] than from [Yahoo] [2] or [MSN] [3] syntax={htmlSyntax} /> ); - const tree = component.html(); - expect(tree).toMatchSnapshot(); + expect(component.html()).toMatchSnapshot(); }); }); }); diff --git a/src/components/Widgets/__tests__/__snapshots__/MarkitupReactRenderer.spec.js.snap b/src/components/Widgets/__tests__/__snapshots__/MarkitupReactRenderer.spec.js.snap index 920539bf..74658e3e 100644 --- a/src/components/Widgets/__tests__/__snapshots__/MarkitupReactRenderer.spec.js.snap +++ b/src/components/Widgets/__tests__/__snapshots__/MarkitupReactRenderer.spec.js.snap @@ -24,8 +24,4 @@ exports[`MarkitupReactRenderer Markdown rendering Links should render links 1`] exports[`MarkitupReactRenderer Markdown rendering Lists should render lists 1`] = `"
  1. ol item 1
  2. ol item 2
    • Sublist 1
    • Sublist 2
    • Sublist 3
      1. Sub-Sublist 1
      2. Sub-Sublist 2
      3. Sub-Sublist 3
  3. ol item 3
"`; -exports[`MarkitupReactRenderer custom elements should extend default renderers with custom ones 1`] = `"

"`; - -exports[`MarkitupReactRenderer custom elements should support custom syntax 1`] = `"
"`; - -exports[`MarkitupReactRenderer custom elements should support custom syntaxes 1`] = `"

"`; +exports[`MarkitupReactRenderer custom elements should extend default renderers with custom ones 1`] = `"

Title

\"mediaproxy

"`; From cdc71a2bbf1063d04b4047c7c3006630a81231a9 Mon Sep 17 00:00:00 2001 From: Andrey Okonetchnikov Date: Mon, 26 Sep 2016 15:37:25 +0200 Subject: [PATCH 15/57] Sanitize unsupported props. This removes all React warnings. Refactored renderToken. --- package.json | 2 +- .../Widgets/MarkitupReactRenderer.js | 26 ++++++++++++------- .../__tests__/MarkitupReactRenderer.spec.js | 2 +- .../MarkitupReactRenderer.spec.js.snap | 4 +-- 4 files changed, 20 insertions(+), 14 deletions(-) diff --git a/package.json b/package.json index 0ededa44..fe2099a5 100644 --- a/package.json +++ b/package.json @@ -98,8 +98,8 @@ "react-addons-css-transition-group": "^15.3.1", "react-datetime": "^2.6.0", "react-portal": "^2.2.1", - "react-toolbox": "^1.2.1", "react-simple-dnd": "^0.1.2", + "react-toolbox": "^1.2.1", "selection-position": "^1.0.0", "semaphore": "^1.0.5", "slate": "^0.13.6" diff --git a/src/components/Widgets/MarkitupReactRenderer.js b/src/components/Widgets/MarkitupReactRenderer.js index 364266bb..40923448 100644 --- a/src/components/Widgets/MarkitupReactRenderer.js +++ b/src/components/Widgets/MarkitupReactRenderer.js @@ -1,5 +1,6 @@ import React, { PropTypes } from 'react'; import MarkupIt, { Syntax, BLOCKS, STYLES, ENTITIES } from 'markup-it'; +import { pick } from 'lodash'; import htmlSyntax from 'markup-it/syntaxes/html'; const defaultSchema = { @@ -9,7 +10,7 @@ const defaultSchema = { [BLOCKS.BLOCKQUOTE]: 'blockquote', [BLOCKS.PARAGRAPH]: 'p', [BLOCKS.FOOTNOTE]: 'footnote', - [BLOCKS.HTML]: (token) => { + [BLOCKS.HTML]: ({ token }) => { return { + 'mediaproxy': ({ token }) => { const src = token.getIn(['data', 'src']); const alt = token.getIn(['data', 'alt']); return {alt}/; diff --git a/src/components/Widgets/__tests__/__snapshots__/MarkitupReactRenderer.spec.js.snap b/src/components/Widgets/__tests__/__snapshots__/MarkitupReactRenderer.spec.js.snap index 74658e3e..9562819e 100644 --- a/src/components/Widgets/__tests__/__snapshots__/MarkitupReactRenderer.spec.js.snap +++ b/src/components/Widgets/__tests__/__snapshots__/MarkitupReactRenderer.spec.js.snap @@ -4,7 +4,7 @@ exports[`MarkitupReactRenderer Markdown rendering Code should render code 1`] = exports[`MarkitupReactRenderer Markdown rendering Code should render code 2 1`] = `"

There is a literal backtick (\`) here.

"`; -exports[`MarkitupReactRenderer Markdown rendering General should render markdown 1`] = `"

H1

Text with bold & em elements

H2

  • ul item 1
  • ul item 2

H3

  1. ol item 1
  2. ol item 2
  3. ol item 3

H4

link title

H5

\"alt

H6
"`; +exports[`MarkitupReactRenderer Markdown rendering General should render markdown 1`] = `"

H1

Text with bold & em elements

H2

  • ul item 1
  • ul item 2

H3

  1. ol item 1
  2. ol item 2
  3. ol item 3

H4

link title

H5

\"alt

H6
"`; exports[`MarkitupReactRenderer Markdown rendering HTML should render HTML as is using Markdown 1`] = `"

Title

  • Test HTML content
  • Testing HTML in Markdown
"`; @@ -20,7 +20,7 @@ exports[`MarkitupReactRenderer Markdown rendering Headings should render Heading exports[`MarkitupReactRenderer Markdown rendering Headings should render Heading 6 1`] = `"
Title
"`; -exports[`MarkitupReactRenderer Markdown rendering Links should render links 1`] = `""`; +exports[`MarkitupReactRenderer Markdown rendering Links should render links 1`] = `""`; exports[`MarkitupReactRenderer Markdown rendering Lists should render lists 1`] = `"
  1. ol item 1
  2. ol item 2
    • Sublist 1
    • Sublist 2
    • Sublist 3
      1. Sub-Sublist 1
      2. Sub-Sublist 2
      3. Sub-Sublist 3
  3. ol item 3
"`; From 71c638bc27bc319ffd5a243f2885633395434625 Mon Sep 17 00:00:00 2001 From: Andrey Okonetchnikov Date: Mon, 26 Sep 2016 15:59:49 +0200 Subject: [PATCH 16/57] Integrated MD Preview component with mediaproxy --- src/components/Widgets/MarkdownPreview.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/components/Widgets/MarkdownPreview.js b/src/components/Widgets/MarkdownPreview.js index d8ababad..a598eda5 100644 --- a/src/components/Widgets/MarkdownPreview.js +++ b/src/components/Widgets/MarkdownPreview.js @@ -2,6 +2,15 @@ import React, { PropTypes } from 'react'; import { getSyntaxes } from './richText'; import MarkitupReactRenderer from './MarkitupReactRenderer'; +const schema = { + 'mediaproxy': ({ token }) => ( + {token.getIn(['data', + ) +}; + const MarkdownPreview = ({ value }) => { if (value == null) { return null; @@ -12,6 +21,7 @@ const MarkdownPreview = ({ value }) => { ); }; From 0a9eb3d6886062522c9eec192e82daec58decc0d Mon Sep 17 00:00:00 2001 From: Andrey Okonetchnikov Date: Tue, 27 Sep 2016 11:14:41 +0200 Subject: [PATCH 17/57] Fixed a typo --- .../Widgets/MarkdownControlElements/RawEditor/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Widgets/MarkdownControlElements/RawEditor/index.js b/src/components/Widgets/MarkdownControlElements/RawEditor/index.js index 175f2471..660ab4bd 100644 --- a/src/components/Widgets/MarkdownControlElements/RawEditor/index.js +++ b/src/components/Widgets/MarkdownControlElements/RawEditor/index.js @@ -91,7 +91,7 @@ class RawEditor extends React.Component { /** * Slate keeps track of selections, scroll position etc. * So, onChange gets dispatched on every interaction (click, arrows, everything...) - * It also have an onDocumentChange, that get's dispached only when the actual + * It also have an onDocumentChange, that get's dispatched only when the actual * content changes */ handleChange(state) { From e0724aa1bd71b5c1f473fb4d9003bff32e22eadf Mon Sep 17 00:00:00 2001 From: Andrey Okonetchnikov Date: Tue, 27 Sep 2016 11:26:28 +0200 Subject: [PATCH 18/57] Use react-htmlparser2 to render HTML to React VDOM --- package.json | 1 + src/components/Widgets/MarkitupReactRenderer.js | 7 ++----- .../Widgets/__tests__/MarkitupReactRenderer.spec.js | 13 +++++++++---- .../MarkitupReactRenderer.spec.js.snap | 12 +++++++++++- 4 files changed, 23 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index fe2099a5..8c9df5af 100644 --- a/package.json +++ b/package.json @@ -97,6 +97,7 @@ "prismjs": "^1.5.1", "react-addons-css-transition-group": "^15.3.1", "react-datetime": "^2.6.0", + "react-htmlparser2": "^0.1.0", "react-portal": "^2.2.1", "react-simple-dnd": "^0.1.2", "react-toolbox": "^1.2.1", diff --git a/src/components/Widgets/MarkitupReactRenderer.js b/src/components/Widgets/MarkitupReactRenderer.js index 40923448..a2e79bfd 100644 --- a/src/components/Widgets/MarkitupReactRenderer.js +++ b/src/components/Widgets/MarkitupReactRenderer.js @@ -1,7 +1,7 @@ import React, { PropTypes } from 'react'; +import reactParser from 'react-htmlparser2'; import MarkupIt, { Syntax, BLOCKS, STYLES, ENTITIES } from 'markup-it'; import { pick } from 'lodash'; -import htmlSyntax from 'markup-it/syntaxes/html'; const defaultSchema = { [BLOCKS.DOCUMENT]: 'article', @@ -11,10 +11,7 @@ const defaultSchema = { [BLOCKS.PARAGRAPH]: 'p', [BLOCKS.FOOTNOTE]: 'footnote', [BLOCKS.HTML]: ({ token }) => { - return ; + return reactParser(token.get('raw'), React); }, [BLOCKS.HR]: 'hr', [BLOCKS.HEADING_1]: 'h1', diff --git a/src/components/Widgets/__tests__/MarkitupReactRenderer.spec.js b/src/components/Widgets/__tests__/MarkitupReactRenderer.spec.js index a7e351a8..d38f2719 100644 --- a/src/components/Widgets/__tests__/MarkitupReactRenderer.spec.js +++ b/src/components/Widgets/__tests__/MarkitupReactRenderer.spec.js @@ -170,10 +170,15 @@ I get 10 times more traffic from [Google] [1] than from [Yahoo] [2] or [MSN] [3] const value = ` # Title -
    -
  • Test HTML content
  • -
  • Testing HTML in Markdown
  • -
+
+ +
+
Test HTML content
+
Testing HTML in Markdown
+
+
`; const component = shallow(

H1

Text with bold & em elements

H2

  • ul item 1
  • ul item 2

H3

  1. ol item 1
  2. ol item 2
  3. ol item 3

H4

link title

H5

\"alt

H6
"`; -exports[`MarkitupReactRenderer Markdown rendering HTML should render HTML as is using Markdown 1`] = `"

Title

  • Test HTML content
  • Testing HTML in Markdown
"`; +exports[`MarkitupReactRenderer Markdown rendering HTML should render HTML as is using Markdown 1`] = ` +"

Title

+ +
+
Test HTML content
+
Testing HTML in Markdown
+
+
" +`; exports[`MarkitupReactRenderer Markdown rendering Headings should render Heading 1 1`] = `"

Title

"`; From 05913d4a1b63cc78a47bc134a4c6d1bef9e3bc42 Mon Sep 17 00:00:00 2001 From: Andrey Okonetchnikov Date: Tue, 27 Sep 2016 11:33:28 +0200 Subject: [PATCH 19/57] Omit not allowed attributes instead of white-listing them --- src/components/Widgets/MarkitupReactRenderer.js | 6 +++--- .../__snapshots__/MarkitupReactRenderer.spec.js.snap | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/Widgets/MarkitupReactRenderer.js b/src/components/Widgets/MarkitupReactRenderer.js index a2e79bfd..a1f5a4cf 100644 --- a/src/components/Widgets/MarkitupReactRenderer.js +++ b/src/components/Widgets/MarkitupReactRenderer.js @@ -1,7 +1,7 @@ import React, { PropTypes } from 'react'; import reactParser from 'react-htmlparser2'; import MarkupIt, { Syntax, BLOCKS, STYLES, ENTITIES } from 'markup-it'; -import { pick } from 'lodash'; +import { omit } from 'lodash'; const defaultSchema = { [BLOCKS.DOCUMENT]: 'article', @@ -39,10 +39,10 @@ const defaultSchema = { [ENTITIES.HARD_BREAK]: 'br' }; -const allowedProps = ['className', 'id', 'name', 'title', 'src', 'alt', 'href']; +const notAllowedAttributes = ['loose']; function sanitizeProps(props) { - return pick(props, allowedProps); + return omit(props, notAllowedAttributes); } function renderToken(schema, token, index = 0, key = '0') { diff --git a/src/components/Widgets/__tests__/__snapshots__/MarkitupReactRenderer.spec.js.snap b/src/components/Widgets/__tests__/__snapshots__/MarkitupReactRenderer.spec.js.snap index c9428b8f..70ba2201 100644 --- a/src/components/Widgets/__tests__/__snapshots__/MarkitupReactRenderer.spec.js.snap +++ b/src/components/Widgets/__tests__/__snapshots__/MarkitupReactRenderer.spec.js.snap @@ -4,7 +4,7 @@ exports[`MarkitupReactRenderer Markdown rendering Code should render code 1`] = exports[`MarkitupReactRenderer Markdown rendering Code should render code 2 1`] = `"

There is a literal backtick (\`) here.

"`; -exports[`MarkitupReactRenderer Markdown rendering General should render markdown 1`] = `"

H1

Text with bold & em elements

H2

  • ul item 1
  • ul item 2

H3

  1. ol item 1
  2. ol item 2
  3. ol item 3

H4

link title

H5

\"alt

H6
"`; +exports[`MarkitupReactRenderer Markdown rendering General should render markdown 1`] = `"

H1

Text with bold & em elements

H2

  • ul item 1
  • ul item 2

H3

  1. ol item 1
  2. ol item 2
  3. ol item 3

H4

link title

H5

\"alt

H6
"`; exports[`MarkitupReactRenderer Markdown rendering HTML should render HTML as is using Markdown 1`] = ` "

Title

@@ -30,7 +30,7 @@ exports[`MarkitupReactRenderer Markdown rendering Headings should render Heading exports[`MarkitupReactRenderer Markdown rendering Headings should render Heading 6 1`] = `"
Title
"`; -exports[`MarkitupReactRenderer Markdown rendering Links should render links 1`] = `""`; +exports[`MarkitupReactRenderer Markdown rendering Links should render links 1`] = `""`; exports[`MarkitupReactRenderer Markdown rendering Lists should render lists 1`] = `"
  1. ol item 1
  2. ol item 2
    • Sublist 1
    • Sublist 2
    • Sublist 3
      1. Sub-Sublist 1
      2. Sub-Sublist 2
      3. Sub-Sublist 3
  3. ol item 3
"`; From f38e6b6f2538cad7ca0e46e1c8f4ffdc0590a90c Mon Sep 17 00:00:00 2001 From: Andrey Okonetchnikov Date: Tue, 27 Sep 2016 12:18:52 +0200 Subject: [PATCH 20/57] Use dangerouslySetInnerHTML since the react parser wasn't working with inline styles correctly. --- package.json | 1 - src/components/Widgets/MarkitupReactRenderer.js | 3 +-- .../Widgets/__tests__/MarkitupReactRenderer.spec.js | 4 +++- .../MarkitupReactRenderer.spec.js.snap | 13 ++++++++----- 4 files changed, 12 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index 8c9df5af..fe2099a5 100644 --- a/package.json +++ b/package.json @@ -97,7 +97,6 @@ "prismjs": "^1.5.1", "react-addons-css-transition-group": "^15.3.1", "react-datetime": "^2.6.0", - "react-htmlparser2": "^0.1.0", "react-portal": "^2.2.1", "react-simple-dnd": "^0.1.2", "react-toolbox": "^1.2.1", diff --git a/src/components/Widgets/MarkitupReactRenderer.js b/src/components/Widgets/MarkitupReactRenderer.js index a1f5a4cf..93faaefd 100644 --- a/src/components/Widgets/MarkitupReactRenderer.js +++ b/src/components/Widgets/MarkitupReactRenderer.js @@ -1,5 +1,4 @@ import React, { PropTypes } from 'react'; -import reactParser from 'react-htmlparser2'; import MarkupIt, { Syntax, BLOCKS, STYLES, ENTITIES } from 'markup-it'; import { omit } from 'lodash'; @@ -11,7 +10,7 @@ const defaultSchema = { [BLOCKS.PARAGRAPH]: 'p', [BLOCKS.FOOTNOTE]: 'footnote', [BLOCKS.HTML]: ({ token }) => { - return reactParser(token.get('raw'), React); + return
; }, [BLOCKS.HR]: 'hr', [BLOCKS.HEADING_1]: 'h1', diff --git a/src/components/Widgets/__tests__/MarkitupReactRenderer.spec.js b/src/components/Widgets/__tests__/MarkitupReactRenderer.spec.js index d38f2719..21d1d44e 100644 --- a/src/components/Widgets/__tests__/MarkitupReactRenderer.spec.js +++ b/src/components/Widgets/__tests__/MarkitupReactRenderer.spec.js @@ -166,7 +166,7 @@ I get 10 times more traffic from [Google] [1] than from [Yahoo] [2] or [MSN] [3] }); describe('HTML', () => { - it('should render HTML as is using Markdown', () => { + it('should render HTML as is when using Markdown', () => { const value = ` # Title @@ -179,6 +179,8 @@ I get 10 times more traffic from [Google] [1] than from [Yahoo] [2] or [MSN] [3]
Testing HTML in Markdown
+ +

Test

`; const component = shallow(

H1

Text with bold & em elements

H2

  • ul item 1
  • ul item 2

H3

  1. ol item 1
  2. ol item 2
  3. ol item 3

H4

link title

H5

\"alt

H6
"`; -exports[`MarkitupReactRenderer Markdown rendering HTML should render HTML as is using Markdown 1`] = ` -"

Title

+exports[`MarkitupReactRenderer Markdown rendering HTML should render HTML as is when using Markdown 1`] = ` +"

Title

-
+
Test HTML content
Testing HTML in Markdown
-
" + + +

Test

+
" `; exports[`MarkitupReactRenderer Markdown rendering Headings should render Heading 1 1`] = `"

Title

"`; From 78eb65bca2af10488c6fd7c97d19ab34e2f0e9dd Mon Sep 17 00:00:00 2001 From: Andrey Okonetchnikov Date: Tue, 27 Sep 2016 12:24:52 +0200 Subject: [PATCH 21/57] Renamed component --- src/components/Widgets/MarkdownPreview.js | 4 ++-- ...ctRenderer.js => MarkupItReactRenderer.js} | 4 ++-- ....spec.js => MarkupItReactRenderer.spec.js} | 24 +++++++++---------- 3 files changed, 16 insertions(+), 16 deletions(-) rename src/components/Widgets/{MarkitupReactRenderer.js => MarkupItReactRenderer.js} (96%) rename src/components/Widgets/__tests__/{MarkitupReactRenderer.spec.js => MarkupItReactRenderer.spec.js} (93%) diff --git a/src/components/Widgets/MarkdownPreview.js b/src/components/Widgets/MarkdownPreview.js index a598eda5..beb1a9aa 100644 --- a/src/components/Widgets/MarkdownPreview.js +++ b/src/components/Widgets/MarkdownPreview.js @@ -1,6 +1,6 @@ import React, { PropTypes } from 'react'; import { getSyntaxes } from './richText'; -import MarkitupReactRenderer from './MarkitupReactRenderer'; +import MarkupItReactRenderer from './MarkupItReactRenderer'; const schema = { 'mediaproxy': ({ token }) => ( @@ -18,7 +18,7 @@ const MarkdownPreview = ({ value }) => { const { markdown } = getSyntaxes(); return ( - { describe('basics', () => { it('should re-render properly after a value and syntax update', () => { const component = shallow( - @@ -31,7 +31,7 @@ describe('MarkitupReactRenderer', () => { it('should not update the parser if syntax didn\'t change', () => { const component = shallow( - @@ -75,7 +75,7 @@ Text with **bold** & _em_ elements ###### H6 `; const component = shallow( - @@ -89,7 +89,7 @@ Text with **bold** & _em_ elements it(`should render Heading ${heading + 1}`, () => { const value = padStart(' Title', heading + 7, '#'); const component = shallow( - @@ -113,7 +113,7 @@ Text with **bold** & _em_ elements 1. ol item 3 `; const component = shallow( - @@ -132,7 +132,7 @@ I get 10 times more traffic from [Google] [1] than from [Yahoo] [2] or [MSN] [3] [3]: http://search.msn.com/ "MSN Search" `; const component = shallow( - @@ -145,7 +145,7 @@ I get 10 times more traffic from [Google] [1] than from [Yahoo] [2] or [MSN] [3] it('should render code', () => { const value = 'Use the `printf()` function.'; const component = shallow( - @@ -156,7 +156,7 @@ I get 10 times more traffic from [Google] [1] than from [Yahoo] [2] or [MSN] [3] it('should render code 2', () => { const value = '``There is a literal backtick (`) here.``'; const component = shallow( - @@ -183,7 +183,7 @@ I get 10 times more traffic from [Google] [1] than from [Yahoo] [2] or [MSN] [3]

Test

`; const component = shallow( - @@ -225,7 +225,7 @@ I get 10 times more traffic from [Google] [1] than from [Yahoo] [2] or [MSN] [3] ![mediaproxy test](http://url.to.image) `; const component = shallow( - { const value = '

Paragraph with inline element

'; const component = shallow( - From 107e8f7104a3571944a9f75434c7d811bb877a6c Mon Sep 17 00:00:00 2001 From: Andrey Okonetchnikov Date: Tue, 27 Sep 2016 12:57:41 +0200 Subject: [PATCH 22/57] Moved MarkupItReactRenderer to /components. Added a story for it. --- .../__tests__/MarkupItReactRenderer.spec.js | 2 +- .../MarkitupReactRenderer.spec.js.snap | 0 .../index.js} | 0 src/components/Widgets/MarkdownPreview.js | 2 +- src/components/stories/FindBar.js | 2 +- .../stories/MarkupItReactRenderer.js | 34 +++++++++++++++++++ src/components/stories/index.js | 1 + 7 files changed, 38 insertions(+), 3 deletions(-) rename src/components/{Widgets => MarkupItReactRenderer}/__tests__/MarkupItReactRenderer.spec.js (98%) rename src/components/{Widgets => MarkupItReactRenderer}/__tests__/__snapshots__/MarkitupReactRenderer.spec.js.snap (100%) rename src/components/{Widgets/MarkupItReactRenderer.js => MarkupItReactRenderer/index.js} (100%) create mode 100644 src/components/stories/MarkupItReactRenderer.js diff --git a/src/components/Widgets/__tests__/MarkupItReactRenderer.spec.js b/src/components/MarkupItReactRenderer/__tests__/MarkupItReactRenderer.spec.js similarity index 98% rename from src/components/Widgets/__tests__/MarkupItReactRenderer.spec.js rename to src/components/MarkupItReactRenderer/__tests__/MarkupItReactRenderer.spec.js index ea9fcee4..c676757f 100644 --- a/src/components/Widgets/__tests__/MarkupItReactRenderer.spec.js +++ b/src/components/MarkupItReactRenderer/__tests__/MarkupItReactRenderer.spec.js @@ -8,7 +8,7 @@ import MarkupIt from 'markup-it'; import markdownSyntax from 'markup-it/syntaxes/markdown'; import htmlSyntax from 'markup-it/syntaxes/html'; import reInline from 'markup-it/syntaxes/markdown/re/inline'; -import MarkupItReactRenderer from '../MarkupItReactRenderer'; +import MarkupItReactRenderer from '../../UI/MarkupItReactRenderer/MarkupItReactRenderer'; describe('MarkitupReactRenderer', () => { diff --git a/src/components/Widgets/__tests__/__snapshots__/MarkitupReactRenderer.spec.js.snap b/src/components/MarkupItReactRenderer/__tests__/__snapshots__/MarkitupReactRenderer.spec.js.snap similarity index 100% rename from src/components/Widgets/__tests__/__snapshots__/MarkitupReactRenderer.spec.js.snap rename to src/components/MarkupItReactRenderer/__tests__/__snapshots__/MarkitupReactRenderer.spec.js.snap diff --git a/src/components/Widgets/MarkupItReactRenderer.js b/src/components/MarkupItReactRenderer/index.js similarity index 100% rename from src/components/Widgets/MarkupItReactRenderer.js rename to src/components/MarkupItReactRenderer/index.js diff --git a/src/components/Widgets/MarkdownPreview.js b/src/components/Widgets/MarkdownPreview.js index beb1a9aa..b88435b9 100644 --- a/src/components/Widgets/MarkdownPreview.js +++ b/src/components/Widgets/MarkdownPreview.js @@ -1,6 +1,6 @@ import React, { PropTypes } from 'react'; import { getSyntaxes } from './richText'; -import MarkupItReactRenderer from './MarkupItReactRenderer'; +import MarkupItReactRenderer from '../MarkupItReactRenderer/index'; const schema = { 'mediaproxy': ({ token }) => ( diff --git a/src/components/stories/FindBar.js b/src/components/stories/FindBar.js index 812d2f8c..b2933b96 100644 --- a/src/components/stories/FindBar.js +++ b/src/components/stories/FindBar.js @@ -1,7 +1,7 @@ import React from 'react'; import { storiesOf, action } from '@kadira/storybook'; -import FindBar from '../UI/FindBar/FindBar'; +import FindBar from '../FindBar/FindBar'; const CREATE_COLLECTION = 'CREATE_COLLECTION'; const CREATE_POST = 'CREATE_POST'; diff --git a/src/components/stories/MarkupItReactRenderer.js b/src/components/stories/MarkupItReactRenderer.js new file mode 100644 index 00000000..2ba1a3ee --- /dev/null +++ b/src/components/stories/MarkupItReactRenderer.js @@ -0,0 +1,34 @@ +import React from 'react'; +import markdownSyntax from 'markup-it/syntaxes/markdown'; +import htmlSyntax from 'markup-it/syntaxes/html'; +import MarkupItReactRenderer from '../MarkupItReactRenderer'; +import { storiesOf } from '@kadira/storybook'; + +const mdContent = ` +# Title + +* List 1 +* List 2 +`; + +const htmlContent = ` +

Title

+
    +
  1. List item 1
  2. +
  3. List item 2
  4. +
+`; + +storiesOf('MarkupItReactRenderer', module) + .add('Markdown', () => ( + + + )).add('HTML', () => ( + + )); diff --git a/src/components/stories/index.js b/src/components/stories/index.js index aef4bed3..30acb086 100644 --- a/src/components/stories/index.js +++ b/src/components/stories/index.js @@ -2,3 +2,4 @@ import './Card'; import './Icon'; import './Toast'; import './FindBar'; +import './MarkupItReactRenderer'; From 4020dfc9123aaf4e561830964e2bf80d8eff59dd Mon Sep 17 00:00:00 2001 From: Andrey Okonetchnikov Date: Tue, 27 Sep 2016 13:07:52 +0200 Subject: [PATCH 23/57] Refactored MarkdownControl to not use constructor and simplified render method --- src/components/Widgets/MarkdownControl.js | 20 +++----------------- 1 file changed, 3 insertions(+), 17 deletions(-) diff --git a/src/components/Widgets/MarkdownControl.js b/src/components/Widgets/MarkdownControl.js index 483be419..e010abd9 100644 --- a/src/components/Widgets/MarkdownControl.js +++ b/src/components/Widgets/MarkdownControl.js @@ -7,26 +7,21 @@ import { connect } from 'react-redux'; import { switchVisualMode } from '../../actions/editor'; class MarkdownControl extends React.Component { - constructor(props, context) { - super(props, context); - this.useVisualEditor = this.useVisualEditor.bind(this); - this.useRawEditor = this.useRawEditor.bind(this); - } componentWillMount() { this.useRawEditor(); processEditorPlugins(registry.getEditorComponents()); } - useVisualEditor() { + useVisualEditor = () => { this.props.switchVisualMode(true); } - useRawEditor() { + useRawEditor = () => { this.props.switchVisualMode(false); } - renderEditor() { + render() { const { editor, onChange, onAddMedia, getMedia, value } = this.props; if (editor.get('useVisualMode')) { return ( @@ -55,15 +50,6 @@ class MarkdownControl extends React.Component { ); } } - - render() { - return ( -
- - {this.renderEditor()} -
- ); - } } MarkdownControl.propTypes = { From 2b5a987945eb8b0371be973781261e47753d837b Mon Sep 17 00:00:00 2001 From: Andrey Okonetchnikov Date: Wed, 28 Sep 2016 11:05:11 +0200 Subject: [PATCH 24/57] WIP on drag'n'drop image uploading --- package.json | 3 ++- .../MarkdownControlElements/RawEditor/index.css | 0 .../MarkdownControlElements/RawEditor/index.js | 16 ++++++++++++---- 3 files changed, 14 insertions(+), 5 deletions(-) delete mode 100644 src/components/Widgets/MarkdownControlElements/RawEditor/index.css diff --git a/package.json b/package.json index fe2099a5..4d467f39 100644 --- a/package.json +++ b/package.json @@ -102,6 +102,7 @@ "react-toolbox": "^1.2.1", "selection-position": "^1.0.0", "semaphore": "^1.0.5", - "slate": "^0.13.6" + "slate": "^0.14.14", + "slate-drop-or-paste-images": "^0.2.0" } } diff --git a/src/components/Widgets/MarkdownControlElements/RawEditor/index.css b/src/components/Widgets/MarkdownControlElements/RawEditor/index.css deleted file mode 100644 index e69de29b..00000000 diff --git a/src/components/Widgets/MarkdownControlElements/RawEditor/index.js b/src/components/Widgets/MarkdownControlElements/RawEditor/index.js index 660ab4bd..c9e640c0 100644 --- a/src/components/Widgets/MarkdownControlElements/RawEditor/index.js +++ b/src/components/Widgets/MarkdownControlElements/RawEditor/index.js @@ -1,9 +1,8 @@ import React, { PropTypes } from 'react'; import { Editor, Plain, Mark } from 'slate'; import Prism from 'prismjs'; +import PluginDropImages from 'slate-drop-or-paste-images'; import marks from './prismMarkdown'; -import styles from './index.css'; - Prism.languages.markdown = Prism.languages.extend('markup', {}); Prism.languages.insertBefore('markdown', 'prolog', marks); @@ -43,7 +42,6 @@ function renderDecorations(text, block) { return characters.asImmutable(); } - const SCHEMA = { rules: [ { @@ -72,6 +70,16 @@ const SCHEMA = { } }; +const plugins = [ + PluginDropImages({ + applyTransform: (transform, file) => { + const state = Plain.deserialize(`\n\n![${file.name}](${file.name})\n\n`); + return transform + .insertFragment(state.get('document')); + } + }) +]; + class RawEditor extends React.Component { constructor(props) { @@ -111,7 +119,7 @@ class RawEditor extends React.Component { schema={SCHEMA} onChange={this.handleChange} onDocumentChange={this.handleDocumentChange} - renderDecorations={this.renderDecorations} + plugins={plugins} /> ); } From 4192945a3b2c85d606e046ec70bffe0f69017ec5 Mon Sep 17 00:00:00 2001 From: Andrey Okonetchnikov Date: Wed, 28 Sep 2016 12:23:26 +0200 Subject: [PATCH 25/57] Updated jsx-indent-props rule --- .eslintrc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.eslintrc b/.eslintrc index 1d2c26bc..daf204af 100644 --- a/.eslintrc +++ b/.eslintrc @@ -74,7 +74,7 @@ rules: react/jsx-curly-spacing: 1 react/jsx-equals-spacing: 1 react/jsx-handler-names: 1 - react/jsx-indent-props: 1 + react/jsx-indent-props: [2, 2] react/jsx-indent: [2, 2] react/jsx-no-bind: 1 react/jsx-no-duplicate-props: 1 From d09e9b40e4fbe08b7dfc838953966573b138fcb9 Mon Sep 17 00:00:00 2001 From: Andrey Okonetchnikov Date: Wed, 28 Sep 2016 12:24:17 +0200 Subject: [PATCH 26/57] Updated styles for entry page layout. This removes the need for resize handlers and height calc anymore. Re-written the component in a functional way. --- src/components/EntryEditor.css | 15 ++++----- src/components/EntryEditor.js | 56 +++++++++++----------------------- src/components/PreviewPane.css | 2 +- 3 files changed, 26 insertions(+), 47 deletions(-) diff --git a/src/components/EntryEditor.css b/src/components/EntryEditor.css index 9cacb875..cb309fad 100644 --- a/src/components/EntryEditor.css +++ b/src/components/EntryEditor.css @@ -1,26 +1,27 @@ -.entryEditor { +.root { + position: absolute; + top: 64px; + bottom: 0; display: flex; flex-direction: column; - height: 100%; } .container { + flex: 1; display: flex; - height: 100%; } .footer { + flex: 0; background: #fff; height: 45px; border-top: 1px solid #e8eae8; padding: 10px 20px; - z-index: 10; } .controlPane { - width: 50%; - max-height: 100%; + flex: 1; overflow: auto; padding: 0 20px; border-right: 1px solid #e8eae8; } .previewPane { - width: 50%; + flex: 1; } diff --git a/src/components/EntryEditor.js b/src/components/EntryEditor.js index fd308698..2bd872ea 100644 --- a/src/components/EntryEditor.js +++ b/src/components/EntryEditor.js @@ -4,57 +4,35 @@ import ControlPane from './ControlPane'; import PreviewPane from './PreviewPane'; import styles from './EntryEditor.css'; -export default class EntryEditor extends React.Component { - constructor(props) { - super(props); - this.state = {}; - this.handleResize = this.handleResize.bind(this); - } +export default function EntryEditor(props) { + const { collection, entry, getMedia, onChange, onAddMedia, onRemoveMedia, onPersist } = props; - componentDidMount() { - this.calculateHeight(); - window.addEventListener('resize', this.handleResize, false); - } - - componengWillUnmount() { - window.removeEventListener('resize', this.handleResize); - } - - handleResize() { - this.calculateHeight(); - } - - calculateHeight() { - const height = window.innerHeight - 54; - console.log('setting height to %s', height); - this.setState({ height }); - } - - render() { - const { collection, entry, getMedia, onChange, onAddMedia, onRemoveMedia, onPersist } = this.props; - const { height } = this.state; - - return
+ return ( +
- +
-
; - } +
+ ); } EntryEditor.propTypes = { diff --git a/src/components/PreviewPane.css b/src/components/PreviewPane.css index af8c2001..6bf62a0a 100644 --- a/src/components/PreviewPane.css +++ b/src/components/PreviewPane.css @@ -1,6 +1,6 @@ .frame { width: 100%; - height: 100vh; + height: 100%; border: none; background: #fff; } From 0a3676204e718f3f890de53e720a8be613e33348 Mon Sep 17 00:00:00 2001 From: Andrey Okonetchnikov Date: Wed, 28 Sep 2016 12:46:39 +0200 Subject: [PATCH 27/57] Localized CSS --- src/components/ControlPane.js | 38 ------------- src/components/ControlPanel/ControlPane.css | 53 ++++++++++++++++++ src/components/ControlPanel/ControlPane.js | 56 +++++++++++++++++++ src/components/EntryEditor.js | 2 +- .../RawEditor/index.css | 13 +++++ .../RawEditor/index.js | 14 +++-- src/index.css | 53 ------------------ 7 files changed, 131 insertions(+), 98 deletions(-) delete mode 100644 src/components/ControlPane.js create mode 100644 src/components/ControlPanel/ControlPane.css create mode 100644 src/components/ControlPanel/ControlPane.js create mode 100644 src/components/Widgets/MarkdownControlElements/RawEditor/index.css diff --git a/src/components/ControlPane.js b/src/components/ControlPane.js deleted file mode 100644 index fc39041e..00000000 --- a/src/components/ControlPane.js +++ /dev/null @@ -1,38 +0,0 @@ -import React, { PropTypes } from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import { resolveWidget } from './Widgets'; - -export default class ControlPane extends React.Component { - controlFor(field) { - const { entry, getMedia, onChange, onAddMedia, onRemoveMedia } = this.props; - const widget = resolveWidget(field.get('widget')); - return
- - {React.createElement(widget.control, { - field: field, - value: entry.getIn(['data', field.get('name')]), - onChange: (value) => onChange(entry.setIn(['data', field.get('name')], value)), - onAddMedia: onAddMedia, - onRemoveMedia: onRemoveMedia, - getMedia: getMedia - })} -
; - } - - render() { - const { collection } = this.props; - if (!collection) { return null; } - return
- {collection.get('fields').map((field) =>
{this.controlFor(field)}
)} -
; - } -} - -ControlPane.propTypes = { - collection: ImmutablePropTypes.map.isRequired, - entry: ImmutablePropTypes.map.isRequired, - getMedia: PropTypes.func.isRequired, - onAddMedia: PropTypes.func.isRequired, - onChange: PropTypes.func.isRequired, - onRemoveMedia: PropTypes.func.isRequired, -}; diff --git a/src/components/ControlPanel/ControlPane.css b/src/components/ControlPanel/ControlPane.css new file mode 100644 index 00000000..328ddf27 --- /dev/null +++ b/src/components/ControlPanel/ControlPane.css @@ -0,0 +1,53 @@ +.control { + color: #7c8382; + 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 { + 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/ControlPanel/ControlPane.js b/src/components/ControlPanel/ControlPane.js new file mode 100644 index 00000000..fed6425f --- /dev/null +++ b/src/components/ControlPanel/ControlPane.js @@ -0,0 +1,56 @@ +import React, { PropTypes } from 'react'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import { resolveWidget } from '../Widgets'; +import styles from './ControlPane.css'; + +export default class ControlPane extends React.Component { + controlFor(field) { + const { entry, getMedia, onChange, onAddMedia, onRemoveMedia } = this.props; + const widget = resolveWidget(field.get('widget')); + return ( +
+ + {React.createElement(widget.control, { + field: field, + value: entry.getIn(['data', field.get('name')]), + onChange: (value) => onChange(entry.setIn(['data', field.get('name')], value)), + onAddMedia: onAddMedia, + onRemoveMedia: onRemoveMedia, + getMedia: getMedia + })} +
+ ); + } + + render() { + const { collection } = this.props; + if (!collection) { + return null; + } + return ( +
+ { + collection + .get('fields') + .map(field => +
+ {this.controlFor(field)} +
+ ) + } +
+ ); + } +} + +ControlPane.propTypes = { + collection: ImmutablePropTypes.map.isRequired, + entry: ImmutablePropTypes.map.isRequired, + getMedia: PropTypes.func.isRequired, + onAddMedia: PropTypes.func.isRequired, + onChange: PropTypes.func.isRequired, + onRemoveMedia: PropTypes.func.isRequired, +}; diff --git a/src/components/EntryEditor.js b/src/components/EntryEditor.js index 2bd872ea..d3a07f2a 100644 --- a/src/components/EntryEditor.js +++ b/src/components/EntryEditor.js @@ -1,6 +1,6 @@ import React, { PropTypes } from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; -import ControlPane from './ControlPane'; +import ControlPane from './ControlPanel/ControlPane'; import PreviewPane from './PreviewPane'; import styles from './EntryEditor.css'; diff --git a/src/components/Widgets/MarkdownControlElements/RawEditor/index.css b/src/components/Widgets/MarkdownControlElements/RawEditor/index.css new file mode 100644 index 00000000..eca2a8d4 --- /dev/null +++ b/src/components/Widgets/MarkdownControlElements/RawEditor/index.css @@ -0,0 +1,13 @@ +.root { + 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; +} diff --git a/src/components/Widgets/MarkdownControlElements/RawEditor/index.js b/src/components/Widgets/MarkdownControlElements/RawEditor/index.js index c9e640c0..0670e1fe 100644 --- a/src/components/Widgets/MarkdownControlElements/RawEditor/index.js +++ b/src/components/Widgets/MarkdownControlElements/RawEditor/index.js @@ -3,6 +3,7 @@ import { Editor, Plain, Mark } from 'slate'; import Prism from 'prismjs'; import PluginDropImages from 'slate-drop-or-paste-images'; import marks from './prismMarkdown'; +import styles from './index.css'; Prism.languages.markdown = Prism.languages.extend('markup', {}); Prism.languages.insertBefore('markdown', 'prolog', marks); @@ -114,12 +115,13 @@ class RawEditor extends React.Component { render() { return ( ); } diff --git a/src/index.css b/src/index.css index 296bdf5a..69aac2cf 100644 --- a/src/index.css +++ b/src/index.css @@ -36,59 +36,6 @@ h1 { font-size: 25px; } -:global { - & .cms-widget { - border-bottom: 1px solid #e8eae8; - position: relative; - } - & .cms-widget: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; - } - & .cms-widget:last-child { - border-bottom: none; - } - & .cms-widget:last-child:after { - display: none; - } - & .cms-control { - color: #7c8382; - position: relative; - padding: 20px 0; - & label { - color: #AAB0AF; - font-size: 12px; - margin-bottom: 18px; - } - & input, - & textarea, - & select, - & .cms-editor-raw { - 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; - } - } -} - :global { & .rdt { position: relative; From 47512001ec936423dfcb0c8df308544e322a5133 Mon Sep 17 00:00:00 2001 From: Andrey Okonetchnikov Date: Wed, 28 Sep 2016 14:05:51 +0200 Subject: [PATCH 28/57] Implement image uploading for the raw editor --- .../RawEditor/index.js | 36 ++++----- .../VisualEditor/index.js | 75 +++++++++---------- src/components/Widgets/MarkdownPreview.js | 27 +++---- 3 files changed, 69 insertions(+), 69 deletions(-) diff --git a/src/components/Widgets/MarkdownControlElements/RawEditor/index.js b/src/components/Widgets/MarkdownControlElements/RawEditor/index.js index 0670e1fe..b6d73067 100644 --- a/src/components/Widgets/MarkdownControlElements/RawEditor/index.js +++ b/src/components/Widgets/MarkdownControlElements/RawEditor/index.js @@ -2,6 +2,7 @@ import React, { PropTypes } from 'react'; import { Editor, Plain, Mark } from 'slate'; import Prism from 'prismjs'; import PluginDropImages from 'slate-drop-or-paste-images'; +import MediaProxy from '../../../../valueObjects/MediaProxy'; import marks from './prismMarkdown'; import styles from './index.css'; @@ -71,16 +72,6 @@ const SCHEMA = { } }; -const plugins = [ - PluginDropImages({ - applyTransform: (transform, file) => { - const state = Plain.deserialize(`\n\n![${file.name}](${file.name})\n\n`); - return transform - .insertFragment(state.get('document')); - } - }) -]; - class RawEditor extends React.Component { constructor(props) { @@ -92,9 +83,18 @@ class RawEditor extends React.Component { state: content }; - this.handleChange = this.handleChange.bind(this); - this.handleDocumentChange = this.handleDocumentChange.bind(this); - + this.plugins = [ + PluginDropImages({ + applyTransform: (transform, file) => { + const mediaProxy = new MediaProxy(file.name, file); + console.log(mediaProxy); + const state = Plain.deserialize(`\n\n![${file.name}](${mediaProxy.public_path})\n\n`); + props.onAddMedia(mediaProxy); + return transform + .insertFragment(state.get('document')); + } + }) + ]; } /** @@ -103,11 +103,11 @@ class RawEditor extends React.Component { * It also have an onDocumentChange, that get's dispatched only when the actual * content changes */ - handleChange(state) { + handleChange = state => { this.setState({ state }); } - handleDocumentChange(document, state) { + handleDocumentChange = (document, state) => { const content = Plain.serialize(state, { terse: true }); this.props.onChange(content); } @@ -121,7 +121,7 @@ class RawEditor extends React.Component { schema={SCHEMA} onChange={this.handleChange} onDocumentChange={this.handleDocumentChange} - plugins={plugins} + plugins={this.plugins} /> ); } @@ -130,6 +130,8 @@ class RawEditor extends React.Component { export default RawEditor; RawEditor.propTypes = { + onAddMedia: PropTypes.func.isRequired, + getMedia: PropTypes.func.isRequired, onChange: PropTypes.func.isRequired, - value: PropTypes.node, + value: PropTypes.string, }; diff --git a/src/components/Widgets/MarkdownControlElements/VisualEditor/index.js b/src/components/Widgets/MarkdownControlElements/VisualEditor/index.js index 0f9eb696..db455166 100644 --- a/src/components/Widgets/MarkdownControlElements/VisualEditor/index.js +++ b/src/components/Widgets/MarkdownControlElements/VisualEditor/index.js @@ -8,7 +8,6 @@ import { DEFAULT_NODE, SCHEMA } from './schema'; import { getNodes, getSyntaxes, getPlugins } from '../../richText'; import StylesMenu from './StylesMenu'; import BlockTypesMenu from './BlockTypesMenu'; -import styles from './index.css'; /** * Slate Render Configuration @@ -178,11 +177,11 @@ class VisualEditor extends React.Component { } /** - * When clicking a link, if the selection has a link in it, remove the link. - * Otherwise, add a new link with an href and text. - * - * @param {Event} e - */ + * When clicking a link, if the selection has a link in it, remove the link. + * Otherwise, add a new link with an href and text. + * + * @param {Event} e + */ handleInlineClick(type, isActive) { let { state } = this.state; @@ -212,17 +211,16 @@ class VisualEditor extends React.Component { this.setState({ state }); } - handleBlockTypeClick(type) { let { state } = this.state; state = state - .transform() - .insertBlock({ - type: type, - isVoid: true - }) - .apply(); + .transform() + .insertBlock({ + type: type, + isVoid: true + }) + .apply(); this.setState({ state }, this.focusAndAddParagraph); } @@ -277,18 +275,17 @@ class VisualEditor extends React.Component { .apply({ snapshot: false }); - this.setState({ state:normalized }); + this.setState({ state: normalized }); } - handleKeyDown(evt) { if (evt.shiftKey && evt.key === 'Enter') { this.blockEdit = true; let { state } = this.state; state = state - .transform() - .insertText(' \n') - .apply(); + .transform() + .insertText(' \n') + .apply(); this.setState({ state }); } @@ -300,12 +297,12 @@ class VisualEditor extends React.Component { return ( ); } @@ -316,14 +313,14 @@ class VisualEditor extends React.Component { return ( ); } @@ -334,12 +331,12 @@ class VisualEditor extends React.Component { {this.renderStylesMenu()} {this.renderBlockTypesMenu()} ); @@ -352,5 +349,5 @@ VisualEditor.propTypes = { onChange: PropTypes.func.isRequired, onAddMedia: PropTypes.func.isRequired, getMedia: PropTypes.func.isRequired, - value: PropTypes.node, + value: PropTypes.string, }; diff --git a/src/components/Widgets/MarkdownPreview.js b/src/components/Widgets/MarkdownPreview.js index b88435b9..3c84d884 100644 --- a/src/components/Widgets/MarkdownPreview.js +++ b/src/components/Widgets/MarkdownPreview.js @@ -2,31 +2,32 @@ import React, { PropTypes } from 'react'; import { getSyntaxes } from './richText'; import MarkupItReactRenderer from '../MarkupItReactRenderer/index'; -const schema = { - 'mediaproxy': ({ token }) => ( - {token.getIn(['data', - ) -}; - -const MarkdownPreview = ({ value }) => { +const MarkdownPreview = ({ value, getMedia }) => { if (value == null) { return null; } + const schema = { + 'mediaproxy': ({ token }) => ( // eslint-disable-line + {token.getIn(['data', + ) + }; + const { markdown } = getSyntaxes(); return ( ); }; MarkdownPreview.propTypes = { + getMedia: PropTypes.func.isRequired, value: PropTypes.string, }; From cfc8be3f362df503eeee180d4251e075d4ad13b1 Mon Sep 17 00:00:00 2001 From: Andrey Okonetchnikov Date: Thu, 29 Sep 2016 18:48:23 +0200 Subject: [PATCH 29/57] Removed console.log call --- .../Widgets/MarkdownControlElements/RawEditor/index.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/Widgets/MarkdownControlElements/RawEditor/index.js b/src/components/Widgets/MarkdownControlElements/RawEditor/index.js index b6d73067..cdc32bed 100644 --- a/src/components/Widgets/MarkdownControlElements/RawEditor/index.js +++ b/src/components/Widgets/MarkdownControlElements/RawEditor/index.js @@ -87,7 +87,6 @@ class RawEditor extends React.Component { PluginDropImages({ applyTransform: (transform, file) => { const mediaProxy = new MediaProxy(file.name, file); - console.log(mediaProxy); const state = Plain.deserialize(`\n\n![${file.name}](${mediaProxy.public_path})\n\n`); props.onAddMedia(mediaProxy); return transform From e454144d31d0ba06e6ae234816bd0770dea7d3ab Mon Sep 17 00:00:00 2001 From: Andrey Okonetchnikov Date: Thu, 29 Sep 2016 18:51:39 +0200 Subject: [PATCH 30/57] Use HoC `withPortalAtCursorPosition` for StylesMenu and BlockTypesMenu to DRY --- .../VisualEditor/BlockTypesMenu.js | 76 +++++-------------- .../VisualEditor/StylesMenu.js | 74 +++++------------- .../withPortalAtCursorPosition.js | 59 ++++++++++++++ 3 files changed, 97 insertions(+), 112 deletions(-) create mode 100644 src/components/Widgets/MarkdownControlElements/VisualEditor/withPortalAtCursorPosition.js diff --git a/src/components/Widgets/MarkdownControlElements/VisualEditor/BlockTypesMenu.js b/src/components/Widgets/MarkdownControlElements/VisualEditor/BlockTypesMenu.js index 0855d40b..78c6b5d0 100644 --- a/src/components/Widgets/MarkdownControlElements/VisualEditor/BlockTypesMenu.js +++ b/src/components/Widgets/MarkdownControlElements/VisualEditor/BlockTypesMenu.js @@ -1,21 +1,18 @@ import React, { Component, PropTypes } from 'react'; -import Portal from 'react-portal'; +import withPortalAtCursorPosition from './withPortalAtCursorPosition'; import { Icon } from '../../../UI'; import MediaProxy from '../../../../valueObjects/MediaProxy'; import styles from './BlockTypesMenu.css'; -export default class BlockTypesMenu extends Component { +class BlockTypesMenu extends Component { constructor(props) { super(props); this.state = { - expanded: false, - menu: null + expanded: false }; - this.updateMenuPosition = this.updateMenuPosition.bind(this); this.toggleMenu = this.toggleMenu.bind(this); - this.handleOpen = this.handleOpen.bind(this); this.handleBlockTypeClick = this.handleBlockTypeClick.bind(this); this.handlePluginClick = this.handlePluginClick.bind(this); this.handleFileUploadClick = this.handleFileUploadClick.bind(this); @@ -24,34 +21,12 @@ export default class BlockTypesMenu extends Component { this.renderPluginButton = this.renderPluginButton.bind(this); } - /** - * On update, update the menu. - */ - componentDidMount() { - this.updateMenuPosition(); - } - componentWillUpdate() { if (this.state.expanded) { this.setState({ expanded: false }); } } - componentDidUpdate() { - this.updateMenuPosition(); - } - - updateMenuPosition() { - const { menu } = this.state; - const { position } = this.props; - if (!menu) return; - - menu.style.opacity = 1; - menu.style.top = `${position.top}px`; - menu.style.left = `${position.left - menu.offsetWidth * 2}px`; - - } - toggleMenu() { this.setState({ expanded: !this.state.expanded }); } @@ -63,7 +38,7 @@ export default class BlockTypesMenu extends Component { handlePluginClick(e, plugin) { const data = {}; plugin.fields.forEach(field => { - data[field.name] = window.prompt(field.label); + data[field.name] = window.prompt(field.label); // eslint-disable-line }); this.props.onClickPlugin(plugin.id, data); } @@ -97,14 +72,14 @@ export default class BlockTypesMenu extends Component { renderBlockTypeButton(type, icon) { const onClick = e => this.handleBlockTypeClick(e, type); return ( - + ); } renderPluginButton(plugin) { const onClick = e => this.handlePluginClick(e, plugin); return ( - + ); } @@ -115,13 +90,15 @@ export default class BlockTypesMenu extends Component {
{this.renderBlockTypeButton('hr', 'dot-3')} {plugins.map(plugin => this.renderPluginButton(plugin))} - + this._fileInput = el} + type="file" + accept="image/*" + onChange={this.handleFileUploadChange} + className={styles.input} + ref={el => { + this._fileInput = el; + }} />
); @@ -130,34 +107,21 @@ export default class BlockTypesMenu extends Component { } } - /** - * When the portal opens, cache the menu element. - */ - handleOpen(portal) { - this.setState({ menu: portal.firstChild }); - } - render() { - const { isOpen } = this.props; return ( - -
- - {this.renderMenu()} -
-
+
+ + {this.renderMenu()} +
); } } BlockTypesMenu.propTypes = { - isOpen: PropTypes.bool.isRequired, plugins: PropTypes.array.isRequired, - position: PropTypes.shape({ - top: PropTypes.number.isRequired, - left: PropTypes.number.isRequired - }), onClickBlock: PropTypes.func.isRequired, onClickPlugin: PropTypes.func.isRequired, onClickImage: PropTypes.func.isRequired }; + +export default withPortalAtCursorPosition(BlockTypesMenu); diff --git a/src/components/Widgets/MarkdownControlElements/VisualEditor/StylesMenu.js b/src/components/Widgets/MarkdownControlElements/VisualEditor/StylesMenu.js index f2aafc3e..e2c464c7 100644 --- a/src/components/Widgets/MarkdownControlElements/VisualEditor/StylesMenu.js +++ b/src/components/Widgets/MarkdownControlElements/VisualEditor/StylesMenu.js @@ -1,48 +1,21 @@ import React, { Component, PropTypes } from 'react'; -import Portal from 'react-portal'; +import withPortalAtCursorPosition from './withPortalAtCursorPosition'; import { Icon } from '../../../UI'; import styles from './StylesMenu.css'; -export default class StylesMenu extends Component { +class StylesMenu extends Component { - constructor(props) { - super(props); - - this.state = { - menu: null - }; + constructor() { + super(); this.hasMark = this.hasMark.bind(this); this.hasBlock = this.hasBlock.bind(this); this.renderMarkButton = this.renderMarkButton.bind(this); this.renderBlockButton = this.renderBlockButton.bind(this); this.renderLinkButton = this.renderLinkButton.bind(this); - this.updateMenuPosition = this.updateMenuPosition.bind(this); this.handleMarkClick = this.handleMarkClick.bind(this); this.handleInlineClick = this.handleInlineClick.bind(this); this.handleBlockClick = this.handleBlockClick.bind(this); - this.handleOpen = this.handleOpen.bind(this); - } - - /** - * On update, update the menu. - */ - componentDidMount() { - this.updateMenuPosition(); - } - - componentDidUpdate() { - this.updateMenuPosition(); - } - - updateMenuPosition() { - const { menu } = this.state; - const { position } = this.props; - if (!menu) return; - - menu.style.opacity = 1; - menu.style.top = `${position.top - menu.offsetHeight}px`; - menu.style.left = `${position.left - menu.offsetWidth / 2 + position.width / 2}px`; } /** @@ -52,10 +25,12 @@ export default class StylesMenu extends Component { const { marks } = this.props; return marks.some(mark => mark.type == type); } + hasBlock(type) { const { blocks } = this.props; return blocks.some(node => node.type == type); } + hasLinks(type) { const { inlines } = this.props; return inlines.some(inline => inline.type == 'link'); @@ -109,39 +84,24 @@ export default class StylesMenu extends Component { ); } - /** - * When the portal opens, cache the menu element. - */ - handleOpen(portal) { - this.setState({ menu: portal.firstChild }); - } - render() { - const { isOpen } = this.props; return ( - -
- {this.renderMarkButton('BOLD', 'bold')} - {this.renderMarkButton('ITALIC', 'italic')} - {this.renderMarkButton('CODE', 'code')} - {this.renderLinkButton()} - {this.renderBlockButton('header_one', 'h1')} - {this.renderBlockButton('header_two', 'h2')} - {this.renderBlockButton('blockquote', 'quote-left')} - {this.renderBlockButton('unordered_list', 'list-bullet', 'list_item')} -
-
+
+ {this.renderMarkButton('BOLD', 'bold')} + {this.renderMarkButton('ITALIC', 'italic')} + {this.renderMarkButton('CODE', 'code')} + {this.renderLinkButton()} + {this.renderBlockButton('header_one', 'h1')} + {this.renderBlockButton('header_two', 'h2')} + {this.renderBlockButton('blockquote', 'quote-left')} + {this.renderBlockButton('unordered_list', 'list-bullet', 'list_item')} +
); } } StylesMenu.propTypes = { - isOpen: PropTypes.bool.isRequired, - position: PropTypes.shape({ - top: PropTypes.number.isRequired, - left: PropTypes.number.isRequired - }), marks: PropTypes.object.isRequired, blocks: PropTypes.object.isRequired, inlines: PropTypes.object.isRequired, @@ -149,3 +109,5 @@ StylesMenu.propTypes = { onClickMark: PropTypes.func.isRequired, onClickInline: PropTypes.func.isRequired }; + +export default withPortalAtCursorPosition(StylesMenu); diff --git a/src/components/Widgets/MarkdownControlElements/VisualEditor/withPortalAtCursorPosition.js b/src/components/Widgets/MarkdownControlElements/VisualEditor/withPortalAtCursorPosition.js new file mode 100644 index 00000000..4aab4ea6 --- /dev/null +++ b/src/components/Widgets/MarkdownControlElements/VisualEditor/withPortalAtCursorPosition.js @@ -0,0 +1,59 @@ +import React from 'react'; +import Portal from 'react-portal'; +import position from 'selection-position'; + +export default function withPortalAtCursorPosition(WrappedComponent) { + return class extends React.Component { + + static propTypes = { + isOpen: React.PropTypes.bool.isRequired + } + + state = { + menu: null, + cursorPosition: null + } + + componentDidMount() { + this.adjustPosition(); + } + + componentDidUpdate() { + this.adjustPosition(); + } + + adjustPosition = () => { + const { menu } = this.state; + + if (!menu) return; + + const cursorPosition = position(); // TODO: Results aren't determenistic + const centerX = Math.ceil( + cursorPosition.left + + cursorPosition.width / 2 + + window.scrollX + - menu.offsetWidth / 2 + ); + const centerY = cursorPosition.top + window.scrollY; + menu.style.opacity = 1; + menu.style.top = `${centerY}px`; + menu.style.left = `${centerX}px`; + } + + /** + * When the portal opens, cache the menu element. + */ + handleOpen = (portal) => { + this.setState({ menu: portal.firstChild }); + } + + render() { + const { isOpen, ...rest } = this.props; + return ( + + + + ); + } + }; +} From e644104542cf2acfbaeb5d8c0d686e58a41a6ee1 Mon Sep 17 00:00:00 2001 From: Andrey Okonetchnikov Date: Thu, 29 Sep 2016 18:53:47 +0200 Subject: [PATCH 31/57] Added drag'n'drop image adding to VisualEditor. Removed StylesMenu and BlockTypesMenu positioning code since it didn't work with adding images using DnD. --- .../VisualEditor/index.js | 78 +++++-------------- .../MarkdownControlElements/constants.js | 17 +++- 2 files changed, 35 insertions(+), 60 deletions(-) diff --git a/src/components/Widgets/MarkdownControlElements/VisualEditor/index.js b/src/components/Widgets/MarkdownControlElements/VisualEditor/index.js index db455166..c3a4f93c 100644 --- a/src/components/Widgets/MarkdownControlElements/VisualEditor/index.js +++ b/src/components/Widgets/MarkdownControlElements/VisualEditor/index.js @@ -1,9 +1,10 @@ import React, { PropTypes } from 'react'; import _ from 'lodash'; import { Editor, Raw } from 'slate'; -import position from 'selection-position'; +import PluginDropImages from 'slate-drop-or-paste-images'; import MarkupIt, { SlateUtils } from 'markup-it'; -import { emptyParagraphBlock } from '../constants'; +import MediaProxy from '../../../../valueObjects/MediaProxy'; +import { emptyParagraphBlock, mediaproxyBlock } from '../constants'; import { DEFAULT_NODE, SCHEMA } from './schema'; import { getNodes, getSyntaxes, getPlugins } from '../../richText'; import StylesMenu from './StylesMenu'; @@ -23,25 +24,10 @@ class VisualEditor extends React.Component { SCHEMA.nodes = _.merge(SCHEMA.nodes, getNodes()); this.blockEdit = false; - this.menuPositions = { - stylesMenu: { - top: 0, - left: 0, - width: 0, - height: 0 - }, - blockTypesMenu: { - top: 0, - left: 0, - width: 0, - height: 0 - } - }; let rawJson; if (props.value !== undefined) { const content = this.markdown.toContent(props.value); - console.log('md: %o', content); rawJson = SlateUtils.encode(content, null, ['mediaproxy'].concat(getPlugins().map(plugin => plugin.id))); } else { rawJson = emptyParagraphBlock; @@ -50,6 +36,17 @@ class VisualEditor extends React.Component { state: Raw.deserialize(rawJson, { terse: true }) }; + this.plugins = [ + PluginDropImages({ + applyTransform: (transform, file) => { + const mediaProxy = new MediaProxy(file.name, file); + props.onAddMedia(mediaProxy); + return transform + .insertBlock(mediaproxyBlock(mediaProxy)); + } + }) + ]; + this.handleChange = this.handleChange.bind(this); this.handleDocumentChange = this.handleDocumentChange.bind(this); this.handleMarkStyleClick = this.handleMarkStyleClick.bind(this); @@ -60,8 +57,6 @@ class VisualEditor extends React.Component { this.handleImageClick = this.handleImageClick.bind(this); this.focusAndAddParagraph = this.focusAndAddParagraph.bind(this); this.handleKeyDown = this.handleKeyDown.bind(this); - this.calculateHoverMenuPosition = _.throttle(this.calculateHoverMenuPosition.bind(this), 30); - this.calculateBlockMenuPosition = _.throttle(this.calculateBlockMenuPosition.bind(this), 100); this.renderBlockTypesMenu = this.renderBlockTypesMenu.bind(this); } @@ -79,8 +74,7 @@ class VisualEditor extends React.Component { if (this.blockEdit) { this.blockEdit = false; } else { - this.calculateHoverMenuPosition(); - this.setState({ state }, this.calculateBlockMenuPosition); + this.setState({ state }); } } @@ -90,32 +84,6 @@ class VisualEditor extends React.Component { this.props.onChange(this.markdown.toText(content)); } - calculateHoverMenuPosition() { - const rect = position(); - this.menuPositions.stylesMenu = { - top: rect.top + window.scrollY, - left: rect.left + window.scrollX, - width: rect.width, - height: rect.height - }; - } - - calculateBlockMenuPosition() { - // Don't bother calculating position if block is not empty - if (this.state.state.blocks.get(0).isEmpty) { - const blockElement = document.querySelectorAll(`[data-key='${this.state.state.selection.focusKey}']`); - if (blockElement.length > 0) { - const rect = blockElement[0].getBoundingClientRect(); - this.menuPositions.blockTypesMenu = { - top: rect.top + window.scrollY, - left: rect.left + window.scrollX - }; - // Force re-render so the menu is positioned on these new coordinates - this.forceUpdate(); - } - } - } - /** * Toggle marks / blocks when button is clicked */ @@ -197,7 +165,7 @@ class VisualEditor extends React.Component { } else { - const href = window.prompt('Enter the URL of the link:', 'http://www.'); + const href = window.prompt('Enter the URL of the link:', 'http://www.'); // eslint-disable-line state = state .transform() .wrapInline({ @@ -249,14 +217,7 @@ class VisualEditor extends React.Component { state = state .transform() - .insertInline({ - type: 'mediaproxy', - isVoid: true, - data: { src: mediaProxy.public_path } - }) - .collapseToEnd() - .insertBlock(DEFAULT_NODE) - .focus() + .insertBlock(mediaproxyBlock(mediaProxy)) .apply(); this.setState({ state }); @@ -284,7 +245,7 @@ class VisualEditor extends React.Component { let { state } = this.state; state = state .transform() - .insertText(' \n') + .insertText('\n') .apply(); this.setState({ state }); @@ -299,7 +260,6 @@ class VisualEditor extends React.Component { ({ + kind: 'block', + type: 'paragraph', + nodes: [{ + kind: 'inline', + type: 'mediaproxy', + isVoid: true, + data: { + alt: mediaproxy.name, + src: mediaproxy.public_path + } + }] +}); From 9f33b160e7d28e4708f2c161d3e46ffa2a2e3a60 Mon Sep 17 00:00:00 2001 From: Andrey Okonetchnikov Date: Thu, 29 Sep 2016 18:55:01 +0200 Subject: [PATCH 32/57] Added some more rules to eslintrc. Removed unnecessary globals from it. --- .eslintrc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.eslintrc b/.eslintrc index daf204af..bc72a70b 100644 --- a/.eslintrc +++ b/.eslintrc @@ -33,6 +33,8 @@ rules: # Stylistic Issues # https://github.com/eslint/eslint/tree/master/docs/rules#stylistic-issues + no-alert: 2 + no-console: [2, { allow: ["warn", "error"] }] comma-spacing: 2 eol-last: 2 indent: [2, 2, {SwitchCase: 1}] @@ -101,8 +103,6 @@ rules: # Global scoped method and vars globals: netlify: true - describe: true - it: true require: true process: true module: true From 841772496a4e9ff5a3c6d516e8a19de48d3978d5 Mon Sep 17 00:00:00 2001 From: Andrey Okonetchnikov Date: Thu, 29 Sep 2016 19:02:28 +0200 Subject: [PATCH 33/57] Moved EntryEditor to a separate directory --- src/components/{ => EntryEditor}/EntryEditor.css | 0 src/components/{ => EntryEditor}/EntryEditor.js | 4 ++-- src/containers/EntryPage.js | 16 ++++++++-------- 3 files changed, 10 insertions(+), 10 deletions(-) rename src/components/{ => EntryEditor}/EntryEditor.css (100%) rename src/components/{ => EntryEditor}/EntryEditor.js (93%) diff --git a/src/components/EntryEditor.css b/src/components/EntryEditor/EntryEditor.css similarity index 100% rename from src/components/EntryEditor.css rename to src/components/EntryEditor/EntryEditor.css diff --git a/src/components/EntryEditor.js b/src/components/EntryEditor/EntryEditor.js similarity index 93% rename from src/components/EntryEditor.js rename to src/components/EntryEditor/EntryEditor.js index d3a07f2a..9095584c 100644 --- a/src/components/EntryEditor.js +++ b/src/components/EntryEditor/EntryEditor.js @@ -1,7 +1,7 @@ import React, { PropTypes } from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; -import ControlPane from './ControlPanel/ControlPane'; -import PreviewPane from './PreviewPane'; +import ControlPane from '../ControlPanel/ControlPane'; +import PreviewPane from '../PreviewPane'; import styles from './EntryEditor.css'; export default function EntryEditor(props) { diff --git a/src/containers/EntryPage.js b/src/containers/EntryPage.js index 36cfb71a..a5426951 100644 --- a/src/containers/EntryPage.js +++ b/src/containers/EntryPage.js @@ -11,7 +11,7 @@ import { } from '../actions/entries'; import { addMedia, removeMedia } from '../actions/media'; import { selectEntry, getMedia } from '../reducers'; -import EntryEditor from '../components/EntryEditor'; +import EntryEditor from '../components/EntryEditor/EntryEditor'; import EntryPageHOC from './editorialWorkflow/EntryPageHOC'; class EntryPage extends React.Component { @@ -63,13 +63,13 @@ class EntryPage extends React.Component { } return ( ); } From edf8abbc429d7d59473394281a2212cd0460ed9a Mon Sep 17 00:00:00 2001 From: Andrey Okonetchnikov Date: Thu, 29 Sep 2016 22:17:29 +0200 Subject: [PATCH 34/57] Implemented scroll sync from control pane to the preview pane. --- src/components/ControlPanel/ControlPane.js | 23 ++++--- src/components/EntryEditor/EntryEditor.js | 77 ++++++++++++++-------- src/components/PreviewPane.js | 62 +++++++++++------ 3 files changed, 106 insertions(+), 56 deletions(-) diff --git a/src/components/ControlPanel/ControlPane.js b/src/components/ControlPanel/ControlPane.js index fed6425f..939dbb24 100644 --- a/src/components/ControlPanel/ControlPane.js +++ b/src/components/ControlPanel/ControlPane.js @@ -1,23 +1,26 @@ -import React, { PropTypes } from 'react'; +import React, { Component, PropTypes } from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; import { resolveWidget } from '../Widgets'; import styles from './ControlPane.css'; -export default class ControlPane extends React.Component { +export default class ControlPane extends Component { + controlFor(field) { const { entry, getMedia, onChange, onAddMedia, onRemoveMedia } = this.props; const widget = resolveWidget(field.get('widget')); return (
- {React.createElement(widget.control, { - field: field, - value: entry.getIn(['data', field.get('name')]), - onChange: (value) => onChange(entry.setIn(['data', field.get('name')], value)), - onAddMedia: onAddMedia, - onRemoveMedia: onRemoveMedia, - getMedia: getMedia - })} + { + React.createElement(widget.control, { + field: field, + value: entry.getIn(['data', field.get('name')]), + onChange: (value) => onChange(entry.setIn(['data', field.get('name')], value)), + onAddMedia: onAddMedia, + onRemoveMedia: onRemoveMedia, + getMedia: getMedia + }) + }
); } diff --git a/src/components/EntryEditor/EntryEditor.js b/src/components/EntryEditor/EntryEditor.js index 9095584c..ff62080d 100644 --- a/src/components/EntryEditor/EntryEditor.js +++ b/src/components/EntryEditor/EntryEditor.js @@ -1,38 +1,63 @@ -import React, { PropTypes } from 'react'; +import React, { Component, PropTypes } from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; import ControlPane from '../ControlPanel/ControlPane'; import PreviewPane from '../PreviewPane'; import styles from './EntryEditor.css'; -export default function EntryEditor(props) { - const { collection, entry, getMedia, onChange, onAddMedia, onRemoveMedia, onPersist } = props; +export default class EntryEditor extends Component { - return ( -
-
-
- + state = { + scrollTop: 0, + scrollHeight: 0, + offsetHeight: 0, + } + + handleControlPaneScroll = evt => { + const { scrollTop, scrollHeight, offsetHeight } = evt.target; + this.setState({ + scrollTop, + scrollHeight, + offsetHeight, + }); + } + + render() { + const { collection, entry, getMedia, onChange, onAddMedia, onRemoveMedia, onPersist } = this.props; + const { scrollTop, scrollHeight, offsetHeight } = this.state; + + return ( +
+
+
+ +
+
+ +
-
- +
+
-
- -
-
- ); + ); + } } EntryEditor.propTypes = { diff --git a/src/components/PreviewPane.js b/src/components/PreviewPane.js index d41185db..36daa6ca 100644 --- a/src/components/PreviewPane.js +++ b/src/components/PreviewPane.js @@ -6,6 +6,7 @@ import { resolveWidget } from './Widgets'; import styles from './PreviewPane.css'; class Preview extends React.Component { + previewFor(field) { const { entry, getMedia } = this.props; const widget = resolveWidget(field.get('widget')); @@ -18,10 +19,20 @@ class Preview extends React.Component { render() { const { collection } = this.props; - if (!collection) { return null; } + if (!collection) { + return null; + } return
- {collection.get('fields').map((field) =>
{this.previewFor(field)}
)} + { + collection.get('fields').map(field => ( +
+ {this.previewFor(field)} +
+ )) + }
; } } @@ -33,19 +44,22 @@ Preview.propTypes = { }; export default class PreviewPane extends React.Component { - constructor(props) { - super(props); - this.handleIframeRef = this.handleIframeRef.bind(this); - this.widgetFor = this.widgetFor.bind(this); + + componentDidUpdate(prevProps) { + // Update scroll position of the iframe + const { scrollTop, scrollHeight, offsetHeight, ...rest } = this.props; + const frameHeight = this.iframeBody.scrollHeight - offsetHeight; + this.iframeBody.scrollTop = frameHeight * scrollTop / (scrollHeight - offsetHeight); + + // We don't want to re-render on scroll + if (prevProps.collection !== this.props.collection || prevProps.entry !== this.props.entry) { + this.renderPreview(rest); + } } - componentDidUpdate() { - this.renderPreview(); - } - - widgetFor(name) { + widgetFor = name => { const { collection, entry, getMedia } = this.props; - const field = collection.get('fields').find((field) => field.get('name') === name); + const field = collection.get('fields').find((field) => field.get('name') === name); const widget = resolveWidget(field.get('widget')); return React.createElement(widget.preview, { field: field, @@ -54,14 +68,16 @@ export default class PreviewPane extends React.Component { }); } - renderPreview() { - const props = Object.assign({}, this.props, { widgetFor: this.widgetFor }); + renderPreview(props) { const component = registry.getPreviewTemplate(props.collection.get('name')) || Preview; - - render(React.createElement(component, props), this.previewEl); + const previewProps = { + ...props, + widgetFor: this.widgetFor + }; + render(React.createElement(component, previewProps), this.previewEl); } - handleIframeRef(ref) { + handleIframeRef = ref => { if (ref) { registry.getPreviewStyles().forEach((style) => { const linkEl = document.createElement('link'); @@ -70,14 +86,17 @@ export default class PreviewPane extends React.Component { ref.contentDocument.head.appendChild(linkEl); }); this.previewEl = document.createElement('div'); - ref.contentDocument.body.appendChild(this.previewEl); - this.renderPreview(); + this.iframeBody = ref.contentDocument.body; + this.iframeBody.appendChild(this.previewEl); + this.renderPreview(this.props); } } render() { const { collection } = this.props; - if (!collection) { return null; } + if (!collection) { + return null; + } return ; } @@ -87,4 +106,7 @@ PreviewPane.propTypes = { collection: ImmutablePropTypes.map.isRequired, entry: ImmutablePropTypes.map.isRequired, getMedia: PropTypes.func.isRequired, + scrollTop: PropTypes.number, + scrollHeight: PropTypes.number, + offsetHeight: PropTypes.number, }; From f1eb93ee7fc2f03620e930ad08c16d4a59fed681 Mon Sep 17 00:00:00 2001 From: Andrey Okonetchnikov Date: Fri, 30 Sep 2016 16:25:15 +0200 Subject: [PATCH 35/57] Moved PreviewPane to a separate directory. Extracted Preview component to a separate file. --- src/components/EntryEditor/EntryEditor.js | 2 +- src/components/PreviewPane/Preview.js | 39 +++++++++++++++++ .../{ => PreviewPane}/PreviewPane.css | 0 .../{ => PreviewPane}/PreviewPane.js | 43 ++----------------- 4 files changed, 43 insertions(+), 41 deletions(-) create mode 100644 src/components/PreviewPane/Preview.js rename src/components/{ => PreviewPane}/PreviewPane.css (100%) rename src/components/{ => PreviewPane}/PreviewPane.js (72%) diff --git a/src/components/EntryEditor/EntryEditor.js b/src/components/EntryEditor/EntryEditor.js index ff62080d..1c04fba8 100644 --- a/src/components/EntryEditor/EntryEditor.js +++ b/src/components/EntryEditor/EntryEditor.js @@ -1,7 +1,7 @@ import React, { Component, PropTypes } from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; import ControlPane from '../ControlPanel/ControlPane'; -import PreviewPane from '../PreviewPane'; +import PreviewPane from '../PreviewPane/PreviewPane'; import styles from './EntryEditor.css'; export default class EntryEditor extends Component { diff --git a/src/components/PreviewPane/Preview.js b/src/components/PreviewPane/Preview.js new file mode 100644 index 00000000..0d1dabc0 --- /dev/null +++ b/src/components/PreviewPane/Preview.js @@ -0,0 +1,39 @@ +import React, { PropTypes } from 'react'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import { resolveWidget } from '../Widgets'; + +export default class Preview extends React.Component { + + previewFor(field) { + const { entry, getMedia } = this.props; + const widget = resolveWidget(field.get('widget')); + return React.createElement(widget.preview, { + field: field, + value: entry.getIn(['data', field.get('name')]), + getMedia: getMedia, + }); + } + + render() { + const { collection } = this.props; + if (!collection) { + return null; + } + + return
+ { + collection.get('fields').map(field => ( +
+ {this.previewFor(field)} +
+ )) + } +
; + } +} + +Preview.propTypes = { + collection: ImmutablePropTypes.map.isRequired, + entry: ImmutablePropTypes.map.isRequired, + getMedia: PropTypes.func.isRequired, +}; diff --git a/src/components/PreviewPane.css b/src/components/PreviewPane/PreviewPane.css similarity index 100% rename from src/components/PreviewPane.css rename to src/components/PreviewPane/PreviewPane.css diff --git a/src/components/PreviewPane.js b/src/components/PreviewPane/PreviewPane.js similarity index 72% rename from src/components/PreviewPane.js rename to src/components/PreviewPane/PreviewPane.js index 36daa6ca..59ab0e4f 100644 --- a/src/components/PreviewPane.js +++ b/src/components/PreviewPane/PreviewPane.js @@ -1,48 +1,11 @@ import React, { PropTypes } from 'react'; import { render } from 'react-dom'; import ImmutablePropTypes from 'react-immutable-proptypes'; -import registry from '../lib/registry'; -import { resolveWidget } from './Widgets'; +import registry from '../../lib/registry'; +import { resolveWidget } from '../Widgets'; +import Preview from './Preview'; import styles from './PreviewPane.css'; -class Preview extends React.Component { - - previewFor(field) { - const { entry, getMedia } = this.props; - const widget = resolveWidget(field.get('widget')); - return React.createElement(widget.preview, { - field: field, - value: entry.getIn(['data', field.get('name')]), - getMedia: getMedia, - }); - } - - render() { - const { collection } = this.props; - if (!collection) { - return null; - } - - return
- { - collection.get('fields').map(field => ( -
- {this.previewFor(field)} -
- )) - } -
; - } -} - -Preview.propTypes = { - collection: ImmutablePropTypes.map.isRequired, - entry: ImmutablePropTypes.map.isRequired, - getMedia: PropTypes.func.isRequired, -}; - export default class PreviewPane extends React.Component { componentDidUpdate(prevProps) { From 7fde1e811ab4cc6504a7265f1b87511e531094e7 Mon Sep 17 00:00:00 2001 From: Andrey Okonetchnikov Date: Fri, 30 Sep 2016 16:38:10 +0200 Subject: [PATCH 36/57] Re-written Preview as a functional component. It also re-uses `widgetFor` prop to get widgets. --- src/components/PreviewPane/Preview.js | 36 ++++++----------------- src/components/PreviewPane/PreviewPane.js | 5 ++-- 2 files changed, 12 insertions(+), 29 deletions(-) diff --git a/src/components/PreviewPane/Preview.js b/src/components/PreviewPane/Preview.js index 0d1dabc0..86f03071 100644 --- a/src/components/PreviewPane/Preview.js +++ b/src/components/PreviewPane/Preview.js @@ -1,39 +1,21 @@ import React, { PropTypes } from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; -import { resolveWidget } from '../Widgets'; -export default class Preview extends React.Component { - - previewFor(field) { - const { entry, getMedia } = this.props; - const widget = resolveWidget(field.get('widget')); - return React.createElement(widget.preview, { - field: field, - value: entry.getIn(['data', field.get('name')]), - getMedia: getMedia, - }); +export default function Preview({ collection, widgetFor }) { + if (!collection) { + return null; } - render() { - const { collection } = this.props; - if (!collection) { - return null; - } - - return
- { - collection.get('fields').map(field => ( -
- {this.previewFor(field)} -
- )) - } -
; - } + return ( +
+ {collection.get('fields').map(field => widgetFor(field.get('name')))} +
+ ); } Preview.propTypes = { collection: ImmutablePropTypes.map.isRequired, entry: ImmutablePropTypes.map.isRequired, getMedia: PropTypes.func.isRequired, + widgetFor: PropTypes.func.isRequired, }; diff --git a/src/components/PreviewPane/PreviewPane.js b/src/components/PreviewPane/PreviewPane.js index 59ab0e4f..e97348bb 100644 --- a/src/components/PreviewPane/PreviewPane.js +++ b/src/components/PreviewPane/PreviewPane.js @@ -25,9 +25,10 @@ export default class PreviewPane extends React.Component { const field = collection.get('fields').find((field) => field.get('name') === name); const widget = resolveWidget(field.get('widget')); return React.createElement(widget.preview, { - field: field, + key: field.get('name'), value: entry.getIn(['data', field.get('name')]), - getMedia: getMedia, + field, + getMedia, }); } From b95bb595f7d60b5c44e8cf397b3588612d060b61 Mon Sep 17 00:00:00 2001 From: Andrey Okonetchnikov Date: Tue, 4 Oct 2016 17:58:26 +0200 Subject: [PATCH 37/57] Implement ScrollSync component for sync scroll between containers --- src/components/EntryEditor/EntryEditor.js | 71 +++++++----------- src/components/PreviewPane/PreviewPane.js | 35 +++++---- src/components/ScrollSync/ScrollSync.js | 81 +++++++++++++++++++++ src/components/ScrollSync/ScrollSyncPane.js | 28 +++++++ src/components/ScrollSync/index.js | 2 + src/components/stories/ScrollSync.js | 55 ++++++++++++++ src/components/stories/index.js | 1 + 7 files changed, 210 insertions(+), 63 deletions(-) create mode 100644 src/components/ScrollSync/ScrollSync.js create mode 100644 src/components/ScrollSync/ScrollSyncPane.js create mode 100644 src/components/ScrollSync/index.js create mode 100644 src/components/stories/ScrollSync.js diff --git a/src/components/EntryEditor/EntryEditor.js b/src/components/EntryEditor/EntryEditor.js index 1c04fba8..ecf523ed 100644 --- a/src/components/EntryEditor/EntryEditor.js +++ b/src/components/EntryEditor/EntryEditor.js @@ -1,63 +1,44 @@ -import React, { Component, PropTypes } from 'react'; +import React, { PropTypes } from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; +import { ScrollSync, ScrollSyncPane } from '../ScrollSync'; import ControlPane from '../ControlPanel/ControlPane'; import PreviewPane from '../PreviewPane/PreviewPane'; import styles from './EntryEditor.css'; -export default class EntryEditor extends Component { - - state = { - scrollTop: 0, - scrollHeight: 0, - offsetHeight: 0, - } - - handleControlPaneScroll = evt => { - const { scrollTop, scrollHeight, offsetHeight } = evt.target; - this.setState({ - scrollTop, - scrollHeight, - offsetHeight, - }); - } - - render() { - const { collection, entry, getMedia, onChange, onAddMedia, onRemoveMedia, onPersist } = this.props; - const { scrollTop, scrollHeight, offsetHeight } = this.state; - - return ( -
+export default function EntryEditor( + { + collection, entry, getMedia, onChange, onAddMedia, onRemoveMedia, onPersist + }) { + return ( +
+
-
- -
+ +
+ +
+
-
- -
+
+
+
- ); - } +
+ ); } EntryEditor.propTypes = { diff --git a/src/components/PreviewPane/PreviewPane.js b/src/components/PreviewPane/PreviewPane.js index e97348bb..8134d0a8 100644 --- a/src/components/PreviewPane/PreviewPane.js +++ b/src/components/PreviewPane/PreviewPane.js @@ -1,6 +1,7 @@ import React, { PropTypes } from 'react'; -import { render } from 'react-dom'; +import ReactDOM from 'react-dom'; import ImmutablePropTypes from 'react-immutable-proptypes'; +import { ScrollSyncPane } from '../ScrollSync'; import registry from '../../lib/registry'; import { resolveWidget } from '../Widgets'; import Preview from './Preview'; @@ -8,16 +9,8 @@ import styles from './PreviewPane.css'; export default class PreviewPane extends React.Component { - componentDidUpdate(prevProps) { - // Update scroll position of the iframe - const { scrollTop, scrollHeight, offsetHeight, ...rest } = this.props; - const frameHeight = this.iframeBody.scrollHeight - offsetHeight; - this.iframeBody.scrollTop = frameHeight * scrollTop / (scrollHeight - offsetHeight); - - // We don't want to re-render on scroll - if (prevProps.collection !== this.props.collection || prevProps.entry !== this.props.entry) { - this.renderPreview(rest); - } + componentDidUpdate() { + this.renderPreview(); } widgetFor = name => { @@ -30,15 +23,21 @@ export default class PreviewPane extends React.Component { field, getMedia, }); - } + }; - renderPreview(props) { - const component = registry.getPreviewTemplate(props.collection.get('name')) || Preview; + renderPreview() { + const component = registry.getPreviewTemplate(this.props.collection.get('name')) || Preview; const previewProps = { - ...props, + ...this.props, widgetFor: this.widgetFor }; - render(React.createElement(component, previewProps), this.previewEl); + // We need to use this API in order to pass context to the iframe + ReactDOM.unstable_renderSubtreeIntoContainer( + this, + + {React.createElement(component, previewProps)} + + , this.previewEl); } handleIframeRef = ref => { @@ -52,9 +51,9 @@ export default class PreviewPane extends React.Component { this.previewEl = document.createElement('div'); this.iframeBody = ref.contentDocument.body; this.iframeBody.appendChild(this.previewEl); - this.renderPreview(this.props); + this.renderPreview(); } - } + }; render() { const { collection } = this.props; diff --git a/src/components/ScrollSync/ScrollSync.js b/src/components/ScrollSync/ScrollSync.js new file mode 100644 index 00000000..35ce2fc4 --- /dev/null +++ b/src/components/ScrollSync/ScrollSync.js @@ -0,0 +1,81 @@ +import React, { Component, PropTypes } from 'react'; +import { without } from 'lodash'; + +export default class ScrollSync extends Component { + + static propTypes = { + children: PropTypes.element.isRequired, + }; + + static childContextTypes = { + registerPane: PropTypes.func, + unregisterPane: PropTypes.func, + }; + + panes = []; + + getChildContext() { + return { + registerPane: this.registerPane, + unregisterPane: this.unregisterPane, + }; + } + + registerPane = node => { + if (!this.findPane(node)) { + this.addEvents(node); + this.panes.push(node); + } + }; + + unregisterPane = node => { + if (this.findPane(node)) { + this.removeEvents(node); + this.panes = without(this.panes, node); + } + }; + + addEvents = node => { + node.onscroll = this.handlePaneScroll.bind(this, node); + // node.addEventListener('scroll', this.handlePaneScroll, false) + }; + + removeEvents = node => { + node.onscroll = null; + // node.removeEventListener('scroll', this.handlePaneScroll, false) + }; + + findPane = node => { + return this.panes.find(p => p === node); + }; + + handlePaneScroll = node => { + // const node = evt.target + window.requestAnimationFrame(() => { + this.syncScrollPositions(node); + }); + }; + + syncScrollPositions = scrolledPane => { + const { scrollTop, scrollHeight, clientHeight } = scrolledPane; + this.panes.forEach(pane => { + /* For all panes beside the currently scrolling one */ + if (scrolledPane !== pane) { + /* Remove event listeners from the node that we'll manipulate */ + this.removeEvents(pane); + /* Calculate the actual pane height */ + const paneHeight = pane.scrollHeight - clientHeight; + /* Adjust the scrollTop position of it accordingly */ + pane.scrollTop = paneHeight * scrollTop / (scrollHeight - clientHeight); + /* Re-attach event listeners after we're done scrolling */ + window.requestAnimationFrame(() => { + this.addEvents(pane); + }); + } + }); + }; + + render() { + return React.Children.only(this.props.children); + } +} diff --git a/src/components/ScrollSync/ScrollSyncPane.js b/src/components/ScrollSync/ScrollSyncPane.js new file mode 100644 index 00000000..840edf59 --- /dev/null +++ b/src/components/ScrollSync/ScrollSyncPane.js @@ -0,0 +1,28 @@ +import { Component, PropTypes } from 'react'; +import ReactDOM from 'react-dom'; + +export default class ScrollSyncPane extends Component { + + static propTypes = { + children: PropTypes.node.isRequired, + attachTo: PropTypes.any + }; + + static contextTypes = { + registerPane: PropTypes.func.isRequired, + unregisterPane: PropTypes.func.isRequired, + }; + + componentDidMount() { + this.node = this.props.attachTo || ReactDOM.findDOMNode(this); + this.context.registerPane(this.node); + } + + componentWillUnmount() { + this.context.unregisterPane(this.node); + } + + render() { + return this.props.children; + } +} diff --git a/src/components/ScrollSync/index.js b/src/components/ScrollSync/index.js new file mode 100644 index 00000000..5a50c651 --- /dev/null +++ b/src/components/ScrollSync/index.js @@ -0,0 +1,2 @@ +export { default as ScrollSync } from './ScrollSync'; +export { default as ScrollSyncPane } from './ScrollSyncPane'; diff --git a/src/components/stories/ScrollSync.js b/src/components/stories/ScrollSync.js new file mode 100644 index 00000000..53a19b94 --- /dev/null +++ b/src/components/stories/ScrollSync.js @@ -0,0 +1,55 @@ +import React from 'react'; +import ScrollSync from '../ScrollSync/ScrollSync'; +import ScrollSyncPane from '../ScrollSync/ScrollSyncPane'; +import { storiesOf } from '@kadira/storybook'; + +const paneStyle = { + border: '1px solid green', + overflow: 'auto', +}; + +storiesOf('ScrollSync', module) + .add('Default', () => ( + +
+ +
+
+

Left Pane Content

+

+ Lorem ipsum dolor sit amet, consectetur adipisicing elit. Ab aperiam doloribus + dolorum est eum eveniet exercitationem iste labore minus, neque nobis odit officiis + omnis possimus quasi rerum sed soluta veritatis. +

+
+
+
+ + +
+
+

Right Pane Content

+

+ Lorem ipsum dolor sit amet, consectetur adipisicing elit. Ab aperiam doloribus + dolorum est eum eveniet exercitationem iste labore minus, neque nobis odit officiis + omnis possimus quasi rerum sed soluta veritatis. +

+
+
+
+ + +
+
+

Third Pane Content

+

+ Lorem ipsum dolor sit amet, consectetur adipisicing elit. Ab aperiam doloribus + dolorum est eum eveniet exercitationem iste labore minus, neque nobis odit officiis + omnis possimus quasi rerum sed soluta veritatis. +

+
+
+
+
+
+ )); diff --git a/src/components/stories/index.js b/src/components/stories/index.js index 30acb086..1e73d155 100644 --- a/src/components/stories/index.js +++ b/src/components/stories/index.js @@ -3,3 +3,4 @@ import './Icon'; import './Toast'; import './FindBar'; import './MarkupItReactRenderer'; +import './ScrollSync'; From 4da6c9708af1fc88cd79764d92b18723cf46550e Mon Sep 17 00:00:00 2001 From: Andrey Okonetchnikov Date: Tue, 11 Oct 2016 10:46:24 +0200 Subject: [PATCH 38/57] Added propTypes to AppHeader and fixed ESLint errors. --- src/components/AppHeader/AppHeader.js | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/src/components/AppHeader/AppHeader.js b/src/components/AppHeader/AppHeader.js index 3f018f5b..b360db72 100644 --- a/src/components/AppHeader/AppHeader.js +++ b/src/components/AppHeader/AppHeader.js @@ -1,4 +1,5 @@ -import React from 'react'; +import React, { PropTypes } from 'react'; +import ImmutablePropTypes from 'react-immutable-proptypes'; import pluralize from 'pluralize'; import { IndexLink } from 'react-router'; import { Menu, MenuItem } from 'react-toolbox'; @@ -8,11 +9,20 @@ import styles from './AppHeader.css'; export default class AppHeader extends React.Component { - state = { - createMenuActive: false + static propTypes = { + collections: ImmutablePropTypes.list.isRequired, + commands: PropTypes.array.isRequired, // eslint-disable-line + defaultCommands: PropTypes.array.isRequired, // eslint-disable-line + runCommand: PropTypes.func.isRequired, + toggleNavDrawer: PropTypes.func.isRequired, + onCreateEntryClick: PropTypes.func.isRequired, }; - handleCreatePostClick = collectionName => { + state = { + createMenuActive: false, + }; + + handleCreatePostClick = (collectionName) => { const { onCreateEntryClick } = this.props; if (onCreateEntryClick) { onCreateEntryClick(collectionName); @@ -21,13 +31,13 @@ export default class AppHeader extends React.Component { handleCreateButtonClick = () => { this.setState({ - createMenuActive: true + createMenuActive: true, }); }; handleCreateMenuHide = () => { this.setState({ - createMenuActive: false + createMenuActive: false, }); }; @@ -37,7 +47,7 @@ export default class AppHeader extends React.Component { commands, defaultCommands, runCommand, - toggleNavDrawer + toggleNavDrawer, } = this.props; const { createMenuActive } = this.state; @@ -69,7 +79,7 @@ export default class AppHeader extends React.Component { ) From 0688a9c5ab414da22d323f807ad2098fed1c97ac Mon Sep 17 00:00:00 2001 From: Andrey Okonetchnikov Date: Tue, 11 Oct 2016 11:33:56 +0200 Subject: [PATCH 39/57] Added propTypes + some code refactoring to fix eslint errors --- src/containers/App.js | 103 ++++++++++++++++++++++++------------------ 1 file changed, 58 insertions(+), 45 deletions(-) diff --git a/src/containers/App.js b/src/containers/App.js index f96364f9..a6908658 100644 --- a/src/containers/App.js +++ b/src/containers/App.js @@ -1,7 +1,10 @@ -import React from 'react'; +import React, { PropTypes } from 'react'; +import ImmutablePropTypes from 'react-immutable-proptypes'; import pluralize from 'pluralize'; import { connect } from 'react-redux'; -import { Layout, Panel, NavDrawer, Navigation, Link } from 'react-toolbox'; +import { Layout, Panel, NavDrawer } from 'react-toolbox/lib/layout'; +import { Navigation } from 'react-toolbox/lib/navigation'; +import { Link } from 'react-toolbox/lib/link'; import { loadConfig } from '../actions/config'; import { loginUser } from '../actions/auth'; import { currentBackend } from '../backends/backend'; @@ -11,7 +14,7 @@ import { HELP, runCommand, navigateToCollection, - createNewEntryInCollection + createNewEntryInCollection, } from '../actions/findbar'; import AppHeader from '../components/AppHeader/AppHeader'; import { Loader } from '../components/UI/index'; @@ -19,31 +22,37 @@ import styles from './App.css'; class App extends React.Component { + static propTypes = { + auth: ImmutablePropTypes.map, + children: PropTypes.node, + config: ImmutablePropTypes.map, + collections: ImmutablePropTypes.orderedMap, + createNewEntryInCollection: PropTypes.func.isRequired, + dispatch: PropTypes.func.isRequired, + navigateToCollection: PropTypes.func.isRequired, + user: ImmutablePropTypes.map, + runCommand: PropTypes.func.isRequired, + }; + + static configError(config) { + return (
+

Error loading the CMS configuration

+ +
+

The config.yml file could not be loaded or failed to parse properly.

+

Error message: {config.get('error')}

+
+
); + } + state = { - navDrawerIsVisible: true + navDrawerIsVisible: true, }; componentDidMount() { this.props.dispatch(loadConfig()); } - configError(config) { - return
-

Error loading the CMS configuration

- -
-

The "config.yml" file could not be loaded or failed to parse properly.

-

Error message: {config.get('error')}

-
-
; - } - - configLoading() { - return
- Loading configuration... -
; - } - handleLogin(credentials) { this.props.dispatch(loginUser(credentials)); } @@ -56,13 +65,17 @@ class App extends React.Component { return

Waiting for backend...

; } - return
- {React.createElement(backend.authComponent(), { - onLogin: this.handleLogin.bind(this), - error: auth && auth.get('error'), - isFetching: auth && auth.get('isFetching') - })} -
; + return ( +
+ { + React.createElement(backend.authComponent(), { + onLogin: this.handleLogin.bind(this), + error: auth && auth.get('error'), + isFetching: auth && auth.get('isFetching'), + }) + } +
+ ); } generateFindBarCommands() { @@ -70,22 +83,22 @@ class App extends React.Component { const commands = []; const defaultCommands = []; - this.props.collections.forEach(collection => { + this.props.collections.forEach((collection) => { commands.push({ - id: `show_${collection.get('name')}`, - pattern: `Show ${pluralize(collection.get('label'))}`, + id: `show_${ collection.get('name') }`, + pattern: `Show ${ pluralize(collection.get('label')) }`, type: SHOW_COLLECTION, - payload: { collectionName: collection.get('name') } + payload: { collectionName: collection.get('name') }, }); - if (defaultCommands.length < 5) defaultCommands.push(`show_${collection.get('name')}`); + if (defaultCommands.length < 5) defaultCommands.push(`show_${ collection.get('name') }`); if (collection.get('create') === true) { commands.push({ - id: `create_${collection.get('name')}`, - pattern: `Create new ${pluralize(collection.get('label'), 1)}(:itemName as ${pluralize(collection.get('label'), 1)} Name)`, + id: `create_${ collection.get('name') }`, + pattern: `Create new ${ pluralize(collection.get('label'), 1) }(:itemName as ${ pluralize(collection.get('label'), 1) } Name)`, type: CREATE_COLLECTION, - payload: { collectionName: collection.get('name') } + payload: { collectionName: collection.get('name') }, }); } }); @@ -98,7 +111,7 @@ class App extends React.Component { toggleNavDrawer = () => { this.setState({ - navDrawerIsVisible: !this.state.navDrawerIsVisible + navDrawerIsVisible: !this.state.navDrawerIsVisible, }); }; @@ -109,9 +122,9 @@ class App extends React.Component { config, children, collections, - runCommand, - navigateToCollection, - createNewEntryInCollection + runCommand, // eslint-disable-line + navigateToCollection, // eslint-disable-line + createNewEntryInCollection, // eslint-disable-line } = this.props; if (config === null) { @@ -119,11 +132,11 @@ class App extends React.Component { } if (config.get('error')) { - return this.configError(config); + return App.configError(config); } if (config.get('isFetching')) { - return this.configLoading(); + return Loading configuration...; } if (user == null) { @@ -142,12 +155,12 @@ class App extends React.Component { >
); @@ -49,4 +67,5 @@ EntryEditor.propTypes = { onChange: PropTypes.func.isRequired, onPersist: PropTypes.func.isRequired, onRemoveMedia: PropTypes.func.isRequired, + onCancelEdit: PropTypes.func.isRequired, }; diff --git a/src/containers/EntryPage.js b/src/containers/EntryPage.js index d3dcace0..b2014112 100644 --- a/src/containers/EntryPage.js +++ b/src/containers/EntryPage.js @@ -7,12 +7,13 @@ import { createEmptyDraft, discardDraft, changeDraft, - persistEntry + persistEntry, } from '../actions/entries'; +import { cancelEdit } from '../actions/editor'; import { addMedia, removeMedia } from '../actions/media'; import { selectEntry, getMedia } from '../reducers'; import EntryEditor from '../components/EntryEditor/EntryEditor'; -import EntryPageHOC from './editorialWorkflow/EntryPageHOC'; +import entryPageHOC from './editorialWorkflow/EntryPageHOC'; class EntryPage extends React.Component { static propTypes = { @@ -28,6 +29,7 @@ class EntryPage extends React.Component { loadEntry: PropTypes.func.isRequired, persistEntry: PropTypes.func.isRequired, removeMedia: PropTypes.func.isRequired, + cancelEdit: PropTypes.func.isRequired, slug: PropTypes.string, newEntry: PropTypes.bool.isRequired, }; @@ -56,7 +58,7 @@ class EntryPage extends React.Component { this.props.discardDraft(); } - createDraft = entry => { + createDraft = (entry) => { if (entry) this.props.createDraftFromEntry(entry); }; @@ -66,10 +68,19 @@ class EntryPage extends React.Component { render() { const { - entry, entryDraft, boundGetMedia, collection, changeDraft, addMedia, removeMedia + entry, + entryDraft, + boundGetMedia, + collection, + changeDraft, + addMedia, + removeMedia, + cancelEdit, } = this.props; - if (entryDraft == null || entryDraft.get('entry') == undefined || entry && entry.get('isFetching')) { + if (entryDraft == null + || entryDraft.get('entry') === undefined + || (entry && entry.get('isFetching'))) { return
Loading...
; } return ( @@ -81,18 +92,12 @@ class EntryPage extends React.Component { onAddMedia={addMedia} onRemoveMedia={removeMedia} onPersist={this.handlePersistEntry} + onCancelEdit={cancelEdit} /> ); } } - -/* - * Instead of checking the publish mode everywhere to dispatch & render the additional editorial workflow stuff, - * We delegate it to a Higher Order Component - */ -EntryPage = EntryPageHOC(EntryPage); - function mapStateToProps(state, ownProps) { const { collections, entryDraft } = state; const collection = collections.get(ownProps.params.name); @@ -113,6 +118,7 @@ export default connect( createDraftFromEntry, createEmptyDraft, discardDraft, - persistEntry + persistEntry, + cancelEdit, } -)(EntryPage); +)(entryPageHOC(EntryPage)); From fee2d8e7316ca463c902a465deabba2a045bbb9b Mon Sep 17 00:00:00 2001 From: Andrey Okonetchnikov Date: Wed, 12 Oct 2016 14:32:53 +0200 Subject: [PATCH 44/57] Updated Jest config to work with webpack and CSS-modules --- __mocks__/fileLoaderMock.js | 3 +++ __mocks__/styleLoaderMock.js | 3 +++ package.json | 23 ++++++++++++++++------- wallaby.config.js | 34 +++++++++++++++++++++++++--------- 4 files changed, 47 insertions(+), 16 deletions(-) create mode 100644 __mocks__/fileLoaderMock.js create mode 100644 __mocks__/styleLoaderMock.js diff --git a/__mocks__/fileLoaderMock.js b/__mocks__/fileLoaderMock.js new file mode 100644 index 00000000..35a63a0e --- /dev/null +++ b/__mocks__/fileLoaderMock.js @@ -0,0 +1,3 @@ +// See http://facebook.github.io/jest/docs/tutorial-webpack.html#content + +module.exports = 'test-file-stub'; diff --git a/__mocks__/styleLoaderMock.js b/__mocks__/styleLoaderMock.js new file mode 100644 index 00000000..bade080f --- /dev/null +++ b/__mocks__/styleLoaderMock.js @@ -0,0 +1,3 @@ +// See http://facebook.github.io/jest/docs/tutorial-webpack.html#content + +module.exports = {}; diff --git a/package.json b/package.json index 534e51ba..b9e9da37 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,13 @@ ] }, "pre-commit": "lint:staged", + "jest": { + "moduleNameMapper": { + "^.+\\.(png|eot|woff|woff2|ttf|svg|gif)$": "/__mocks__/fileLoaderMock.js", + "^.+\\.scss$": "/__mocks__/styleLoaderMock.js", + "^.+\\.css$": "identity-obj-proxy" + } + }, "keywords": [ "netlify", "cms" @@ -56,6 +63,7 @@ "expect": "^1.20.2", "exports-loader": "^0.6.3", "file-loader": "^0.8.5", + "identity-obj-proxy": "^3.0.0", "imports-loader": "^0.6.5", "jest-cli": "^15.1.1", "lint-staged": "^3.0.3", @@ -65,6 +73,7 @@ "postcss-import": "^8.1.2", "postcss-loader": "^0.9.1", "pre-commit": "^1.1.3", + "react-addons-test-utils": "^15.3.2", "sass-loader": "^4.0.2", "style-loader": "^0.13.0", "stylefmt": "^4.3.1", @@ -84,8 +93,8 @@ "bricks.js": "^1.7.0", "dateformat": "^1.0.12", "fuzzy": "^0.1.1", - "immutable": "^3.7.6", "immutability-helper": "^2.0.0", + "immutable": "^3.7.6", "js-base64": "^2.1.9", "js-yaml": "^3.5.3", "json-loader": "^0.5.4", @@ -98,20 +107,20 @@ "pluralize": "^3.0.0", "prismjs": "^1.5.1", "react": "^15.1.0", - "react-dom": "^15.1.0", - "react-hot-loader": "^3.0.0-beta.2", "react-addons-css-transition-group": "^15.3.1", "react-datetime": "^2.6.0", - "react-portal": "^2.2.1", - "react-simple-dnd": "^0.1.2", - "react-toolbox": "^1.2.1", - "react-waypoint": "^3.1.3", + "react-dom": "^15.1.0", + "react-hot-loader": "^3.0.0-beta.2", "react-immutable-proptypes": "^1.6.0", "react-lazy-load": "^3.0.3", + "react-portal": "^2.2.1", "react-pure-render": "^1.0.2", "react-redux": "^4.4.0", "react-router": "^2.5.1", "react-router-redux": "^4.0.5", + "react-simple-dnd": "^0.1.2", + "react-toolbox": "^1.2.1", + "react-waypoint": "^3.1.3", "redux": "^3.3.1", "redux-thunk": "^1.0.3", "selection-position": "^1.0.0", diff --git a/wallaby.config.js b/wallaby.config.js index fa4a6e78..c0bb1bab 100644 --- a/wallaby.config.js +++ b/wallaby.config.js @@ -1,24 +1,40 @@ +/* eslint global-require: 0 */ +/* eslint import/no-extraneous-dependencies: 0 */ + process.env.BABEL_ENV = 'test'; module.exports = wallaby => ({ files: [ - { pattern: 'src/**/*.js' }, - { pattern: 'src/**/*.js.snap' }, - { pattern: 'src/**/*.spec.js', ignore: true } + 'package.json', + 'src/**/*.js', + 'src/**/*.js.snap', + '!src/**/*.spec.js', ], - tests: [ - { pattern: 'src/**/*.spec.js' } - ], + tests: ['src/**/*.spec.js'], compilers: { - 'src/**/*.js': wallaby.compilers.babel() + 'src/**/*.js': wallaby.compilers.babel(), }, env: { type: 'node', - runner: 'node' + runner: 'node', + params: { + runner: '--harmony_proxies', + }, + }, + + testFramework: 'jest', + + setup: () => { + wallaby.testFramework.configure({ + moduleNameMapper: { + '^.+\\.(png|eot|woff|woff2|ttf|svg|gif)$': require('path').join(wallaby.localProjectDir, '__mocks__', 'fileLoaderMock.js'), + '^.+\\.scss$': require('path').join(wallaby.localProjectDir, '__mocks__', 'styleLoaderMock.js'), + '^.+\\.css$': require('identity-obj-proxy'), + }, + }); }, - testFramework: 'jest' }); From 4c5e72a2ebc7d12dedf7e4a92c5a0d3a3954ef80 Mon Sep 17 00:00:00 2001 From: Andrey Okonetchnikov Date: Wed, 12 Oct 2016 15:52:41 +0200 Subject: [PATCH 45/57] Updated MarkitupReactRenderer test --- .../__tests__/MarkupItReactRenderer.spec.js | 67 +++++++++---------- .../MarkupItReactRenderer.spec.js.snap | 40 +++++++++++ 2 files changed, 73 insertions(+), 34 deletions(-) create mode 100644 src/components/MarkupItReactRenderer/__tests__/__snapshots__/MarkupItReactRenderer.spec.js.snap diff --git a/src/components/MarkupItReactRenderer/__tests__/MarkupItReactRenderer.spec.js b/src/components/MarkupItReactRenderer/__tests__/MarkupItReactRenderer.spec.js index c676757f..cdf49cc5 100644 --- a/src/components/MarkupItReactRenderer/__tests__/MarkupItReactRenderer.spec.js +++ b/src/components/MarkupItReactRenderer/__tests__/MarkupItReactRenderer.spec.js @@ -8,22 +8,21 @@ import MarkupIt from 'markup-it'; import markdownSyntax from 'markup-it/syntaxes/markdown'; import htmlSyntax from 'markup-it/syntaxes/html'; import reInline from 'markup-it/syntaxes/markdown/re/inline'; -import MarkupItReactRenderer from '../../UI/MarkupItReactRenderer/MarkupItReactRenderer'; +import MarkupItReactRenderer from '../'; describe('MarkitupReactRenderer', () => { - describe('basics', () => { it('should re-render properly after a value and syntax update', () => { const component = shallow( ); const tree1 = component.html(); component.setProps({ value: '

Title

', - syntax: htmlSyntax + syntax: htmlSyntax, }); const tree2 = component.html(); expect(tree1).toEqual(tree2); @@ -32,8 +31,8 @@ describe('MarkitupReactRenderer', () => { it('should not update the parser if syntax didn\'t change', () => { const component = shallow( ); const syntax1 = component.instance().props.syntax; @@ -76,8 +75,8 @@ Text with **bold** & _em_ elements `; const component = shallow( ); expect(component.html()).toMatchSnapshot(); @@ -86,12 +85,12 @@ Text with **bold** & _em_ elements describe('Headings', () => { for (const heading of [...Array(6).keys()]) { - it(`should render Heading ${heading + 1}`, () => { + it(`should render Heading ${ heading + 1 }`, () => { const value = padStart(' Title', heading + 7, '#'); const component = shallow( ); expect(component.html()).toMatchSnapshot(); @@ -114,8 +113,8 @@ Text with **bold** & _em_ elements `; const component = shallow( ); expect(component.html()).toMatchSnapshot(); @@ -133,8 +132,8 @@ I get 10 times more traffic from [Google] [1] than from [Yahoo] [2] or [MSN] [3] `; const component = shallow( ); expect(component.html()).toMatchSnapshot(); @@ -146,8 +145,8 @@ I get 10 times more traffic from [Google] [1] than from [Yahoo] [2] or [MSN] [3] const value = 'Use the `printf()` function.'; const component = shallow( ); expect(component.html()).toMatchSnapshot(); @@ -157,8 +156,8 @@ I get 10 times more traffic from [Google] [1] than from [Yahoo] [2] or [MSN] [3] const value = '``There is a literal backtick (`) here.``'; const component = shallow( ); expect(component.html()).toMatchSnapshot(); @@ -184,8 +183,8 @@ I get 10 times more traffic from [Google] [1] than from [Yahoo] [2] or [MSN] [3] `; const component = shallow( ); expect(component.html()).toMatchSnapshot(); @@ -195,27 +194,27 @@ I get 10 times more traffic from [Google] [1] than from [Yahoo] [2] or [MSN] [3] describe('custom elements', () => { it('should extend default renderers with custom ones', () => { - const myRule = MarkupIt.Rule('mediaproxy') + const myRule = MarkupIt.Rule('mediaproxy') // eslint-disable-line .regExp(reInline.link, (state, match) => { if (match[0].charAt(0) !== '!') { - return; + return null; } return { data: Map({ alt: match[1], src: match[2], - title: match[3] - }).filter(Boolean) + title: match[3], + }).filter(Boolean), }; }); const myCustomSchema = { - 'mediaproxy': ({ token }) => { + mediaproxy: ({ token }) => { //eslint-disable-line const src = token.getIn(['data', 'src']); const alt = token.getIn(['data', 'alt']); - return {alt}/; - } + return {alt}; + }, }; const myMarkdownSyntax = markdownSyntax.addInlineRules(myRule); @@ -226,9 +225,9 @@ I get 10 times more traffic from [Google] [1] than from [Yahoo] [2] or [MSN] [3] `; const component = shallow( ); expect(component.html()).toMatchSnapshot(); @@ -240,8 +239,8 @@ I get 10 times more traffic from [Google] [1] than from [Yahoo] [2] or [MSN] [3] const value = '

Paragraph with inline element

'; const component = shallow( ); expect(component.html()).toMatchSnapshot(); diff --git a/src/components/MarkupItReactRenderer/__tests__/__snapshots__/MarkupItReactRenderer.spec.js.snap b/src/components/MarkupItReactRenderer/__tests__/__snapshots__/MarkupItReactRenderer.spec.js.snap new file mode 100644 index 00000000..82d17c3b --- /dev/null +++ b/src/components/MarkupItReactRenderer/__tests__/__snapshots__/MarkupItReactRenderer.spec.js.snap @@ -0,0 +1,40 @@ +exports[`MarkitupReactRenderer HTML rendering should render HTML 1`] = `"

Paragraph with inline element

"`; + +exports[`MarkitupReactRenderer Markdown rendering Code should render code 1`] = `"

Use the printf() function.

"`; + +exports[`MarkitupReactRenderer Markdown rendering Code should render code 2 1`] = `"

There is a literal backtick (\`) here.

"`; + +exports[`MarkitupReactRenderer Markdown rendering General should render markdown 1`] = `"

H1

Text with bold & em elements

H2

  • ul item 1
  • ul item 2

H3

  1. ol item 1
  2. ol item 2
  3. ol item 3

H4

link title

H5

\"alt

H6
"`; + +exports[`MarkitupReactRenderer Markdown rendering HTML should render HTML as is when using Markdown 1`] = ` +"

Title

+ +
+
Test HTML content
+
Testing HTML in Markdown
+
+
+ +

Test

+
" +`; + +exports[`MarkitupReactRenderer Markdown rendering Headings should render Heading 1 1`] = `"

Title

"`; + +exports[`MarkitupReactRenderer Markdown rendering Headings should render Heading 2 1`] = `"

Title

"`; + +exports[`MarkitupReactRenderer Markdown rendering Headings should render Heading 3 1`] = `"

Title

"`; + +exports[`MarkitupReactRenderer Markdown rendering Headings should render Heading 4 1`] = `"

Title

"`; + +exports[`MarkitupReactRenderer Markdown rendering Headings should render Heading 5 1`] = `"
Title
"`; + +exports[`MarkitupReactRenderer Markdown rendering Headings should render Heading 6 1`] = `"
Title
"`; + +exports[`MarkitupReactRenderer Markdown rendering Links should render links 1`] = `""`; + +exports[`MarkitupReactRenderer Markdown rendering Lists should render lists 1`] = `"
  1. ol item 1
  2. ol item 2
    • Sublist 1
    • Sublist 2
    • Sublist 3
      1. Sub-Sublist 1
      2. Sub-Sublist 2
      3. Sub-Sublist 3
  3. ol item 3
"`; + +exports[`MarkitupReactRenderer custom elements should extend default renderers with custom ones 1`] = `"

Title

\"mediaproxy

"`; From 2f37b3df123705bceee7da5e4e88c7a40a0438a4 Mon Sep 17 00:00:00 2001 From: Andrey Okonetchnikov Date: Wed, 12 Oct 2016 16:01:27 +0200 Subject: [PATCH 46/57] Fixed eslint errors --- src/actions/entries.js | 76 ++++++++++++++++++++++-------------------- 1 file changed, 40 insertions(+), 36 deletions(-) diff --git a/src/actions/entries.js b/src/actions/entries.js index bf5d5b46..f2fae8a5 100644 --- a/src/actions/entries.js +++ b/src/actions/entries.js @@ -35,8 +35,8 @@ export function entryLoading(collection, slug) { type: ENTRY_REQUEST, payload: { collection: collection.get('name'), - slug: slug - } + slug, + }, }; } @@ -45,8 +45,8 @@ export function entryLoaded(collection, entry) { type: ENTRY_SUCCESS, payload: { collection: collection.get('name'), - entry: entry - } + entry, + }, }; } @@ -54,8 +54,8 @@ export function entriesLoading(collection) { return { type: ENTRIES_REQUEST, payload: { - collection: collection.get('name') - } + collection: collection.get('name'), + }, }; } @@ -64,9 +64,9 @@ export function entriesLoaded(collection, entries, pagination) { type: ENTRIES_SUCCESS, payload: { collection: collection.get('name'), - entries: entries, - page: pagination - } + entries, + page: pagination, + }, }; } @@ -75,7 +75,7 @@ export function entriesFailed(collection, error) { type: ENTRIES_FAILURE, error: 'Failed to load entries', payload: error.toString(), - meta: { collection: collection.get('name') } + meta: { collection: collection.get('name') }, }; } @@ -83,9 +83,9 @@ export function entryPersisting(collection, entry) { return { type: ENTRY_PERSIST_REQUEST, payload: { - collection: collection, - entry: entry - } + collection, + entry, + }, }; } @@ -93,9 +93,9 @@ export function entryPersisted(collection, entry) { return { type: ENTRY_PERSIST_SUCCESS, payload: { - collection: collection, - entry: entry - } + collection, + entry, + }, }; } @@ -103,42 +103,42 @@ export function entryPersistFail(collection, entry, error) { return { type: ENTRIES_FAILURE, error: 'Failed to persist entry', - payload: error.toString() + payload: error.toString(), }; } export function emmptyDraftCreated(entry) { return { type: DRAFT_CREATE_EMPTY, - payload: entry + payload: entry, }; } export function searchingEntries(searchTerm) { return { type: SEARCH_ENTRIES_REQUEST, - payload: { searchTerm } + payload: { searchTerm }, }; } -export function SearchSuccess(searchTerm, entries, page) { +export function searchSuccess(searchTerm, entries, page) { return { type: SEARCH_ENTRIES_SUCCESS, payload: { searchTerm, entries, - page - } + page, + }, }; } -export function SearchFailure(searchTerm, error) { +export function searchFailure(searchTerm, error) { return { type: SEARCH_ENTRIES_FAILURE, payload: { searchTerm, - error - } + error, + }, }; } @@ -148,20 +148,20 @@ export function SearchFailure(searchTerm, error) { export function createDraftFromEntry(entry) { return { type: DRAFT_CREATE_FROM_ENTRY, - payload: entry + payload: entry, }; } export function discardDraft() { return { - type: DRAFT_DISCARD + type: DRAFT_DISCARD, }; } export function changeDraft(entry) { return { type: DRAFT_CHANGE, - payload: entry + payload: entry, }; } @@ -180,7 +180,7 @@ export function loadEntry(entry, collection, slug) { } else { getPromise = backend.lookupEntry(collection, slug); } - return getPromise.then((loadedEntry) => dispatch(entryLoaded(collection, loadedEntry))); + return getPromise.then(loadedEntry => dispatch(entryLoaded(collection, loadedEntry))); }; } @@ -192,8 +192,8 @@ export function loadEntries(collection, page = 0) { const provider = integration ? getIntegrationProvider(state.integrations, integration) : currentBackend(state.config); dispatch(entriesLoading(collection)); provider.listEntries(collection, page).then( - (response) => dispatch(entriesLoaded(collection, response.entries, response.pagination)), - (error) => dispatch(entriesFailed(collection, error)) + response => dispatch(entriesLoaded(collection, response.entries, response.pagination)), + error => dispatch(entriesFailed(collection, error)) ); }; } @@ -217,7 +217,7 @@ export function persistEntry(collection, entry) { () => { dispatch(entryPersisted(collection, entry)); }, - (error) => dispatch(entryPersistFail(collection, entry, error)) + error => dispatch(entryPersistFail(collection, entry, error)) ); }; } @@ -228,12 +228,16 @@ export function searchEntries(searchTerm, page = 0) { let collections = state.collections.keySeq().toArray(); collections = collections.filter(collection => selectIntegration(state, collection, 'search')); const integration = selectIntegration(state, collections[0], 'search'); - if (!integration) console.warn('There isn\'t a search integration configured.'); - const provider = integration ? getIntegrationProvider(state.integrations, integration) : currentBackend(state.config); + if (!integration) { + dispatch(searchFailure(searchTerm, 'Search integration is not configured.')); + } + const provider = integration ? + getIntegrationProvider(state.integrations, integration) + : currentBackend(state.config); dispatch(searchingEntries(searchTerm)); provider.search(collections, searchTerm, page).then( - (response) => dispatch(SearchSuccess(searchTerm, response.entries, response.pagination)), - (error) => dispatch(SearchFailure(searchTerm, error)) + response => dispatch(searchSuccess(searchTerm, response.entries, response.pagination)), + error => dispatch(searchFailure(searchTerm, error)) ); }; } From 8d51f9be3ec7471511e7453ab848c996fb5a976b Mon Sep 17 00:00:00 2001 From: Andrey Okonetchnikov Date: Wed, 12 Oct 2016 16:04:58 +0200 Subject: [PATCH 47/57] Fixed ESLint errors and tests for entries reducer --- src/reducers/__tests__/entries.spec.js | 21 ++++++------- src/reducers/entries.js | 41 +++++++++++++++----------- 2 files changed, 35 insertions(+), 27 deletions(-) diff --git a/src/reducers/__tests__/entries.spec.js b/src/reducers/__tests__/entries.spec.js index 2e71da03..19ea470c 100644 --- a/src/reducers/__tests__/entries.spec.js +++ b/src/reducers/__tests__/entries.spec.js @@ -6,27 +6,27 @@ import reducer from '../entries'; describe('entries', () => { it('should mark entries as fetching', () => { const state = OrderedMap({ - 'posts': Map({ name: 'posts' }) + posts: Map({ name: 'posts' }), }); expect( reducer(state, entriesLoading(Map({ name: 'posts' }))) ).toEqual( OrderedMap(fromJS({ - 'posts': { name: 'posts' }, - 'pages': { - 'posts': { isFetching: true } - } + posts: { name: 'posts' }, + pages: { + posts: { isFetching: true }, + }, })) ); }); it('should handle loaded entries', () => { const state = OrderedMap({ - 'posts': Map({ name: 'posts' }) + posts: Map({ name: 'posts' }), }); const entries = [{ slug: 'a', path: '' }, { slug: 'b', title: 'B' }]; expect( - reducer(state, entriesLoaded(Map({ name: 'posts' }), entries)) + reducer(state, entriesLoaded(Map({ name: 'posts' }), entries, 0)) ).toEqual( OrderedMap(fromJS( { @@ -37,9 +37,10 @@ describe('entries', () => { }, pages: { posts: { - ids: ['a', 'b'] - } - } + page: 0, + ids: ['a', 'b'], + }, + }, } )) ); diff --git a/src/reducers/entries.js b/src/reducers/entries.js index a609fa80..c1971af4 100644 --- a/src/reducers/entries.js +++ b/src/reducers/entries.js @@ -1,18 +1,26 @@ import { Map, List, fromJS } from 'immutable'; import { - ENTRY_REQUEST, ENTRY_SUCCESS, ENTRIES_REQUEST, ENTRIES_SUCCESS, SEARCH_ENTRIES_REQUEST, SEARCH_ENTRIES_SUCCESS + ENTRY_REQUEST, + ENTRY_SUCCESS, + ENTRIES_REQUEST, + ENTRIES_SUCCESS, + SEARCH_ENTRIES_REQUEST, + SEARCH_ENTRIES_SUCCESS, } from '../actions/entries'; -let collection, loadedEntries, page, searchTerm; +let collection; +let loadedEntries; +let page; +let searchTerm; const entries = (state = Map({ entities: Map(), pages: Map() }), action) => { switch (action.type) { case ENTRY_REQUEST: - return state.setIn(['entities', `${action.payload.collection}.${action.payload.slug}`, 'isFetching'], true); + return state.setIn(['entities', `${ action.payload.collection }.${ action.payload.slug }`, 'isFetching'], true); case ENTRY_SUCCESS: return state.setIn( - ['entities', `${action.payload.collection}.${action.payload.entry.slug}`], + ['entities', `${ action.payload.collection }.${ action.payload.entry.slug }`], fromJS(action.payload.entry) ); @@ -24,15 +32,15 @@ const entries = (state = Map({ entities: Map(), pages: Map() }), action) => { 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)) + loadedEntries.forEach(entry => ( + map.setIn(['entities', `${ collection }.${ entry.slug }`], fromJS(entry).set('isFetching', false)) )); - const ids = List(loadedEntries.map((entry) => entry.slug)); + const ids = List(loadedEntries.map(entry => entry.slug)); map.setIn(['pages', collection], Map({ - page: page, - ids: page === 0 ? ids : map.getIn(['pages', collection, 'ids'], List()).concat(ids) + page, + ids: page === 0 ? ids : map.getIn(['pages', collection, 'ids'], List()).concat(ids), })); }); @@ -42,23 +50,22 @@ const entries = (state = Map({ entities: Map(), pages: Map() }), action) => { map.setIn(['search', 'isFetching'], true); map.setIn(['search', 'term'], action.payload.searchTerm); }); - } else { - return state; } + return state; case SEARCH_ENTRIES_SUCCESS: loadedEntries = action.payload.entries; page = action.payload.page; searchTerm = action.payload.searchTerm; return state.withMutations((map) => { - loadedEntries.forEach((entry) => ( - map.setIn(['entities', `${entry.collection}.${entry.slug}`], fromJS(entry).set('isFetching', false)) + loadedEntries.forEach(entry => ( + map.setIn(['entities', `${ entry.collection }.${ entry.slug }`], fromJS(entry).set('isFetching', false)) )); const ids = List(loadedEntries.map(entry => ({ collection: entry.collection, slug: entry.slug }))); map.set('search', Map({ - page: page, + page, term: searchTerm, - ids: page === 0 ? ids : map.getIn(['search', 'ids'], List()).concat(ids) + ids: page === 0 ? ids : map.getIn(['search', 'ids'], List()).concat(ids), })); }); @@ -68,12 +75,12 @@ const entries = (state = Map({ entities: Map(), pages: Map() }), action) => { }; export const selectEntry = (state, collection, slug) => ( - state.getIn(['entities', `${collection}.${slug}`]) + state.getIn(['entities', `${ collection }.${ slug }`]) ); export const selectEntries = (state, collection) => { const slugs = state.getIn(['pages', collection, 'ids']); - return slugs && slugs.map((slug) => selectEntry(state, collection, slug)); + return slugs && slugs.map(slug => selectEntry(state, collection, slug)); }; export const selectSearchedEntries = (state) => { From 06b0a7bdaa2ada13cf362852a99fbc911e23899b Mon Sep 17 00:00:00 2001 From: Andrey Okonetchnikov Date: Wed, 12 Oct 2016 17:13:11 +0200 Subject: [PATCH 48/57] Added fsevents as devDep to reduce the CPU load of webpack-dev-server on Mac OS X. See https://github.com/webpack/webpack-dev-middleware/issues/40 --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index b9e9da37..f16ee9ca 100644 --- a/package.json +++ b/package.json @@ -63,6 +63,7 @@ "expect": "^1.20.2", "exports-loader": "^0.6.3", "file-loader": "^0.8.5", + "fsevents": "^1.0.14", "identity-obj-proxy": "^3.0.0", "imports-loader": "^0.6.5", "jest-cli": "^15.1.1", From a80d1087b29e56746cd45f9de52c9a97144f55e6 Mon Sep 17 00:00:00 2001 From: Andrey Okonetchnikov Date: Wed, 12 Oct 2016 19:17:37 +0200 Subject: [PATCH 49/57] Simplified a regexp in the webpack config --- webpack.base.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webpack.base.js b/webpack.base.js index 2052d935..cf66559c 100644 --- a/webpack.base.js +++ b/webpack.base.js @@ -6,7 +6,7 @@ module.exports = { module: { loaders: [ { - test: /\.((png)|(eot)|(woff)|(woff2)|(ttf)|(svg)|(gif))(\?v=\d+\.\d+\.\d+)?$/, + test: /\.(png|eot|woff|woff2|ttf|svg|gif)(\?v=\d+\.\d+\.\d+)?$/, loader: 'url-loader?limit=100000', }, { From 077e83dfc961f44c62097c3c70cf3a6841b0c18f Mon Sep 17 00:00:00 2001 From: Andrey Okonetchnikov Date: Wed, 12 Oct 2016 19:19:05 +0200 Subject: [PATCH 50/57] Handle entry persisting state in actions and reducer + added tests. --- src/actions/entries.js | 24 ++++++----- src/reducers/__tests__/entries.spec.js | 58 +++++++++++++++++++++++--- src/reducers/entries.js | 18 ++++++++ 3 files changed, 84 insertions(+), 16 deletions(-) diff --git a/src/actions/entries.js b/src/actions/entries.js index f2fae8a5..0269385b 100644 --- a/src/actions/entries.js +++ b/src/actions/entries.js @@ -83,8 +83,8 @@ export function entryPersisting(collection, entry) { return { type: ENTRY_PERSIST_REQUEST, payload: { - collection, - entry, + collectionName: collection.get('name'), + entrySlug: entry.get('slug'), }, }; } @@ -93,17 +93,21 @@ export function entryPersisted(collection, entry) { return { type: ENTRY_PERSIST_SUCCESS, payload: { - collection, - entry, + collectionName: collection.get('name'), + entrySlug: entry.get('slug'), }, }; } export function entryPersistFail(collection, entry, error) { return { - type: ENTRIES_FAILURE, + type: ENTRY_PERSIST_FAILURE, error: 'Failed to persist entry', - payload: error.toString(), + payload: { + collectionName: collection.get('name'), + entrySlug: entry.get('slug'), + error: error.toString(), + }, }; } @@ -211,12 +215,10 @@ export function persistEntry(collection, entry) { return (dispatch, getState) => { const state = getState(); const backend = currentBackend(state.config); - const MediaProxies = entry.get('mediaFiles').map(path => getMedia(state, path)); + const mediaProxies = entry.get('mediaFiles').map(path => getMedia(state, path)); dispatch(entryPersisting(collection, entry)); - backend.persistEntry(state.config, collection, entry, MediaProxies.toJS()).then( - () => { - dispatch(entryPersisted(collection, entry)); - }, + backend.persistEntry(state.config, collection, entry, mediaProxies.toJS()).then( + () => dispatch(entryPersisted(collection, entry)), error => dispatch(entryPersistFail(collection, entry, error)) ); }; diff --git a/src/reducers/__tests__/entries.spec.js b/src/reducers/__tests__/entries.spec.js index 19ea470c..29978765 100644 --- a/src/reducers/__tests__/entries.spec.js +++ b/src/reducers/__tests__/entries.spec.js @@ -1,15 +1,16 @@ -import expect from 'expect'; -import { Map, OrderedMap, fromJS } from 'immutable'; -import { entriesLoading, entriesLoaded } from '../../actions/entries'; +import Immutable, { Map, OrderedMap, fromJS } from 'immutable'; +import * as actions from '../../actions/entries'; import reducer from '../entries'; +let initialState; + describe('entries', () => { it('should mark entries as fetching', () => { const state = OrderedMap({ posts: Map({ name: 'posts' }), }); expect( - reducer(state, entriesLoading(Map({ name: 'posts' }))) + reducer(state, actions.entriesLoading(Map({ name: 'posts' }))) ).toEqual( OrderedMap(fromJS({ posts: { name: 'posts' }, @@ -26,7 +27,7 @@ describe('entries', () => { }); const entries = [{ slug: 'a', path: '' }, { slug: 'b', title: 'B' }]; expect( - reducer(state, entriesLoaded(Map({ name: 'posts' }), entries, 0)) + reducer(state, actions.entriesLoaded(Map({ name: 'posts' }), entries, 0)) ).toEqual( OrderedMap(fromJS( { @@ -45,4 +46,51 @@ describe('entries', () => { )) ); }); + + describe('entry persisting', () => { + beforeEach(() => { + initialState = Immutable.fromJS({ + entities: { + 'posts.slug': { + collection: 'posts', + slug: 'slug', + path: 'content/blog/art-and-wine-festival.md', + partial: false, + raw: '', + data: {}, + metaData: null, + }, + }, + pages: {}, + }); + }); + + it('should handle persisting request', () => { + const newState = reducer( + initialState, + actions.entryPersisting(Map({ name: 'posts' }), Map({ slug: 'slug' })) + ); + expect(newState.getIn(['entities', 'posts.slug', 'isPersisting'])).toBe(true); + }); + + it('should handle persisting success', () => { + let newState = reducer(initialState, + actions.entryPersisting(Map({ name: 'posts' }), Map({ slug: 'slug' })) + ); + newState = reducer(newState, + actions.entryPersisted(Map({ name: 'posts' }), Map({ slug: 'slug' })) + ); + expect(newState.getIn(['entities', 'posts.slug', 'isPersisting'])).toBeUndefined(); + }); + + it('should handle persisting error', () => { + let newState = reducer(initialState, + actions.entryPersisting(Map({ name: 'posts' }), Map({ slug: 'slug' })) + ); + newState = reducer(newState, + actions.entryPersistFail(Map({ name: 'posts' }), Map({ slug: 'slug' }), 'Error message') + ); + expect(newState.getIn(['entities', 'posts.slug', 'isPersisting'])).toBeUndefined(); + }); + }); }); diff --git a/src/reducers/entries.js b/src/reducers/entries.js index c1971af4..4fccb962 100644 --- a/src/reducers/entries.js +++ b/src/reducers/entries.js @@ -2,6 +2,9 @@ import { Map, List, fromJS } from 'immutable'; import { ENTRY_REQUEST, ENTRY_SUCCESS, + ENTRY_PERSIST_REQUEST, + ENTRY_PERSIST_SUCCESS, + ENTRY_PERSIST_FAILURE, ENTRIES_REQUEST, ENTRIES_SUCCESS, SEARCH_ENTRIES_REQUEST, @@ -13,6 +16,10 @@ let loadedEntries; let page; let searchTerm; +function getEntryPath(collectionName, entrySlug) { + return `${ collectionName }.${ entrySlug }`; +} + const entries = (state = Map({ entities: Map(), pages: Map() }), action) => { switch (action.type) { case ENTRY_REQUEST: @@ -24,6 +31,17 @@ const entries = (state = Map({ entities: Map(), pages: Map() }), action) => { fromJS(action.payload.entry) ); + case ENTRY_PERSIST_REQUEST: { + const { collectionName, entrySlug } = action.payload; + return state.setIn(['entities', getEntryPath(collectionName, entrySlug), 'isPersisting'], true); + } + + case ENTRY_PERSIST_SUCCESS: + case ENTRY_PERSIST_FAILURE: { + const { collectionName, entrySlug } = action.payload; + return state.deleteIn(['entities', getEntryPath(collectionName, entrySlug), 'isPersisting']); + } + case ENTRIES_REQUEST: return state.setIn(['pages', action.payload.collection, 'isFetching'], true); From 27659b77f12e1aeee66b71ba45100fbdf6401a43 Mon Sep 17 00:00:00 2001 From: Andrey Okonetchnikov Date: Thu, 13 Oct 2016 11:34:55 +0200 Subject: [PATCH 51/57] Removed console.error from auth reducer to cleanup tests outputs. We should use Netlify error wrapper. --- src/reducers/auth.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/reducers/auth.js b/src/reducers/auth.js index 04776859..6a47ef4b 100644 --- a/src/reducers/auth.js +++ b/src/reducers/auth.js @@ -8,7 +8,6 @@ const auth = (state = null, action) => { case AUTH_SUCCESS: return Immutable.fromJS({ user: action.payload }); case AUTH_FAILURE: - console.error(action.payload); return Immutable.Map({ error: action.payload.toString() }); default: return state; From 5e333aca20b129c3686c863eee497d22bc6f8e6f Mon Sep 17 00:00:00 2001 From: Andrey Okonetchnikov Date: Thu, 13 Oct 2016 11:55:46 +0200 Subject: [PATCH 52/57] Removed obsolte snapshot --- .../MarkitupReactRenderer.spec.js.snap | 40 ------------------- 1 file changed, 40 deletions(-) delete mode 100644 src/components/MarkupItReactRenderer/__tests__/__snapshots__/MarkitupReactRenderer.spec.js.snap diff --git a/src/components/MarkupItReactRenderer/__tests__/__snapshots__/MarkitupReactRenderer.spec.js.snap b/src/components/MarkupItReactRenderer/__tests__/__snapshots__/MarkitupReactRenderer.spec.js.snap deleted file mode 100644 index 82d17c3b..00000000 --- a/src/components/MarkupItReactRenderer/__tests__/__snapshots__/MarkitupReactRenderer.spec.js.snap +++ /dev/null @@ -1,40 +0,0 @@ -exports[`MarkitupReactRenderer HTML rendering should render HTML 1`] = `"

Paragraph with inline element

"`; - -exports[`MarkitupReactRenderer Markdown rendering Code should render code 1`] = `"

Use the printf() function.

"`; - -exports[`MarkitupReactRenderer Markdown rendering Code should render code 2 1`] = `"

There is a literal backtick (\`) here.

"`; - -exports[`MarkitupReactRenderer Markdown rendering General should render markdown 1`] = `"

H1

Text with bold & em elements

H2

  • ul item 1
  • ul item 2

H3

  1. ol item 1
  2. ol item 2
  3. ol item 3

H4

link title

H5

\"alt

H6
"`; - -exports[`MarkitupReactRenderer Markdown rendering HTML should render HTML as is when using Markdown 1`] = ` -"

Title

- -
-
Test HTML content
-
Testing HTML in Markdown
-
-
- -

Test

-
" -`; - -exports[`MarkitupReactRenderer Markdown rendering Headings should render Heading 1 1`] = `"

Title

"`; - -exports[`MarkitupReactRenderer Markdown rendering Headings should render Heading 2 1`] = `"

Title

"`; - -exports[`MarkitupReactRenderer Markdown rendering Headings should render Heading 3 1`] = `"

Title

"`; - -exports[`MarkitupReactRenderer Markdown rendering Headings should render Heading 4 1`] = `"

Title

"`; - -exports[`MarkitupReactRenderer Markdown rendering Headings should render Heading 5 1`] = `"
Title
"`; - -exports[`MarkitupReactRenderer Markdown rendering Headings should render Heading 6 1`] = `"
Title
"`; - -exports[`MarkitupReactRenderer Markdown rendering Links should render links 1`] = `""`; - -exports[`MarkitupReactRenderer Markdown rendering Lists should render lists 1`] = `"
  1. ol item 1
  2. ol item 2
    • Sublist 1
    • Sublist 2
    • Sublist 3
      1. Sub-Sublist 1
      2. Sub-Sublist 2
      3. Sub-Sublist 3
  3. ol item 3
"`; - -exports[`MarkitupReactRenderer custom elements should extend default renderers with custom ones 1`] = `"

Title

\"mediaproxy

"`; From f9c43bd275d8ef5bbcfbd29688ac8b1d130252a8 Mon Sep 17 00:00:00 2001 From: Andrey Okonetchnikov Date: Thu, 13 Oct 2016 11:56:26 +0200 Subject: [PATCH 53/57] Removed expect import --- src/reducers/__tests__/config.spec.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/reducers/__tests__/config.spec.js b/src/reducers/__tests__/config.spec.js index 5639d92f..6185af82 100644 --- a/src/reducers/__tests__/config.spec.js +++ b/src/reducers/__tests__/config.spec.js @@ -1,4 +1,3 @@ -import expect from 'expect'; import Immutable from 'immutable'; import { configLoaded, configLoading, configFailed } from '../../actions/config'; import config from '../config'; @@ -14,9 +13,9 @@ describe('config', () => { it('should handle an update', () => { expect( - config(Immutable.Map({ 'a': 'b', 'c': 'd' }), configLoaded({ 'a': 'changed', 'e': 'new' })) + config(Immutable.Map({ a: 'b', c: 'd' }), configLoaded({ a: 'changed', e: 'new' })) ).toEqual( - Immutable.Map({ 'a': 'changed', 'e': 'new' }) + Immutable.Map({ a: 'changed', e: 'new' }) ); }); From e53262d92c40e8350a10a4e3562fb6ed0e8ba2a1 Mon Sep 17 00:00:00 2001 From: Andrey Okonetchnikov Date: Thu, 13 Oct 2016 14:30:11 +0200 Subject: [PATCH 54/57] Moved persisting logic to entryDraft reducer + added tests. --- src/actions/entries.js | 7 +- src/containers/EntryPage.js | 21 ++-- src/reducers/__tests__/entries.spec.js | 63 ++--------- src/reducers/__tests__/entryDraft.spec.js | 126 ++++++++++++++++++++++ src/reducers/entries.js | 18 ---- src/reducers/entryDraft.js | 32 ++++-- 6 files changed, 177 insertions(+), 90 deletions(-) create mode 100644 src/reducers/__tests__/entryDraft.spec.js diff --git a/src/actions/entries.js b/src/actions/entries.js index 0269385b..0421278d 100644 --- a/src/actions/entries.js +++ b/src/actions/entries.js @@ -211,13 +211,14 @@ export function createEmptyDraft(collection) { }; } -export function persistEntry(collection, entry) { +export function persistEntry(collection, entryDraft) { return (dispatch, getState) => { const state = getState(); const backend = currentBackend(state.config); - const mediaProxies = entry.get('mediaFiles').map(path => getMedia(state, path)); + const mediaProxies = entryDraft.get('mediaFiles').map(path => getMedia(state, path)); + const entry = entryDraft.get('entry'); dispatch(entryPersisting(collection, entry)); - backend.persistEntry(state.config, collection, entry, mediaProxies.toJS()).then( + backend.persistEntry(state.config, collection, entryDraft, mediaProxies.toJS()).then( () => dispatch(entryPersisted(collection, entry)), error => dispatch(entryPersistFail(collection, entry, error)) ); diff --git a/src/containers/EntryPage.js b/src/containers/EntryPage.js index b2014112..05a583a7 100644 --- a/src/containers/EntryPage.js +++ b/src/containers/EntryPage.js @@ -35,12 +35,12 @@ class EntryPage extends React.Component { }; componentDidMount() { - const { entry, collection, slug } = this.props; + const { entry, newEntry, collection, slug, createEmptyDraft, loadEntry } = this.props; - if (this.props.newEntry) { - this.props.createEmptyDraft(this.props.collection); + if (newEntry) { + createEmptyDraft(collection); } else { - this.props.loadEntry(entry, collection, slug); + loadEntry(entry, collection, slug); this.createDraft(entry); } } @@ -63,7 +63,8 @@ class EntryPage extends React.Component { }; handlePersistEntry = () => { - this.props.persistEntry(this.props.collection, this.props.entryDraft); + const { persistEntry, collection, entryDraft } = this.props; + persistEntry(collection, entryDraft); }; render() { @@ -105,7 +106,15 @@ function mapStateToProps(state, ownProps) { const slug = ownProps.params.slug; const entry = newEntry ? null : selectEntry(state, collection.get('name'), slug); const boundGetMedia = getMedia.bind(null, state); - return { collection, collections, newEntry, entryDraft, boundGetMedia, slug, entry }; + return { + collection, + collections, + newEntry, + entryDraft, + boundGetMedia, + slug, + entry, + }; } export default connect( diff --git a/src/reducers/__tests__/entries.spec.js b/src/reducers/__tests__/entries.spec.js index 29978765..591e7709 100644 --- a/src/reducers/__tests__/entries.spec.js +++ b/src/reducers/__tests__/entries.spec.js @@ -1,16 +1,15 @@ -import Immutable, { Map, OrderedMap, fromJS } from 'immutable'; +import { Map, OrderedMap, fromJS } from 'immutable'; import * as actions from '../../actions/entries'; import reducer from '../entries'; -let initialState; +const initialState = OrderedMap({ + posts: Map({ name: 'posts' }), +}); describe('entries', () => { it('should mark entries as fetching', () => { - const state = OrderedMap({ - posts: Map({ name: 'posts' }), - }); expect( - reducer(state, actions.entriesLoading(Map({ name: 'posts' }))) + reducer(initialState, actions.entriesLoading(Map({ name: 'posts' }))) ).toEqual( OrderedMap(fromJS({ posts: { name: 'posts' }, @@ -22,12 +21,9 @@ describe('entries', () => { }); it('should handle loaded entries', () => { - const state = OrderedMap({ - posts: Map({ name: 'posts' }), - }); const entries = [{ slug: 'a', path: '' }, { slug: 'b', title: 'B' }]; expect( - reducer(state, actions.entriesLoaded(Map({ name: 'posts' }), entries, 0)) + reducer(initialState, actions.entriesLoaded(Map({ name: 'posts' }), entries, 0)) ).toEqual( OrderedMap(fromJS( { @@ -46,51 +42,4 @@ describe('entries', () => { )) ); }); - - describe('entry persisting', () => { - beforeEach(() => { - initialState = Immutable.fromJS({ - entities: { - 'posts.slug': { - collection: 'posts', - slug: 'slug', - path: 'content/blog/art-and-wine-festival.md', - partial: false, - raw: '', - data: {}, - metaData: null, - }, - }, - pages: {}, - }); - }); - - it('should handle persisting request', () => { - const newState = reducer( - initialState, - actions.entryPersisting(Map({ name: 'posts' }), Map({ slug: 'slug' })) - ); - expect(newState.getIn(['entities', 'posts.slug', 'isPersisting'])).toBe(true); - }); - - it('should handle persisting success', () => { - let newState = reducer(initialState, - actions.entryPersisting(Map({ name: 'posts' }), Map({ slug: 'slug' })) - ); - newState = reducer(newState, - actions.entryPersisted(Map({ name: 'posts' }), Map({ slug: 'slug' })) - ); - expect(newState.getIn(['entities', 'posts.slug', 'isPersisting'])).toBeUndefined(); - }); - - it('should handle persisting error', () => { - let newState = reducer(initialState, - actions.entryPersisting(Map({ name: 'posts' }), Map({ slug: 'slug' })) - ); - newState = reducer(newState, - actions.entryPersistFail(Map({ name: 'posts' }), Map({ slug: 'slug' }), 'Error message') - ); - expect(newState.getIn(['entities', 'posts.slug', 'isPersisting'])).toBeUndefined(); - }); - }); }); diff --git a/src/reducers/__tests__/entryDraft.spec.js b/src/reducers/__tests__/entryDraft.spec.js new file mode 100644 index 00000000..d9c76705 --- /dev/null +++ b/src/reducers/__tests__/entryDraft.spec.js @@ -0,0 +1,126 @@ +import { Map, List, fromJS } from 'immutable'; +import * as actions from '../../actions/entries'; +import reducer from '../entryDraft'; + +let initialState = Map({ entry: Map(), mediaFiles: List() }); + +const entry = { + collection: 'posts', + slug: 'slug', + path: 'content/blog/art-and-wine-festival.md', + partial: false, + raw: '', + data: {}, + metaData: null, +}; + +describe('entryDraft reducer', () => { + describe('DRAFT_CREATE_FROM_ENTRY', () => { + it('should create draft from the entry', () => { + expect( + reducer( + initialState, + actions.createDraftFromEntry(fromJS(entry)) + ) + ).toEqual( + fromJS({ + entry: { + ...entry, + newRecord: false, + }, + mediaFiles: [], + }) + ); + }); + }); + + describe('DRAFT_CREATE_EMPTY', () => { + it('should create a new draft ', () => { + expect( + reducer( + initialState, + actions.emmptyDraftCreated(fromJS(entry)) + ) + ).toEqual( + fromJS({ + entry: { + ...entry, + newRecord: true, + }, + mediaFiles: [], + }) + ); + }); + }); + + describe('DRAFT_DISCARD', () => { + it('should discard the draft and return initial state', () => { + expect(reducer(initialState, actions.discardDraft())) + .toEqual(initialState); + }); + }); + + describe('DRAFT_CHANGE', () => { + it.skip('should update the draft', () => { + const newEntry = { + ...entry, + raw: 'updated', + }; + expect(reducer(initialState, actions.changeDraft(newEntry))) + .toEqual(fromJS({ + entry: { + ...entry, + raw: 'updated', + }, + mediaFiles: [], + })); + }); + }); + + describe('persisting', () => { + beforeEach(() => { + initialState = fromJS({ + entities: { + 'posts.slug': { + collection: 'posts', + slug: 'slug', + path: 'content/blog/art-and-wine-festival.md', + partial: false, + raw: '', + data: {}, + metaData: null, + }, + }, + pages: {}, + }); + }); + + it('should handle persisting request', () => { + const newState = reducer( + initialState, + actions.entryPersisting(Map({ name: 'posts' }), Map({ slug: 'slug' })) + ); + expect(newState.getIn(['entry', 'isPersisting'])).toBe(true); + }); + + it('should handle persisting success', () => { + let newState = reducer(initialState, + actions.entryPersisting(Map({ name: 'posts' }), Map({ slug: 'slug' })) + ); + newState = reducer(newState, + actions.entryPersisted(Map({ name: 'posts' }), Map({ slug: 'slug' })) + ); + expect(newState.getIn(['entry', 'isPersisting'])).toBeUndefined(); + }); + + it('should handle persisting error', () => { + let newState = reducer(initialState, + actions.entryPersisting(Map({ name: 'posts' }), Map({ slug: 'slug' })) + ); + newState = reducer(newState, + actions.entryPersistFail(Map({ name: 'posts' }), Map({ slug: 'slug' }), 'Error message') + ); + expect(newState.getIn(['entry', 'isPersisting'])).toBeUndefined(); + }); + }); +}); diff --git a/src/reducers/entries.js b/src/reducers/entries.js index 4fccb962..c1971af4 100644 --- a/src/reducers/entries.js +++ b/src/reducers/entries.js @@ -2,9 +2,6 @@ import { Map, List, fromJS } from 'immutable'; import { ENTRY_REQUEST, ENTRY_SUCCESS, - ENTRY_PERSIST_REQUEST, - ENTRY_PERSIST_SUCCESS, - ENTRY_PERSIST_FAILURE, ENTRIES_REQUEST, ENTRIES_SUCCESS, SEARCH_ENTRIES_REQUEST, @@ -16,10 +13,6 @@ let loadedEntries; let page; let searchTerm; -function getEntryPath(collectionName, entrySlug) { - return `${ collectionName }.${ entrySlug }`; -} - const entries = (state = Map({ entities: Map(), pages: Map() }), action) => { switch (action.type) { case ENTRY_REQUEST: @@ -31,17 +24,6 @@ const entries = (state = Map({ entities: Map(), pages: Map() }), action) => { fromJS(action.payload.entry) ); - case ENTRY_PERSIST_REQUEST: { - const { collectionName, entrySlug } = action.payload; - return state.setIn(['entities', getEntryPath(collectionName, entrySlug), 'isPersisting'], true); - } - - case ENTRY_PERSIST_SUCCESS: - case ENTRY_PERSIST_FAILURE: { - const { collectionName, entrySlug } = action.payload; - return state.deleteIn(['entities', getEntryPath(collectionName, entrySlug), 'isPersisting']); - } - case ENTRIES_REQUEST: return state.setIn(['pages', action.payload.collection, 'isFetching'], true); diff --git a/src/reducers/entryDraft.js b/src/reducers/entryDraft.js index d9100ca0..a617932d 100644 --- a/src/reducers/entryDraft.js +++ b/src/reducers/entryDraft.js @@ -1,10 +1,21 @@ import { Map, List, fromJS } from 'immutable'; -import { DRAFT_CREATE_FROM_ENTRY, DRAFT_CREATE_EMPTY, DRAFT_DISCARD, DRAFT_CHANGE } from '../actions/entries'; -import { ADD_MEDIA, REMOVE_MEDIA } from '../actions/media'; +import { + DRAFT_CREATE_FROM_ENTRY, + DRAFT_CREATE_EMPTY, + DRAFT_DISCARD, + DRAFT_CHANGE, + ENTRY_PERSIST_REQUEST, + ENTRY_PERSIST_SUCCESS, + ENTRY_PERSIST_FAILURE, +} from '../actions/entries'; +import { + ADD_MEDIA, + REMOVE_MEDIA, +} from '../actions/media'; const initialState = Map({ entry: Map(), mediaFiles: List() }); -const entryDraft = (state = Map(), action) => { +const entryDraftReducer = (state = Map(), action) => { switch (action.type) { case DRAFT_CREATE_FROM_ENTRY: // Existing Entry @@ -25,14 +36,23 @@ const entryDraft = (state = Map(), action) => { case DRAFT_CHANGE: return state.set('entry', action.payload); + case ENTRY_PERSIST_REQUEST: { + return state.setIn(['entry', 'isPersisting'], true); + } + + case ENTRY_PERSIST_SUCCESS: + case ENTRY_PERSIST_FAILURE: { + return state.deleteIn(['entry', 'isPersisting']); + } + case ADD_MEDIA: - return state.update('mediaFiles', (list) => list.push(action.payload.public_path)); + return state.update('mediaFiles', list => list.push(action.payload.public_path)); case REMOVE_MEDIA: - return state.update('mediaFiles', (list) => list.filterNot((path) => path === action.payload)); + return state.update('mediaFiles', list => list.filterNot(path => path === action.payload)); default: return state; } }; -export default entryDraft; +export default entryDraftReducer; From 3b18fb4f87c69fa136e86fa4688e6947e0b74bef Mon Sep 17 00:00:00 2001 From: Andrey Okonetchnikov Date: Thu, 13 Oct 2016 14:31:44 +0200 Subject: [PATCH 55/57] Added visual feedback during saving of the entry. Related to #101 --- src/components/EntryEditor/EntryEditor.js | 18 ++++------ .../EntryEditor/EntryEditorToolbar.js | 35 +++++++++++++++++++ .../__tests__/EntryEditorToolbar.spec.js | 28 +++++++++++++++ .../EntryEditorToolbar.spec.js.snap | 3 ++ 4 files changed, 72 insertions(+), 12 deletions(-) create mode 100644 src/components/EntryEditor/EntryEditorToolbar.js create mode 100644 src/components/EntryEditor/__tests__/EntryEditorToolbar.spec.js create mode 100644 src/components/EntryEditor/__tests__/__snapshots__/EntryEditorToolbar.spec.js.snap diff --git a/src/components/EntryEditor/EntryEditor.js b/src/components/EntryEditor/EntryEditor.js index e7948ee5..dfab685d 100644 --- a/src/components/EntryEditor/EntryEditor.js +++ b/src/components/EntryEditor/EntryEditor.js @@ -1,9 +1,9 @@ import React, { PropTypes } from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; -import { Button } from 'react-toolbox/lib/button'; import { ScrollSync, ScrollSyncPane } from '../ScrollSync'; import ControlPane from '../ControlPanel/ControlPane'; import PreviewPane from '../PreviewPane/PreviewPane'; +import Toolbar from './EntryEditorToolbar'; import styles from './EntryEditor.css'; export default function EntryEditor( @@ -43,17 +43,11 @@ export default function EntryEditor(
- - {' '} - +
); diff --git a/src/components/EntryEditor/EntryEditorToolbar.js b/src/components/EntryEditor/EntryEditorToolbar.js new file mode 100644 index 00000000..a5e29ba1 --- /dev/null +++ b/src/components/EntryEditor/EntryEditorToolbar.js @@ -0,0 +1,35 @@ +import React, { PropTypes } from 'react'; +import { Button } from 'react-toolbox/lib/button'; + +const EntryEditorToolbar = ( + { + isPersisting, + onPersist, + onCancelEdit, + }) => { + const disabled = isPersisting; + return ( +
+ + {' '} + +
+ ); +}; + +EntryEditorToolbar.propTypes = { + isPersisting: PropTypes.bool, + onPersist: PropTypes.func.isRequired, + onCancelEdit: PropTypes.func.isRequired, +}; + +export default EntryEditorToolbar; diff --git a/src/components/EntryEditor/__tests__/EntryEditorToolbar.spec.js b/src/components/EntryEditor/__tests__/EntryEditorToolbar.spec.js new file mode 100644 index 00000000..e907c195 --- /dev/null +++ b/src/components/EntryEditor/__tests__/EntryEditorToolbar.spec.js @@ -0,0 +1,28 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import EntryEditorToolbar from '../EntryEditorToolbar'; + +describe('EntryEditorToolbar', () => { + it('should have both buttons enabled initially', () => { + const component = shallow( + {}} + onCancelEdit={() => {}} + /> + ); + const tree = component.html(); + expect(tree).toMatchSnapshot(); + }); + + it('should disable and update label of Save button when persisting', () => { + const component = shallow( + {}} + onCancelEdit={() => {}} + /> + ); + const tree = component.html(); + expect(tree).toMatchSnapshot(); + }); +}); diff --git a/src/components/EntryEditor/__tests__/__snapshots__/EntryEditorToolbar.spec.js.snap b/src/components/EntryEditor/__tests__/__snapshots__/EntryEditorToolbar.spec.js.snap new file mode 100644 index 00000000..071091dc --- /dev/null +++ b/src/components/EntryEditor/__tests__/__snapshots__/EntryEditorToolbar.spec.js.snap @@ -0,0 +1,3 @@ +exports[`EntryEditorToolbar should disable and update label of Save button when persisting 1`] = `"
"`; + +exports[`EntryEditorToolbar should have both buttons enabled initially 1`] = `"
"`; From 863d90c8ee3684934b24595115a6f8e2456b7abf Mon Sep 17 00:00:00 2001 From: Andrey Okonetchnikov Date: Fri, 14 Oct 2016 13:42:58 +0200 Subject: [PATCH 56/57] Use Loader on the entry page. Closes #102 --- src/containers/EntryPage.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/containers/EntryPage.js b/src/containers/EntryPage.js index 05a583a7..9f366bd0 100644 --- a/src/containers/EntryPage.js +++ b/src/containers/EntryPage.js @@ -14,6 +14,7 @@ import { addMedia, removeMedia } from '../actions/media'; import { selectEntry, getMedia } from '../reducers'; import EntryEditor from '../components/EntryEditor/EntryEditor'; import entryPageHOC from './editorialWorkflow/EntryPageHOC'; +import { Loader } from '../components/UI'; class EntryPage extends React.Component { static propTypes = { @@ -82,7 +83,7 @@ class EntryPage extends React.Component { if (entryDraft == null || entryDraft.get('entry') === undefined || (entry && entry.get('isFetching'))) { - return
Loading...
; + return Loading entry...; } return ( Date: Mon, 17 Oct 2016 12:35:31 +0200 Subject: [PATCH 57/57] Added notifications. Closes #101 - Using react-notifications to manage redux state - Refactored Toast component to be stateless - Toasts can be stacked - Cleaned up CSS - Updated stories --- package.json | 3 +- src/actions/entries.js | 29 ++++++++++-- src/components/UI/theme.css | 11 +++-- src/components/UI/toast/Toast.css | 45 +++++++++--------- src/components/UI/toast/Toast.js | 76 ++++++------------------------- src/components/stories/Toast.js | 40 ++++++++++++---- src/containers/App.css | 20 ++++++-- src/containers/App.js | 7 ++- src/reducers/combinedReducer.js | 4 +- 9 files changed, 126 insertions(+), 109 deletions(-) diff --git a/package.json b/package.json index dab352d7..643db793 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,6 @@ "author": "Netlify", "license": "MIT", "devDependencies": { - "@kadira/storybook": "^1.36.0", "babel-core": "^6.5.1", "babel-jest": "^15.0.0", "babel-loader": "^6.2.2", @@ -91,6 +90,7 @@ "webpack-postcss-tools": "^1.1.1" }, "dependencies": { + "@kadira/storybook": "^1.36.0", "autoprefixer": "^6.3.3", "bricks.js": "^1.7.0", "dateformat": "^1.0.12", @@ -124,6 +124,7 @@ "react-toolbox": "^1.2.1", "react-waypoint": "^3.1.3", "redux": "^3.3.1", + "redux-notifications": "^2.1.1", "redux-thunk": "^1.0.3", "selection-position": "^1.0.0", "semaphore": "^1.0.5", diff --git a/src/actions/entries.js b/src/actions/entries.js index 0421278d..bd604d93 100644 --- a/src/actions/entries.js +++ b/src/actions/entries.js @@ -1,7 +1,10 @@ +import { actions as notifActions } from 'redux-notifications'; import { currentBackend } from '../backends/backend'; import { getIntegrationProvider } from '../integrations'; import { getMedia, selectIntegration } from '../reducers'; +const { notifSend } = notifActions; + /* * Contant Declarations */ @@ -190,7 +193,9 @@ export function loadEntry(entry, collection, slug) { export function loadEntries(collection, page = 0) { return (dispatch, getState) => { - if (collection.get('isFetching')) { return; } + if (collection.get('isFetching')) { + return; + } const state = getState(); const integration = selectIntegration(state, collection.get('name'), 'listEntries'); const provider = integration ? getIntegrationProvider(state.integrations, integration) : currentBackend(state.config); @@ -218,10 +223,24 @@ export function persistEntry(collection, entryDraft) { const mediaProxies = entryDraft.get('mediaFiles').map(path => getMedia(state, path)); const entry = entryDraft.get('entry'); dispatch(entryPersisting(collection, entry)); - backend.persistEntry(state.config, collection, entryDraft, mediaProxies.toJS()).then( - () => dispatch(entryPersisted(collection, entry)), - error => dispatch(entryPersistFail(collection, entry, error)) - ); + backend + .persistEntry(state.config, collection, entryDraft, mediaProxies.toJS()) + .then(() => { + dispatch(notifSend({ + message: 'Entry saved', + kind: 'success', + dismissAfter: 4000, + })); + dispatch(entryPersisted(collection, entry)); + }) + .catch((error) => { + dispatch(notifSend({ + message: 'Failed to persist entry', + kind: 'danger', + dismissAfter: 4000, + })); + dispatch(entryPersistFail(collection, entry, error)); + }); }; } diff --git a/src/components/UI/theme.css b/src/components/UI/theme.css index 87c78c64..e2a167aa 100644 --- a/src/components/UI/theme.css +++ b/src/components/UI/theme.css @@ -2,10 +2,13 @@ --defaultColor: #333; --defaultColorLight: #eee; --backgroundColor: #fff; - --shadowColor: rgba(0, 0, 0, 0.117647); + --shadowColor: rgba(0, 0, 0, .25); + --infoColor: #69c; --successColor: #1c7; --warningColor: #fa0; --errorColor: #f52; + --borderRadius: 2px; + --topmostZindex: 99999; } .base { @@ -13,14 +16,14 @@ } .container { - color: var(--defaultColor); background-color: var(--backgroundColor); + color: var(--defaultColor); } .rounded { - border-radius: 2px; + border-radius: var(--borderRadius); } .depth { - box-shadow: var(--shadowColor) 0px 1px 6px, var(--shadowColor) 0px 1px 4px; + box-shadow: var(--shadowColor) 0 1px 6px; } diff --git a/src/components/UI/toast/Toast.css b/src/components/UI/toast/Toast.css index 2c5bb930..9c8dfa4e 100644 --- a/src/components/UI/toast/Toast.css +++ b/src/components/UI/toast/Toast.css @@ -1,40 +1,41 @@ -@import "../theme.css"; +@import '../theme.css'; -.toast { - composes: base container rounded depth; - position: absolute; - top: 10px; - right: 10px; - z-index: 100; - width: 350px; - padding: 20px 10px; - font-size: 0.9rem; - text-align: center; - color: var(--defaultColorLight); - overflow: hidden; - opacity: 1; - transition: opacity .3s ease-in; +:root { + --iconSize: 30px; } -.hidden { - opacity: 0; +.root { + composes: base container rounded depth from '../theme.css'; + overflow: hidden; + margin: 10px; + padding: 10px 10px 15px; + color: var(--defaultColorLight); } .icon { - position: absolute; - top: calc(50% - 15px); - left: 15px; - font-size: 30px; + position: relative; + top: .15em; + margin-right: .25em; + font-size: var(--iconSize); + line-height: var(--iconSize); +} + +.info { + composes: root; + background-color: var(--infoColor); } .success { + composes: root; background-color: var(--successColor); } .warning { + composes: root; background-color: var(--warningColor); } -.error { +.danger { + composes: root; background-color: var(--errorColor); } diff --git a/src/components/UI/toast/Toast.js b/src/components/UI/toast/Toast.js index e50f2d06..36157815 100644 --- a/src/components/UI/toast/Toast.js +++ b/src/components/UI/toast/Toast.js @@ -2,69 +2,23 @@ import React, { PropTypes } from 'react'; import { Icon } from '../index'; import styles from './Toast.css'; -export default class Toast extends React.Component { +const icons = { + info: 'info', + success: 'check', + warning: 'attention', + danger: 'alert', +}; - state = { - shown: false - }; - - componentWillMount() { - if (this.props.show) { - this.autoHideTimeout(); - this.setState({ shown: true }); - } - } - - componentWillReceiveProps(nextProps) { - if (nextProps !== this.props) { - if (nextProps.show) this.autoHideTimeout(); - this.setState({ shown: nextProps.show }); - } - } - - componentWillUnmount() { - if (this.timeOut) { - clearTimeout(this.timeOut); - } - } - - autoHideTimeout = () => { - clearTimeout(this.timeOut); - this.timeOut = setTimeout(() => { - this.setState({ shown: false }); - }, 4000); - }; - - render() { - const { style, type, className, children } = this.props; - const icons = { - success: 'check', - warning: 'attention', - error: 'alert' - }; - const classes = [styles.toast]; - if (className) classes.push(className); - - let icon = ''; - if (type) { - classes.push(styles[type]); - icon = ; - } - - if (!this.state.shown) { - classes.push(styles.hidden); - } - - return ( -
{icon}{children}
- ); - } +export default function Toast({ kind, message }) { + return ( +
+ + {message} +
+ ); } Toast.propTypes = { - style: PropTypes.object, - type: PropTypes.oneOf(['success', 'warning', 'error']).isRequired, - className: PropTypes.string, - show: PropTypes.bool, - children: PropTypes.node + kind: PropTypes.oneOf(['info', 'success', 'warning', 'danger']).isRequired, + message: PropTypes.string, }; diff --git a/src/components/stories/Toast.js b/src/components/stories/Toast.js index 6ac4b7c6..b93e4bf7 100644 --- a/src/components/stories/Toast.js +++ b/src/components/stories/Toast.js @@ -1,19 +1,41 @@ import React from 'react'; -import { Toast } from '../UI'; import { storiesOf } from '@kadira/storybook'; +import { Toast } from '../UI'; +const containerStyle = { + position: 'fixed', + top: 0, + right: 0, + width: 360, + height: '100%', +}; storiesOf('Toast', module) + .add('All kinds stacked', () => ( +
+ + + + +
+ )) + .add('Info', () => ( +
+ +
+ )) .add('Success', () => ( -
- A Toast Message +
+
- )).add('Waring', () => ( -
- A Toast Message + )) + .add('Waring', () => ( +
+
- )).add('Error', () => ( -
- A Toast Message + )) + .add('Error', () => ( +
+
)); diff --git a/src/containers/App.css b/src/containers/App.css index 7c0c10f0..2a9b5418 100644 --- a/src/containers/App.css +++ b/src/containers/App.css @@ -1,20 +1,30 @@ +@import '../components/UI/theme.css'; + .layout .navDrawer .drawerContent { padding-top: 54px; + max-width: 240px; } + .nav { display: block; padding: 1rem; + & .heading { border: none; } } + .main { padding-top: 54px; } -.navDrawer { - max-width: 240px !important; - & .drawerContent { - max-width: 240px !important; - } +.notifsContainer { + position: fixed; + top: 60px; + right: 0; + bottom: 60px; + z-index: var(--topmostZindex); + width: 360px; + pointer-events: none; } + diff --git a/src/containers/App.js b/src/containers/App.js index 3505c897..561bc2e4 100644 --- a/src/containers/App.js +++ b/src/containers/App.js @@ -5,6 +5,7 @@ import { connect } from 'react-redux'; import { Layout, Panel, NavDrawer } from 'react-toolbox/lib/layout'; import { Navigation } from 'react-toolbox/lib/navigation'; import { Link } from 'react-toolbox/lib/link'; +import { Notifs } from 'redux-notifications'; import { loadConfig } from '../actions/config'; import { loginUser } from '../actions/auth'; import { currentBackend } from '../backends/backend'; @@ -17,7 +18,7 @@ import { createNewEntryInCollection, } from '../actions/findbar'; import AppHeader from '../components/AppHeader/AppHeader'; -import { Loader } from '../components/UI/index'; +import { Loader, Toast } from '../components/UI/index'; import styles from './App.css'; class App extends React.Component { @@ -147,6 +148,10 @@ class App extends React.Component { return ( +