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:
@ -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') || [];
|
||||
}
|
||||
|
Reference in New Issue
Block a user