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:
Erez Rokah
2020-02-13 02:12:36 +02:00
committed by GitHub
parent 2457c29baf
commit ab685e8594
30 changed files with 317 additions and 355 deletions

View File

@ -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);
});
});
});

View File

@ -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;
};
}

View File

@ -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),
};
};

View File

@ -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)));

View File

@ -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) => {

View File

@ -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,

View File

@ -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);

View File

@ -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
);
}

View File

@ -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 } }),
);
});
});

View File

@ -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;

View File

@ -1,5 +1,5 @@
import { createHashHistory } from 'history';
let history = createHashHistory();
const history = createHashHistory();
export default history;

View File

@ -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> {