feat: field based media/public folders (#3208)

This commit is contained in:
Erez Rokah
2020-02-10 18:05:47 +02:00
committed by GitHub
parent ee7445d49d
commit 97bc0c8dc4
30 changed files with 738 additions and 127 deletions

View File

@ -1,6 +1,12 @@
import { OrderedMap, fromJS } from 'immutable';
import { configLoaded } from 'Actions/config';
import collections, { selectAllowDeletion, selectEntryPath, selectEntrySlug } from '../collections';
import collections, {
selectAllowDeletion,
selectEntryPath,
selectEntrySlug,
selectFieldsMediaFolders,
selectMediaFolders,
} from '../collections';
import { FILES, FOLDER } from 'Constants/collectionTypes';
describe('collections', () => {
@ -76,4 +82,159 @@ describe('collections', () => {
).toBe('dir1/dir2/slug');
});
});
describe('selectFieldsMediaFolders', () => {
it('should return empty array for invalid collection', () => {
expect(selectFieldsMediaFolders(fromJS({}))).toEqual([]);
});
it('should return configs for folder collection', () => {
expect(
selectFieldsMediaFolders(
fromJS({
folder: 'posts',
fields: [
{
name: 'image',
media_folder: 'image_media_folder',
},
{
name: 'body',
media_folder: 'body_media_folder',
},
{
name: 'list_1',
field: {
name: 'list_1_item',
media_folder: 'list_1_item_media_folder',
},
},
{
name: 'list_2',
fields: [
{
name: 'list_2_item',
media_folder: 'list_2_item_media_folder',
},
],
},
],
}),
),
).toEqual([
'image_media_folder',
'body_media_folder',
'list_1_item_media_folder',
'list_2_item_media_folder',
]);
});
it('should return configs for files collection', () => {
expect(
selectFieldsMediaFolders(
fromJS({
files: [
{
fields: [
{
name: 'image',
media_folder: 'image_media_folder',
},
],
},
{
fields: [
{
name: 'body',
media_folder: 'body_media_folder',
},
],
},
{
fields: [
{
name: 'list_1',
field: {
name: 'list_1_item',
media_folder: 'list_1_item_media_folder',
},
},
],
},
{
fields: [
{
name: 'list_2',
fields: [
{
name: 'list_2_item',
media_folder: 'list_2_item_media_folder',
},
],
},
],
},
],
}),
),
).toEqual([
'image_media_folder',
'body_media_folder',
'list_1_item_media_folder',
'list_2_item_media_folder',
]);
});
});
describe('selectMediaFolders', () => {
const slug = {
encoding: 'unicode',
clean_accents: false,
sanitize_replacement: '-',
};
const config = fromJS({ slug });
it('should return fields and collection folder', () => {
expect(
selectMediaFolders(
{ config },
fromJS({
folder: 'posts',
media_folder: '/collection_media_folder',
fields: [
{
name: 'image',
media_folder: '/image_media_folder',
},
],
}),
fromJS({ slug: 'name', path: 'src/post/post1.md' }),
),
).toEqual(['collection_media_folder', 'image_media_folder']);
});
it('should return fields and collection folder', () => {
expect(
selectMediaFolders(
{ config },
fromJS({
files: [
{
name: 'name',
file: 'src/post/post1.md',
media_folder: '/file_media_folder',
fields: [
{
name: 'image',
media_folder: '/image_media_folder',
},
],
},
],
}),
fromJS({ slug: 'name', path: 'src/post/post1.md' }),
),
).toEqual(['file_media_folder', 'image_media_folder']);
});
});
});

View File

@ -74,16 +74,22 @@ describe('entries', () => {
describe('selectMediaFolder', () => {
it("should return global media folder when collection doesn't specify media_folder", () => {
expect(
selectMediaFolder(Map({ media_folder: 'static/media' }), Map({ name: 'posts' })),
selectMediaFolder(
Map({ media_folder: 'static/media' }),
Map({ name: 'posts' }),
undefined,
undefined,
),
).toEqual('static/media');
});
it('should return draft media folder when collection specifies media_folder and entry path is null', () => {
it('should return draft media folder when collection specifies media_folder and entry is undefined', () => {
expect(
selectMediaFolder(
Map({ media_folder: 'static/media' }),
Map({ name: 'posts', folder: 'posts', media_folder: '' }),
null,
undefined,
undefined,
),
).toEqual('posts/DRAFT_MEDIA_FILES');
});
@ -94,6 +100,7 @@ describe('entries', () => {
Map({ media_folder: 'static/media' }),
Map({ name: 'posts', folder: 'posts', media_folder: '' }),
Map({ path: 'posts/title/index.md' }),
undefined,
),
).toEqual('posts/title');
});
@ -104,11 +111,12 @@ describe('entries', () => {
Map({ media_folder: 'static/media' }),
Map({ name: 'posts', folder: 'posts', media_folder: '../' }),
Map({ path: 'posts/title/index.md' }),
undefined,
),
).toEqual('posts/');
});
it('should return collection absolute media folder as is', () => {
it('should return collection absolute media folder without leading slash', () => {
expect(
selectMediaFolder(
Map({ media_folder: '/static/Images' }),
@ -118,8 +126,9 @@ describe('entries', () => {
media_folder: '/static/images/docs/getting-started',
}),
Map({ path: 'src/docs/getting-started/with-github.md' }),
undefined,
),
).toEqual('/static/images/docs/getting-started');
).toEqual('static/images/docs/getting-started');
});
it('should compile relative media folder template', () => {
@ -145,6 +154,7 @@ describe('entries', () => {
fromJS({ media_folder: 'static/media', slug: slugConfig }),
collection,
entry,
undefined,
),
).toEqual('static/media/hosting-and-deployment/deployment-with-nanobox');
});
@ -172,8 +182,85 @@ describe('entries', () => {
fromJS({ media_folder: '/static/images', slug: slugConfig }),
collection,
entry,
undefined,
),
).toEqual('/static/images/docs/extending');
).toEqual('static/images/docs/extending');
});
it('should compile field media folder template', () => {
const slugConfig = {
encoding: 'unicode',
clean_accents: false,
sanitize_replacement: '-',
};
const entry = fromJS({
path: 'content/en/hosting-and-deployment/deployment-with-nanobox.md',
data: { title: 'Deployment With NanoBox', category: 'Hosting And Deployment' },
});
const collection = fromJS({
name: 'posts',
folder: 'content',
fields: [{ name: 'title', widget: 'string' }],
});
expect(
selectMediaFolder(
fromJS({ media_folder: 'static/media', slug: slugConfig }),
collection,
entry,
'../../../{{media_folder}}/{{category}}/{{slug}}',
),
).toEqual('static/media/hosting-and-deployment/deployment-with-nanobox');
});
it('should handle double slashes', () => {
const slugConfig = {
encoding: 'unicode',
clean_accents: false,
sanitize_replacement: '-',
};
const entry = fromJS({
path: 'content/en/hosting-and-deployment/deployment-with-nanobox.md',
data: { title: 'Deployment With NanoBox', category: 'Hosting And Deployment' },
});
const collection = fromJS({
name: 'posts',
folder: 'content',
media_folder: '{{media_folder}}/blog',
fields: [{ name: 'title', widget: 'string' }],
});
expect(
selectMediaFolder(
fromJS({ media_folder: '/static/img/', slug: slugConfig }),
collection,
entry,
undefined,
),
).toEqual('static/img/blog');
expect(
selectMediaFolder(
fromJS({ media_folder: 'static/img/', slug: slugConfig }),
collection,
entry,
undefined,
),
).toEqual('content/en/hosting-and-deployment/static/img/blog');
});
it('should handle file media_folder', () => {
expect(
selectMediaFolder(
fromJS({ media_folder: 'static/media' }),
fromJS({ name: 'posts', files: [{ name: 'index', media_folder: '/static/images/' }] }),
fromJS({ path: 'posts/title/index.md', slug: 'index' }),
undefined,
),
).toBe('static/images/');
});
});
@ -189,8 +276,9 @@ describe('entries', () => {
selectMediaFilePath(
Map({ media_folder: 'static/media' }),
Map({ name: 'posts', folder: 'posts' }),
null,
undefined,
'image.png',
undefined,
),
).toBe('static/media/image.png');
});
@ -200,8 +288,9 @@ describe('entries', () => {
selectMediaFilePath(
Map({ media_folder: 'static/media' }),
Map({ name: 'posts', folder: 'posts', media_folder: '' }),
null,
undefined,
'image.png',
undefined,
),
).toBe('posts/DRAFT_MEDIA_FILES/image.png');
});
@ -213,6 +302,19 @@ describe('entries', () => {
Map({ name: 'posts', folder: 'posts', media_folder: '../../static/media/' }),
Map({ path: 'posts/title/index.md' }),
'image.png',
undefined,
),
).toBe('static/media/image.png');
});
it('should handle field media_folder', () => {
expect(
selectMediaFilePath(
Map({ media_folder: 'static/media' }),
Map({ name: 'posts', folder: 'posts' }),
Map({ path: 'posts/title/index.md' }),
'image.png',
'../../static/media/',
),
).toBe('static/media/image.png');
});
@ -227,7 +329,13 @@ describe('entries', () => {
it('should resolve path from public folder for collection with no media folder', () => {
expect(
selectMediaFilePublicPath(Map({ public_folder: '/media' }), null, '/media/image.png'),
selectMediaFilePublicPath(
Map({ public_folder: '/media' }),
null,
'/media/image.png',
undefined,
undefined,
),
).toBe('/media/image.png');
});
@ -237,6 +345,8 @@ describe('entries', () => {
Map({ public_folder: '/media' }),
Map({ name: 'posts', folder: 'posts', public_folder: '' }),
'image.png',
undefined,
undefined,
),
).toBe('image.png');
});
@ -247,11 +357,13 @@ describe('entries', () => {
Map({ public_folder: '/media' }),
Map({ name: 'posts', folder: 'posts', public_folder: '../../static/media/' }),
'image.png',
undefined,
undefined,
),
).toBe('../../static/media/image.png');
});
it('should compile public folder template', () => {
it('should compile collection public folder template', () => {
const slugConfig = {
encoding: 'unicode',
clean_accents: false,
@ -275,8 +387,93 @@ describe('entries', () => {
collection,
'image.png',
entry,
undefined,
),
).toEqual('/static/media/hosting-and-deployment/deployment-with-nanobox/image.png');
});
it('should compile field public folder template', () => {
const slugConfig = {
encoding: 'unicode',
clean_accents: false,
sanitize_replacement: '-',
};
const entry = fromJS({
path: 'content/en/hosting-and-deployment/deployment-with-nanobox.md',
data: { title: 'Deployment With NanoBox', category: 'Hosting And Deployment' },
});
const collection = fromJS({
name: 'posts',
folder: 'content',
fields: [{ name: 'title', widget: 'string' }],
});
expect(
selectMediaFilePublicPath(
fromJS({ public_folder: 'static/media', slug: slugConfig }),
collection,
'image.png',
entry,
'/{{public_folder}}/{{category}}/{{slug}}',
),
).toEqual('/static/media/hosting-and-deployment/deployment-with-nanobox/image.png');
});
it('should handle double slashes', () => {
const slugConfig = {
encoding: 'unicode',
clean_accents: false,
sanitize_replacement: '-',
};
const entry = fromJS({
path: 'content/en/hosting-and-deployment/deployment-with-nanobox.md',
data: { title: 'Deployment With NanoBox', category: 'Hosting And Deployment' },
});
const collection = fromJS({
name: 'posts',
folder: 'content',
fields: [{ name: 'title', widget: 'string' }],
});
expect(
selectMediaFilePublicPath(
fromJS({ public_folder: 'static/media/', slug: slugConfig }),
collection,
'image.png',
entry,
'/{{public_folder}}/{{category}}/{{slug}}',
),
).toEqual('/static/media/hosting-and-deployment/deployment-with-nanobox/image.png');
});
it('should handle file public_folder', () => {
const entry = fromJS({
path: 'src/posts/index.md',
slug: 'index',
});
const collection = fromJS({
name: 'posts',
files: [
{
name: 'index',
public_folder: '/images',
fields: [{ name: 'title', widget: 'string' }],
},
],
});
expect(
selectMediaFilePublicPath(
fromJS({ public_folder: 'static/media/' }),
collection,
'image.png',
entry,
undefined,
),
).toBe('/images/image.png');
});
});
});

View File

@ -5,7 +5,15 @@ import { CONFIG_SUCCESS } from '../actions/config';
import { FILES, FOLDER } from '../constants/collectionTypes';
import { INFERABLE_FIELDS, IDENTIFIER_FIELDS } from '../constants/fieldInference';
import { formatExtensions } from '../formats/formats';
import { CollectionsAction, Collection, CollectionFiles, EntryField } from '../types/redux';
import {
CollectionsAction,
Collection,
CollectionFiles,
EntryField,
State,
EntryMap,
} from '../types/redux';
import { selectMediaFolder } from './entries';
const collections = (state = null, action: CollectionsAction) => {
switch (action.type) {
@ -106,6 +114,62 @@ const selectors = {
},
};
const getFieldsMediaFolders = (fields: EntryField[]) => {
const mediaFolders = fields.reduce((acc, f) => {
if (f.has('media_folder')) {
acc = [...acc, f.get('media_folder') as string];
}
if (f.has('fields')) {
const fields = f.get('fields')?.toArray() as EntryField[];
acc = [...acc, ...getFieldsMediaFolders(fields)];
}
if (f.has('field')) {
const field = f.get('field') as EntryField;
acc = [...acc, ...getFieldsMediaFolders([field])];
}
return acc;
}, [] as string[]);
return mediaFolders;
};
export const selectFieldsMediaFolders = (collection: Collection) => {
if (collection.has('folder')) {
const fields = collection.get('fields').toArray();
return getFieldsMediaFolders(fields);
} else if (collection.has('files')) {
const fields = collection
.get('files')
?.toArray()
.map(f => f.get('fields').toArray()) as EntryField[][];
const flattened = [] as EntryField[];
return getFieldsMediaFolders(flattened.concat(...fields));
}
return [];
};
export const selectMediaFolders = (state: State, collection: Collection, entry: EntryMap) => {
const fieldsFolders = selectFieldsMediaFolders(collection);
const folders = fieldsFolders.map(folder =>
selectMediaFolder(state.config, collection, entry, folder),
);
if (
collection.has('media_folder') ||
collection
.get('files')
?.find(file => file?.get('name') === entry?.get('slug') && file?.has('media_folder'))
) {
folders.unshift(selectMediaFolder(state.config, collection, entry, undefined));
}
return folders;
};
export const selectFields = (collection: Collection, slug: string) =>
selectors[collection.get('type')].fields(collection, slug);
export const selectFolderEntryExtension = (collection: Collection) =>

View File

@ -27,6 +27,7 @@ import {
} from '../types/redux';
import { folderFormatter } from '../lib/formatters';
import { isAbsolutePath, basename } from 'netlify-cms-lib-util';
import { trimStart } from 'lodash';
let collection: string;
let loadedEntries: EntryObject[];
@ -138,32 +139,67 @@ export const selectEntries = (state: Entries, collection: string) => {
const DRAFT_MEDIA_FILES = 'DRAFT_MEDIA_FILES';
const getCustomFolder = (
name: 'media_folder' | 'public_folder',
collection: Collection | null,
slug: string | undefined,
fieldFolder: string | undefined,
) => {
if (!collection) {
return undefined;
}
if (fieldFolder !== undefined) {
return fieldFolder;
}
if (collection.has('files') && slug) {
const file = collection.get('files')?.find(f => f?.get('name') === slug);
if (file && file.has(name)) {
return file.get(name);
}
}
if (collection.has(name)) {
return collection.get(name);
}
return undefined;
};
export const selectMediaFolder = (
config: Config,
collection: Collection | null,
entryMap: EntryMap | undefined,
fieldMediaFolder: string | undefined,
) => {
let mediaFolder = config.get('media_folder');
if (collection && collection.has('media_folder')) {
const customFolder = getCustomFolder(
'media_folder',
collection,
entryMap?.get('slug'),
fieldMediaFolder,
);
if (customFolder !== undefined) {
const entryPath = entryMap?.get('path');
if (entryPath) {
const entryDir = dirname(entryPath);
const folder = folderFormatter(
collection.get('media_folder') as string,
customFolder,
entryMap as EntryMap,
collection,
collection!,
mediaFolder,
'media_folder',
config.get('slug'),
);
// return absolute paths as is
// return absolute paths as is without the leading '/'
if (folder.startsWith('/')) {
return folder;
mediaFolder = join(trimStart(folder, '/'));
} else {
mediaFolder = join(entryDir, folder as string);
}
mediaFolder = join(entryDir, folder as string);
} else {
mediaFolder = join(collection.get('folder') as string, DRAFT_MEDIA_FILES);
mediaFolder = join(collection!.get('folder') as string, DRAFT_MEDIA_FILES);
}
}
@ -175,12 +211,13 @@ export const selectMediaFilePath = (
collection: Collection | null,
entryMap: EntryMap | undefined,
mediaPath: string,
fieldMediaFolder: string | undefined,
) => {
if (isAbsolutePath(mediaPath)) {
return mediaPath;
}
const mediaFolder = selectMediaFolder(config, collection, entryMap);
const mediaFolder = selectMediaFolder(config, collection, entryMap, fieldMediaFolder);
return join(mediaFolder, basename(mediaPath));
};
@ -190,6 +227,7 @@ export const selectMediaFilePublicPath = (
collection: Collection | null,
mediaPath: string,
entryMap: EntryMap | undefined,
fieldPublicFolder: string | undefined,
) => {
if (isAbsolutePath(mediaPath)) {
return mediaPath;
@ -197,11 +235,18 @@ export const selectMediaFilePublicPath = (
let publicFolder = config.get('public_folder');
if (collection && collection.has('public_folder')) {
const customFolder = getCustomFolder(
'public_folder',
collection,
entryMap?.get('slug'),
fieldPublicFolder,
);
if (customFolder !== undefined) {
publicFolder = folderFormatter(
collection.get('public_folder') as string,
customFolder,
entryMap,
collection,
collection!,
publicFolder,
'public_folder',
config.get('slug'),

View File

@ -56,7 +56,14 @@ const mediaLibrary = (state = Map(defaultState), action: MediaLibraryAction) =>
map.set('showMediaButton', action.payload.enableStandalone());
});
case MEDIA_LIBRARY_OPEN: {
const { controlID, forImage, privateUpload, config } = action.payload;
const {
controlID,
forImage,
privateUpload,
config,
mediaFolder,
publicFolder,
} = action.payload;
const libConfig = config || Map();
const privateUploadChanged = state.get('privateUpload') !== privateUpload;
if (privateUploadChanged) {
@ -77,6 +84,8 @@ const mediaLibrary = (state = Map(defaultState), action: MediaLibraryAction) =>
map.set('canInsert', !!controlID);
map.set('privateUpload', privateUpload);
map.set('config', libConfig);
map.set('mediaFolder', mediaFolder);
map.set('publicFolder', publicFolder);
});
}
case MEDIA_LIBRARY_CLOSE: