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:
committed by
Shawn Erquhart
parent
7e4d4c1cc4
commit
2b41d8a838
@ -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();
|
||||
};
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
||||

|
||||
`;
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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, {
|
||||
|
@ -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);
|
||||
|
Reference in New Issue
Block a user