diff --git a/packages/netlify-cms-core/src/actions/__tests__/media.js b/packages/netlify-cms-core/src/actions/__tests__/media.js index 3baa915a..06ed517e 100644 --- a/packages/netlify-cms-core/src/actions/__tests__/media.js +++ b/packages/netlify-cms-core/src/actions/__tests__/media.js @@ -105,5 +105,26 @@ describe('media', () => { }); expect(result).toEqual(emptyAsset); }); + + it('should return asset with original path on load error', () => { + const path = 'static/media/image.png'; + const store = mockStore({ + medias: Map({ [path]: { error: true } }), + }); + + selectMediaFilePath.mockReturnValue(path); + const payload = { path }; + + const result = store.dispatch(getAsset(payload)); + const actions = store.getActions(); + + const asset = new AssetProxy({ url: path, path }); + expect(actions).toHaveLength(1); + expect(actions[0]).toEqual({ + type: ADD_ASSET, + payload: asset, + }); + expect(result).toEqual(asset); + }); }); }); diff --git a/packages/netlify-cms-core/src/actions/entries.ts b/packages/netlify-cms-core/src/actions/entries.ts index 9116d56a..43f29bd3 100644 --- a/packages/netlify-cms-core/src/actions/entries.ts +++ b/packages/netlify-cms-core/src/actions/entries.ts @@ -297,14 +297,14 @@ export function retrieveLocalBackup(collection: Collection, slug: string) { path: file.path, file: file.file, url: file.url, - folder: file.folder, + field: file.field, }); } else { return getAsset({ collection, entry: fromJS(entry), path: file.path, - folder: file.folder, + field: file.field, })(dispatch, getState); } }), @@ -557,12 +557,15 @@ export async function getMediaAssets({ entry: EntryMap; dispatch: Dispatch; }) { - const filesArray = entry.get('mediaFiles').toJS() as MediaFile[]; + const filesArray = entry.get('mediaFiles').toArray(); const assets = await Promise.all( filesArray - .filter(file => file.draft) + .filter(file => file.get('draft')) .map(file => - getAsset({ collection, entry, path: file.path, folder: file.folder })(dispatch, getState), + getAsset({ collection, entry, path: file.get('path'), field: file.get('field') })( + dispatch, + getState, + ), ), ); diff --git a/packages/netlify-cms-core/src/actions/media.ts b/packages/netlify-cms-core/src/actions/media.ts index b246de6c..bb5f5faa 100644 --- a/packages/netlify-cms-core/src/actions/media.ts +++ b/packages/netlify-cms-core/src/actions/media.ts @@ -1,5 +1,5 @@ import AssetProxy, { createAssetProxy } from '../valueObjects/AssetProxy'; -import { Collection, State, EntryMap } from '../types/redux'; +import { Collection, State, EntryMap, EntryField } from '../types/redux'; import { ThunkDispatch } from 'redux-thunk'; import { AnyAction } from 'redux'; import { isAbsolutePath } from 'netlify-cms-lib-util'; @@ -67,7 +67,7 @@ interface GetAssetArgs { collection: Collection; entry: EntryMap; path: string; - folder?: string; + field?: EntryField; } const emptyAsset = createAssetProxy({ @@ -82,26 +82,27 @@ export function boundGetAsset( collection: Collection, entry: EntryMap, ) { - const bound = (path: string, folder: string) => { - const asset = dispatch(getAsset({ collection, entry, path, folder })); + const bound = (path: string, field: EntryField) => { + const asset = dispatch(getAsset({ collection, entry, path, field })); return asset; }; return bound; } -export function getAsset({ collection, entry, path, folder }: GetAssetArgs) { +export function getAsset({ collection, entry, path, field }: GetAssetArgs) { return (dispatch: ThunkDispatch, getState: () => State) => { if (!path) return emptyAsset; const state = getState(); - const resolvedPath = selectMediaFilePath(state.config, collection, entry, path, folder); + const resolvedPath = selectMediaFilePath(state.config, collection, entry, path, field); let { asset, isLoading, error } = state.medias.get(resolvedPath) || {}; if (isLoading) { return emptyAsset; } - if (asset && !error) { + + if (asset) { // There is already an AssetProxy in memory for this path. Use it. return asset; } @@ -111,8 +112,14 @@ export function getAsset({ collection, entry, path, folder }: GetAssetArgs) { asset = createAssetProxy({ path: resolvedPath, url: path }); dispatch(addAsset(asset)); } else { - dispatch(loadAsset(resolvedPath)); - asset = emptyAsset; + if (error) { + // on load error default back to original path + asset = createAssetProxy({ path, url: path }); + dispatch(addAsset(asset)); + } else { + dispatch(loadAsset(resolvedPath)); + asset = emptyAsset; + } } return asset; diff --git a/packages/netlify-cms-core/src/actions/mediaLibrary.ts b/packages/netlify-cms-core/src/actions/mediaLibrary.ts index 377b17ca..9bf2943e 100644 --- a/packages/netlify-cms-core/src/actions/mediaLibrary.ts +++ b/packages/netlify-cms-core/src/actions/mediaLibrary.ts @@ -14,7 +14,13 @@ import { getIntegrationProvider } from '../integrations'; import { addAsset, removeAsset } from './media'; import { addDraftEntryMediaFile, removeDraftEntryMediaFile } from './entries'; import { sanitizeSlug } from '../lib/urlHelper'; -import { State, MediaFile, DisplayURLState, MediaLibraryInstance } from '../types/redux'; +import { + State, + MediaFile, + DisplayURLState, + MediaLibraryInstance, + EntryField, +} from '../types/redux'; import { AnyAction } from 'redux'; import { ThunkDispatch } from 'redux-thunk'; import { waitUntilWithTimeout } from './waitUntil'; @@ -103,7 +109,7 @@ export function closeMediaLibrary() { }; } -export function insertMedia(mediaPath: string | string[], publicFolder: string | undefined) { +export function insertMedia(mediaPath: string | string[], field: EntryField | undefined) { return (dispatch: ThunkDispatch, getState: () => State) => { const state = getState(); const config = state.config; @@ -112,16 +118,10 @@ export function insertMedia(mediaPath: string | string[], publicFolder: string | const collection = state.collections.get(collectionName); if (Array.isArray(mediaPath)) { mediaPath = mediaPath.map(path => - selectMediaFilePublicPath(config, collection, path, entry, publicFolder), + selectMediaFilePublicPath(config, collection, path, entry, field), ); } else { - mediaPath = selectMediaFilePublicPath( - config, - collection, - mediaPath as string, - entry, - publicFolder, - ); + mediaPath = selectMediaFilePublicPath(config, collection, mediaPath as string, entry, field); } dispatch({ type: MEDIA_INSERT, payload: { mediaPath } }); }; @@ -201,18 +201,18 @@ function createMediaFileFromAsset({ size: file.size, url: assetProxy.url, path: assetProxy.path, - folder: assetProxy.folder, + field: assetProxy.field, }; return mediaFile; } export function persistMedia(file: File, opts: MediaOptions = {}) { - const { privateUpload, mediaFolder } = opts; + const { privateUpload, field } = opts; return async (dispatch: ThunkDispatch, getState: () => State) => { const state = getState(); const backend = currentBackend(state.config); const integration = selectIntegration(state, null, 'assetStore'); - const files: MediaFile[] = selectMediaFiles(state); + const files: MediaFile[] = selectMediaFiles(state, field); const fileName = sanitizeSlug(file.name.toLowerCase(), state.config.get('slug')); const existingFile = files.find(existingFile => existingFile.name.toLowerCase() === fileName); @@ -261,11 +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, mediaFolder); + const path = selectMediaFilePath(state.config, collection, entry, file.name, field); assetProxy = createAssetProxy({ file, path, - folder: mediaFolder, + field, }); } @@ -358,12 +358,8 @@ export function deleteMedia(file: MediaFile, opts: MediaOptions = {}) { export async function getMediaFile(state: State, path: string) { const backend = currentBackend(state.config); - try { - const { url } = await backend.getMediaFile(path); - return { url }; - } catch (e) { - return { url: path }; - } + const { url } = await backend.getMediaFile(path); + return { url }; } export function loadMediaDisplayURL(file: MediaFile) { @@ -409,7 +405,7 @@ export function mediaLoading(page: number) { interface MediaOptions { privateUpload?: boolean; - mediaFolder?: string; + field?: EntryField; } 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 8306f0ae..412a122b 100644 --- a/packages/netlify-cms-core/src/backend.ts +++ b/packages/netlify-cms-core/src/backend.ts @@ -27,7 +27,6 @@ import { Implementation as BackendImplementation, DisplayURL, ImplementationEntry, - ImplementationMediaFile, Credentials, User, getPathDepth, @@ -45,6 +44,7 @@ import { EntryDraft, CollectionFile, State, + EntryField, } from './types/redux'; import AssetProxy from './valueObjects/AssetProxy'; import { FOLDER, FILES } from './constants/collectionTypes'; @@ -104,10 +104,22 @@ interface BackendOptions { config?: Config; } +export interface MediaFile { + name: string; + id: string; + size?: number; + displayURL?: DisplayURL; + path: string; + draft?: boolean; + url?: string; + file?: File; + field?: EntryField; +} + interface BackupEntry { raw: string; path: string; - mediaFiles: ImplementationMediaFile[]; + mediaFiles: MediaFile[]; } interface PersistArgs { @@ -444,11 +456,11 @@ export class Backend { return; } - const mediaFiles = await Promise.all( + const mediaFiles = await Promise.all( entry .get('mediaFiles') .toJS() - .map(async (file: ImplementationMediaFile) => { + .map(async (file: MediaFile) => { // make sure to serialize the file if (file.url?.startsWith('blob:')) { const blob = await fetch(file.url as string).then(res => res.blob()); @@ -485,7 +497,6 @@ export class Backend { const integration = selectIntegration(state.integrations, null, 'assetStore'); const loadedEntry = await this.implementation.getEntry(path); - const entry = createEntry(collection.get('name'), slug, loadedEntry.file.path, { raw: loadedEntry.data, label, @@ -700,7 +711,7 @@ export class Backend { collection, entryDraft.get('entry').set('path', path), oldPath, - asset.folder, + asset.field, ); 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 babe0f71..99f1454d 100644 --- a/packages/netlify-cms-core/src/components/Collection/Entries/EntryCard.js +++ b/packages/netlify-cms-core/src/components/Collection/Entries/EntryCard.js @@ -89,7 +89,7 @@ const EntryCard = ({ path, summary, image, - imageFolder, + imageField, collectionLabel, viewStyle = VIEW_STYLE_LIST, getAsset, @@ -105,7 +105,7 @@ const EntryCard = ({ ); } - const asset = getAsset(image, imageFolder); + const asset = getAsset(image, imageField); const src = asset.toString(); if (viewStyle === VIEW_STYLE_GRID) { @@ -148,8 +148,7 @@ const mapStateToProps = (state, ownProps) => { image, imageFolder: collection .get('fields') - ?.find(f => f.get('name') === inferedFields.imageField && f.get('widget') === 'image') - ?.get('media_folder'), + ?.find(f => f.get('name') === inferedFields.imageField && f.get('widget') === 'image'), isLoadingAsset, }; }; diff --git a/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibrary.js b/packages/netlify-cms-core/src/components/MediaLibrary/MediaLibrary.js index f85cce07..497a6cd5 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, mediaFolder } = this.props; + const { persistMedia, privateUpload, config, t, field } = 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, mediaFolder }); + await persistMedia(file, { privateUpload, field }); 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, publicFolder } = this.props; - insertMedia(path, publicFolder); + const { insertMedia, field } = this.props; + insertMedia(path, field); this.handleClose(); }; @@ -315,10 +315,11 @@ class MediaLibrary extends React.Component { const mapStateToProps = state => { const { mediaLibrary } = state; + const field = mediaLibrary.get('field'); const mediaLibraryProps = { isVisible: mediaLibrary.get('isVisible'), canInsert: mediaLibrary.get('canInsert'), - files: selectMediaFiles(state), + files: selectMediaFiles(state, field), displayURLs: mediaLibrary.get('displayURLs'), dynamicSearch: mediaLibrary.get('dynamicSearch'), dynamicSearchActive: mediaLibrary.get('dynamicSearchActive'), @@ -332,8 +333,7 @@ const mapStateToProps = state => { page: mediaLibrary.get('page'), hasNextPage: mediaLibrary.get('hasNextPage'), isPaginating: mediaLibrary.get('isPaginating'), - mediaFolder: mediaLibrary.get('mediaFolder'), - publicFolder: mediaLibrary.get('publicFolder'), + field, }; return { ...mediaLibraryProps }; }; diff --git a/packages/netlify-cms-core/src/lib/formatters.ts b/packages/netlify-cms-core/src/lib/formatters.ts index 08a38eb6..ccbac198 100644 --- a/packages/netlify-cms-core/src/lib/formatters.ts +++ b/packages/netlify-cms-core/src/lib/formatters.ts @@ -218,6 +218,7 @@ export const folderFormatter = ( if (!entry || !entry.get('data')) { return folderTemplate; } + let fields = (entry.get('data') as Map).set(folderKey, defaultFolder); fields = addFileTemplateFields(entry.get('path'), fields); @@ -232,5 +233,6 @@ export const folderFormatter = ( fields, (value: string) => (value === defaultFolder ? defaultFolder : processSegment(value)), ); + return mediaFolder; }; 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 7462982f..2b861649 100644 --- a/packages/netlify-cms-core/src/reducers/__tests__/collections.spec.js +++ b/packages/netlify-cms-core/src/reducers/__tests__/collections.spec.js @@ -4,7 +4,7 @@ import collections, { selectAllowDeletion, selectEntryPath, selectEntrySlug, - selectFieldsMediaFolders, + selectFieldsWithMediaFolders, selectMediaFolders, getFieldsNames, selectField, @@ -87,12 +87,12 @@ describe('collections', () => { describe('selectFieldsMediaFolders', () => { it('should return empty array for invalid collection', () => { - expect(selectFieldsMediaFolders(fromJS({}))).toEqual([]); + expect(selectFieldsWithMediaFolders(fromJS({}))).toEqual([]); }); it('should return configs for folder collection', () => { expect( - selectFieldsMediaFolders( + selectFieldsWithMediaFolders( fromJS({ folder: 'posts', fields: [ @@ -124,19 +124,26 @@ describe('collections', () => { }), ), ).toEqual([ - 'image_media_folder', - 'body_media_folder', - 'list_1_item_media_folder', - 'list_2_item_media_folder', + fromJS({ + name: 'image', + media_folder: 'image_media_folder', + }), + fromJS({ name: 'body', media_folder: 'body_media_folder' }), + fromJS({ name: 'list_1_item', media_folder: 'list_1_item_media_folder' }), + fromJS({ + name: 'list_2_item', + media_folder: 'list_2_item_media_folder', + }), ]); }); it('should return configs for files collection', () => { expect( - selectFieldsMediaFolders( + selectFieldsWithMediaFolders( fromJS({ files: [ { + name: 'file1', fields: [ { name: 'image', @@ -145,6 +152,7 @@ describe('collections', () => { ], }, { + name: 'file2', fields: [ { name: 'body', @@ -153,6 +161,7 @@ describe('collections', () => { ], }, { + name: 'file3', fields: [ { name: 'list_1', @@ -164,6 +173,7 @@ describe('collections', () => { ], }, { + name: 'file4', fields: [ { name: 'list_2', @@ -178,12 +188,13 @@ describe('collections', () => { }, ], }), + 'file4', ), ).toEqual([ - 'image_media_folder', - 'body_media_folder', - 'list_1_item_media_folder', - 'list_2_item_media_folder', + fromJS({ + name: 'list_2_item', + media_folder: 'list_2_item_media_folder', + }), ]); }); }); @@ -195,48 +206,53 @@ describe('collections', () => { sanitize_replacement: '-', }; - const config = fromJS({ slug }); - it('should return fields and collection folder', () => { + const config = fromJS({ slug, media_folder: '/static/img' }); + it('should return fields and collection folders', () => { expect( selectMediaFolders( { config }, fromJS({ folder: 'posts', - media_folder: '/collection_media_folder', + media_folder: '{{media_folder}}/general/', fields: [ { name: 'image', - media_folder: '/image_media_folder', + media_folder: '{{media_folder}}/customers/', }, ], }), - fromJS({ slug: 'name', path: 'src/post/post1.md' }), + fromJS({ slug: 'name', path: 'src/post/post1.md', data: {} }), ), - ).toEqual(['collection_media_folder', 'image_media_folder']); + ).toEqual(['static/img/general', 'static/img/general/customers']); }); - it('should return fields and collection folder', () => { + it('should return fields, file and collection folders', () => { expect( selectMediaFolders( { config }, fromJS({ + media_folder: '{{media_folder}}/general/', files: [ { name: 'name', file: 'src/post/post1.md', - media_folder: '/file_media_folder', + media_folder: '{{media_folder}}/customers/', fields: [ { name: 'image', - media_folder: '/image_media_folder', + media_folder: '{{media_folder}}/logos/', }, ], }, ], }), - fromJS({ slug: 'name', path: 'src/post/post1.md' }), + fromJS({ slug: 'name', path: 'src/post/post1.md', data: {} }), ), - ).toEqual(['file_media_folder', 'image_media_folder']); + ).toEqual([ + 'static/img/general', + 'static/img/general/customers', + 'static/img/general/customers/logos', + ]); }); }); 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 22bcaa5b..bb02dca6 100644 --- a/packages/netlify-cms-core/src/reducers/__tests__/entries.spec.js +++ b/packages/netlify-cms-core/src/reducers/__tests__/entries.spec.js @@ -1,4 +1,4 @@ -import { Map, OrderedMap, fromJS } from 'immutable'; +import { OrderedMap, fromJS } from 'immutable'; import * as actions from 'Actions/entries'; import reducer, { selectMediaFolder, @@ -7,13 +7,13 @@ import reducer, { } from '../entries'; const initialState = OrderedMap({ - posts: Map({ name: 'posts' }), + posts: fromJS({ name: 'posts' }), }); describe('entries', () => { describe('reducer', () => { it('should mark entries as fetching', () => { - expect(reducer(initialState, actions.entriesLoading(Map({ name: 'posts' })))).toEqual( + expect(reducer(initialState, actions.entriesLoading(fromJS({ name: 'posts' })))).toEqual( OrderedMap( fromJS({ posts: { name: 'posts' }, @@ -31,7 +31,7 @@ describe('entries', () => { { slug: 'b', title: 'B' }, ]; expect( - reducer(initialState, actions.entriesLoaded(Map({ name: 'posts' }), entries, 0)), + reducer(initialState, actions.entriesLoaded(fromJS({ name: 'posts' }), entries, 0)), ).toEqual( OrderedMap( fromJS({ @@ -53,7 +53,7 @@ describe('entries', () => { it('should handle loaded entry', () => { const entry = { slug: 'a', path: '' }; - expect(reducer(initialState, actions.entryLoaded(Map({ name: 'posts' }), entry))).toEqual( + expect(reducer(initialState, actions.entryLoaded(fromJS({ name: 'posts' }), entry))).toEqual( OrderedMap( fromJS({ posts: { name: 'posts' }, @@ -75,8 +75,8 @@ describe('entries', () => { it("should return global media folder when collection doesn't specify media_folder", () => { expect( selectMediaFolder( - Map({ media_folder: 'static/media' }), - Map({ name: 'posts' }), + fromJS({ media_folder: 'static/media' }), + fromJS({ name: 'posts' }), undefined, undefined, ), @@ -86,8 +86,8 @@ describe('entries', () => { 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: '' }), + fromJS({ media_folder: 'static/media' }), + fromJS({ name: 'posts', folder: 'posts', media_folder: '' }), undefined, undefined, ), @@ -97,9 +97,9 @@ describe('entries', () => { it('should return relative media folder when collection specifies media_folder and entry path is not null', () => { expect( selectMediaFolder( - Map({ media_folder: 'static/media' }), - Map({ name: 'posts', folder: 'posts', media_folder: '' }), - Map({ path: 'posts/title/index.md' }), + fromJS({ media_folder: 'static/media' }), + fromJS({ name: 'posts', folder: 'posts', media_folder: '' }), + fromJS({ path: 'posts/title/index.md' }), undefined, ), ).toEqual('posts/title'); @@ -108,24 +108,41 @@ describe('entries', () => { it('should resolve collection relative media folder', () => { expect( selectMediaFolder( - Map({ media_folder: 'static/media' }), - Map({ name: 'posts', folder: 'posts', media_folder: '../' }), - Map({ path: 'posts/title/index.md' }), + fromJS({ media_folder: 'static/media' }), + fromJS({ name: 'posts', folder: 'posts', media_folder: '../' }), + fromJS({ path: 'posts/title/index.md' }), undefined, ), - ).toEqual('posts/'); + ).toEqual('posts'); + }); + + it('should resolve field relative media folder', () => { + const field = fromJS({ media_folder: '' }); + expect( + selectMediaFolder( + fromJS({ media_folder: '/static/img' }), + fromJS({ + name: 'other', + folder: 'other', + fields: [field], + media_folder: '../', + }), + fromJS({ path: 'src/other/other.md', data: {} }), + field, + ), + ).toEqual('src/other'); }); it('should return collection absolute media folder without leading slash', () => { expect( selectMediaFolder( - Map({ media_folder: '/static/Images' }), - Map({ + fromJS({ media_folder: '/static/Images' }), + fromJS({ name: 'getting-started', folder: 'src/docs/getting-started', media_folder: '/static/images/docs/getting-started', }), - Map({ path: 'src/docs/getting-started/with-github.md' }), + fromJS({ path: 'src/docs/getting-started/with-github.md' }), undefined, ), ).toEqual('static/images/docs/getting-started'); @@ -201,7 +218,13 @@ describe('entries', () => { const collection = fromJS({ name: 'posts', folder: 'content', - fields: [{ name: 'title', widget: 'string' }], + fields: [ + { + name: 'title', + widget: 'string', + media_folder: '../../../{{media_folder}}/{{category}}/{{slug}}', + }, + ], }); expect( @@ -209,7 +232,7 @@ describe('entries', () => { fromJS({ media_folder: 'static/media', slug: slugConfig }), collection, entry, - '../../../{{media_folder}}/{{category}}/{{slug}}', + collection.get('fields').get(0), ), ).toEqual('static/media/hosting-and-deployment/deployment-with-nanobox'); }); @@ -260,7 +283,47 @@ describe('entries', () => { fromJS({ path: 'posts/title/index.md', slug: 'index' }), undefined, ), - ).toBe('static/images/'); + ).toBe('static/images'); + }); + + it('should cascade media_folders', () => { + const mainImageField = fromJS({ name: 'main_image' }); + const logoField = fromJS({ name: 'logo', media_folder: '{{media_folder}}/logos/' }); + const nestedField2 = fromJS({ name: 'nested', media_folder: '{{media_folder}}/nested2/' }); + const nestedField1 = fromJS({ + name: 'nested', + media_folder: '{{media_folder}}/nested1/', + fields: [nestedField2], + }); + + const args = [ + fromJS({ media_folder: '/static/img' }), + fromJS({ + name: 'general', + media_folder: '{{media_folder}}/general/', + files: [ + { + name: 'customers', + media_folder: '{{media_folder}}/customers/', + fields: [ + mainImageField, + logoField, + { media_folder: '{{media_folder}}/nested', field: nestedField1 }, + ], + }, + ], + }), + fromJS({ path: 'src/customers/customers.md', slug: 'customers', data: { title: 'title' } }), + ]; + + expect(selectMediaFolder(...args, mainImageField)).toBe('static/img/general/customers'); + expect(selectMediaFolder(...args, logoField)).toBe('static/img/general/customers/logos'); + expect(selectMediaFolder(...args, nestedField1)).toBe( + 'static/img/general/customers/nested/nested1', + ); + expect(selectMediaFolder(...args, nestedField2)).toBe( + 'static/img/general/customers/nested/nested1/nested2', + ); }); }); @@ -274,8 +337,8 @@ describe('entries', () => { it('should resolve path from global media folder for collection with no media folder', () => { expect( selectMediaFilePath( - Map({ media_folder: 'static/media' }), - Map({ name: 'posts', folder: 'posts' }), + fromJS({ media_folder: 'static/media' }), + fromJS({ name: 'posts', folder: 'posts' }), undefined, 'image.png', undefined, @@ -286,8 +349,8 @@ describe('entries', () => { it('should resolve path from collection media folder for collection with media folder', () => { expect( selectMediaFilePath( - Map({ media_folder: 'static/media' }), - Map({ name: 'posts', folder: 'posts', media_folder: '' }), + fromJS({ media_folder: 'static/media' }), + fromJS({ name: 'posts', folder: 'posts', media_folder: '' }), undefined, 'image.png', undefined, @@ -298,9 +361,9 @@ describe('entries', () => { it('should handle relative media_folder', () => { expect( selectMediaFilePath( - Map({ media_folder: 'static/media' }), - Map({ name: 'posts', folder: 'posts', media_folder: '../../static/media/' }), - Map({ path: 'posts/title/index.md' }), + fromJS({ media_folder: 'static/media' }), + fromJS({ name: 'posts', folder: 'posts', media_folder: '../../static/media/' }), + fromJS({ path: 'posts/title/index.md' }), 'image.png', undefined, ), @@ -308,13 +371,14 @@ describe('entries', () => { }); it('should handle field media_folder', () => { + const field = fromJS({ media_folder: '../../static/media/' }); expect( selectMediaFilePath( - Map({ media_folder: 'static/media' }), - Map({ name: 'posts', folder: 'posts' }), - Map({ path: 'posts/title/index.md' }), + fromJS({ media_folder: 'static/media' }), + fromJS({ name: 'posts', folder: 'posts', fields: [field] }), + fromJS({ path: 'posts/title/index.md' }), 'image.png', - '../../static/media/', + field, ), ).toBe('static/media/image.png'); }); @@ -330,7 +394,7 @@ describe('entries', () => { it('should resolve path from public folder for collection with no media folder', () => { expect( selectMediaFilePublicPath( - Map({ public_folder: '/media' }), + fromJS({ public_folder: '/media' }), null, '/media/image.png', undefined, @@ -342,8 +406,8 @@ describe('entries', () => { it('should resolve path from collection public folder for collection with public folder', () => { expect( selectMediaFilePublicPath( - Map({ public_folder: '/media' }), - Map({ name: 'posts', folder: 'posts', public_folder: '' }), + fromJS({ public_folder: '/media' }), + fromJS({ name: 'posts', folder: 'posts', public_folder: '' }), 'image.png', undefined, undefined, @@ -354,8 +418,8 @@ describe('entries', () => { it('should handle relative public_folder', () => { expect( selectMediaFilePublicPath( - Map({ public_folder: '/media' }), - Map({ name: 'posts', folder: 'posts', public_folder: '../../static/media/' }), + fromJS({ public_folder: '/media' }), + fromJS({ name: 'posts', folder: 'posts', public_folder: '../../static/media/' }), 'image.png', undefined, undefined, @@ -403,10 +467,16 @@ describe('entries', () => { path: 'content/en/hosting-and-deployment/deployment-with-nanobox.md', data: { title: 'Deployment With NanoBox', category: 'Hosting And Deployment' }, }); + + const field = fromJS({ + name: 'title', + widget: 'string', + public_folder: '/{{public_folder}}/{{category}}/{{slug}}', + }); const collection = fromJS({ name: 'posts', folder: 'content', - fields: [{ name: 'title', widget: 'string' }], + fields: [field], }); expect( @@ -415,7 +485,7 @@ describe('entries', () => { collection, 'image.png', entry, - '/{{public_folder}}/{{category}}/{{slug}}', + field, ), ).toEqual('/static/media/hosting-and-deployment/deployment-with-nanobox/image.png'); }); @@ -431,10 +501,16 @@ describe('entries', () => { path: 'content/en/hosting-and-deployment/deployment-with-nanobox.md', data: { title: 'Deployment With NanoBox', category: 'Hosting And Deployment' }, }); + + const field = fromJS({ + name: 'title', + widget: 'string', + public_folder: '/{{public_folder}}/{{category}}/{{slug}}', + }); const collection = fromJS({ name: 'posts', folder: 'content', - fields: [{ name: 'title', widget: 'string' }], + fields: [field], }); expect( @@ -443,7 +519,7 @@ describe('entries', () => { collection, 'image.png', entry, - '/{{public_folder}}/{{category}}/{{slug}}', + field, ), ).toEqual('/static/media/hosting-and-deployment/deployment-with-nanobox/image.png'); }); diff --git a/packages/netlify-cms-core/src/reducers/__tests__/mediaLibrary.spec.js b/packages/netlify-cms-core/src/reducers/__tests__/mediaLibrary.spec.js index 0c322428..fbb2700d 100644 --- a/packages/netlify-cms-core/src/reducers/__tests__/mediaLibrary.spec.js +++ b/packages/netlify-cms-core/src/reducers/__tests__/mediaLibrary.spec.js @@ -43,16 +43,68 @@ describe('mediaLibrary', () => { ); }); - it('should select draft media files when editing a draft', () => { - const { selectEditingDraft } = require('Reducers/entries'); + it('should select draft media files from field when editing a draft', () => { + const { selectEditingDraft, selectMediaFolder } = require('Reducers/entries'); selectEditingDraft.mockReturnValue(true); + selectMediaFolder.mockReturnValue('/static/images/posts/logos'); + const imageField = fromJS({ name: 'image' }); + const collection = fromJS({ fields: [imageField] }); + const entry = fromJS({ + collection: 'posts', + mediaFiles: [ + { id: 1, path: '/static/images/posts/logos/logo.png' }, + { id: 2, path: '/static/images/posts/general/image.png' }, + { id: 3, path: '/static/images/posts/index.png' }, + ], + data: {}, + }); const state = { - entryDraft: fromJS({ entry: { mediaFiles: [{ id: 1 }] } }), + config: {}, + collections: fromJS({ posts: collection }), + entryDraft: fromJS({ + entry, + }), }; - expect(selectMediaFiles(state)).toEqual([{ key: 1, id: 1 }]); + expect(selectMediaFiles(state, imageField)).toEqual([ + { id: 1, key: 1, path: '/static/images/posts/logos/logo.png' }, + ]); + + expect(selectMediaFolder).toHaveBeenCalledWith(state.config, collection, entry, imageField); + }); + + it('should select draft media files from collection when editing a draft', () => { + const { selectEditingDraft, selectMediaFolder } = require('Reducers/entries'); + + selectEditingDraft.mockReturnValue(true); + selectMediaFolder.mockReturnValue('/static/images/posts'); + + const imageField = fromJS({ name: 'image' }); + const collection = fromJS({ fields: [imageField] }); + const entry = fromJS({ + collection: 'posts', + mediaFiles: [ + { id: 1, path: '/static/images/posts/logos/logo.png' }, + { id: 2, path: '/static/images/posts/general/image.png' }, + { id: 3, path: '/static/images/posts/index.png' }, + ], + data: {}, + }); + const state = { + config: {}, + collections: fromJS({ posts: collection }), + entryDraft: fromJS({ + entry, + }), + }; + + expect(selectMediaFiles(state, imageField)).toEqual([ + { id: 3, key: 3, path: '/static/images/posts/index.png' }, + ]); + + expect(selectMediaFolder).toHaveBeenCalledWith(state.config, collection, entry, imageField); }); it('should select global media files when not editing a draft', () => { diff --git a/packages/netlify-cms-core/src/reducers/collections.ts b/packages/netlify-cms-core/src/reducers/collections.ts index 80b747ff..ea8e0220 100644 --- a/packages/netlify-cms-core/src/reducers/collections.ts +++ b/packages/netlify-cms-core/src/reducers/collections.ts @@ -115,56 +115,61 @@ const selectors = { }, }; -const getFieldsMediaFolders = (fields: EntryField[]) => { - const mediaFolders = fields.reduce((acc, f) => { +const getFieldsWithMediaFolders = (fields: EntryField[]) => { + const fieldsWithMediaFolders = fields.reduce((acc, f) => { if (f.has('media_folder')) { - acc = [...acc, f.get('media_folder') as string]; + acc = [...acc, f]; } if (f.has('fields')) { const fields = f.get('fields')?.toArray() as EntryField[]; - acc = [...acc, ...getFieldsMediaFolders(fields)]; + acc = [...acc, ...getFieldsWithMediaFolders(fields)]; } if (f.has('field')) { const field = f.get('field') as EntryField; - acc = [...acc, ...getFieldsMediaFolders([field])]; + acc = [...acc, ...getFieldsWithMediaFolders([field])]; } return acc; - }, [] as string[]); + }, [] as EntryField[]); - return mediaFolders; + return fieldsWithMediaFolders; }; -export const selectFieldsMediaFolders = (collection: Collection) => { +const getFileFromSlug = (collection: Collection, slug: string) => { + return collection + .get('files') + ?.toArray() + .filter(f => f.get('name') === slug)[0]; +}; + +export const selectFieldsWithMediaFolders = (collection: Collection, slug: string) => { if (collection.has('folder')) { const fields = collection.get('fields').toArray(); - return getFieldsMediaFolders(fields); + return getFieldsWithMediaFolders(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)); + const fields = + getFileFromSlug(collection, slug) + ?.get('fields') + .toArray() || []; + return getFieldsWithMediaFolders(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')) - ) { + const fields = selectFieldsWithMediaFolders(collection, entry.get('slug')); + const folders = fields.map(f => selectMediaFolder(state.config, collection, entry, f)); + if (collection.has('files')) { + const file = getFileFromSlug(collection, entry.get('slug')); + if (file) { + folders.unshift(selectMediaFolder(state.config, collection, entry, undefined)); + } + } + if (collection.has('media_folder')) { + // stop evaluating media folders at collection level + collection = collection.delete('files'); folders.unshift(selectMediaFolder(state.config, collection, entry, undefined)); } diff --git a/packages/netlify-cms-core/src/reducers/entries.ts b/packages/netlify-cms-core/src/reducers/entries.ts index 973e36c0..2d415e75 100644 --- a/packages/netlify-cms-core/src/reducers/entries.ts +++ b/packages/netlify-cms-core/src/reducers/entries.ts @@ -24,10 +24,12 @@ import { EntriesRequestPayload, EntryDraft, EntryMap, + EntryField, + CollectionFiles, } from '../types/redux'; import { folderFormatter } from '../lib/formatters'; import { isAbsolutePath, basename } from 'netlify-cms-lib-util'; -import { trimStart } from 'lodash'; +import { trim } from 'lodash'; let collection: string; let loadedEntries: EntryObject[]; @@ -139,62 +141,209 @@ export const selectEntries = (state: Entries, collection: string) => { const DRAFT_MEDIA_FILES = 'DRAFT_MEDIA_FILES'; -const getCustomFolder = ( - name: 'media_folder' | 'public_folder', +const getFileField = (collectionFiles: CollectionFiles, slug: string | undefined) => { + const file = collectionFiles.find(f => f?.get('name') === slug); + return file; +}; + +const hasCustomFolder = ( + folderKey: 'media_folder' | 'public_folder', collection: Collection | null, slug: string | undefined, - fieldFolder: string | undefined, + field: EntryField | undefined, ) => { if (!collection) { - return undefined; + return false; } - if (fieldFolder !== undefined) { - return fieldFolder; + + if (field && field.has(folderKey)) { + return true; } - 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('files')) { + const file = getFileField(collection.get('files')!, slug); + if (file && file.has(folderKey)) { + return true; } } - if (collection.has(name)) { - return collection.get(name); + if (collection.has(folderKey)) { + return true; } - return undefined; + return false; +}; + +const traverseFields = ( + folderKey: 'media_folder' | 'public_folder', + config: Config, + collection: Collection, + entryMap: EntryMap | undefined, + field: EntryField, + fields: EntryField[], + currentFolder: string, +): string | null => { + const matchedField = fields.filter(f => f === field)[0]; + if (matchedField) { + return folderFormatter( + matchedField.has(folderKey) ? matchedField.get(folderKey)! : `{{${folderKey}}}`, + entryMap, + collection, + currentFolder, + folderKey, + config.get('slug'), + ); + } + + for (let f of fields) { + if (!f.has(folderKey)) { + // add identity template if doesn't exist + f = f.set(folderKey, `{{${folderKey}}}`); + } + const folder = folderFormatter( + f.get(folderKey)!, + entryMap, + collection, + currentFolder, + folderKey, + config.get('slug'), + ); + if (f.has('fields')) { + return traverseFields( + folderKey, + config, + collection, + entryMap, + field, + f.get('fields')!.toArray(), + folder, + ); + } else if (f.has('field')) { + return traverseFields( + folderKey, + config, + collection, + entryMap, + field, + [f.get('field')!], + folder, + ); + } + } + + return null; +}; + +const evaluateFolder = ( + folderKey: 'media_folder' | 'public_folder', + config: Config, + collection: Collection, + entryMap: EntryMap | undefined, + field: EntryField | undefined, +) => { + let currentFolder = config.get(folderKey); + + // add identity template if doesn't exist + if (!collection.has(folderKey)) { + collection = collection.set(folderKey, `{{${folderKey}}}`); + } + + if (collection.has('files')) { + // files collection evaluate the collection template + // then move on to the specific file configuration denoted by the slug + currentFolder = folderFormatter( + collection.get(folderKey)!, + entryMap, + collection, + currentFolder, + folderKey, + config.get('slug'), + ); + + let file = getFileField(collection.get('files')!, entryMap?.get('slug')); + if (file) { + if (!file.has(folderKey)) { + // add identity template if doesn't exist + file = file.set(folderKey, `{{${folderKey}}}`); + } + + // evaluate the file template and keep evaluating until we match our field + currentFolder = folderFormatter( + file.get(folderKey)!, + entryMap, + collection, + currentFolder, + folderKey, + config.get('slug'), + ); + + if (field) { + const fieldFolder = traverseFields( + folderKey, + config, + collection, + entryMap, + field, + file.get('fields')!.toArray(), + currentFolder, + ); + + if (fieldFolder !== null) { + currentFolder = fieldFolder; + } + } + } + } else { + // folder collection, evaluate the collection template + // and keep evaluating until we match our field + currentFolder = folderFormatter( + collection.get(folderKey)!, + entryMap, + collection, + currentFolder, + folderKey, + config.get('slug'), + ); + + if (field) { + const fieldFolder = traverseFields( + folderKey, + config, + collection, + entryMap, + field, + collection.get('fields')!.toArray(), + currentFolder, + ); + + if (fieldFolder !== null) { + currentFolder = fieldFolder; + } + } + } + + return currentFolder; }; export const selectMediaFolder = ( config: Config, collection: Collection | null, entryMap: EntryMap | undefined, - fieldMediaFolder: string | undefined, + field: EntryField | undefined, ) => { - let mediaFolder = config.get('media_folder'); + const name = 'media_folder'; + let mediaFolder = config.get(name); - const customFolder = getCustomFolder( - 'media_folder', - collection, - entryMap?.get('slug'), - fieldMediaFolder, - ); + const customFolder = hasCustomFolder(name, collection, entryMap?.get('slug'), field); - if (customFolder !== undefined) { + if (customFolder) { const entryPath = entryMap?.get('path'); if (entryPath) { const entryDir = dirname(entryPath); - const folder = folderFormatter( - customFolder, - entryMap as EntryMap, - collection!, - mediaFolder, - 'media_folder', - config.get('slug'), - ); - // return absolute paths as is without the leading '/' + const folder = evaluateFolder(name, config, collection!, entryMap, field); + // return absolute paths as is if (folder.startsWith('/')) { - mediaFolder = join(trimStart(folder, '/')); + mediaFolder = join(folder); } else { mediaFolder = join(entryDir, folder as string); } @@ -203,7 +352,7 @@ export const selectMediaFolder = ( } } - return mediaFolder; + return trim(mediaFolder, '/'); }; export const selectMediaFilePath = ( @@ -211,13 +360,13 @@ export const selectMediaFilePath = ( collection: Collection | null, entryMap: EntryMap | undefined, mediaPath: string, - fieldMediaFolder: string | undefined, + field: EntryField | undefined, ) => { if (isAbsolutePath(mediaPath)) { return mediaPath; } - const mediaFolder = selectMediaFolder(config, collection, entryMap, fieldMediaFolder); + const mediaFolder = selectMediaFolder(config, collection, entryMap, field); return join(mediaFolder, basename(mediaPath)); }; @@ -227,30 +376,19 @@ export const selectMediaFilePublicPath = ( collection: Collection | null, mediaPath: string, entryMap: EntryMap | undefined, - fieldPublicFolder: string | undefined, + field: EntryField | undefined, ) => { if (isAbsolutePath(mediaPath)) { return mediaPath; } - let publicFolder = config.get('public_folder'); + const name = 'public_folder'; + let publicFolder = config.get(name); - const customFolder = getCustomFolder( - 'public_folder', - collection, - entryMap?.get('slug'), - fieldPublicFolder, - ); + const customFolder = hasCustomFolder(name, collection, entryMap?.get('slug'), field); - if (customFolder !== undefined) { - publicFolder = folderFormatter( - customFolder, - entryMap, - collection!, - publicFolder, - 'public_folder', - config.get('slug'), - ); + if (customFolder) { + publicFolder = evaluateFolder(name, config, collection!, entryMap, field); } return join(publicFolder, basename(mediaPath)); diff --git a/packages/netlify-cms-core/src/reducers/mediaLibrary.ts b/packages/netlify-cms-core/src/reducers/mediaLibrary.ts index bf37c4b9..b72196d1 100644 --- a/packages/netlify-cms-core/src/reducers/mediaLibrary.ts +++ b/packages/netlify-cms-core/src/reducers/mediaLibrary.ts @@ -19,7 +19,7 @@ import { MEDIA_DISPLAY_URL_SUCCESS, MEDIA_DISPLAY_URL_FAILURE, } from '../actions/mediaLibrary'; -import { selectEditingDraft } from './entries'; +import { selectEditingDraft, selectMediaFolder } from './entries'; import { selectIntegration } from './'; import { State, @@ -28,7 +28,9 @@ import { MediaFile, MediaFileMap, DisplayURLState, + EntryField, } from '../types/redux'; +import { dirname } from 'path'; const defaultState: { isVisible: boolean; @@ -40,6 +42,7 @@ const defaultState: { page?: number; files?: MediaFile[]; config: Map; + field?: EntryField; } = { isVisible: false, showMediaButton: true, @@ -56,14 +59,7 @@ const mediaLibrary = (state = Map(defaultState), action: MediaLibraryAction) => map.set('showMediaButton', action.payload.enableStandalone()); }); case MEDIA_LIBRARY_OPEN: { - const { - controlID, - forImage, - privateUpload, - config, - mediaFolder, - publicFolder, - } = action.payload; + const { controlID, forImage, privateUpload, config, field } = action.payload; const libConfig = config || Map(); const privateUploadChanged = state.get('privateUpload') !== privateUpload; if (privateUploadChanged) { @@ -75,6 +71,7 @@ const mediaLibrary = (state = Map(defaultState), action: MediaLibraryAction) => privateUpload, config: libConfig, controlMedia: Map(), + field, }); } return state.withMutations(map => { @@ -84,8 +81,7 @@ 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); + map.set('field', field); }); } case MEDIA_LIBRARY_CLOSE: @@ -218,7 +214,7 @@ const mediaLibrary = (state = Map(defaultState), action: MediaLibraryAction) => } }; -export function selectMediaFiles(state: State) { +export function selectMediaFiles(state: State, field?: EntryField) { const { mediaLibrary, entryDraft } = state; const editingDraft = selectEditingDraft(state.entryDraft); const integration = selectIntegration(state, null, 'assetStore'); @@ -228,7 +224,12 @@ export function selectMediaFiles(state: State) { const entryFiles = entryDraft .getIn(['entry', 'mediaFiles'], List()) .toJS() as MediaFile[]; - files = entryFiles.map(file => ({ key: file.id, ...file })); + const entry = entryDraft.get('entry'); + const collection = state.collections.get(entry?.get('collection')); + const mediaFolder = selectMediaFolder(state.config, collection, entry, field); + files = entryFiles + .filter(f => dirname(f.path) === mediaFolder) + .map(file => ({ key: file.id, ...file })); } else { files = mediaLibrary.get('files') || []; } diff --git a/packages/netlify-cms-core/src/types/redux.ts b/packages/netlify-cms-core/src/types/redux.ts index 32694359..c862fa66 100644 --- a/packages/netlify-cms-core/src/types/redux.ts +++ b/packages/netlify-cms-core/src/types/redux.ts @@ -2,7 +2,7 @@ import { Action } from 'redux'; import { StaticallyTypedRecord } from './immutable'; import { Map, List } from 'immutable'; import AssetProxy from '../valueObjects/AssetProxy'; -import { ImplementationMediaFile } from 'netlify-cms-lib-util'; +import { MediaFile as BackendMediaFile } from '../backend'; export type SlugConfig = StaticallyTypedRecord<{ encoding: string; @@ -164,7 +164,7 @@ export interface MediaLibraryInstance { export type DisplayURL = { id: string; path: string } | string; -export type MediaFile = ImplementationMediaFile & { key?: string }; +export type MediaFile = BackendMediaFile & { key?: string }; export type MediaFileMap = StaticallyTypedRecord; @@ -311,8 +311,7 @@ export interface MediaLibraryAction extends Action { forImage: boolean; privateUpload: boolean; config: Map; - mediaFolder?: string; - publicFolder?: string; + field?: EntryField; } & { 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 7549b88b..511d3e86 100644 --- a/packages/netlify-cms-core/src/valueObjects/AssetProxy.ts +++ b/packages/netlify-cms-core/src/valueObjects/AssetProxy.ts @@ -1,21 +1,23 @@ +import { EntryField } from '../types/redux'; + interface AssetProxyArgs { path: string; url?: string; file?: File; - folder?: string; + field?: EntryField; } export default class AssetProxy { url: string; fileObj?: File; path: string; - folder?: string; + field?: EntryField; - constructor({ url, file, path, folder }: AssetProxyArgs) { + constructor({ url, file, path, field }: AssetProxyArgs) { this.url = url ? url : window.URL.createObjectURL(file); this.fileObj = file; this.path = path; - this.folder = folder; + this.field = field; } toString(): string { @@ -38,6 +40,6 @@ export default class AssetProxy { } } -export function createAssetProxy({ url, file, path, folder }: AssetProxyArgs): AssetProxy { - return new AssetProxy({ url, file, path, folder }); +export function createAssetProxy({ url, file, path, field }: AssetProxyArgs): AssetProxy { + return new AssetProxy({ url, file, path, field }); } diff --git a/packages/netlify-cms-core/src/valueObjects/Entry.ts b/packages/netlify-cms-core/src/valueObjects/Entry.ts index d067fd55..825fcfc2 100644 --- a/packages/netlify-cms-core/src/valueObjects/Entry.ts +++ b/packages/netlify-cms-core/src/valueObjects/Entry.ts @@ -1,5 +1,5 @@ import { isBoolean } from 'lodash'; -import { ImplementationMediaFile } from 'netlify-cms-lib-util'; +import { MediaFile } from '../backend'; interface Options { partial?: boolean; @@ -9,7 +9,7 @@ interface Options { label?: string | null; metaData?: unknown | null; isModification?: boolean | null; - mediaFiles?: ImplementationMediaFile[] | null; + mediaFiles?: MediaFile[] | null; } export interface EntryValue { @@ -23,7 +23,7 @@ export interface EntryValue { label: string | null; metaData: unknown | null; isModification: boolean | null; - mediaFiles: ImplementationMediaFile[]; + mediaFiles: MediaFile[]; } export function createEntry(collection: string, slug = '', path = '', options: Options = {}) { diff --git a/packages/netlify-cms-editor-component-image/src/index.js b/packages/netlify-cms-editor-component-image/src/index.js index c0974734..cffc12b0 100644 --- a/packages/netlify-cms-editor-component-image/src/index.js +++ b/packages/netlify-cms-editor-component-image/src/index.js @@ -14,8 +14,7 @@ const image = { // eslint-disable-next-line react/display-name toPreview: ({ alt, image, title }, getAsset, fields) => { const imageField = fields?.find(f => f.get('widget') === 'image'); - const folder = imageField?.get('media_folder'); - const src = getAsset(image, folder); + const src = getAsset(image, imageField); 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 b1e8984c..6c65d8b2 100644 --- a/packages/netlify-cms-lib-util/src/implementation.ts +++ b/packages/netlify-cms-lib-util/src/implementation.ts @@ -18,7 +18,6 @@ export interface ImplementationMediaFile { draft?: boolean; url?: string; file?: File; - folder?: string; } export interface UnpublishedEntryMediaFile { diff --git a/packages/netlify-cms-widget-file/src/FilePreview.js b/packages/netlify-cms-widget-file/src/FilePreview.js index fba20f37..5bfe285f 100644 --- a/packages/netlify-cms-widget-file/src/FilePreview.js +++ b/packages/netlify-cms-widget-file/src/FilePreview.js @@ -12,11 +12,11 @@ const FileLink = styled(({ href, path }) => ( display: block; `; -function FileLinkList({ values, getAsset, folder }) { +function FileLinkList({ values, getAsset, field }) { return (
{values.map(value => ( - + ))}
); @@ -24,12 +24,10 @@ function FileLinkList({ values, getAsset, folder }) { function FileContent(props) { const { value, getAsset, field } = props; - const folder = field.get('media_folder'); - if (Array.isArray(value) || List.isList(value)) { - return ; + return ; } - return ; + return ; } const FilePreview = props => ( diff --git a/packages/netlify-cms-widget-file/src/withFileControl.js b/packages/netlify-cms-widget-file/src/withFileControl.js index 074e2acc..09ea8576 100644 --- a/packages/netlify-cms-widget-file/src/withFileControl.js +++ b/packages/netlify-cms-widget-file/src/withFileControl.js @@ -165,8 +165,7 @@ export default function withFileControl({ forImage } = {}) { value, allowMultiple: !!mediaLibraryFieldOptions.get('allow_multiple', true), config: mediaLibraryFieldOptions.get('config'), - mediaFolder: field.get('media_folder'), - publicFolder: field.get('public_folder'), + field, }); }; @@ -211,21 +210,20 @@ export default function withFileControl({ forImage } = {}) { renderImages = () => { const { getAsset, value, field } = this.props; - const folder = field.get('media_folder'); if (isMultiple(value)) { return ( {value.map(val => ( - + ))} ); } - const src = getAsset(value, folder); + const src = getAsset(value, field); return ( diff --git a/packages/netlify-cms-widget-image/src/ImagePreview.js b/packages/netlify-cms-widget-image/src/ImagePreview.js index da61b52a..c3230456 100644 --- a/packages/netlify-cms-widget-image/src/ImagePreview.js +++ b/packages/netlify-cms-widget-image/src/ImagePreview.js @@ -11,7 +11,7 @@ const StyledImage = styled(({ src }) => { - return ; + return ; }; const ImagePreviewContent = props => {