Allow for relative paths of media files (#2394)

* Allow for relative paths of media files

fixes #325

* Switch to calculating the relative path based on collection

The required relative path is now calculated depending on the
location of the collection of the current entry having the
media inserted into. And the configuration option has now been
changed to a boolean flag.

This allows collections to not neccesarily all be in the same
location relative to the media folder, and simplifies config.

* Clean up code and fix linting

* Add unit tests to resolveMediaFilename()

* Rework insertMedia action to fetch own config

This moves more of the media path resolution logic into the action
which makes it easier to unit test

* Add unit tests for the mediaLibrary.insertMedia action

* yarn run format

* add dependabot config (#2580)
This commit is contained in:
Sam Lanning
2019-08-24 21:03:09 +01:00
committed by Shawn Erquhart
parent 861c36f658
commit a47a29fb8b
10 changed files with 279 additions and 10 deletions

View File

@ -78,5 +78,8 @@
"react": "^16.8.4",
"react-dom": "^16.8.4",
"react-immutable-proptypes": "^2.1.0"
},
"devDependencies": {
"redux-mock-store": "^1.5.3"
}
}

View File

@ -0,0 +1,94 @@
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import { fromJS } from 'immutable';
import { insertMedia } from '../mediaLibrary';
const middlewares = [thunk];
const mockStore = configureMockStore(middlewares);
describe('mediaLibrary', () => {
describe('insertMedia', () => {
it('test public URL is returned directly', () => {
const store = mockStore({});
store.dispatch(insertMedia({ url: '//localhost/foo.png' }));
expect(store.getActions()[0]).toEqual({
type: 'MEDIA_INSERT',
payload: { mediaPath: '//localhost/foo.png' },
});
});
it('Test relative path resolution', () => {
const store = mockStore({
config: fromJS({
media_folder_relative: true,
media_folder: 'content/media',
}),
entryDraft: fromJS({
entry: {
collection: 'blog-posts',
},
}),
collections: fromJS({
'blog-posts': {
folder: 'content/blog/posts',
},
}),
});
store.dispatch(insertMedia({ name: 'foo.png' }));
expect(store.getActions()[0]).toEqual({
type: 'MEDIA_INSERT',
payload: { mediaPath: '../../media/foo.png' },
});
});
// media_folder_relative will be used even if public_folder is specified
it('Test relative path resolution, with public folder specified', () => {
const store = mockStore({
config: fromJS({
media_folder_relative: true,
media_folder: 'content/media',
public_folder: '/static/assets/media',
}),
entryDraft: fromJS({
entry: {
collection: 'blog-posts',
},
}),
collections: fromJS({
'blog-posts': {
folder: 'content/blog/posts',
},
}),
});
store.dispatch(insertMedia({ name: 'foo.png' }));
expect(store.getActions()[0]).toEqual({
type: 'MEDIA_INSERT',
payload: { mediaPath: '../../media/foo.png' },
});
});
it('Test public_folder resolution', () => {
const store = mockStore({
config: fromJS({
public_folder: '/static/assets/media',
}),
});
store.dispatch(insertMedia({ name: 'foo.png' }));
expect(store.getActions()[0]).toEqual({
type: 'MEDIA_INSERT',
payload: { mediaPath: '/static/assets/media/foo.png' },
});
});
it('Test incorrect usage', () => {
const store = mockStore();
try {
store.dispatch(insertMedia({ foo: 'foo.png' }));
throw new Error('Expected Exception');
} catch (e) {
expect(e.message).toEqual('Incorrect usage, expected {url} or {file}');
}
});
});
});

View File

@ -1,6 +1,6 @@
import { Map } from 'immutable';
import { actions as notifActions } from 'redux-notifications';
import { getBlobSHA } from 'netlify-cms-lib-util';
import { resolveMediaFilename, getBlobSHA } from 'netlify-cms-lib-util';
import { currentBackend } from 'coreSrc/backend';
import { createAssetProxy } from 'ValueObjects/AssetProxy';
import { selectIntegration } from 'Reducers';
@ -82,8 +82,33 @@ export function closeMediaLibrary() {
};
}
export function insertMedia(mediaPath) {
return { type: MEDIA_INSERT, payload: { mediaPath } };
export function insertMedia(media) {
return (dispatch, getState) => {
let mediaPath;
if (media.url) {
// media.url is public, and already resolved
mediaPath = media.url;
} else if (media.name) {
// media.name still needs to be resolved to the appropriate URL
const state = getState();
const config = state.config;
if (config.get('media_folder_relative')) {
// the path is being resolved relatively
// and we need to know the path of the entry to resolve it
const mediaFolder = config.get('media_folder');
const collection = state.entryDraft.getIn(['entry', 'collection']);
const collectionFolder = state.collections.getIn([collection, 'folder']);
mediaPath = resolveMediaFilename(media.name, { mediaFolder, collectionFolder });
} else {
// the path is being resolved to a public URL
const publicFolder = config.get('public_folder');
mediaPath = resolveMediaFilename(media.name, { publicFolder });
}
} else {
throw new Error('Incorrect usage, expected {url} or {file}');
}
dispatch({ type: MEDIA_INSERT, payload: { mediaPath } });
};
}
export function removeInsertedMedia(controlID) {

View File

@ -5,7 +5,7 @@ import { connect } from 'react-redux';
import { orderBy, map } from 'lodash';
import { translate } from 'react-polyglot';
import fuzzy from 'fuzzy';
import { resolvePath, fileExtension } from 'netlify-cms-lib-util';
import { fileExtension } from 'netlify-cms-lib-util';
import {
loadMedia as loadMediaAction,
persistMedia as persistMediaAction,
@ -56,7 +56,6 @@ class MediaLibrary extends React.Component {
persistMedia: PropTypes.func.isRequired,
deleteMedia: PropTypes.func.isRequired,
insertMedia: PropTypes.func.isRequired,
publicFolder: PropTypes.string,
closeMediaLibrary: PropTypes.func.isRequired,
t: PropTypes.func.isRequired,
};
@ -189,9 +188,8 @@ class MediaLibrary extends React.Component {
handleInsert = () => {
const { selectedFile } = this.state;
const { name, url, urlIsPublicPath } = selectedFile;
const { insertMedia, publicFolder } = this.props;
const publicPath = urlIsPublicPath ? url : resolvePath(name, publicFolder);
insertMedia(publicPath);
const { insertMedia } = this.props;
insertMedia(urlIsPublicPath ? { url } : { name });
this.handleClose();
};

View File

@ -40,6 +40,7 @@ const getConfigSchema = () => ({
show_preview_links: { type: 'boolean' },
media_folder: { type: 'string', examples: ['assets/uploads'] },
public_folder: { type: 'string', examples: ['/uploads'] },
media_folder_relative: { type: 'boolean' },
media_library: {
type: 'object',
properties: {