fix: change getAsset to not return a promise (#3232)
* fix: change getAsset to not return a promise * fix: update markdown widget per getAsset changes * test: fix editor component image test * docs: update getAsset docs
This commit is contained in:
parent
2457c29baf
commit
ab685e8594
@ -30,6 +30,12 @@ module.exports = {
|
||||
'emotion/styled-import': 'error',
|
||||
'require-atomic-updates': [0],
|
||||
'object-shorthand': ['error', 'always'],
|
||||
'prefer-const': [
|
||||
'error',
|
||||
{
|
||||
destructuring: 'all',
|
||||
},
|
||||
],
|
||||
},
|
||||
plugins: ['babel', 'emotion', 'cypress'],
|
||||
settings: {
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { Map } from 'immutable';
|
||||
import { getAsset, ADD_ASSET } from '../media';
|
||||
import { getAsset, ADD_ASSET, LOAD_ASSET_REQUEST } from '../media';
|
||||
import configureMockStore from 'redux-mock-store';
|
||||
import thunk from 'redux-thunk';
|
||||
import AssetProxy from '../../valueObjects/AssetProxy';
|
||||
@ -9,15 +9,19 @@ const mockStore = configureMockStore(middlewares);
|
||||
|
||||
jest.mock('../../reducers/entries');
|
||||
jest.mock('../mediaLibrary');
|
||||
jest.mock('../../reducers/mediaLibrary');
|
||||
|
||||
describe('media', () => {
|
||||
const emptyAsset = new AssetProxy({
|
||||
path: 'empty.svg',
|
||||
file: new File([`<svg xmlns="http://www.w3.org/2000/svg"></svg>`], 'empty.svg', {
|
||||
type: 'image/svg+xml',
|
||||
}),
|
||||
});
|
||||
|
||||
describe('getAsset', () => {
|
||||
global.URL = { createObjectURL: jest.fn() };
|
||||
|
||||
const { selectMediaFilePath } = require('../../reducers/entries');
|
||||
const { selectMediaFileByPath } = require('../../reducers/mediaLibrary');
|
||||
const { getMediaDisplayURL, getMediaFile } = require('../mediaLibrary');
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
@ -28,12 +32,10 @@ describe('media', () => {
|
||||
|
||||
const payload = { collection: null, entryPath: null, path: null };
|
||||
|
||||
return store.dispatch(getAsset(payload)).then(result => {
|
||||
const actions = store.getActions();
|
||||
expect(actions).toHaveLength(0);
|
||||
|
||||
expect(result).toEqual(new AssetProxy({ file: new File([], 'empty'), path: '' }));
|
||||
});
|
||||
const result = store.dispatch(getAsset(payload));
|
||||
const actions = store.getActions();
|
||||
expect(actions).toHaveLength(0);
|
||||
expect(result).toEqual(emptyAsset);
|
||||
});
|
||||
|
||||
it('should return asset from medias state', () => {
|
||||
@ -42,27 +44,26 @@ describe('media', () => {
|
||||
const store = mockStore({
|
||||
config: Map(),
|
||||
medias: Map({
|
||||
[path]: asset,
|
||||
[path]: { asset },
|
||||
}),
|
||||
});
|
||||
|
||||
selectMediaFilePath.mockReturnValue(path);
|
||||
const payload = { collection: Map(), entry: Map({ path: 'entryPath' }), path };
|
||||
|
||||
return store.dispatch(getAsset(payload)).then(result => {
|
||||
const actions = store.getActions();
|
||||
expect(actions).toHaveLength(0);
|
||||
const result = store.dispatch(getAsset(payload));
|
||||
const actions = store.getActions();
|
||||
expect(actions).toHaveLength(0);
|
||||
|
||||
expect(result).toBe(asset);
|
||||
expect(selectMediaFilePath).toHaveBeenCalledTimes(1);
|
||||
expect(selectMediaFilePath).toHaveBeenCalledWith(
|
||||
store.getState().config,
|
||||
payload.collection,
|
||||
payload.entry,
|
||||
path,
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
expect(result).toBe(asset);
|
||||
expect(selectMediaFilePath).toHaveBeenCalledTimes(1);
|
||||
expect(selectMediaFilePath).toHaveBeenCalledWith(
|
||||
store.getState().config,
|
||||
payload.collection,
|
||||
payload.entry,
|
||||
path,
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
it('should create asset for absolute path when not in medias state', () => {
|
||||
@ -76,64 +77,33 @@ describe('media', () => {
|
||||
selectMediaFilePath.mockReturnValue(path);
|
||||
const payload = { collection: null, entryPath: null, path };
|
||||
|
||||
return store.dispatch(getAsset(payload)).then(result => {
|
||||
const actions = store.getActions();
|
||||
expect(actions).toHaveLength(1);
|
||||
expect(actions[0]).toEqual({
|
||||
type: ADD_ASSET,
|
||||
payload: asset,
|
||||
});
|
||||
expect(result).toEqual(asset);
|
||||
const result = store.dispatch(getAsset(payload));
|
||||
const actions = store.getActions();
|
||||
expect(actions).toHaveLength(1);
|
||||
expect(actions[0]).toEqual({
|
||||
type: ADD_ASSET,
|
||||
payload: asset,
|
||||
});
|
||||
expect(result).toEqual(asset);
|
||||
});
|
||||
|
||||
it('should create asset from media file when not in medias state', () => {
|
||||
it('should return empty asset and initiate load when not in medias state', () => {
|
||||
const path = 'static/media/image.png';
|
||||
const mediaFile = { file: new File([], '') };
|
||||
const url = 'blob://displayURL';
|
||||
const asset = new AssetProxy({ url, path });
|
||||
const store = mockStore({
|
||||
medias: Map({}),
|
||||
});
|
||||
|
||||
selectMediaFilePath.mockReturnValue(path);
|
||||
selectMediaFileByPath.mockReturnValue(mediaFile);
|
||||
getMediaDisplayURL.mockResolvedValue(url);
|
||||
const payload = { path };
|
||||
|
||||
return store.dispatch(getAsset(payload)).then(result => {
|
||||
const actions = store.getActions();
|
||||
expect(actions).toHaveLength(1);
|
||||
expect(actions[0]).toEqual({
|
||||
type: ADD_ASSET,
|
||||
payload: asset,
|
||||
});
|
||||
expect(result).toEqual(asset);
|
||||
});
|
||||
});
|
||||
|
||||
it('should fetch asset media file when not in redux store', () => {
|
||||
const path = 'static/media/image.png';
|
||||
const url = 'blob://displayURL';
|
||||
const asset = new AssetProxy({ url, path });
|
||||
const store = mockStore({
|
||||
medias: Map({}),
|
||||
});
|
||||
|
||||
selectMediaFilePath.mockReturnValue(path);
|
||||
selectMediaFileByPath.mockReturnValue(undefined);
|
||||
getMediaFile.mockResolvedValue({ url });
|
||||
const payload = { path };
|
||||
|
||||
return store.dispatch(getAsset(payload)).then(result => {
|
||||
const actions = store.getActions();
|
||||
expect(actions).toHaveLength(1);
|
||||
expect(actions[0]).toEqual({
|
||||
type: ADD_ASSET,
|
||||
payload: asset,
|
||||
});
|
||||
expect(result).toEqual(asset);
|
||||
const result = store.dispatch(getAsset(payload));
|
||||
const actions = store.getActions();
|
||||
expect(actions).toHaveLength(1);
|
||||
expect(actions[0]).toEqual({
|
||||
type: LOAD_ASSET_REQUEST,
|
||||
payload: { path },
|
||||
});
|
||||
expect(result).toEqual(emptyAsset);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -11,6 +11,10 @@ export const ADD_ASSETS = 'ADD_ASSETS';
|
||||
export const ADD_ASSET = 'ADD_ASSET';
|
||||
export const REMOVE_ASSET = 'REMOVE_ASSET';
|
||||
|
||||
export const LOAD_ASSET_REQUEST = 'LOAD_ASSET_REQUEST';
|
||||
export const LOAD_ASSET_SUCCESS = 'LOAD_ASSET_SUCCESS';
|
||||
export const LOAD_ASSET_FAILURE = 'LOAD_ASSET_FAILURE';
|
||||
|
||||
export function addAssets(assets: AssetProxy[]) {
|
||||
return { type: ADD_ASSETS, payload: assets };
|
||||
}
|
||||
@ -23,6 +27,42 @@ export function removeAsset(path: string) {
|
||||
return { type: REMOVE_ASSET, payload: path };
|
||||
}
|
||||
|
||||
export function loadAssetRequest(path: string) {
|
||||
return { type: LOAD_ASSET_REQUEST, payload: { path } };
|
||||
}
|
||||
|
||||
export function loadAssetSuccess(path: string) {
|
||||
return { type: LOAD_ASSET_SUCCESS, payload: { path } };
|
||||
}
|
||||
|
||||
export function loadAssetFailure(path: string, error: Error) {
|
||||
return { type: LOAD_ASSET_FAILURE, payload: { path, error } };
|
||||
}
|
||||
|
||||
export function loadAsset(resolvedPath: string) {
|
||||
return async (dispatch: ThunkDispatch<State, {}, AnyAction>, getState: () => State) => {
|
||||
try {
|
||||
dispatch(loadAssetRequest(resolvedPath));
|
||||
// load asset url from backend
|
||||
await waitForMediaLibraryToLoad(dispatch, getState());
|
||||
const file = selectMediaFileByPath(getState(), resolvedPath);
|
||||
|
||||
if (file) {
|
||||
const url = await getMediaDisplayURL(dispatch, getState(), file);
|
||||
const asset = createAssetProxy({ path: resolvedPath, url: url || resolvedPath });
|
||||
dispatch(addAsset(asset));
|
||||
} else {
|
||||
const { url } = await getMediaFile(getState(), resolvedPath);
|
||||
const asset = createAssetProxy({ path: resolvedPath, url });
|
||||
dispatch(addAsset(asset));
|
||||
}
|
||||
dispatch(loadAssetSuccess(resolvedPath));
|
||||
} catch (e) {
|
||||
dispatch(loadAssetFailure(resolvedPath, e));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
interface GetAssetArgs {
|
||||
collection: Collection;
|
||||
entry: EntryMap;
|
||||
@ -30,38 +70,51 @@ interface GetAssetArgs {
|
||||
folder?: string;
|
||||
}
|
||||
|
||||
const emptyAsset = createAssetProxy({
|
||||
path: 'empty.svg',
|
||||
file: new File([`<svg xmlns="http://www.w3.org/2000/svg"></svg>`], 'empty.svg', {
|
||||
type: 'image/svg+xml',
|
||||
}),
|
||||
});
|
||||
|
||||
export function boundGetAsset(
|
||||
dispatch: ThunkDispatch<State, {}, AnyAction>,
|
||||
collection: Collection,
|
||||
entry: EntryMap,
|
||||
) {
|
||||
const bound = (path: string, folder: string) => {
|
||||
const asset = dispatch(getAsset({ collection, entry, path, folder }));
|
||||
return asset;
|
||||
};
|
||||
|
||||
return bound;
|
||||
}
|
||||
|
||||
export function getAsset({ collection, entry, path, folder }: GetAssetArgs) {
|
||||
return async (dispatch: ThunkDispatch<State, {}, AnyAction>, getState: () => State) => {
|
||||
if (!path) return createAssetProxy({ path: '', file: new File([], 'empty') });
|
||||
return (dispatch: ThunkDispatch<State, {}, AnyAction>, getState: () => State) => {
|
||||
if (!path) return emptyAsset;
|
||||
|
||||
const state = getState();
|
||||
const resolvedPath = selectMediaFilePath(state.config, collection, entry, path, folder);
|
||||
|
||||
let asset = state.medias.get(resolvedPath);
|
||||
if (asset) {
|
||||
let { asset, isLoading, error } = state.medias.get(resolvedPath) || {};
|
||||
if (isLoading) {
|
||||
return emptyAsset;
|
||||
}
|
||||
if (asset && !error) {
|
||||
// There is already an AssetProxy in memory for this path. Use it.
|
||||
return asset;
|
||||
}
|
||||
|
||||
// Create a new AssetProxy (for consistency) and return it.
|
||||
if (isAbsolutePath(resolvedPath)) {
|
||||
// asset path is a public url so we can just use it as is
|
||||
asset = createAssetProxy({ path: resolvedPath, url: path });
|
||||
dispatch(addAsset(asset));
|
||||
} else {
|
||||
// load asset url from backend
|
||||
await waitForMediaLibraryToLoad(dispatch, getState());
|
||||
const file = selectMediaFileByPath(state, resolvedPath);
|
||||
|
||||
if (file) {
|
||||
const url = await getMediaDisplayURL(dispatch, getState(), file);
|
||||
asset = createAssetProxy({ path: resolvedPath, url: url || resolvedPath });
|
||||
} else {
|
||||
const { url } = await getMediaFile(state, resolvedPath);
|
||||
asset = createAssetProxy({ path: resolvedPath, url });
|
||||
}
|
||||
dispatch(loadAsset(resolvedPath));
|
||||
asset = emptyAsset;
|
||||
}
|
||||
|
||||
dispatch(addAsset(asset));
|
||||
|
||||
return asset;
|
||||
};
|
||||
}
|
||||
|
@ -1,12 +1,13 @@
|
||||
import React from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
import { connect } from 'react-redux';
|
||||
import { getAsset } from 'Actions/media';
|
||||
import { boundGetAsset } from 'Actions/media';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { colors, colorsRaw, components, lengths, Asset } from 'netlify-cms-ui-default';
|
||||
import { colors, colorsRaw, components, lengths } from 'netlify-cms-ui-default';
|
||||
import { VIEW_STYLE_LIST, VIEW_STYLE_GRID } from 'Constants/collectionViews';
|
||||
import { summaryFormatter } from 'Lib/formatters';
|
||||
import { keyToPathArray } from 'Lib/stringTemplate';
|
||||
import { selectIsLoadingAsset } from 'Reducers/medias';
|
||||
|
||||
const ListCard = styled.li`
|
||||
${components.card};
|
||||
@ -77,17 +78,13 @@ const CardBody = styled.div`
|
||||
`;
|
||||
|
||||
const CardImage = styled.div`
|
||||
background-image: url(${props => props.value?.toString()});
|
||||
background-image: url(${props => props.src});
|
||||
background-position: center center;
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
height: 150px;
|
||||
`;
|
||||
|
||||
const CardImageAsset = ({ getAsset, image, folder }) => {
|
||||
return <Asset folder={folder} path={image} getAsset={getAsset} component={CardImage} />;
|
||||
};
|
||||
|
||||
const EntryCard = ({
|
||||
path,
|
||||
summary,
|
||||
@ -95,7 +92,7 @@ const EntryCard = ({
|
||||
imageFolder,
|
||||
collectionLabel,
|
||||
viewStyle = VIEW_STYLE_LIST,
|
||||
boundGetAsset,
|
||||
getAsset,
|
||||
}) => {
|
||||
if (viewStyle === VIEW_STYLE_LIST) {
|
||||
return (
|
||||
@ -108,6 +105,9 @@ const EntryCard = ({
|
||||
);
|
||||
}
|
||||
|
||||
const asset = getAsset(image, imageFolder);
|
||||
const src = asset.toString();
|
||||
|
||||
if (viewStyle === VIEW_STYLE_GRID) {
|
||||
return (
|
||||
<GridCard>
|
||||
@ -116,9 +116,7 @@ const EntryCard = ({
|
||||
{collectionLabel ? <CollectionLabel>{collectionLabel}</CollectionLabel> : null}
|
||||
<CardHeading>{summary}</CardHeading>
|
||||
</CardBody>
|
||||
{image ? (
|
||||
<CardImageAsset getAsset={boundGetAsset} image={image} folder={imageFolder} />
|
||||
) : null}
|
||||
{image ? <CardImage src={src} /> : null}
|
||||
</GridCardLink>
|
||||
</GridCard>
|
||||
);
|
||||
@ -142,6 +140,8 @@ const mapStateToProps = (state, ownProps) => {
|
||||
image = encodeURI(image);
|
||||
}
|
||||
|
||||
const isLoadingAsset = selectIsLoadingAsset(state.medias);
|
||||
|
||||
return {
|
||||
summary,
|
||||
path: `/collections/${collection.get('name')}/entries/${entry.get('slug')}`,
|
||||
@ -150,13 +150,14 @@ const mapStateToProps = (state, ownProps) => {
|
||||
.get('fields')
|
||||
?.find(f => f.get('name') === inferedFields.imageField && f.get('widget') === 'image')
|
||||
?.get('media_folder'),
|
||||
isLoadingAsset,
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = {
|
||||
boundGetAsset: (collection, entry) => (dispatch, getState) => (path, folder) => {
|
||||
return getAsset({ collection, entry, path, folder })(dispatch, getState);
|
||||
},
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
boundGetAsset: (collection, entry) => boundGetAsset(dispatch, collection, entry),
|
||||
};
|
||||
};
|
||||
|
||||
const mergeProps = (stateProps, dispatchProps, ownProps) => {
|
||||
@ -164,7 +165,7 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => {
|
||||
...stateProps,
|
||||
...dispatchProps,
|
||||
...ownProps,
|
||||
boundGetAsset: dispatchProps.boundGetAsset(ownProps.collection, ownProps.entry),
|
||||
getAsset: dispatchProps.boundGetAsset(ownProps.collection, ownProps.entry),
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -32,7 +32,6 @@ import {
|
||||
import { loadDeployPreview } from 'Actions/deploys';
|
||||
import { deserializeValues } from 'Lib/serializeEntryValues';
|
||||
import { selectEntry, selectUnpublishedEntry, selectDeployPreview } from 'Reducers';
|
||||
import { getAsset } from 'Actions/media';
|
||||
import { selectFields } from 'Reducers/collections';
|
||||
import { status, EDITORIAL_WORKFLOW } from 'Constants/publishModes';
|
||||
import EditorInterface from './EditorInterface';
|
||||
@ -46,7 +45,6 @@ const navigateToEntry = (collectionName, slug) =>
|
||||
|
||||
export class Editor extends React.Component {
|
||||
static propTypes = {
|
||||
boundGetAsset: PropTypes.func.isRequired,
|
||||
changeDraftField: PropTypes.func.isRequired,
|
||||
changeDraftFieldValidation: PropTypes.func.isRequired,
|
||||
collection: ImmutablePropTypes.map.isRequired,
|
||||
@ -379,7 +377,6 @@ export class Editor extends React.Component {
|
||||
entry,
|
||||
entryDraft,
|
||||
fields,
|
||||
boundGetAsset,
|
||||
collection,
|
||||
changeDraftField,
|
||||
changeDraftFieldValidation,
|
||||
@ -420,7 +417,6 @@ export class Editor extends React.Component {
|
||||
<EditorInterface
|
||||
draftKey={draftKey}
|
||||
entry={entryDraft.get('entry')}
|
||||
getAsset={boundGetAsset}
|
||||
collection={collection}
|
||||
fields={fields}
|
||||
fieldsMetaData={entryDraft.get('fieldsMetaData')}
|
||||
@ -472,6 +468,7 @@ function mapStateToProps(state, ownProps) {
|
||||
const deployPreview = selectDeployPreview(state, collectionName, slug);
|
||||
const localBackup = entryDraft.get('localBackup');
|
||||
const draftKey = entryDraft.get('key');
|
||||
|
||||
return {
|
||||
collection,
|
||||
collections,
|
||||
@ -515,25 +512,6 @@ const mapDispatchToProps = {
|
||||
unpublishPublishedEntry,
|
||||
deleteUnpublishedEntry,
|
||||
logoutUser,
|
||||
boundGetAsset: (collection, entry) => (dispatch, getState) => (path, folder) => {
|
||||
return getAsset({ collection, entry, path, folder })(dispatch, getState);
|
||||
},
|
||||
};
|
||||
|
||||
const mergeProps = (stateProps, dispatchProps, ownProps) => {
|
||||
return {
|
||||
...stateProps,
|
||||
...dispatchProps,
|
||||
...ownProps,
|
||||
boundGetAsset: dispatchProps.boundGetAsset(
|
||||
stateProps.collection,
|
||||
stateProps.entryDraft.get('entry'),
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps,
|
||||
mergeProps,
|
||||
)(withWorkflow(translate()(Editor)));
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(withWorkflow(translate()(Editor)));
|
||||
|
@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { translate } from 'react-polyglot';
|
||||
@ -9,7 +10,8 @@ import { connect } from 'react-redux';
|
||||
import { FieldLabel, colors, transitions, lengths, borders } from 'netlify-cms-ui-default';
|
||||
import { resolveWidget, getEditorComponents } from 'Lib/registry';
|
||||
import { clearFieldErrors, loadEntry } from 'Actions/entries';
|
||||
import { addAsset, getAsset } from 'Actions/media';
|
||||
import { addAsset, boundGetAsset } from 'Actions/media';
|
||||
import { selectIsLoadingAsset } from 'Reducers/medias';
|
||||
import { query, clearSearch } from 'Actions/search';
|
||||
import {
|
||||
openMediaLibrary,
|
||||
@ -265,6 +267,7 @@ const mapStateToProps = state => {
|
||||
const { collections, entryDraft } = state;
|
||||
const entry = entryDraft.get('entry');
|
||||
const collection = collections.get(entryDraft.getIn(['entry', 'collection']));
|
||||
const isLoadingAsset = selectIsLoadingAsset(state.medias);
|
||||
|
||||
return {
|
||||
mediaPaths: state.mediaLibrary.get('controlMedia'),
|
||||
@ -272,25 +275,32 @@ const mapStateToProps = state => {
|
||||
queryHits: state.search.get('queryHits'),
|
||||
collection,
|
||||
entry,
|
||||
isLoadingAsset,
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = {
|
||||
openMediaLibrary,
|
||||
clearMediaControl,
|
||||
removeMediaControl,
|
||||
removeInsertedMedia,
|
||||
addAsset,
|
||||
query,
|
||||
loadEntry: (collectionName, slug) => (dispatch, getState) => {
|
||||
const collection = getState().collections.get(collectionName);
|
||||
return loadEntry(collection, slug)(dispatch, getState);
|
||||
},
|
||||
clearSearch,
|
||||
clearFieldErrors,
|
||||
boundGetAsset: (collection, entry) => (dispatch, getState) => (path, folder) => {
|
||||
return getAsset({ collection, entry, path, folder })(dispatch, getState);
|
||||
},
|
||||
const mapDispatchToProps = dispatch => {
|
||||
const creators = bindActionCreators(
|
||||
{
|
||||
openMediaLibrary,
|
||||
clearMediaControl,
|
||||
removeMediaControl,
|
||||
removeInsertedMedia,
|
||||
addAsset,
|
||||
query,
|
||||
clearSearch,
|
||||
clearFieldErrors,
|
||||
},
|
||||
dispatch,
|
||||
);
|
||||
return {
|
||||
...creators,
|
||||
loadEntry: (collectionName, slug) => (dispatch, getState) => {
|
||||
const collection = getState().collections.get(collectionName);
|
||||
return loadEntry(collection, slug)(dispatch, getState);
|
||||
},
|
||||
boundGetAsset: (collection, entry) => boundGetAsset(dispatch, collection, entry),
|
||||
};
|
||||
};
|
||||
|
||||
const mergeProps = (stateProps, dispatchProps, ownProps) => {
|
||||
|
@ -156,7 +156,6 @@ class EditorInterface extends Component {
|
||||
fields,
|
||||
fieldsMetaData,
|
||||
fieldsErrors,
|
||||
getAsset,
|
||||
onChange,
|
||||
showDelete,
|
||||
onDelete,
|
||||
@ -218,7 +217,6 @@ class EditorInterface extends Component {
|
||||
entry={entry}
|
||||
fields={fields}
|
||||
fieldsMetaData={fieldsMetaData}
|
||||
getAsset={getAsset}
|
||||
/>
|
||||
</PreviewPaneContainer>
|
||||
</StyledSplitPane>
|
||||
@ -297,7 +295,6 @@ EditorInterface.propTypes = {
|
||||
fields: ImmutablePropTypes.list.isRequired,
|
||||
fieldsMetaData: ImmutablePropTypes.map.isRequired,
|
||||
fieldsErrors: ImmutablePropTypes.map.isRequired,
|
||||
getAsset: PropTypes.func.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onValidate: PropTypes.func.isRequired,
|
||||
onPersist: PropTypes.func.isRequired,
|
||||
|
@ -8,6 +8,9 @@ import { lengths } from 'netlify-cms-ui-default';
|
||||
import { resolveWidget, getPreviewTemplate, getPreviewStyles } from 'Lib/registry';
|
||||
import { ErrorBoundary } from 'UI';
|
||||
import { selectTemplateName, selectInferedField, selectField } from 'Reducers/collections';
|
||||
import { connect } from 'react-redux';
|
||||
import { boundGetAsset } from 'Actions/media';
|
||||
import { selectIsLoadingAsset } from 'Reducers/medias';
|
||||
import { INFERABLE_FIELDS } from 'Constants/fieldInference';
|
||||
import EditorPreviewContent from './EditorPreviewContent.js';
|
||||
import PreviewHOC from './PreviewHOC';
|
||||
@ -21,7 +24,7 @@ const PreviewPaneFrame = styled(Frame)`
|
||||
border-radius: ${lengths.borderRadius};
|
||||
`;
|
||||
|
||||
export default class PreviewPane extends React.Component {
|
||||
export class PreviewPane extends React.Component {
|
||||
getWidget = (field, value, metadata, props, idx = null) => {
|
||||
const { getAsset, entry } = props;
|
||||
const widget = resolveWidget(field.get('widget'));
|
||||
@ -74,9 +77,9 @@ export default class PreviewPane extends React.Component {
|
||||
// custom preview templates, where the field object can't be passed in.
|
||||
let field = fields && fields.find(f => f.get('name') === name);
|
||||
let value = values && values.get(field.get('name'));
|
||||
let nestedFields = field.get('fields');
|
||||
let singleField = field.get('field');
|
||||
let metadata = fieldsMetaData && fieldsMetaData.get(field.get('name'), Map());
|
||||
const nestedFields = field.get('fields');
|
||||
const singleField = field.get('field');
|
||||
const metadata = fieldsMetaData && fieldsMetaData.get(field.get('name'), Map());
|
||||
|
||||
if (nestedFields) {
|
||||
field = field.set('fields', this.getNestedWidgets(nestedFields, value, metadata));
|
||||
@ -233,3 +236,25 @@ PreviewPane.propTypes = {
|
||||
fieldsMetaData: ImmutablePropTypes.map.isRequired,
|
||||
getAsset: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const isLoadingAsset = selectIsLoadingAsset(state.medias);
|
||||
return { isLoadingAsset };
|
||||
};
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
boundGetAsset: (collection, entry) => boundGetAsset(dispatch, collection, entry),
|
||||
};
|
||||
};
|
||||
|
||||
const mergeProps = (stateProps, dispatchProps, ownProps) => {
|
||||
return {
|
||||
...stateProps,
|
||||
...dispatchProps,
|
||||
...ownProps,
|
||||
getAsset: dispatchProps.boundGetAsset(ownProps.collection, ownProps.entry),
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps, mergeProps)(PreviewPane);
|
||||
|
@ -13,7 +13,8 @@ class PreviewHOC extends React.Component {
|
||||
return (
|
||||
isWidgetContainer ||
|
||||
this.props.value !== nextProps.value ||
|
||||
this.props.fieldsMetaData !== nextProps.fieldsMetaData
|
||||
this.props.fieldsMetaData !== nextProps.fieldsMetaData ||
|
||||
this.props.getAsset !== nextProps.getAsset
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,12 @@
|
||||
import { Map, fromJS } from 'immutable';
|
||||
import { addAssets, addAsset, removeAsset } from '../../actions/media';
|
||||
import {
|
||||
addAssets,
|
||||
addAsset,
|
||||
removeAsset,
|
||||
loadAssetRequest,
|
||||
loadAssetSuccess,
|
||||
loadAssetFailure,
|
||||
} from '../../actions/media';
|
||||
import reducer from '../medias';
|
||||
import { createAssetProxy } from '../../valueObjects/AssetProxy';
|
||||
|
||||
@ -7,14 +14,37 @@ describe('medias', () => {
|
||||
const asset = createAssetProxy({ url: 'url', path: 'path' });
|
||||
|
||||
it('should add assets', () => {
|
||||
expect(reducer(fromJS({}), addAssets([asset]))).toEqual(Map({ path: asset }));
|
||||
expect(reducer(fromJS({}), addAssets([asset]))).toEqual(
|
||||
Map({ path: { asset, isLoading: false, error: null } }),
|
||||
);
|
||||
});
|
||||
|
||||
it('should add asset', () => {
|
||||
expect(reducer(fromJS({}), addAsset(asset))).toEqual(Map({ path: asset }));
|
||||
expect(reducer(fromJS({}), addAsset(asset))).toEqual(
|
||||
Map({ path: { asset, isLoading: false, error: null } }),
|
||||
);
|
||||
});
|
||||
|
||||
it('should remove asset', () => {
|
||||
expect(reducer(fromJS({ path: asset }), removeAsset(asset.path))).toEqual(Map());
|
||||
});
|
||||
|
||||
it('should mark asset as loading', () => {
|
||||
expect(reducer(fromJS({}), loadAssetRequest(asset.path))).toEqual(
|
||||
Map({ path: { isLoading: true } }),
|
||||
);
|
||||
});
|
||||
|
||||
it('should mark asset as not loading', () => {
|
||||
expect(reducer(fromJS({}), loadAssetSuccess(asset.path))).toEqual(
|
||||
Map({ path: { isLoading: false, error: null } }),
|
||||
);
|
||||
});
|
||||
|
||||
it('should set loading error', () => {
|
||||
const error = new Error('some error');
|
||||
expect(reducer(fromJS({}), loadAssetFailure(asset.path, error))).toEqual(
|
||||
Map({ path: { isLoading: false, error } }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@ -1,5 +1,12 @@
|
||||
import { fromJS } from 'immutable';
|
||||
import { ADD_ASSETS, ADD_ASSET, REMOVE_ASSET } from '../actions/media';
|
||||
import {
|
||||
ADD_ASSETS,
|
||||
ADD_ASSET,
|
||||
REMOVE_ASSET,
|
||||
LOAD_ASSET_REQUEST,
|
||||
LOAD_ASSET_SUCCESS,
|
||||
LOAD_ASSET_FAILURE,
|
||||
} from '../actions/media';
|
||||
import AssetProxy from '../valueObjects/AssetProxy';
|
||||
import { Medias, MediasAction } from '../types/redux';
|
||||
|
||||
@ -9,21 +16,36 @@ const medias = (state: Medias = fromJS({}), action: MediasAction) => {
|
||||
const payload = action.payload as AssetProxy[];
|
||||
let newState = state;
|
||||
payload.forEach(asset => {
|
||||
newState = newState.set(asset.path, asset);
|
||||
newState = newState.set(asset.path, { asset, isLoading: false, error: null });
|
||||
});
|
||||
return newState;
|
||||
}
|
||||
case ADD_ASSET: {
|
||||
const payload = action.payload as AssetProxy;
|
||||
return state.set(payload.path, payload);
|
||||
const asset = action.payload as AssetProxy;
|
||||
return state.set(asset.path, { asset, isLoading: false, error: null });
|
||||
}
|
||||
case REMOVE_ASSET: {
|
||||
const payload = action.payload as string;
|
||||
return state.delete(payload);
|
||||
}
|
||||
case LOAD_ASSET_REQUEST: {
|
||||
const { path } = action.payload as { path: string };
|
||||
return state.set(path, { ...state.get(path), isLoading: true });
|
||||
}
|
||||
case LOAD_ASSET_SUCCESS: {
|
||||
const { path } = action.payload as { path: string };
|
||||
return state.set(path, { ...state.get(path), isLoading: false, error: null });
|
||||
}
|
||||
case LOAD_ASSET_FAILURE: {
|
||||
const { path, error } = action.payload as { path: string; error: Error };
|
||||
return state.set(path, { ...state.get(path), isLoading: false, error });
|
||||
}
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export const selectIsLoadingAsset = (state: Medias) =>
|
||||
Object.values(state.toJS()).some(state => state.isLoading);
|
||||
|
||||
export default medias;
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { createHashHistory } from 'history';
|
||||
|
||||
let history = createHashHistory();
|
||||
const history = createHashHistory();
|
||||
|
||||
export default history;
|
||||
|
@ -144,7 +144,9 @@ export type Collection = StaticallyTypedRecord<CollectionObject>;
|
||||
|
||||
export type Collections = StaticallyTypedRecord<{ [path: string]: Collection & CollectionObject }>;
|
||||
|
||||
export type Medias = StaticallyTypedRecord<{ [path: string]: AssetProxy | undefined }>;
|
||||
export type Medias = StaticallyTypedRecord<{
|
||||
[path: string]: { asset: AssetProxy | undefined; isLoading: boolean; error: Error | null };
|
||||
}>;
|
||||
|
||||
export interface MediaLibraryInstance {
|
||||
show: (args: {
|
||||
@ -216,7 +218,7 @@ export interface State {
|
||||
}
|
||||
|
||||
export interface MediasAction extends Action<string> {
|
||||
payload: string | AssetProxy | AssetProxy[];
|
||||
payload: string | AssetProxy | AssetProxy[] | { path: string } | { path: string; error: Error };
|
||||
}
|
||||
|
||||
export interface ConfigAction extends Action<string> {
|
||||
|
@ -1,6 +1,6 @@
|
||||
import component from '../index';
|
||||
|
||||
const getAsset = path => Promise.resolve(path);
|
||||
const getAsset = path => path;
|
||||
const image = '/image';
|
||||
const alt = 'alt';
|
||||
const title = 'title';
|
||||
@ -32,8 +32,8 @@ describe('editor component image', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should generate valid react props', async () => {
|
||||
await expect(component.toPreview({ image, alt, title }, getAsset)).resolves.toMatchObject({
|
||||
it('should generate valid react props', () => {
|
||||
expect(component.toPreview({ image, alt, title }, getAsset)).toMatchObject({
|
||||
props: { src: image, alt, title },
|
||||
});
|
||||
});
|
||||
|
@ -12,10 +12,10 @@ const image = {
|
||||
toBlock: ({ alt, image, title }) =>
|
||||
`}"` : ''})`,
|
||||
// eslint-disable-next-line react/display-name
|
||||
toPreview: async ({ alt, image, title }, getAsset, fields) => {
|
||||
toPreview: ({ alt, image, title }, getAsset, fields) => {
|
||||
const imageField = fields?.find(f => f.get('widget') === 'image');
|
||||
const folder = imageField?.get('media_folder');
|
||||
const src = await getAsset(image, folder);
|
||||
const src = getAsset(image, folder);
|
||||
return <img src={src || ''} alt={alt || ''} title={title || ''} />;
|
||||
},
|
||||
pattern: /^!\[(.*)\]\((.*?)(\s"(.*)")?\)$/,
|
||||
|
@ -1,50 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
class Asset extends React.Component {
|
||||
static propTypes = {
|
||||
path: PropTypes.string.isRequired,
|
||||
getAsset: PropTypes.func.isRequired,
|
||||
component: PropTypes.elementType.isRequired,
|
||||
folder: PropTypes.string,
|
||||
};
|
||||
|
||||
subscribed = true;
|
||||
|
||||
state = {
|
||||
value: null,
|
||||
};
|
||||
|
||||
async _fetchAsset() {
|
||||
const { getAsset, path, folder } = this.props;
|
||||
const value = await getAsset(path, folder);
|
||||
if (this.subscribed) {
|
||||
this.setState({ value });
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this._fetchAsset();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.subscribed = false;
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (
|
||||
prevProps.path !== this.props.path ||
|
||||
prevProps.getAsset !== this.props.getAsset ||
|
||||
prevProps.folder !== this.props.folder
|
||||
) {
|
||||
this._fetchAsset();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { component, ...props } = this.props;
|
||||
return React.createElement(component, { ...props, value: this.state.value });
|
||||
}
|
||||
}
|
||||
|
||||
export default Asset;
|
@ -9,7 +9,6 @@ import AuthenticationPage from './AuthenticationPage';
|
||||
import WidgetPreviewContainer from './WidgetPreviewContainer';
|
||||
import ObjectWidgetTopBar from './ObjectWidgetTopBar';
|
||||
import GoBackButton from './GoBackButton';
|
||||
import Asset from './Asset';
|
||||
import {
|
||||
fonts,
|
||||
colorsRaw,
|
||||
@ -56,7 +55,6 @@ export const NetlifyCmsUiDefault = {
|
||||
effects,
|
||||
reactSelectStyles,
|
||||
GlobalStyles,
|
||||
Asset,
|
||||
};
|
||||
export {
|
||||
Dropdown,
|
||||
@ -89,5 +87,4 @@ export {
|
||||
reactSelectStyles,
|
||||
GlobalStyles,
|
||||
GoBackButton,
|
||||
Asset,
|
||||
};
|
||||
|
@ -2,9 +2,9 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import styled from '@emotion/styled';
|
||||
import { List } from 'immutable';
|
||||
import { WidgetPreviewContainer, Asset } from 'netlify-cms-ui-default';
|
||||
import { WidgetPreviewContainer } from 'netlify-cms-ui-default';
|
||||
|
||||
const FileLink = styled(({ value: href, path }) => (
|
||||
const FileLink = styled(({ href, path }) => (
|
||||
<a href={href} rel="noopener noreferrer" target="_blank">
|
||||
{path}
|
||||
</a>
|
||||
@ -12,25 +12,24 @@ const FileLink = styled(({ value: href, path }) => (
|
||||
display: block;
|
||||
`;
|
||||
|
||||
const FileLinkAsset = ({ value, getAsset }) => {
|
||||
return <Asset path={value} getAsset={getAsset} component={FileLink} />;
|
||||
};
|
||||
|
||||
function FileLinkList({ values, getAsset }) {
|
||||
function FileLinkList({ values, getAsset, folder }) {
|
||||
return (
|
||||
<div>
|
||||
{values.map(value => (
|
||||
<FileLinkAsset key={value} value={value} getAsset={getAsset} />
|
||||
<FileLink key={value} path={value} href={getAsset(value, folder)} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function FileContent({ value, getAsset }) {
|
||||
function FileContent(props) {
|
||||
const { value, getAsset, field } = props;
|
||||
const folder = field.get('media_folder');
|
||||
|
||||
if (Array.isArray(value) || List.isList(value)) {
|
||||
return <FileLinkList values={value} getAsset={getAsset} />;
|
||||
return <FileLinkList values={value} getAsset={getAsset} folder={folder} />;
|
||||
}
|
||||
return <FileLinkAsset value={value} getAsset={getAsset} />;
|
||||
return <FileLink key={value} path={value} href={getAsset(value, folder)} />;
|
||||
}
|
||||
|
||||
const FilePreview = props => (
|
||||
|
@ -6,15 +6,7 @@ import { Map, List } from 'immutable';
|
||||
import { once } from 'lodash';
|
||||
import uuid from 'uuid/v4';
|
||||
import { oneLine } from 'common-tags';
|
||||
import {
|
||||
lengths,
|
||||
components,
|
||||
buttons,
|
||||
borders,
|
||||
effects,
|
||||
shadows,
|
||||
Asset,
|
||||
} from 'netlify-cms-ui-default';
|
||||
import { lengths, components, buttons, borders, effects, shadows } from 'netlify-cms-ui-default';
|
||||
|
||||
const MAX_DISPLAY_LENGTH = 50;
|
||||
|
||||
@ -31,15 +23,13 @@ const ImageWrapper = styled.div`
|
||||
${shadows.inset};
|
||||
`;
|
||||
|
||||
const Image = styled(({ value: src }) => <img src={src || ''} role="presentation" />)`
|
||||
const StyledImage = styled.img`
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
`;
|
||||
|
||||
const ImageAsset = ({ getAsset, value, folder }) => {
|
||||
return <Asset folder={folder} path={value} getAsset={getAsset} component={Image} />;
|
||||
};
|
||||
const Image = props => <StyledImage role="presentation" {...props} />;
|
||||
|
||||
const MultiImageWrapper = styled.div`
|
||||
display: flex;
|
||||
@ -118,9 +108,9 @@ export default function withFileControl({ forImage } = {}) {
|
||||
|
||||
shouldComponentUpdate(nextProps) {
|
||||
/**
|
||||
* Always update if the value changes.
|
||||
* Always update if the value or getAsset changes.
|
||||
*/
|
||||
if (this.props.value !== nextProps.value) {
|
||||
if (this.props.value !== nextProps.value || this.props.getAsset !== nextProps.getAsset) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -228,15 +218,17 @@ export default function withFileControl({ forImage } = {}) {
|
||||
<MultiImageWrapper>
|
||||
{value.map(val => (
|
||||
<ImageWrapper key={val}>
|
||||
<ImageAsset getAsset={getAsset} value={val} folder={folder} />
|
||||
<Image src={getAsset(val, folder) || ''} />
|
||||
</ImageWrapper>
|
||||
))}
|
||||
</MultiImageWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
const src = getAsset(value, folder);
|
||||
return (
|
||||
<ImageWrapper>
|
||||
<ImageAsset getAsset={getAsset} value={value} folder={folder} />
|
||||
<Image src={src || ''} />
|
||||
</ImageWrapper>
|
||||
);
|
||||
};
|
||||
|
@ -2,23 +2,16 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import styled from '@emotion/styled';
|
||||
import { List } from 'immutable';
|
||||
import { WidgetPreviewContainer, Asset } from 'netlify-cms-ui-default';
|
||||
import { WidgetPreviewContainer } from 'netlify-cms-ui-default';
|
||||
|
||||
const StyledImage = styled(({ value: src }) => <img src={src || ''} role="presentation" />)`
|
||||
const StyledImage = styled(({ src }) => <img src={src || ''} role="presentation" />)`
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
`;
|
||||
|
||||
const StyledImageAsset = ({ getAsset, value, field }) => {
|
||||
return (
|
||||
<Asset
|
||||
folder={field.get('media_folder')}
|
||||
path={value}
|
||||
getAsset={getAsset}
|
||||
component={StyledImage}
|
||||
/>
|
||||
);
|
||||
return <StyledImage src={getAsset(value, field.get('media_folder'))} />;
|
||||
};
|
||||
|
||||
const ImagePreviewContent = props => {
|
||||
|
@ -186,7 +186,7 @@ export default class ListControl extends React.Component {
|
||||
|
||||
handleAddType = (type, typeKey) => {
|
||||
const { value, onChange } = this.props;
|
||||
let parsedValue = fromJS(this.mixedDefault(typeKey, type));
|
||||
const parsedValue = fromJS(this.mixedDefault(typeKey, type));
|
||||
this.setState({ itemsCollapsed: this.state.itemsCollapsed.push(false) });
|
||||
onChange((value || List()).push(parsedValue));
|
||||
};
|
||||
|
@ -50,18 +50,17 @@ export default class RawEditor extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
handleCopy = async (event, editor) => {
|
||||
event.persist();
|
||||
handleCopy = (event, editor) => {
|
||||
const { getAsset, resolveWidget } = this.props;
|
||||
const markdown = Plain.serialize(Value.create({ document: editor.value.fragment }));
|
||||
const html = await markdownToHtml(markdown, { getAsset, resolveWidget });
|
||||
const html = markdownToHtml(markdown, { getAsset, resolveWidget });
|
||||
setEventTransfer(event, 'text', markdown);
|
||||
setEventTransfer(event, 'html', html);
|
||||
event.preventDefault();
|
||||
};
|
||||
|
||||
handleCut = async (event, editor, next) => {
|
||||
await this.handleCopy(event, editor, next);
|
||||
handleCut = (event, editor, next) => {
|
||||
this.handleCopy(event, editor, next);
|
||||
editor.delete();
|
||||
};
|
||||
|
||||
|
@ -5,10 +5,9 @@ import isHotkey from 'is-hotkey';
|
||||
import { slateToMarkdown, markdownToSlate, htmlToSlate, markdownToHtml } from '../../serializers';
|
||||
|
||||
const CopyPasteVisual = ({ getAsset, resolveWidget }) => {
|
||||
const handleCopy = async (event, editor) => {
|
||||
event.persist();
|
||||
const handleCopy = (event, editor) => {
|
||||
const markdown = slateToMarkdown(editor.value.fragment.toJS());
|
||||
const html = await markdownToHtml(markdown, { getAsset, resolveWidget });
|
||||
const html = markdownToHtml(markdown, { getAsset, resolveWidget });
|
||||
setEventTransfer(event, 'text', markdown);
|
||||
setEventTransfer(event, 'html', html);
|
||||
setEventTransfer(event, 'fragment', base64.serializeNode(editor.value.fragment));
|
||||
@ -32,8 +31,8 @@ const CopyPasteVisual = ({ getAsset, resolveWidget }) => {
|
||||
const doc = Document.fromJSON(ast);
|
||||
return editor.insertFragment(doc);
|
||||
},
|
||||
async onCopy(event, editor, next) {
|
||||
await handleCopy(event, editor, next);
|
||||
onCopy(event, editor, next) {
|
||||
handleCopy(event, editor, next);
|
||||
},
|
||||
onCut(event, editor, next) {
|
||||
handleCopy(event, editor, next);
|
||||
|
@ -10,43 +10,14 @@ class MarkdownPreview extends React.Component {
|
||||
value: PropTypes.string,
|
||||
};
|
||||
|
||||
subscribed = true;
|
||||
|
||||
state = {
|
||||
html: null,
|
||||
};
|
||||
|
||||
async _renderHtml() {
|
||||
const { value, getAsset, resolveWidget } = this.props;
|
||||
if (value) {
|
||||
const html = await markdownToHtml(value, { getAsset, resolveWidget });
|
||||
if (this.subscribed) {
|
||||
this.setState({ html });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
const { value, getAsset, resolveWidget } = this.props;
|
||||
if (value === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const html = markdownToHtml(value, { getAsset, resolveWidget });
|
||||
|
||||
return <WidgetPreviewContainer dangerouslySetInnerHTML={{ __html: html }} />;
|
||||
}
|
||||
}
|
||||
|
@ -148,13 +148,13 @@ export const remarkToMarkdown = obj => {
|
||||
/**
|
||||
* Convert Markdown to HTML.
|
||||
*/
|
||||
export const markdownToHtml = async (markdown, { getAsset, resolveWidget } = {}) => {
|
||||
export const markdownToHtml = (markdown, { getAsset, resolveWidget } = {}) => {
|
||||
const mdast = markdownToRemark(markdown);
|
||||
|
||||
const hast = await unified()
|
||||
const hast = unified()
|
||||
.use(remarkToRehypeShortcodes, { plugins: getEditorComponents(), getAsset, resolveWidget })
|
||||
.use(remarkToRehype, { allowDangerousHTML: true })
|
||||
.run(mdast);
|
||||
.runSync(mdast);
|
||||
|
||||
const html = unified()
|
||||
.use(rehypeToHtml, {
|
||||
|
@ -12,15 +12,15 @@ import u from 'unist-builder';
|
||||
export default function remarkToRehypeShortcodes({ plugins, getAsset, resolveWidget }) {
|
||||
return transform;
|
||||
|
||||
async function transform(root) {
|
||||
const transformedChildren = await Promise.all(map(root.children, processShortcodes));
|
||||
function transform(root) {
|
||||
const transformedChildren = map(root.children, processShortcodes);
|
||||
return { ...root, children: transformedChildren };
|
||||
}
|
||||
|
||||
/**
|
||||
* Mapping function to transform nodes that contain shortcodes.
|
||||
*/
|
||||
async function processShortcodes(node) {
|
||||
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 = await getPreview(plugin, shortcodeData);
|
||||
const value = 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.
|
||||
*/
|
||||
async function getPreview(plugin, shortcodeData) {
|
||||
function getPreview(plugin, shortcodeData) {
|
||||
const { toPreview, widget, fields } = plugin;
|
||||
if (toPreview) {
|
||||
return toPreview(shortcodeData, getAsset, fields);
|
||||
|
@ -114,7 +114,7 @@ export default class RelationControl extends React.Component {
|
||||
};
|
||||
|
||||
parseNestedFields = (targetObject, field) => {
|
||||
let nestedField = field.split('.');
|
||||
const nestedField = field.split('.');
|
||||
let f = targetObject;
|
||||
for (let i = 0; i < nestedField.length; i++) {
|
||||
f = f[nestedField[i]];
|
||||
|
@ -5,4 +5,5 @@ import * as emotion from 'emotion';
|
||||
import { createSerializer } from 'jest-emotion';
|
||||
|
||||
window.fetch = fetch;
|
||||
window.URL.createObjectURL = jest.fn();
|
||||
expect.addSnapshotSerializer(createSerializer(emotion));
|
||||
|
@ -59,7 +59,7 @@ The control component receives one (1) callback as a prop: `onChange`.
|
||||
* onChange (required): Should be called when the users changes the current value. It will ultimately end up updating the EntryDraft object in the Redux Store, thus updating the preview component.
|
||||
* onAddAsset & onRemoveAsset (optionals): Should be invoked with an `AssetProxy` value object if the field accepts file uploads for media (images, for example). `onAddAsset` will get the current media stored in the Redux state tree while `onRemoveAsset` will remove it. AssetProxy objects are stored in the `Medias` object and referenced in the `EntryDraft` object on the state tree.
|
||||
|
||||
Both control and preview widgets receive a `getAsset` selector via props. Displaying the media (or its URI) for the user should always be done via `getAsset`, as it returns a Promise that resolves to an AssetProxy that can return the correct value for both medias already persisted on the server and cached media not yet uploaded.
|
||||
Both control and preview widgets receive a `getAsset` selector via props. Displaying the media (or its URI) for the user should always be done via `getAsset`, as it returns an AssetProxy that can return the correct value for both medias already persisted on the server and cached media not yet uploaded.
|
||||
|
||||
The actual persistence of the content and medias inserted into the control component is delegated to the backend implementation. The backend will be called with the updated values and a list of assetProxy objects for each field of the entry, and should return a promise that can resolve into the persisted entry object and the list of the persisted media URIs.
|
||||
|
||||
|
@ -72,53 +72,19 @@ Registers a template for a folder collection or an individual file in a file col
|
||||
<script src="https://unpkg.com/netlify-cms@^2.0.0/dist/netlify-cms.js"></script>
|
||||
<script>
|
||||
var PostPreview = createClass({
|
||||
subscribed: true,
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
src: '',
|
||||
};
|
||||
},
|
||||
|
||||
_fetchAsset: function() {
|
||||
const path = this.props.entry.getIn(['data', 'image']);
|
||||
path &&
|
||||
this.props.getAsset(path).then(value => {
|
||||
if (this.subscribed) {
|
||||
this.setState({ src: value.toString() });
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
this._fetchAsset();
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
this.subscribed = false;
|
||||
},
|
||||
|
||||
componentDidUpdate: function(prevProps) {
|
||||
const prevPath = prevProps.entry.getIn(['data', 'image']);
|
||||
const path = this.props.entry.getIn(['data', 'image']);
|
||||
if (prevPath !== path || prevProps.getAsset !== this.props.getAsset) {
|
||||
this._fetchAsset();
|
||||
}
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var entry = this.props.entry;
|
||||
return h(
|
||||
'div',
|
||||
{},
|
||||
var image = entry.getIn(['data', 'image']);
|
||||
var bg = this.props.getAsset(image);
|
||||
return h('div', {},
|
||||
h('h1', {}, entry.getIn(['data', 'title'])),
|
||||
h('img', { src: this.state.src }),
|
||||
h('div', { className: 'text' }, this.props.widgetFor('body')),
|
||||
h('img', {src: bg.toString()}),
|
||||
h('div', {"className": "text"}, this.props.widgetFor('body'))
|
||||
);
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
CMS.registerPreviewTemplate('posts', PostPreview);
|
||||
CMS.registerPreviewTemplate("posts", PostPreview);
|
||||
</script>
|
||||
```
|
||||
### Lists and Objects
|
||||
@ -212,4 +178,4 @@ Registers a template for a folder collection or an individual file in a file col
|
||||
</article>
|
||||
}
|
||||
}
|
||||
```
|
||||
```
|
Loading…
x
Reference in New Issue
Block a user