Fix: show specific field media files in library, cascade folder templates (#3252)
* feat: cascade & compose media folders - initial commit * refactor: code cleanup * fix: pass field instead of folder to getAsset * fix: only show field media files in library * test: fix medial library selector test * fix: fallback to original path when asset not found * fix: only show field media files in media library * fix: properly handle empty strings in field folders
This commit is contained in:
parent
8d67de0e68
commit
02ef2010e7
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
|
@ -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<State, {}, AnyAction>, 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;
|
||||
|
@ -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<State, {}, AnyAction>, 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<State, {}, AnyAction>, 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 = {}) {
|
||||
|
@ -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<ImplementationMediaFile>(
|
||||
const mediaFiles = await Promise.all<MediaFile>(
|
||||
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;
|
||||
});
|
||||
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
@ -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 };
|
||||
};
|
||||
|
@ -218,6 +218,7 @@ export const folderFormatter = (
|
||||
if (!entry || !entry.get('data')) {
|
||||
return folderTemplate;
|
||||
}
|
||||
|
||||
let fields = (entry.get('data') as Map<string, string>).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;
|
||||
};
|
||||
|
@ -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',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -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');
|
||||
});
|
||||
|
@ -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', () => {
|
||||
|
@ -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));
|
||||
}
|
||||
|
||||
|
@ -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));
|
||||
|
@ -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<string, string>;
|
||||
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<MediaFileMap>())
|
||||
.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') || [];
|
||||
}
|
||||
|
@ -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<MediaFile>;
|
||||
|
||||
@ -311,8 +311,7 @@ export interface MediaLibraryAction extends Action<string> {
|
||||
forImage: boolean;
|
||||
privateUpload: boolean;
|
||||
config: Map<string, string>;
|
||||
mediaFolder?: string;
|
||||
publicFolder?: string;
|
||||
field?: EntryField;
|
||||
} & { mediaPath: string | string[] } & { page: number } & {
|
||||
files: MediaFile[];
|
||||
page: number;
|
||||
|
@ -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 });
|
||||
}
|
||||
|
@ -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 = {}) {
|
||||
|
@ -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 <img src={src || ''} alt={alt || ''} title={title || ''} />;
|
||||
},
|
||||
pattern: /^!\[(.*)\]\((.*?)(\s"(.*)")?\)$/,
|
||||
|
@ -18,7 +18,6 @@ export interface ImplementationMediaFile {
|
||||
draft?: boolean;
|
||||
url?: string;
|
||||
file?: File;
|
||||
folder?: string;
|
||||
}
|
||||
|
||||
export interface UnpublishedEntryMediaFile {
|
||||
|
@ -12,11 +12,11 @@ const FileLink = styled(({ href, path }) => (
|
||||
display: block;
|
||||
`;
|
||||
|
||||
function FileLinkList({ values, getAsset, folder }) {
|
||||
function FileLinkList({ values, getAsset, field }) {
|
||||
return (
|
||||
<div>
|
||||
{values.map(value => (
|
||||
<FileLink key={value} path={value} href={getAsset(value, folder)} />
|
||||
<FileLink key={value} path={value} href={getAsset(value, field)} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
@ -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 <FileLinkList values={value} getAsset={getAsset} folder={folder} />;
|
||||
return <FileLinkList values={value} getAsset={getAsset} field={field} />;
|
||||
}
|
||||
return <FileLink key={value} path={value} href={getAsset(value, folder)} />;
|
||||
return <FileLink key={value} path={value} href={getAsset(value, field)} />;
|
||||
}
|
||||
|
||||
const FilePreview = props => (
|
||||
|
@ -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 (
|
||||
<MultiImageWrapper>
|
||||
{value.map(val => (
|
||||
<ImageWrapper key={val}>
|
||||
<Image src={getAsset(val, folder) || ''} />
|
||||
<Image src={getAsset(val, field) || ''} />
|
||||
</ImageWrapper>
|
||||
))}
|
||||
</MultiImageWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
const src = getAsset(value, folder);
|
||||
const src = getAsset(value, field);
|
||||
return (
|
||||
<ImageWrapper>
|
||||
<Image src={src || ''} />
|
||||
|
@ -11,7 +11,7 @@ const StyledImage = styled(({ src }) => <img src={src || ''} role="presentation"
|
||||
`;
|
||||
|
||||
const StyledImageAsset = ({ getAsset, value, field }) => {
|
||||
return <StyledImage src={getAsset(value, field.get('media_folder'))} />;
|
||||
return <StyledImage src={getAsset(value, field)} />;
|
||||
};
|
||||
|
||||
const ImagePreviewContent = props => {
|
||||
|
Loading…
x
Reference in New Issue
Block a user