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:
@ -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> {
|
||||
|
Reference in New Issue
Block a user