feat: bundle assets with content (#2958)

* fix(media_folder_relative): use collection name in unpublished entry

* refactor: pass arguments as object to AssetProxy ctor

* feat: support media folders per collection

* feat: resolve media files path based on entry path

* fix: asset public path resolving

* refactor: introduce typescript for AssetProxy

* refactor: code cleanup

* refactor(asset-proxy): add tests,switch to typescript,extract arguments

* refactor: typescript for editorialWorkflow

* refactor: add typescript for media library actions

* refactor: fix type error on map set

* refactor: move locale selector into reducer

* refactor: add typescript for entries actions

* refactor: remove duplication between asset store and media lib

* feat: load assets from backend using API

* refactor(github): add typescript, cache media files

* fix: don't load media URL if already loaded

* feat: add media folder config to collection

* fix: load assets from API when not in UI state

* feat: load entry media files when opening media library

* fix: editorial workflow draft media files bug fixes

* test(unit): fix unit tests

* fix: editor control losing focus

* style: add eslint object-shorthand rule

* test(cypress): re-record mock data

* fix: fix non github backends, large media

* test: uncomment only in tests

* fix(backend-test): add missing displayURL property

* test(e2e): add media library tests

* test(e2e): enable visual testing

* test(e2e): add github backend media library tests

* test(e2e): add git-gateway large media tests

* chore: post rebase fixes

* test: fix tests

* test: fix tests

* test(cypress): fix tests

* docs: add media_folder docs

* test(e2e): add media library delete test

* test(e2e): try and fix image comparison on CI

* ci: reduce test machines from 9 to 8

* test: add reducers and selectors unit tests

* test(e2e): disable visual regression testing for now

* test: add getAsset unit tests

* refactor: use Asset class component instead of hooks

* build: don't inline source maps

* test: add more media path tests
This commit is contained in:
Erez Rokah
2019-12-18 18:16:02 +02:00
committed by Shawn Erquhart
parent 7e4d4c1cc4
commit 2b41d8a838
231 changed files with 37961 additions and 18373 deletions

View File

@ -49,17 +49,18 @@ export default class RawEditor extends React.Component {
}
}
handleCopy = (event, editor) => {
handleCopy = async (event, editor) => {
event.persist();
const { getAsset, resolveWidget } = this.props;
const markdown = Plain.serialize(editor.value);
const html = markdownToHtml(markdown, { getAsset, resolveWidget });
const html = await markdownToHtml(markdown, { getAsset, resolveWidget });
setEventTransfer(event, 'text', markdown);
setEventTransfer(event, 'html', html);
event.preventDefault();
};
handleCut = (event, editor, next) => {
this.handleCopy(event, editor, next);
handleCut = async (event, editor, next) => {
await this.handleCopy(event, editor, next);
editor.delete();
};

View File

@ -5,9 +5,10 @@ import isHotkey from 'is-hotkey';
import { slateToMarkdown, markdownToSlate, htmlToSlate, markdownToHtml } from '../../serializers';
const CopyPasteVisual = ({ getAsset, resolveWidget }) => {
const handleCopy = (event, editor) => {
const handleCopy = async (event, editor) => {
event.persist();
const markdown = slateToMarkdown(editor.value.fragment.toJS());
const html = markdownToHtml(markdown, { getAsset, resolveWidget });
const html = await markdownToHtml(markdown, { getAsset, resolveWidget });
setEventTransfer(event, 'text', markdown);
setEventTransfer(event, 'html', html);
setEventTransfer(event, 'fragment', base64.serializeNode(editor.value.fragment));
@ -31,8 +32,8 @@ const CopyPasteVisual = ({ getAsset, resolveWidget }) => {
const doc = Document.fromJSON(ast);
return editor.insertFragment(doc);
},
onCopy(event, editor, next) {
handleCopy(event, editor, next);
async onCopy(event, editor, next) {
await handleCopy(event, editor, next);
},
onCut(event, editor, next) {
handleCopy(event, editor, next);

View File

@ -1,30 +1,54 @@
import React, { useEffect } from 'react';
import React from 'react';
import PropTypes from 'prop-types';
import { WidgetPreviewContainer } from 'netlify-cms-ui-default';
import { markdownToHtml } from './serializers';
let editorPreview;
class MarkdownPreview extends React.Component {
static propTypes = {
getAsset: PropTypes.func.isRequired,
resolveWidget: PropTypes.func.isRequired,
value: PropTypes.string,
};
export const getEditorPreview = () => editorPreview;
subscribed = true;
const MarkdownPreview = props => {
const { value, getAsset, resolveWidget } = props;
useEffect(() => {
editorPreview = props.editorPreview;
}, []);
state = {
html: null,
};
if (value === null) {
return null;
async _renderHtml() {
const { value, getAsset, resolveWidget } = this.props;
if (value) {
const html = await markdownToHtml(value, { getAsset, resolveWidget });
if (this.subscribed) {
this.setState({ html });
}
}
}
const html = markdownToHtml(value, { getAsset, resolveWidget });
return <WidgetPreviewContainer dangerouslySetInnerHTML={{ __html: html }} />;
};
MarkdownPreview.propTypes = {
getAsset: PropTypes.func.isRequired,
editorPreview: PropTypes.func.isRequired,
resolveWidget: PropTypes.func.isRequired,
value: PropTypes.string,
};
componentDidMount() {
this._renderHtml();
}
componentDidUpdate(prevProps) {
if (prevProps.value !== this.props.value || prevProps.getAsset !== this.props.getAsset) {
this._renderHtml();
}
}
componentWillUnmount() {
this.subscribed = false;
}
render() {
const { html } = this.state;
if (html === null) {
return null;
}
return <WidgetPreviewContainer dangerouslySetInnerHTML={{ __html: html }} />;
}
}
export default MarkdownPreview;

View File

@ -1,5 +1,5 @@
import React from 'react';
import renderer from 'react-test-renderer';
import { create, act } from 'react-test-renderer';
import { padStart } from 'lodash';
import MarkdownPreview from '../MarkdownPreview';
import { markdownToHtml } from '../serializers';
@ -7,7 +7,7 @@ import { markdownToHtml } from '../serializers';
describe('Markdown Preview renderer', () => {
describe('Markdown rendering', () => {
describe('General', () => {
it('should render markdown', () => {
it('should render markdown', async () => {
const value = `
# H1
@ -36,29 +36,39 @@ Text with **bold** & _em_ elements
![](https://pbs.twimg.com/profile_images/678903331176214528/TQTdqGwD.jpg)
`;
expect(
renderer
.create(<MarkdownPreview value={markdownToHtml(value)} getAsset={jest.fn()} />)
.toJSON(),
).toMatchSnapshot();
const html = await markdownToHtml(value);
let root;
await act(async () => {
root = create(
<MarkdownPreview value={html} getAsset={jest.fn()} resolveWidget={jest.fn()} />,
);
});
expect(root.toJSON()).toMatchSnapshot();
});
});
describe('Headings', () => {
for (const heading of [...Array(6).keys()]) {
it(`should render Heading ${heading + 1}`, () => {
it(`should render Heading ${heading + 1}`, async () => {
const value = padStart(' Title', heading + 7, '#');
expect(
renderer
.create(<MarkdownPreview value={markdownToHtml(value)} getAsset={jest.fn()} />)
.toJSON(),
).toMatchSnapshot();
const html = await markdownToHtml(value);
let root;
await act(async () => {
root = create(
<MarkdownPreview value={html} getAsset={jest.fn()} resolveWidget={jest.fn()} />,
);
});
expect(root.toJSON()).toMatchSnapshot();
});
}
});
describe('Lists', () => {
it('should render lists', () => {
it('should render lists', async () => {
const value = `
1. ol item 1
1. ol item 2
@ -70,11 +80,16 @@ Text with **bold** & _em_ elements
1. Sub-Sublist 3
1. ol item 3
`;
expect(
renderer
.create(<MarkdownPreview value={markdownToHtml(value)} getAsset={jest.fn()} />)
.toJSON(),
).toMatchInlineSnapshot(`
const html = await markdownToHtml(value);
let root;
await act(async () => {
root = create(
<MarkdownPreview value={html} getAsset={jest.fn()} resolveWidget={jest.fn()} />,
);
});
expect(root.toJSON()).toMatchInlineSnapshot(`
.emotion-0 {
margin: 15px 2px;
}
@ -104,7 +119,7 @@ Text with **bold** & _em_ elements
});
describe('Links', () => {
it('should render links', () => {
it('should render links', async () => {
const value = `
I get 10 times more traffic from [Google] than from [Yahoo] or [MSN].
@ -112,36 +127,51 @@ I get 10 times more traffic from [Google] than from [Yahoo] or [MSN].
[Yahoo]: http://search.yahoo.com/ "Yahoo Search"
[MSN]: http://search.msn.com/ "MSN Search"
`;
expect(
renderer
.create(<MarkdownPreview value={markdownToHtml(value)} getAsset={jest.fn()} />)
.toJSON(),
).toMatchSnapshot();
const html = await markdownToHtml(value);
let root;
await act(async () => {
root = create(
<MarkdownPreview value={html} getAsset={jest.fn()} resolveWidget={jest.fn()} />,
);
});
expect(root.toJSON()).toMatchSnapshot();
});
});
describe('Code', () => {
it('should render code', () => {
it('should render code', async () => {
const value = 'Use the `printf()` function.';
expect(
renderer
.create(<MarkdownPreview value={markdownToHtml(value)} getAsset={jest.fn()} />)
.toJSON(),
).toMatchSnapshot();
const html = await markdownToHtml(value);
let root;
await act(async () => {
root = create(
<MarkdownPreview value={html} getAsset={jest.fn()} resolveWidget={jest.fn()} />,
);
});
expect(root.toJSON()).toMatchSnapshot();
});
it('should render code 2', () => {
it('should render code 2', async () => {
const value = '``There is a literal backtick (`) here.``';
expect(
renderer
.create(<MarkdownPreview value={markdownToHtml(value)} getAsset={jest.fn()} />)
.toJSON(),
).toMatchSnapshot();
const html = await markdownToHtml(value);
let root;
await act(async () => {
root = create(
<MarkdownPreview value={html} getAsset={jest.fn()} resolveWidget={jest.fn()} />,
);
});
expect(root.toJSON()).toMatchSnapshot();
});
});
describe('HTML', () => {
it('should render HTML as is when using Markdown', () => {
it('should render HTML as is when using Markdown', async () => {
const value = `
# Title
@ -157,23 +187,33 @@ I get 10 times more traffic from [Google] than from [Yahoo] or [MSN].
<h1 style="display: block; border: 10px solid #f00; width: 100%">Test</h1>
`;
expect(
renderer
.create(<MarkdownPreview value={markdownToHtml(value)} getAsset={jest.fn()} />)
.toJSON(),
).toMatchSnapshot();
const html = await markdownToHtml(value);
let root;
await act(async () => {
root = create(
<MarkdownPreview value={html} getAsset={jest.fn()} resolveWidget={jest.fn()} />,
);
});
expect(root.toJSON()).toMatchSnapshot();
});
});
});
describe('HTML rendering', () => {
it('should render HTML', () => {
it('should render HTML', async () => {
const value = '<p>Paragraph with <em>inline</em> element</p>';
expect(
renderer
.create(<MarkdownPreview value={markdownToHtml(value)} getAsset={jest.fn()} />)
.toJSON(),
).toMatchSnapshot();
const html = await markdownToHtml(value);
let root;
await act(async () => {
root = create(
<MarkdownPreview value={html} getAsset={jest.fn()} resolveWidget={jest.fn()} />,
);
});
expect(root.toJSON()).toMatchSnapshot();
});
});
});

View File

@ -148,13 +148,13 @@ export const remarkToMarkdown = obj => {
/**
* Convert Markdown to HTML.
*/
export const markdownToHtml = (markdown, { getAsset, resolveWidget } = {}) => {
export const markdownToHtml = async (markdown, { getAsset, resolveWidget } = {}) => {
const mdast = markdownToRemark(markdown);
const hast = unified()
const hast = await unified()
.use(remarkToRehypeShortcodes, { plugins: getEditorComponents(), getAsset, resolveWidget })
.use(remarkToRehype, { allowDangerousHTML: true })
.runSync(mdast);
.run(mdast);
const html = unified()
.use(rehypeToHtml, {

View File

@ -12,15 +12,15 @@ import u from 'unist-builder';
export default function remarkToRehypeShortcodes({ plugins, getAsset, resolveWidget }) {
return transform;
function transform(root) {
const transformedChildren = map(root.children, processShortcodes);
async function transform(root) {
const transformedChildren = await Promise.all(map(root.children, processShortcodes));
return { ...root, children: transformedChildren };
}
/**
* Mapping function to transform nodes that contain shortcodes.
*/
function processShortcodes(node) {
async function processShortcodes(node) {
/**
* If the node doesn't contain shortcode data, return the original node.
*/
@ -38,7 +38,7 @@ export default function remarkToRehypeShortcodes({ plugins, getAsset, resolveWid
* an HTML string or a React component. If a React component is returned,
* render it to an HTML string.
*/
const value = getPreview(plugin, shortcodeData);
const value = await getPreview(plugin, shortcodeData);
const valueHtml = typeof value === 'string' ? value : renderToString(value);
/**
@ -52,7 +52,7 @@ export default function remarkToRehypeShortcodes({ plugins, getAsset, resolveWid
/**
* Retrieve the shortcode preview component.
*/
function getPreview(plugin, shortcodeData) {
async function getPreview(plugin, shortcodeData) {
const { toPreview, widget } = plugin;
if (toPreview) {
return toPreview(shortcodeData, getAsset);