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:
parent
861c36f658
commit
a47a29fb8b
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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}');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
@ -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) {
|
||||
|
@ -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();
|
||||
};
|
||||
|
||||
|
@ -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: {
|
||||
|
@ -17,6 +17,7 @@
|
||||
"build:esm": "cross-env NODE_ENV=esm babel src --out-dir dist/esm --ignore \"**/__tests__\" --root-mode upward"
|
||||
},
|
||||
"dependencies": {
|
||||
"get-relative-path": "^1.0.2",
|
||||
"js-sha256": "^0.9.0",
|
||||
"localforage": "^1.7.3"
|
||||
},
|
||||
|
@ -1,4 +1,93 @@
|
||||
import { fileExtensionWithSeparator, fileExtension } from '../path';
|
||||
import { resolveMediaFilename, fileExtensionWithSeparator, fileExtension } from '../path';
|
||||
|
||||
describe('resolveMediaFilename', () => {
|
||||
it('publicly Accessible URL, no slash', () => {
|
||||
expect(
|
||||
resolveMediaFilename('image.png', {
|
||||
publicFolder: 'static/assets',
|
||||
}),
|
||||
).toEqual('/static/assets/image.png');
|
||||
});
|
||||
|
||||
it('publicly Accessible URL, with slash', () => {
|
||||
expect(
|
||||
resolveMediaFilename('image.png', {
|
||||
publicFolder: '/static/assets',
|
||||
}),
|
||||
).toEqual('/static/assets/image.png');
|
||||
});
|
||||
|
||||
it('publicly Accessible URL, root', () => {
|
||||
expect(
|
||||
resolveMediaFilename('image.png', {
|
||||
publicFolder: '/',
|
||||
}),
|
||||
).toEqual('/image.png');
|
||||
});
|
||||
|
||||
it('relative URL, same folder', () => {
|
||||
expect(
|
||||
resolveMediaFilename('image.png', {
|
||||
mediaFolder: '/content/posts',
|
||||
collectionFolder: '/content/posts',
|
||||
}),
|
||||
).toEqual('image.png');
|
||||
});
|
||||
|
||||
it('relative URL, same folder, with slash', () => {
|
||||
expect(
|
||||
resolveMediaFilename('image.png', {
|
||||
mediaFolder: '/content/posts/',
|
||||
collectionFolder: '/content/posts',
|
||||
}),
|
||||
).toEqual('image.png');
|
||||
});
|
||||
|
||||
it('relative URL, same folder, with slashes', () => {
|
||||
expect(
|
||||
resolveMediaFilename('image.png', {
|
||||
mediaFolder: '/content/posts/',
|
||||
collectionFolder: '/content/posts/',
|
||||
}),
|
||||
).toEqual('image.png');
|
||||
});
|
||||
|
||||
it('relative URL, sibling folder', () => {
|
||||
expect(
|
||||
resolveMediaFilename('image.png', {
|
||||
mediaFolder: '/content/images/',
|
||||
collectionFolder: '/content/posts/',
|
||||
}),
|
||||
).toEqual('../images/image.png');
|
||||
});
|
||||
|
||||
it('relative URL, cousin folder', () => {
|
||||
expect(
|
||||
resolveMediaFilename('image.png', {
|
||||
mediaFolder: '/content/images/pngs/',
|
||||
collectionFolder: '/content/markdown/posts/',
|
||||
}),
|
||||
).toEqual('../../images/pngs/image.png');
|
||||
});
|
||||
|
||||
it('relative URL, parent folder', () => {
|
||||
expect(
|
||||
resolveMediaFilename('image.png', {
|
||||
mediaFolder: '/content/',
|
||||
collectionFolder: '/content/posts',
|
||||
}),
|
||||
).toEqual('../image.png');
|
||||
});
|
||||
|
||||
it('relative URL, child folder', () => {
|
||||
expect(
|
||||
resolveMediaFilename('image.png', {
|
||||
mediaFolder: '/content/images',
|
||||
collectionFolder: '/content/',
|
||||
}),
|
||||
).toEqual('images/image.png');
|
||||
});
|
||||
});
|
||||
|
||||
describe('fileExtensionWithSeparator', () => {
|
||||
it('should return the extension of a file', () => {
|
||||
|
@ -2,7 +2,13 @@ import APIError from './APIError';
|
||||
import Cursor, { CURSOR_COMPATIBILITY_SYMBOL } from './Cursor';
|
||||
import EditorialWorkflowError, { EDITORIAL_WORKFLOW_ERROR } from './EditorialWorkflowError';
|
||||
import localForage from './localForage';
|
||||
import { resolvePath, basename, fileExtensionWithSeparator, fileExtension } from './path';
|
||||
import {
|
||||
resolvePath,
|
||||
resolveMediaFilename,
|
||||
basename,
|
||||
fileExtensionWithSeparator,
|
||||
fileExtension,
|
||||
} from './path';
|
||||
import {
|
||||
filterPromises,
|
||||
filterPromisesWith,
|
||||
@ -29,6 +35,7 @@ export const NetlifyCmsLibUtil = {
|
||||
EDITORIAL_WORKFLOW_ERROR,
|
||||
localForage,
|
||||
resolvePath,
|
||||
resolveMediaFilename,
|
||||
basename,
|
||||
fileExtensionWithSeparator,
|
||||
fileExtension,
|
||||
@ -53,6 +60,7 @@ export {
|
||||
EDITORIAL_WORKFLOW_ERROR,
|
||||
localForage,
|
||||
resolvePath,
|
||||
resolveMediaFilename,
|
||||
basename,
|
||||
fileExtensionWithSeparator,
|
||||
fileExtension,
|
||||
|
@ -1,3 +1,5 @@
|
||||
import getRelativePath from 'get-relative-path';
|
||||
|
||||
const absolutePath = new RegExp('^(?:[a-z]+:)?//', 'i');
|
||||
const normalizePath = path => path.replace(/[\\/]+/g, '/');
|
||||
|
||||
@ -17,6 +19,37 @@ export function resolvePath(path, basePath) {
|
||||
return normalizePath(`/${path}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Take a media filename and resolve it with respect to a
|
||||
* certain collection entry, either as an absolute URL, or
|
||||
* a path relative to the collection entry's folder.
|
||||
*
|
||||
* @param {*} filename the filename of the media item within the media_folder
|
||||
* @param {*} options how the filename should be resolved, see examples below:
|
||||
*
|
||||
* @example Resolving to publicly accessible URL
|
||||
* mediaFilenameToUse('image.jpg', {
|
||||
* publicFolder: '/static/assets' // set by public_folder
|
||||
* }) // -> "/static/assets/image.jpg"
|
||||
*
|
||||
* @example Resolving URL relatively to a specific collection entry
|
||||
* mediaFilenameToUse('image.jpg', {
|
||||
* mediaFolder: '/content/media', // set by media_folder
|
||||
* collectionFolder: 'content/posts'
|
||||
* }) // -> "../media/image.jpg"
|
||||
*
|
||||
*/
|
||||
export function resolveMediaFilename(filename, options) {
|
||||
if (options.publicFolder) {
|
||||
return resolvePath(filename, options.publicFolder);
|
||||
} else if (options.mediaFolder && options.collectionFolder) {
|
||||
const media = normalizePath(`/${options.mediaFolder}/${filename}`);
|
||||
const collection = normalizePath(`/${options.collectionFolder}/`);
|
||||
return getRelativePath(collection, media);
|
||||
}
|
||||
throw new Error('incorrect usage');
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the last portion of a path. Similar to the Unix basename command.
|
||||
* @example Usage example
|
||||
|
17
yarn.lock
17
yarn.lock
@ -5695,6 +5695,11 @@ get-port@^3.2.0:
|
||||
resolved "https://registry.yarnpkg.com/get-port/-/get-port-3.2.0.tgz#dd7ce7de187c06c8bf353796ac71e099f0980ebc"
|
||||
integrity sha1-3Xzn3hh8Bsi/NTeWrHHgmfCYDrw=
|
||||
|
||||
get-relative-path@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/get-relative-path/-/get-relative-path-1.0.2.tgz#45c26fc4247f0c8541cec4b57d0de993ec965cda"
|
||||
integrity sha512-dGkopYfmB4sXMTcZslq5SojEYakpdCSj/SVSHLhv7D6RBHzvDtd/3Q8lTEOAhVKxPPeAHu/YYkENbbz3PaH+8w==
|
||||
|
||||
get-stdin@^4.0.1:
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe"
|
||||
@ -7902,6 +7907,11 @@ lodash.ismatch@^4.4.0:
|
||||
resolved "https://registry.yarnpkg.com/lodash.ismatch/-/lodash.ismatch-4.4.0.tgz#756cb5150ca3ba6f11085a78849645f188f85f37"
|
||||
integrity sha1-dWy1FQyjum8RCFp4hJZF8Yj4Xzc=
|
||||
|
||||
lodash.isplainobject@^4.0.6:
|
||||
version "4.0.6"
|
||||
resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb"
|
||||
integrity sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=
|
||||
|
||||
lodash.once@^4.1.1:
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac"
|
||||
@ -10469,6 +10479,13 @@ redent@^2.0.0:
|
||||
indent-string "^3.0.0"
|
||||
strip-indent "^2.0.0"
|
||||
|
||||
redux-mock-store@^1.5.3:
|
||||
version "1.5.3"
|
||||
resolved "https://registry.yarnpkg.com/redux-mock-store/-/redux-mock-store-1.5.3.tgz#1f10528949b7ce8056c2532624f7cafa98576c6d"
|
||||
integrity sha512-ryhkkb/4D4CUGpAV2ln1GOY/uh51aczjcRz9k2L2bPx/Xja3c5pSGJJPyR25GNVRXtKIExScdAgFdiXp68GmJA==
|
||||
dependencies:
|
||||
lodash.isplainobject "^4.0.6"
|
||||
|
||||
redux-notifications@^4.0.1:
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/redux-notifications/-/redux-notifications-4.0.1.tgz#66c9f11bb1eb375c633beaaf7378005eab303bfb"
|
||||
|
Loading…
x
Reference in New Issue
Block a user