diff --git a/packages/netlify-cms-backend-bitbucket/src/implementation.ts b/packages/netlify-cms-backend-bitbucket/src/implementation.ts index ff6970cf..df095eb4 100644 --- a/packages/netlify-cms-backend-bitbucket/src/implementation.ts +++ b/packages/netlify-cms-backend-bitbucket/src/implementation.ts @@ -33,6 +33,7 @@ import { getPointerFileForMediaFileObj, getLargeMediaFilteredMediaFiles, FetchError, + blobToFileObj, } from 'netlify-cms-lib-util'; import NetlifyAuthenticator from 'netlify-cms-lib-auth'; import AuthenticationPage from './AuthenticationPage'; @@ -325,7 +326,7 @@ export default class BitbucketBackend implements Implementation { async getMediaFile(path: string) { const name = basename(path); const blob = await getMediaAsBlob(path, null, this.api!.readFile.bind(this.api!)); - const fileObj = new File([blob], name); + const fileObj = blobToFileObj(name, blob); const url = URL.createObjectURL(fileObj); const id = await getBlobSHA(fileObj); @@ -423,7 +424,7 @@ export default class BitbucketBackend implements Implementation { return getMediaAsBlob(file.path, null, readFile).then(blob => { const name = basename(file.path); - const fileObj = new File([blob], name); + const fileObj = blobToFileObj(name, blob); return { id: file.path, displayURL: URL.createObjectURL(fileObj), diff --git a/packages/netlify-cms-backend-github/src/implementation.tsx b/packages/netlify-cms-backend-github/src/implementation.tsx index 4246bd6d..e950bbd4 100644 --- a/packages/netlify-cms-backend-github/src/implementation.tsx +++ b/packages/netlify-cms-backend-github/src/implementation.tsx @@ -24,6 +24,7 @@ import { getPreviewStatus, UnpublishedEntryMediaFile, runWithLock, + blobToFileObj, } from 'netlify-cms-lib-util'; import AuthenticationPage from './AuthenticationPage'; import { UsersGetAuthenticatedResponse as GitHubUser } from '@octokit/rest'; @@ -324,7 +325,7 @@ export default class GitHub implements Implementation { const blob = await getMediaAsBlob(path, null, this.api!.readFile.bind(this.api!)); const name = basename(path); - const fileObj = new File([blob], name); + const fileObj = blobToFileObj(name, blob); const url = URL.createObjectURL(fileObj); const id = await getBlobSHA(blob); @@ -388,7 +389,7 @@ export default class GitHub implements Implementation { return getMediaAsBlob(file.path, file.id, readFile).then(blob => { const name = basename(file.path); - const fileObj = new File([blob], name); + const fileObj = blobToFileObj(name, blob); return { id: file.id, displayURL: URL.createObjectURL(fileObj), diff --git a/packages/netlify-cms-backend-gitlab/src/implementation.ts b/packages/netlify-cms-backend-gitlab/src/implementation.ts index 9a2313b8..38ae3c5b 100644 --- a/packages/netlify-cms-backend-gitlab/src/implementation.ts +++ b/packages/netlify-cms-backend-gitlab/src/implementation.ts @@ -25,10 +25,11 @@ import { asyncLock, AsyncLock, runWithLock, + getBlobSHA, + blobToFileObj, } from 'netlify-cms-lib-util'; import AuthenticationPage from './AuthenticationPage'; import API, { API_NAME } from './API'; -import { getBlobSHA } from 'netlify-cms-lib-util/src'; const MAX_CONCURRENT_DOWNLOADS = 10; @@ -194,7 +195,7 @@ export default class GitLab implements Implementation { async getMediaFile(path: string) { const name = basename(path); const blob = await getMediaAsBlob(path, null, this.api!.readFile.bind(this.api!)); - const fileObj = new File([blob], name); + const fileObj = blobToFileObj(name, blob); const url = URL.createObjectURL(fileObj); const id = await getBlobSHA(blob); @@ -275,7 +276,7 @@ export default class GitLab implements Implementation { return getMediaAsBlob(file.path, null, readFile).then(blob => { const name = basename(file.path); - const fileObj = new File([blob], name); + const fileObj = blobToFileObj(name, blob); return { id: file.path, displayURL: URL.createObjectURL(fileObj), diff --git a/packages/netlify-cms-core/src/actions/__tests__/media.js b/packages/netlify-cms-core/src/actions/__tests__/media.js index eb1962f2..a596cc7e 100644 --- a/packages/netlify-cms-core/src/actions/__tests__/media.js +++ b/packages/netlify-cms-core/src/actions/__tests__/media.js @@ -60,6 +60,7 @@ describe('media', () => { payload.collection, payload.entry, path, + undefined, ); }); }); diff --git a/packages/netlify-cms-core/src/actions/entries.ts b/packages/netlify-cms-core/src/actions/entries.ts index c16c58d4..9116d56a 100644 --- a/packages/netlify-cms-core/src/actions/entries.ts +++ b/packages/netlify-cms-core/src/actions/entries.ts @@ -293,12 +293,19 @@ export function retrieveLocalBackup(collection: Collection, slug: string) { const assetProxies: AssetProxy[] = await Promise.all( mediaFiles.map(file => { if (file.file || file.url) { - return createAssetProxy({ path: file.path, file: file.file, url: file.url }); + return createAssetProxy({ + path: file.path, + file: file.file, + url: file.url, + folder: file.folder, + }); } else { - return getAsset({ collection, entry: fromJS(entry), path: file.path })( - dispatch, - getState, - ); + return getAsset({ + collection, + entry: fromJS(entry), + path: file.path, + folder: file.folder, + })(dispatch, getState); } }), ); @@ -554,7 +561,9 @@ export async function getMediaAssets({ const assets = await Promise.all( filesArray .filter(file => file.draft) - .map(file => getAsset({ collection, entry, path: file.path })(dispatch, getState)), + .map(file => + getAsset({ collection, entry, path: file.path, folder: file.folder })(dispatch, getState), + ), ); return assets; diff --git a/packages/netlify-cms-core/src/actions/media.ts b/packages/netlify-cms-core/src/actions/media.ts index 829eb892..bf185aad 100644 --- a/packages/netlify-cms-core/src/actions/media.ts +++ b/packages/netlify-cms-core/src/actions/media.ts @@ -27,14 +27,14 @@ interface GetAssetArgs { collection: Collection; entry: EntryMap; path: string; + folder?: string; } -export function getAsset({ collection, entry, path }: GetAssetArgs) { +export function getAsset({ collection, entry, path, folder }: GetAssetArgs) { return async (dispatch: ThunkDispatch, getState: () => State) => { if (!path) return createAssetProxy({ path: '', file: new File([], 'empty') }); - const state = getState(); - const resolvedPath = selectMediaFilePath(state.config, collection, entry, path); + const resolvedPath = selectMediaFilePath(state.config, collection, entry, path, folder); let asset = state.medias.get(resolvedPath); if (asset) { diff --git a/packages/netlify-cms-core/src/actions/mediaLibrary.ts b/packages/netlify-cms-core/src/actions/mediaLibrary.ts index 21babcb2..377b17ca 100644 --- a/packages/netlify-cms-core/src/actions/mediaLibrary.ts +++ b/packages/netlify-cms-core/src/actions/mediaLibrary.ts @@ -77,6 +77,8 @@ export function openMediaLibrary( config?: Map; allowMultiple?: boolean; forImage?: boolean; + mediaFolder?: string; + publicFolder?: string; } = {}, ) { return (dispatch: ThunkDispatch, getState: () => State) => { @@ -101,7 +103,7 @@ export function closeMediaLibrary() { }; } -export function insertMedia(mediaPath: string | string[]) { +export function insertMedia(mediaPath: string | string[], publicFolder: string | undefined) { return (dispatch: ThunkDispatch, getState: () => State) => { const state = getState(); const config = state.config; @@ -109,9 +111,17 @@ export function insertMedia(mediaPath: string | string[]) { const collectionName = state.entryDraft.getIn(['entry', 'collection']); const collection = state.collections.get(collectionName); if (Array.isArray(mediaPath)) { - mediaPath = mediaPath.map(path => selectMediaFilePublicPath(config, collection, path, entry)); + mediaPath = mediaPath.map(path => + selectMediaFilePublicPath(config, collection, path, entry, publicFolder), + ); } else { - mediaPath = selectMediaFilePublicPath(config, collection, mediaPath as string, entry); + mediaPath = selectMediaFilePublicPath( + config, + collection, + mediaPath as string, + entry, + publicFolder, + ); } dispatch({ type: MEDIA_INSERT, payload: { mediaPath } }); }; @@ -191,12 +201,13 @@ function createMediaFileFromAsset({ size: file.size, url: assetProxy.url, path: assetProxy.path, + folder: assetProxy.folder, }; return mediaFile; } export function persistMedia(file: File, opts: MediaOptions = {}) { - const { privateUpload } = opts; + const { privateUpload, mediaFolder } = opts; return async (dispatch: ThunkDispatch, getState: () => State) => { const state = getState(); const backend = currentBackend(state.config); @@ -250,10 +261,11 @@ export function persistMedia(file: File, opts: MediaOptions = {}) { } else { const entry = state.entryDraft.get('entry'); const collection = state.collections.get(entry?.get('collection')); - const path = selectMediaFilePath(state.config, collection, entry, file.name); + const path = selectMediaFilePath(state.config, collection, entry, file.name, mediaFolder); assetProxy = createAssetProxy({ file, path, + folder: mediaFolder, }); } @@ -397,6 +409,7 @@ export function mediaLoading(page: number) { interface MediaOptions { privateUpload?: boolean; + mediaFolder?: string; } export function mediaLoaded(files: ImplementationMediaFile[], opts: MediaOptions = {}) { diff --git a/packages/netlify-cms-core/src/backend.ts b/packages/netlify-cms-core/src/backend.ts index 1ea9facf..8306f0ae 100644 --- a/packages/netlify-cms-core/src/backend.ts +++ b/packages/netlify-cms-core/src/backend.ts @@ -3,7 +3,7 @@ import { List, Map, fromJS } from 'immutable'; import * as fuzzy from 'fuzzy'; import { resolveFormat } from './formats/formats'; import { selectUseWorkflow } from './reducers/config'; -import { selectMediaFilePath, selectMediaFolder, selectEntry } from './reducers/entries'; +import { selectMediaFilePath, selectEntry } from './reducers/entries'; import { selectIntegration } from './reducers/integrations'; import { selectEntrySlug, @@ -13,6 +13,7 @@ import { selectAllowDeletion, selectFolderEntryExtension, selectInferedField, + selectMediaFolders, } from './reducers/collections'; import { createEntry, EntryValue } from './valueObjects/Entry'; import { sanitizeChar } from './lib/urlHelper'; @@ -31,6 +32,7 @@ import { User, getPathDepth, Config as ImplementationConfig, + blobToFileObj, } from 'netlify-cms-lib-util'; import { status } from './constants/publishModes'; import { extractTemplateVars, dateParsers } from './lib/stringTemplate'; @@ -450,8 +452,7 @@ export class Backend { // make sure to serialize the file if (file.url?.startsWith('blob:')) { const blob = await fetch(file.url as string).then(res => res.blob()); - const options = file.name.match(/.svg$/) ? { type: 'image/svg+xml' } : {}; - return { ...file, file: new File([blob], file.name, options) }; + return { ...file, file: blobToFileObj(file.name, blob) }; } return file; }), @@ -492,10 +493,12 @@ export class Backend { }); const entryWithFormat = this.entryWithFormat(collection)(entry); - if (collection.has('media_folder') && !integration) { - entry.mediaFiles = await this.implementation.getMedia( - selectMediaFolder(state.config, collection, fromJS(entryWithFormat)), - ); + const mediaFolders = selectMediaFolders(state, collection, fromJS(entryWithFormat)); + if (mediaFolders.length > 0 && !integration) { + entry.mediaFiles = []; + for (const folder of mediaFolders) { + entry.mediaFiles = [...entry.mediaFiles, ...(await this.implementation.getMedia(folder))]; + } } else { entry.mediaFiles = state.mediaLibrary.get('files') || []; } @@ -697,6 +700,7 @@ export class Backend { collection, entryDraft.get('entry').set('path', path), oldPath, + asset.folder, ); asset.path = newPath; }); diff --git a/packages/netlify-cms-core/src/components/Collection/Entries/EntryCard.js b/packages/netlify-cms-core/src/components/Collection/Entries/EntryCard.js index 9eca3eb1..27728032 100644 --- a/packages/netlify-cms-core/src/components/Collection/Entries/EntryCard.js +++ b/packages/netlify-cms-core/src/components/Collection/Entries/EntryCard.js @@ -83,14 +83,15 @@ const CardImage = styled.div` height: 150px; `; -const CardImageAsset = ({ getAsset, image }) => { - return ; +const CardImageAsset = ({ getAsset, image, folder }) => { + return ; }; const EntryCard = ({ path, summary, image, + imageFolder, collectionLabel, viewStyle = VIEW_STYLE_LIST, boundGetAsset, @@ -114,7 +115,9 @@ const EntryCard = ({ {collectionLabel ? {collectionLabel} : null} {summary} - {image ? : null} + {image ? ( + + ) : null} ); @@ -140,12 +143,16 @@ const mapStateToProps = (state, ownProps) => { summary, path: `/collections/${collection.get('name')}/entries/${entry.get('slug')}`, image, + imageFolder: collection + .get('fields') + ?.find(f => f.get('name') === inferedFields.imageField && f.get('widget') === 'image') + ?.get('media_folder'), }; }; const mapDispatchToProps = { - boundGetAsset: (collection, entry) => (dispatch, getState) => path => { - return getAsset({ collection, entry, path })(dispatch, getState); + boundGetAsset: (collection, entry) => (dispatch, getState) => (path, folder) => { + return getAsset({ collection, entry, path, folder })(dispatch, getState); }, }; diff --git a/packages/netlify-cms-core/src/components/Editor/Editor.js b/packages/netlify-cms-core/src/components/Editor/Editor.js index e69e587f..7492e09d 100644 --- a/packages/netlify-cms-core/src/components/Editor/Editor.js +++ b/packages/netlify-cms-core/src/components/Editor/Editor.js @@ -515,8 +515,8 @@ const mapDispatchToProps = { unpublishPublishedEntry, deleteUnpublishedEntry, logoutUser, - boundGetAsset: (collection, entry) => (dispatch, getState) => path => { - return getAsset({ collection, entry, path })(dispatch, getState); + boundGetAsset: (collection, entry) => (dispatch, getState) => (path, folder) => { + return getAsset({ collection, entry, path, folder })(dispatch, getState); }, }; diff --git a/packages/netlify-cms-core/src/components/Editor/EditorControlPane/EditorControl.js b/packages/netlify-cms-core/src/components/Editor/EditorControlPane/EditorControl.js index 3b29443c..5a8927b2 100644 --- a/packages/netlify-cms-core/src/components/Editor/EditorControlPane/EditorControl.js +++ b/packages/netlify-cms-core/src/components/Editor/EditorControlPane/EditorControl.js @@ -288,8 +288,8 @@ const mapDispatchToProps = { }, clearSearch, clearFieldErrors, - boundGetAsset: (collection, entry) => (dispatch, getState) => path => { - return getAsset({ collection, entry, path })(dispatch, getState); + boundGetAsset: (collection, entry) => (dispatch, getState) => (path, folder) => { + return getAsset({ collection, entry, path, folder })(dispatch, getState); }, }; diff --git a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibrary.js b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibrary.js index a4c3b826..f85cce07 100644 --- a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibrary.js +++ b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibrary.js @@ -160,7 +160,7 @@ class MediaLibrary extends React.Component { event.persist(); event.stopPropagation(); event.preventDefault(); - const { persistMedia, privateUpload, config, t } = this.props; + const { persistMedia, privateUpload, config, t, mediaFolder } = this.props; const { files: fileList } = event.dataTransfer || event.target; const files = [...fileList]; const file = files[0]; @@ -173,7 +173,7 @@ class MediaLibrary extends React.Component { }), ); } else { - await persistMedia(file, { privateUpload }); + await persistMedia(file, { privateUpload, mediaFolder }); this.setState({ selectedFile: this.props.files[0] }); @@ -190,8 +190,8 @@ class MediaLibrary extends React.Component { handleInsert = () => { const { selectedFile } = this.state; const { path } = selectedFile; - const { insertMedia } = this.props; - insertMedia(path); + const { insertMedia, publicFolder } = this.props; + insertMedia(path, publicFolder); this.handleClose(); }; @@ -332,6 +332,8 @@ const mapStateToProps = state => { page: mediaLibrary.get('page'), hasNextPage: mediaLibrary.get('hasNextPage'), isPaginating: mediaLibrary.get('isPaginating'), + mediaFolder: mediaLibrary.get('mediaFolder'), + publicFolder: mediaLibrary.get('publicFolder'), }; return { ...mediaLibraryProps }; }; diff --git a/packages/netlify-cms-core/src/mediaLibrary.ts b/packages/netlify-cms-core/src/mediaLibrary.ts index d784191c..f85d782a 100644 --- a/packages/netlify-cms-core/src/mediaLibrary.ts +++ b/packages/netlify-cms-core/src/mediaLibrary.ts @@ -19,7 +19,7 @@ interface MediaLibrary { const initializeMediaLibrary = once(async function initializeMediaLibrary(name, options) { const lib = (getMediaLibrary(name) as unknown) as MediaLibrary; - const handleInsert = (url: string) => store.dispatch(insertMedia(url)); + const handleInsert = (url: string) => store.dispatch(insertMedia(url, undefined)); const instance = await lib.init({ options, handleInsert }); store.dispatch(createMediaLibrary(instance)); }); diff --git a/packages/netlify-cms-core/src/reducers/__tests__/collections.spec.js b/packages/netlify-cms-core/src/reducers/__tests__/collections.spec.js index 82877598..043139de 100644 --- a/packages/netlify-cms-core/src/reducers/__tests__/collections.spec.js +++ b/packages/netlify-cms-core/src/reducers/__tests__/collections.spec.js @@ -1,6 +1,12 @@ import { OrderedMap, fromJS } from 'immutable'; import { configLoaded } from 'Actions/config'; -import collections, { selectAllowDeletion, selectEntryPath, selectEntrySlug } from '../collections'; +import collections, { + selectAllowDeletion, + selectEntryPath, + selectEntrySlug, + selectFieldsMediaFolders, + selectMediaFolders, +} from '../collections'; import { FILES, FOLDER } from 'Constants/collectionTypes'; describe('collections', () => { @@ -76,4 +82,159 @@ describe('collections', () => { ).toBe('dir1/dir2/slug'); }); }); + + describe('selectFieldsMediaFolders', () => { + it('should return empty array for invalid collection', () => { + expect(selectFieldsMediaFolders(fromJS({}))).toEqual([]); + }); + + it('should return configs for folder collection', () => { + expect( + selectFieldsMediaFolders( + fromJS({ + folder: 'posts', + fields: [ + { + name: 'image', + media_folder: 'image_media_folder', + }, + { + name: 'body', + media_folder: 'body_media_folder', + }, + { + name: 'list_1', + field: { + name: 'list_1_item', + media_folder: 'list_1_item_media_folder', + }, + }, + { + name: 'list_2', + fields: [ + { + name: 'list_2_item', + media_folder: 'list_2_item_media_folder', + }, + ], + }, + ], + }), + ), + ).toEqual([ + 'image_media_folder', + 'body_media_folder', + 'list_1_item_media_folder', + 'list_2_item_media_folder', + ]); + }); + + it('should return configs for files collection', () => { + expect( + selectFieldsMediaFolders( + fromJS({ + files: [ + { + fields: [ + { + name: 'image', + media_folder: 'image_media_folder', + }, + ], + }, + { + fields: [ + { + name: 'body', + media_folder: 'body_media_folder', + }, + ], + }, + { + fields: [ + { + name: 'list_1', + field: { + name: 'list_1_item', + media_folder: 'list_1_item_media_folder', + }, + }, + ], + }, + { + fields: [ + { + name: 'list_2', + fields: [ + { + name: 'list_2_item', + media_folder: 'list_2_item_media_folder', + }, + ], + }, + ], + }, + ], + }), + ), + ).toEqual([ + 'image_media_folder', + 'body_media_folder', + 'list_1_item_media_folder', + 'list_2_item_media_folder', + ]); + }); + }); + + describe('selectMediaFolders', () => { + const slug = { + encoding: 'unicode', + clean_accents: false, + sanitize_replacement: '-', + }; + + const config = fromJS({ slug }); + it('should return fields and collection folder', () => { + expect( + selectMediaFolders( + { config }, + fromJS({ + folder: 'posts', + media_folder: '/collection_media_folder', + fields: [ + { + name: 'image', + media_folder: '/image_media_folder', + }, + ], + }), + fromJS({ slug: 'name', path: 'src/post/post1.md' }), + ), + ).toEqual(['collection_media_folder', 'image_media_folder']); + }); + + it('should return fields and collection folder', () => { + expect( + selectMediaFolders( + { config }, + fromJS({ + files: [ + { + name: 'name', + file: 'src/post/post1.md', + media_folder: '/file_media_folder', + fields: [ + { + name: 'image', + media_folder: '/image_media_folder', + }, + ], + }, + ], + }), + fromJS({ slug: 'name', path: 'src/post/post1.md' }), + ), + ).toEqual(['file_media_folder', 'image_media_folder']); + }); + }); }); diff --git a/packages/netlify-cms-core/src/reducers/__tests__/entries.spec.js b/packages/netlify-cms-core/src/reducers/__tests__/entries.spec.js index 946ab677..22bcaa5b 100644 --- a/packages/netlify-cms-core/src/reducers/__tests__/entries.spec.js +++ b/packages/netlify-cms-core/src/reducers/__tests__/entries.spec.js @@ -74,16 +74,22 @@ describe('entries', () => { describe('selectMediaFolder', () => { it("should return global media folder when collection doesn't specify media_folder", () => { expect( - selectMediaFolder(Map({ media_folder: 'static/media' }), Map({ name: 'posts' })), + selectMediaFolder( + Map({ media_folder: 'static/media' }), + Map({ name: 'posts' }), + undefined, + undefined, + ), ).toEqual('static/media'); }); - it('should return draft media folder when collection specifies media_folder and entry path is null', () => { + it('should return draft media folder when collection specifies media_folder and entry is undefined', () => { expect( selectMediaFolder( Map({ media_folder: 'static/media' }), Map({ name: 'posts', folder: 'posts', media_folder: '' }), - null, + undefined, + undefined, ), ).toEqual('posts/DRAFT_MEDIA_FILES'); }); @@ -94,6 +100,7 @@ describe('entries', () => { Map({ media_folder: 'static/media' }), Map({ name: 'posts', folder: 'posts', media_folder: '' }), Map({ path: 'posts/title/index.md' }), + undefined, ), ).toEqual('posts/title'); }); @@ -104,11 +111,12 @@ describe('entries', () => { Map({ media_folder: 'static/media' }), Map({ name: 'posts', folder: 'posts', media_folder: '../' }), Map({ path: 'posts/title/index.md' }), + undefined, ), ).toEqual('posts/'); }); - it('should return collection absolute media folder as is', () => { + it('should return collection absolute media folder without leading slash', () => { expect( selectMediaFolder( Map({ media_folder: '/static/Images' }), @@ -118,8 +126,9 @@ describe('entries', () => { media_folder: '/static/images/docs/getting-started', }), Map({ path: 'src/docs/getting-started/with-github.md' }), + undefined, ), - ).toEqual('/static/images/docs/getting-started'); + ).toEqual('static/images/docs/getting-started'); }); it('should compile relative media folder template', () => { @@ -145,6 +154,7 @@ describe('entries', () => { fromJS({ media_folder: 'static/media', slug: slugConfig }), collection, entry, + undefined, ), ).toEqual('static/media/hosting-and-deployment/deployment-with-nanobox'); }); @@ -172,8 +182,85 @@ describe('entries', () => { fromJS({ media_folder: '/static/images', slug: slugConfig }), collection, entry, + undefined, ), - ).toEqual('/static/images/docs/extending'); + ).toEqual('static/images/docs/extending'); + }); + + it('should compile field media folder template', () => { + const slugConfig = { + encoding: 'unicode', + clean_accents: false, + sanitize_replacement: '-', + }; + + const entry = fromJS({ + path: 'content/en/hosting-and-deployment/deployment-with-nanobox.md', + data: { title: 'Deployment With NanoBox', category: 'Hosting And Deployment' }, + }); + const collection = fromJS({ + name: 'posts', + folder: 'content', + fields: [{ name: 'title', widget: 'string' }], + }); + + expect( + selectMediaFolder( + fromJS({ media_folder: 'static/media', slug: slugConfig }), + collection, + entry, + '../../../{{media_folder}}/{{category}}/{{slug}}', + ), + ).toEqual('static/media/hosting-and-deployment/deployment-with-nanobox'); + }); + + it('should handle double slashes', () => { + const slugConfig = { + encoding: 'unicode', + clean_accents: false, + sanitize_replacement: '-', + }; + + const entry = fromJS({ + path: 'content/en/hosting-and-deployment/deployment-with-nanobox.md', + data: { title: 'Deployment With NanoBox', category: 'Hosting And Deployment' }, + }); + + const collection = fromJS({ + name: 'posts', + folder: 'content', + media_folder: '{{media_folder}}/blog', + fields: [{ name: 'title', widget: 'string' }], + }); + + expect( + selectMediaFolder( + fromJS({ media_folder: '/static/img/', slug: slugConfig }), + collection, + entry, + undefined, + ), + ).toEqual('static/img/blog'); + + expect( + selectMediaFolder( + fromJS({ media_folder: 'static/img/', slug: slugConfig }), + collection, + entry, + undefined, + ), + ).toEqual('content/en/hosting-and-deployment/static/img/blog'); + }); + + it('should handle file media_folder', () => { + expect( + selectMediaFolder( + fromJS({ media_folder: 'static/media' }), + fromJS({ name: 'posts', files: [{ name: 'index', media_folder: '/static/images/' }] }), + fromJS({ path: 'posts/title/index.md', slug: 'index' }), + undefined, + ), + ).toBe('static/images/'); }); }); @@ -189,8 +276,9 @@ describe('entries', () => { selectMediaFilePath( Map({ media_folder: 'static/media' }), Map({ name: 'posts', folder: 'posts' }), - null, + undefined, 'image.png', + undefined, ), ).toBe('static/media/image.png'); }); @@ -200,8 +288,9 @@ describe('entries', () => { selectMediaFilePath( Map({ media_folder: 'static/media' }), Map({ name: 'posts', folder: 'posts', media_folder: '' }), - null, + undefined, 'image.png', + undefined, ), ).toBe('posts/DRAFT_MEDIA_FILES/image.png'); }); @@ -213,6 +302,19 @@ describe('entries', () => { Map({ name: 'posts', folder: 'posts', media_folder: '../../static/media/' }), Map({ path: 'posts/title/index.md' }), 'image.png', + undefined, + ), + ).toBe('static/media/image.png'); + }); + + it('should handle field media_folder', () => { + expect( + selectMediaFilePath( + Map({ media_folder: 'static/media' }), + Map({ name: 'posts', folder: 'posts' }), + Map({ path: 'posts/title/index.md' }), + 'image.png', + '../../static/media/', ), ).toBe('static/media/image.png'); }); @@ -227,7 +329,13 @@ describe('entries', () => { it('should resolve path from public folder for collection with no media folder', () => { expect( - selectMediaFilePublicPath(Map({ public_folder: '/media' }), null, '/media/image.png'), + selectMediaFilePublicPath( + Map({ public_folder: '/media' }), + null, + '/media/image.png', + undefined, + undefined, + ), ).toBe('/media/image.png'); }); @@ -237,6 +345,8 @@ describe('entries', () => { Map({ public_folder: '/media' }), Map({ name: 'posts', folder: 'posts', public_folder: '' }), 'image.png', + undefined, + undefined, ), ).toBe('image.png'); }); @@ -247,11 +357,13 @@ describe('entries', () => { Map({ public_folder: '/media' }), Map({ name: 'posts', folder: 'posts', public_folder: '../../static/media/' }), 'image.png', + undefined, + undefined, ), ).toBe('../../static/media/image.png'); }); - it('should compile public folder template', () => { + it('should compile collection public folder template', () => { const slugConfig = { encoding: 'unicode', clean_accents: false, @@ -275,8 +387,93 @@ describe('entries', () => { collection, 'image.png', entry, + undefined, ), ).toEqual('/static/media/hosting-and-deployment/deployment-with-nanobox/image.png'); }); + + it('should compile field public folder template', () => { + const slugConfig = { + encoding: 'unicode', + clean_accents: false, + sanitize_replacement: '-', + }; + + const entry = fromJS({ + path: 'content/en/hosting-and-deployment/deployment-with-nanobox.md', + data: { title: 'Deployment With NanoBox', category: 'Hosting And Deployment' }, + }); + const collection = fromJS({ + name: 'posts', + folder: 'content', + fields: [{ name: 'title', widget: 'string' }], + }); + + expect( + selectMediaFilePublicPath( + fromJS({ public_folder: 'static/media', slug: slugConfig }), + collection, + 'image.png', + entry, + '/{{public_folder}}/{{category}}/{{slug}}', + ), + ).toEqual('/static/media/hosting-and-deployment/deployment-with-nanobox/image.png'); + }); + + it('should handle double slashes', () => { + const slugConfig = { + encoding: 'unicode', + clean_accents: false, + sanitize_replacement: '-', + }; + + const entry = fromJS({ + path: 'content/en/hosting-and-deployment/deployment-with-nanobox.md', + data: { title: 'Deployment With NanoBox', category: 'Hosting And Deployment' }, + }); + const collection = fromJS({ + name: 'posts', + folder: 'content', + fields: [{ name: 'title', widget: 'string' }], + }); + + expect( + selectMediaFilePublicPath( + fromJS({ public_folder: 'static/media/', slug: slugConfig }), + collection, + 'image.png', + entry, + '/{{public_folder}}/{{category}}/{{slug}}', + ), + ).toEqual('/static/media/hosting-and-deployment/deployment-with-nanobox/image.png'); + }); + + it('should handle file public_folder', () => { + const entry = fromJS({ + path: 'src/posts/index.md', + slug: 'index', + }); + + const collection = fromJS({ + name: 'posts', + files: [ + { + name: 'index', + public_folder: '/images', + fields: [{ name: 'title', widget: 'string' }], + }, + ], + }); + + expect( + selectMediaFilePublicPath( + fromJS({ public_folder: 'static/media/' }), + collection, + 'image.png', + entry, + undefined, + ), + ).toBe('/images/image.png'); + }); }); }); diff --git a/packages/netlify-cms-core/src/reducers/collections.ts b/packages/netlify-cms-core/src/reducers/collections.ts index ca4528b2..a6a3f1e1 100644 --- a/packages/netlify-cms-core/src/reducers/collections.ts +++ b/packages/netlify-cms-core/src/reducers/collections.ts @@ -5,7 +5,15 @@ import { CONFIG_SUCCESS } from '../actions/config'; import { FILES, FOLDER } from '../constants/collectionTypes'; import { INFERABLE_FIELDS, IDENTIFIER_FIELDS } from '../constants/fieldInference'; import { formatExtensions } from '../formats/formats'; -import { CollectionsAction, Collection, CollectionFiles, EntryField } from '../types/redux'; +import { + CollectionsAction, + Collection, + CollectionFiles, + EntryField, + State, + EntryMap, +} from '../types/redux'; +import { selectMediaFolder } from './entries'; const collections = (state = null, action: CollectionsAction) => { switch (action.type) { @@ -106,6 +114,62 @@ const selectors = { }, }; +const getFieldsMediaFolders = (fields: EntryField[]) => { + const mediaFolders = fields.reduce((acc, f) => { + if (f.has('media_folder')) { + acc = [...acc, f.get('media_folder') as string]; + } + + if (f.has('fields')) { + const fields = f.get('fields')?.toArray() as EntryField[]; + acc = [...acc, ...getFieldsMediaFolders(fields)]; + } + if (f.has('field')) { + const field = f.get('field') as EntryField; + acc = [...acc, ...getFieldsMediaFolders([field])]; + } + + return acc; + }, [] as string[]); + + return mediaFolders; +}; + +export const selectFieldsMediaFolders = (collection: Collection) => { + if (collection.has('folder')) { + const fields = collection.get('fields').toArray(); + return getFieldsMediaFolders(fields); + } else if (collection.has('files')) { + const fields = collection + .get('files') + ?.toArray() + .map(f => f.get('fields').toArray()) as EntryField[][]; + + const flattened = [] as EntryField[]; + return getFieldsMediaFolders(flattened.concat(...fields)); + } + + return []; +}; + +export const selectMediaFolders = (state: State, collection: Collection, entry: EntryMap) => { + const fieldsFolders = selectFieldsMediaFolders(collection); + const folders = fieldsFolders.map(folder => + selectMediaFolder(state.config, collection, entry, folder), + ); + + if ( + collection.has('media_folder') || + collection + .get('files') + ?.find(file => file?.get('name') === entry?.get('slug') && file?.has('media_folder')) + ) { + folders.unshift(selectMediaFolder(state.config, collection, entry, undefined)); + } + + return folders; +}; + export const selectFields = (collection: Collection, slug: string) => selectors[collection.get('type')].fields(collection, slug); export const selectFolderEntryExtension = (collection: Collection) => diff --git a/packages/netlify-cms-core/src/reducers/entries.ts b/packages/netlify-cms-core/src/reducers/entries.ts index 988f95d4..973e36c0 100644 --- a/packages/netlify-cms-core/src/reducers/entries.ts +++ b/packages/netlify-cms-core/src/reducers/entries.ts @@ -27,6 +27,7 @@ import { } from '../types/redux'; import { folderFormatter } from '../lib/formatters'; import { isAbsolutePath, basename } from 'netlify-cms-lib-util'; +import { trimStart } from 'lodash'; let collection: string; let loadedEntries: EntryObject[]; @@ -138,32 +139,67 @@ export const selectEntries = (state: Entries, collection: string) => { const DRAFT_MEDIA_FILES = 'DRAFT_MEDIA_FILES'; +const getCustomFolder = ( + name: 'media_folder' | 'public_folder', + collection: Collection | null, + slug: string | undefined, + fieldFolder: string | undefined, +) => { + if (!collection) { + return undefined; + } + if (fieldFolder !== undefined) { + return fieldFolder; + } + if (collection.has('files') && slug) { + const file = collection.get('files')?.find(f => f?.get('name') === slug); + if (file && file.has(name)) { + return file.get(name); + } + } + + if (collection.has(name)) { + return collection.get(name); + } + + return undefined; +}; + export const selectMediaFolder = ( config: Config, collection: Collection | null, entryMap: EntryMap | undefined, + fieldMediaFolder: string | undefined, ) => { let mediaFolder = config.get('media_folder'); - if (collection && collection.has('media_folder')) { + const customFolder = getCustomFolder( + 'media_folder', + collection, + entryMap?.get('slug'), + fieldMediaFolder, + ); + + if (customFolder !== undefined) { const entryPath = entryMap?.get('path'); if (entryPath) { const entryDir = dirname(entryPath); const folder = folderFormatter( - collection.get('media_folder') as string, + customFolder, entryMap as EntryMap, - collection, + collection!, mediaFolder, 'media_folder', config.get('slug'), ); - // return absolute paths as is + // return absolute paths as is without the leading '/' if (folder.startsWith('/')) { - return folder; + mediaFolder = join(trimStart(folder, '/')); + } else { + mediaFolder = join(entryDir, folder as string); } - mediaFolder = join(entryDir, folder as string); } else { - mediaFolder = join(collection.get('folder') as string, DRAFT_MEDIA_FILES); + mediaFolder = join(collection!.get('folder') as string, DRAFT_MEDIA_FILES); } } @@ -175,12 +211,13 @@ export const selectMediaFilePath = ( collection: Collection | null, entryMap: EntryMap | undefined, mediaPath: string, + fieldMediaFolder: string | undefined, ) => { if (isAbsolutePath(mediaPath)) { return mediaPath; } - const mediaFolder = selectMediaFolder(config, collection, entryMap); + const mediaFolder = selectMediaFolder(config, collection, entryMap, fieldMediaFolder); return join(mediaFolder, basename(mediaPath)); }; @@ -190,6 +227,7 @@ export const selectMediaFilePublicPath = ( collection: Collection | null, mediaPath: string, entryMap: EntryMap | undefined, + fieldPublicFolder: string | undefined, ) => { if (isAbsolutePath(mediaPath)) { return mediaPath; @@ -197,11 +235,18 @@ export const selectMediaFilePublicPath = ( let publicFolder = config.get('public_folder'); - if (collection && collection.has('public_folder')) { + const customFolder = getCustomFolder( + 'public_folder', + collection, + entryMap?.get('slug'), + fieldPublicFolder, + ); + + if (customFolder !== undefined) { publicFolder = folderFormatter( - collection.get('public_folder') as string, + customFolder, entryMap, - collection, + collection!, publicFolder, 'public_folder', config.get('slug'), diff --git a/packages/netlify-cms-core/src/reducers/mediaLibrary.ts b/packages/netlify-cms-core/src/reducers/mediaLibrary.ts index d3e77963..bf37c4b9 100644 --- a/packages/netlify-cms-core/src/reducers/mediaLibrary.ts +++ b/packages/netlify-cms-core/src/reducers/mediaLibrary.ts @@ -56,7 +56,14 @@ const mediaLibrary = (state = Map(defaultState), action: MediaLibraryAction) => map.set('showMediaButton', action.payload.enableStandalone()); }); case MEDIA_LIBRARY_OPEN: { - const { controlID, forImage, privateUpload, config } = action.payload; + const { + controlID, + forImage, + privateUpload, + config, + mediaFolder, + publicFolder, + } = action.payload; const libConfig = config || Map(); const privateUploadChanged = state.get('privateUpload') !== privateUpload; if (privateUploadChanged) { @@ -77,6 +84,8 @@ const mediaLibrary = (state = Map(defaultState), action: MediaLibraryAction) => map.set('canInsert', !!controlID); map.set('privateUpload', privateUpload); map.set('config', libConfig); + map.set('mediaFolder', mediaFolder); + map.set('publicFolder', publicFolder); }); } case MEDIA_LIBRARY_CLOSE: diff --git a/packages/netlify-cms-core/src/types/redux.ts b/packages/netlify-cms-core/src/types/redux.ts index c601e8d5..f47fee4b 100644 --- a/packages/netlify-cms-core/src/types/redux.ts +++ b/packages/netlify-cms-core/src/types/redux.ts @@ -94,6 +94,8 @@ export type EntryField = StaticallyTypedRecord<{ widget: string; name: string; default: string | null; + media_folder?: string; + public_folder?: string; }>; export type EntryFields = List; @@ -108,6 +110,8 @@ export type CollectionFile = StaticallyTypedRecord<{ name: string; fields: EntryFields; label: string; + media_folder?: string; + public_folder?: string; }>; export type CollectionFiles = List; @@ -305,6 +309,8 @@ export interface MediaLibraryAction extends Action { forImage: boolean; privateUpload: boolean; config: Map; + mediaFolder?: string; + publicFolder?: string; } & { mediaPath: string | string[] } & { page: number } & { files: MediaFile[]; page: number; diff --git a/packages/netlify-cms-core/src/valueObjects/AssetProxy.ts b/packages/netlify-cms-core/src/valueObjects/AssetProxy.ts index c9d40896..7549b88b 100644 --- a/packages/netlify-cms-core/src/valueObjects/AssetProxy.ts +++ b/packages/netlify-cms-core/src/valueObjects/AssetProxy.ts @@ -2,17 +2,20 @@ interface AssetProxyArgs { path: string; url?: string; file?: File; + folder?: string; } export default class AssetProxy { url: string; fileObj?: File; path: string; + folder?: string; - constructor({ url, file, path }: AssetProxyArgs) { + constructor({ url, file, path, folder }: AssetProxyArgs) { this.url = url ? url : window.URL.createObjectURL(file); this.fileObj = file; this.path = path; + this.folder = folder; } toString(): string { @@ -35,6 +38,6 @@ export default class AssetProxy { } } -export function createAssetProxy({ url, file, path }: AssetProxyArgs): AssetProxy { - return new AssetProxy({ url, file, path }); +export function createAssetProxy({ url, file, path, folder }: AssetProxyArgs): AssetProxy { + return new AssetProxy({ url, file, path, folder }); } diff --git a/packages/netlify-cms-editor-component-image/src/index.js b/packages/netlify-cms-editor-component-image/src/index.js index 950af58f..dcf066f3 100644 --- a/packages/netlify-cms-editor-component-image/src/index.js +++ b/packages/netlify-cms-editor-component-image/src/index.js @@ -12,8 +12,10 @@ const image = { toBlock: ({ alt, image, title }) => `![${alt || ''}](${image || ''}${title ? ` "${title.replace(/"/g, '\\"')}"` : ''})`, // eslint-disable-next-line react/display-name - toPreview: async ({ alt, image, title }, getAsset) => { - const src = await getAsset(image); + toPreview: async ({ 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); return {alt; }, pattern: /^!\[(.*)\]\((.*?)(\s"(.*)")?\)$/, diff --git a/packages/netlify-cms-lib-util/src/implementation.ts b/packages/netlify-cms-lib-util/src/implementation.ts index 8dbd4c7b..b1e8984c 100644 --- a/packages/netlify-cms-lib-util/src/implementation.ts +++ b/packages/netlify-cms-lib-util/src/implementation.ts @@ -18,6 +18,7 @@ export interface ImplementationMediaFile { draft?: boolean; url?: string; file?: File; + folder?: string; } export interface UnpublishedEntryMediaFile { @@ -264,6 +265,11 @@ export const unpublishedEntries = async ( } }; +export const blobToFileObj = (name: string, blob: Blob) => { + const options = name.match(/.svg$/) ? { type: 'image/svg+xml' } : {}; + return new File([blob], name, options); +}; + export const getMediaAsBlob = async (path: string, id: string | null, readFile: ReadFile) => { let blob: Blob; if (path.match(/.svg$/)) { diff --git a/packages/netlify-cms-lib-util/src/index.ts b/packages/netlify-cms-lib-util/src/index.ts index fb29323e..caff7acd 100644 --- a/packages/netlify-cms-lib-util/src/index.ts +++ b/packages/netlify-cms-lib-util/src/index.ts @@ -36,6 +36,7 @@ import { runWithLock, Config as C, UnpublishedEntryMediaFile as UEMF, + blobToFileObj, } from './implementation'; import { readFile, @@ -136,6 +137,7 @@ export const NetlifyCmsLibUtil = { getPointerFileForMediaFileObj, branchFromContentKey, contentKeyFromBranch, + blobToFileObj, }; export { APIError, @@ -186,4 +188,5 @@ export { getPointerFileForMediaFileObj, branchFromContentKey, contentKeyFromBranch, + blobToFileObj, }; diff --git a/packages/netlify-cms-ui-default/src/Asset.js b/packages/netlify-cms-ui-default/src/Asset.js index f434fd51..9bf8ab46 100644 --- a/packages/netlify-cms-ui-default/src/Asset.js +++ b/packages/netlify-cms-ui-default/src/Asset.js @@ -6,6 +6,7 @@ class Asset extends React.Component { path: PropTypes.string.isRequired, getAsset: PropTypes.func.isRequired, component: PropTypes.elementType.isRequired, + folder: PropTypes.string, }; subscribed = true; @@ -14,13 +15,12 @@ class Asset extends React.Component { value: null, }; - _fetchAsset() { - const { getAsset, path } = this.props; - getAsset(path).then(value => { - if (this.subscribed) { - this.setState({ value }); - } - }); + async _fetchAsset() { + const { getAsset, path, folder } = this.props; + const value = await getAsset(path, folder); + if (this.subscribed) { + this.setState({ value }); + } } componentDidMount() { @@ -32,7 +32,11 @@ class Asset extends React.Component { } componentDidUpdate(prevProps) { - if (prevProps.path !== this.props.path || prevProps.getAsset !== this.props.getAsset) { + if ( + prevProps.path !== this.props.path || + prevProps.getAsset !== this.props.getAsset || + prevProps.folder !== this.props.folder + ) { this._fetchAsset(); } } diff --git a/packages/netlify-cms-widget-file/src/withFileControl.js b/packages/netlify-cms-widget-file/src/withFileControl.js index a8af6e4e..236b851e 100644 --- a/packages/netlify-cms-widget-file/src/withFileControl.js +++ b/packages/netlify-cms-widget-file/src/withFileControl.js @@ -37,8 +37,8 @@ const Image = styled(({ value: src }) => { - return ; +const StyledImageAsset = ({ getAsset, value, field }) => { + return ( + + ); }; const ImagePreviewContent = props => { - const { value, getAsset } = props; + const { value, getAsset, field } = props; if (Array.isArray(value) || List.isList(value)) { - return value.map(val => ); + return value.map(val => ( + + )); } return ; }; diff --git a/packages/netlify-cms-widget-markdown/src/MarkdownControl/VisualEditor.js b/packages/netlify-cms-widget-markdown/src/MarkdownControl/VisualEditor.js index e5e2732f..e80ff024 100644 --- a/packages/netlify-cms-widget-markdown/src/MarkdownControl/VisualEditor.js +++ b/packages/netlify-cms-widget-markdown/src/MarkdownControl/VisualEditor.js @@ -49,6 +49,38 @@ const createSlateValue = (rawValue, { voidCodeBlock }) => { return Value.create({ document }); }; +export const mergeMediaConfig = (editorComponents, field) => { + // merge editor media library config to image components + if (editorComponents.has('image')) { + const imageComponent = editorComponents.get('image'); + const fields = imageComponent?.fields; + + if (fields) { + imageComponent.fields = fields.update( + fields.findIndex(f => f.get('widget') === 'image'), + f => { + // merge `media_library` config + if (field.has('media_library')) { + f = f.set( + 'media_library', + field.get('media_library').mergeDeep(f.get('media_library')), + ); + } + // merge 'media_folder' + if (field.has('media_folder') && !f.has('media_folder')) { + f = f.set('media_folder', field.get('media_folder')); + } + // merge 'public_folder' + if (field.has('public_folder') && !f.has('public_folder')) { + f = f.set('public_folder', field.get('public_folder')); + } + return f; + }, + ); + } + } +}; + export default class Editor extends React.Component { constructor(props) { super(props); @@ -59,6 +91,8 @@ export default class Editor extends React.Component { this.codeBlockComponent || editorComponents.has('code-block') ? editorComponents : editorComponents.set('code-block', { label: 'Code Block', type: 'code-block' }); + + mergeMediaConfig(this.editorComponents, this.props.field); this.renderBlock = renderBlock({ classNameWrapper: props.className, resolveWidget: props.resolveWidget, diff --git a/packages/netlify-cms-widget-markdown/src/MarkdownControl/__tests__/VisualEditor.spec.js b/packages/netlify-cms-widget-markdown/src/MarkdownControl/__tests__/VisualEditor.spec.js new file mode 100644 index 00000000..4b370c11 --- /dev/null +++ b/packages/netlify-cms-widget-markdown/src/MarkdownControl/__tests__/VisualEditor.spec.js @@ -0,0 +1,55 @@ +import { Map, fromJS } from 'immutable'; +import { mergeMediaConfig } from '../VisualEditor'; + +describe('VisualEditor', () => { + describe('mergeMediaConfig', () => { + it('should copy editor media settings to image component', () => { + const editorComponents = Map({ + image: { + id: 'image', + label: 'Image', + type: 'shortcode', + icon: 'exclamation-triangle', + widget: 'object', + pattern: {}, + fields: fromJS([ + { + label: 'Image', + name: 'image', + widget: 'image', + media_library: { allow_multiple: false }, + }, + { label: 'Alt Text', name: 'alt' }, + { label: 'Title', name: 'title' }, + ]), + }, + }); + + const field = fromJS({ + label: 'Body', + name: 'body', + widget: 'markdown', + media_folder: '/{{media_folder}}/posts/images/widget/body', + public_folder: '{{public_folder}}/posts/images/widget/body', + media_library: { config: { max_file_size: 1234 } }, + }); + + mergeMediaConfig(editorComponents, field); + + expect(editorComponents.get('image').fields).toEqual( + fromJS([ + { + label: 'Image', + name: 'image', + widget: 'image', + media_library: { allow_multiple: false, config: { max_file_size: 1234 } }, + media_folder: '/{{media_folder}}/posts/images/widget/body', + public_folder: '{{public_folder}}/posts/images/widget/body', + }, + { label: 'Alt Text', name: 'alt' }, + { label: 'Title', name: 'title' }, + ]), + ); + }); + }); +}); diff --git a/packages/netlify-cms-widget-markdown/src/serializers/remarkRehypeShortcodes.js b/packages/netlify-cms-widget-markdown/src/serializers/remarkRehypeShortcodes.js index 41065db4..f7bb625f 100644 --- a/packages/netlify-cms-widget-markdown/src/serializers/remarkRehypeShortcodes.js +++ b/packages/netlify-cms-widget-markdown/src/serializers/remarkRehypeShortcodes.js @@ -53,9 +53,9 @@ export default function remarkToRehypeShortcodes({ plugins, getAsset, resolveWid * Retrieve the shortcode preview component. */ async function getPreview(plugin, shortcodeData) { - const { toPreview, widget } = plugin; + const { toPreview, widget, fields } = plugin; if (toPreview) { - return toPreview(shortcodeData, getAsset); + return toPreview(shortcodeData, getAsset, fields); } const preview = resolveWidget(widget); return React.createElement(preview.preview, { diff --git a/yarn.lock b/yarn.lock index 8d340372..b5667086 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6538,11 +6538,6 @@ detect-indent@^5.0.0: resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-5.0.0.tgz#3871cc0a6a002e8c3e5b3cf7f336264675f06b9d" integrity sha1-OHHMCmoALow+Wzz38zYmRnXwa50= -detect-libc@^1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" - integrity sha1-+hN8S9aY7fVc1c0CrFWfkaTEups= - detect-newline@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-2.1.0.tgz#f41f1c10be4b00e87b5f13da680759f2c5bfd3e2" @@ -9106,7 +9101,7 @@ husky@^3.0.9: run-node "^1.0.0" slash "^3.0.0" -iconv-lite@0.4.24, iconv-lite@^0.4.24, iconv-lite@^0.4.4, iconv-lite@~0.4.13: +iconv-lite@0.4.24, iconv-lite@^0.4.24, iconv-lite@~0.4.13: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== @@ -11802,15 +11797,6 @@ ncp@^2.0.0: resolved "https://registry.yarnpkg.com/ncp/-/ncp-2.0.0.tgz#195a21d6c46e361d2fb1281ba38b91e9df7bdbb3" integrity sha1-GVoh1sRuNh0vsSgbo4uR6d9727M= -needle@^2.2.1: - version "2.4.0" - resolved "https://registry.yarnpkg.com/needle/-/needle-2.4.0.tgz#6833e74975c444642590e15a750288c5f939b57c" - integrity sha512-4Hnwzr3mi5L97hMYeNl8wRW/Onhy4nUKR/lVemJ8gJedxxUyBLm9kkrDColJvoSfwi0jCNhD+xCdOtiGDQiRZg== - dependencies: - debug "^3.2.6" - iconv-lite "^0.4.4" - sax "^1.2.4" - negotiator@0.6.2: version "0.6.2" resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb" @@ -11974,22 +11960,6 @@ node-polyglot@^2.3.0: string.prototype.trim "^1.1.2" warning "^4.0.3" -node-pre-gyp@*: - version "0.14.0" - resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.14.0.tgz#9a0596533b877289bcad4e143982ca3d904ddc83" - integrity sha512-+CvDC7ZttU/sSt9rFjix/P05iS43qHCOOGzcr3Ry99bXG7VX953+vFyEuph/tfqoYu8dttBkE86JSKBO2OzcxA== - dependencies: - detect-libc "^1.0.2" - mkdirp "^0.5.1" - needle "^2.2.1" - nopt "^4.0.1" - npm-packlist "^1.1.6" - npmlog "^4.0.2" - rc "^1.2.7" - rimraf "^2.6.1" - semver "^5.3.0" - tar "^4.4.2" - node-releases@^1.1.29, node-releases@^1.1.47: version "1.1.47" resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.47.tgz#c59ef739a1fd7ecbd9f0b7cf5b7871e8a8b591e4" @@ -12111,7 +12081,7 @@ npm-normalize-package-bin@^1.0.0, npm-normalize-package-bin@^1.0.1: semver "^5.6.0" validate-npm-package-name "^3.0.0" -npm-packlist@^1.1.6, npm-packlist@^1.4.4: +npm-packlist@^1.4.4: version "1.4.8" resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-1.4.8.tgz#56ee6cc135b9f98ad3d51c1c95da22bbb9b2ef3e" integrity sha512-5+AZgwru5IevF5ZdnFglB5wNlHG1AOOuw28WhUq8/8emhBmLv6jX5by4WJCh7lW0uSYZYS6DXqIsyZVIXRZU9A== @@ -12158,7 +12128,7 @@ npm-run-path@^4.0.0: dependencies: path-key "^3.0.0" -npmlog@^4.0.2, npmlog@^4.1.2: +npmlog@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b" integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg== @@ -13637,7 +13607,7 @@ rbush@2.0.2: dependencies: quickselect "^1.0.1" -rc@^1.0.1, rc@^1.1.6, rc@^1.2.7: +rc@^1.0.1, rc@^1.1.6: version "1.2.8" resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw== @@ -14793,7 +14763,7 @@ rimraf@2.6.3: dependencies: glob "^7.1.3" -rimraf@^2.2.8, rimraf@^2.5.2, rimraf@^2.5.4, rimraf@^2.6.1, rimraf@^2.6.2, rimraf@^2.6.3, rimraf@^2.7.1: +rimraf@^2.2.8, rimraf@^2.5.2, rimraf@^2.5.4, rimraf@^2.6.2, rimraf@^2.6.3, rimraf@^2.7.1: version "2.7.1" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== @@ -14984,7 +14954,7 @@ semver-diff@^2.0.0: dependencies: semver "^5.0.3" -"semver@2 || 3 || 4 || 5", "semver@2.x || 3.x || 4 || 5", semver@^5.0.3, semver@^5.1.0, semver@^5.3.0, semver@^5.4.1, semver@^5.5, semver@^5.5.0, semver@^5.5.1, semver@^5.6.0, semver@^5.7.0, semver@^5.7.1: +"semver@2 || 3 || 4 || 5", "semver@2.x || 3.x || 4 || 5", semver@^5.0.3, semver@^5.1.0, semver@^5.4.1, semver@^5.5, semver@^5.5.0, semver@^5.5.1, semver@^5.6.0, semver@^5.7.0, semver@^5.7.1: version "5.7.1" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== @@ -16178,7 +16148,7 @@ tapable@^1.0.0, tapable@^1.1.3: resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.3.tgz#a1fccc06b58db61fd7a45da2da44f5f3a3e67ba2" integrity sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA== -tar@^4.4.10, tar@^4.4.12, tar@^4.4.2, tar@^4.4.8: +tar@^4.4.10, tar@^4.4.12, tar@^4.4.8: version "4.4.13" resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.13.tgz#43b364bc52888d555298637b10d60790254ab525" integrity sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA==