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

@ -60,6 +60,7 @@ describe('media', () => {
payload.collection,
payload.entry,
path,
undefined,
);
});
});

View File

@ -293,12 +293,19 @@ export function retrieveLocalBackup(collection: Collection, slug: string) {
const assetProxies: AssetProxy[] = await Promise.all(
mediaFiles.map(file => {
if (file.file || file.url) {
return createAssetProxy({ path: file.path, file: file.file, url: file.url });
return createAssetProxy({
path: file.path,
file: file.file,
url: file.url,
folder: file.folder,
});
} else {
return getAsset({ collection, entry: fromJS(entry), path: file.path })(
dispatch,
getState,
);
return getAsset({
collection,
entry: fromJS(entry),
path: file.path,
folder: file.folder,
})(dispatch, getState);
}
}),
);
@ -554,7 +561,9 @@ export async function getMediaAssets({
const assets = await Promise.all(
filesArray
.filter(file => file.draft)
.map(file => getAsset({ collection, entry, path: file.path })(dispatch, getState)),
.map(file =>
getAsset({ collection, entry, path: file.path, folder: file.folder })(dispatch, getState),
),
);
return assets;

View File

@ -27,14 +27,14 @@ interface GetAssetArgs {
collection: Collection;
entry: EntryMap;
path: string;
folder?: string;
}
export function getAsset({ collection, entry, path }: GetAssetArgs) {
export function getAsset({ collection, entry, path, folder }: GetAssetArgs) {
return async (dispatch: ThunkDispatch<State, {}, AnyAction>, getState: () => State) => {
if (!path) return createAssetProxy({ path: '', file: new File([], 'empty') });
const state = getState();
const resolvedPath = selectMediaFilePath(state.config, collection, entry, path);
const resolvedPath = selectMediaFilePath(state.config, collection, entry, path, folder);
let asset = state.medias.get(resolvedPath);
if (asset) {

View File

@ -77,6 +77,8 @@ export function openMediaLibrary(
config?: Map<string, unknown>;
allowMultiple?: boolean;
forImage?: boolean;
mediaFolder?: string;
publicFolder?: string;
} = {},
) {
return (dispatch: ThunkDispatch<State, {}, AnyAction>, getState: () => State) => {
@ -101,7 +103,7 @@ export function closeMediaLibrary() {
};
}
export function insertMedia(mediaPath: string | string[]) {
export function insertMedia(mediaPath: string | string[], publicFolder: string | undefined) {
return (dispatch: ThunkDispatch<State, {}, AnyAction>, getState: () => State) => {
const state = getState();
const config = state.config;
@ -109,9 +111,17 @@ export function insertMedia(mediaPath: string | string[]) {
const collectionName = state.entryDraft.getIn(['entry', 'collection']);
const collection = state.collections.get(collectionName);
if (Array.isArray(mediaPath)) {
mediaPath = mediaPath.map(path => selectMediaFilePublicPath(config, collection, path, entry));
mediaPath = mediaPath.map(path =>
selectMediaFilePublicPath(config, collection, path, entry, publicFolder),
);
} else {
mediaPath = selectMediaFilePublicPath(config, collection, mediaPath as string, entry);
mediaPath = selectMediaFilePublicPath(
config,
collection,
mediaPath as string,
entry,
publicFolder,
);
}
dispatch({ type: MEDIA_INSERT, payload: { mediaPath } });
};
@ -191,12 +201,13 @@ function createMediaFileFromAsset({
size: file.size,
url: assetProxy.url,
path: assetProxy.path,
folder: assetProxy.folder,
};
return mediaFile;
}
export function persistMedia(file: File, opts: MediaOptions = {}) {
const { privateUpload } = opts;
const { privateUpload, mediaFolder } = opts;
return async (dispatch: ThunkDispatch<State, {}, AnyAction>, getState: () => State) => {
const state = getState();
const backend = currentBackend(state.config);
@ -250,10 +261,11 @@ export function persistMedia(file: File, opts: MediaOptions = {}) {
} else {
const entry = state.entryDraft.get('entry');
const collection = state.collections.get(entry?.get('collection'));
const path = selectMediaFilePath(state.config, collection, entry, file.name);
const path = selectMediaFilePath(state.config, collection, entry, file.name, mediaFolder);
assetProxy = createAssetProxy({
file,
path,
folder: mediaFolder,
});
}
@ -397,6 +409,7 @@ export function mediaLoading(page: number) {
interface MediaOptions {
privateUpload?: boolean;
mediaFolder?: string;
}
export function mediaLoaded(files: ImplementationMediaFile[], opts: MediaOptions = {}) {

View File

@ -3,7 +3,7 @@ import { List, Map, fromJS } from 'immutable';
import * as fuzzy from 'fuzzy';
import { resolveFormat } from './formats/formats';
import { selectUseWorkflow } from './reducers/config';
import { selectMediaFilePath, selectMediaFolder, selectEntry } from './reducers/entries';
import { selectMediaFilePath, selectEntry } from './reducers/entries';
import { selectIntegration } from './reducers/integrations';
import {
selectEntrySlug,
@ -13,6 +13,7 @@ import {
selectAllowDeletion,
selectFolderEntryExtension,
selectInferedField,
selectMediaFolders,
} from './reducers/collections';
import { createEntry, EntryValue } from './valueObjects/Entry';
import { sanitizeChar } from './lib/urlHelper';
@ -31,6 +32,7 @@ import {
User,
getPathDepth,
Config as ImplementationConfig,
blobToFileObj,
} from 'netlify-cms-lib-util';
import { status } from './constants/publishModes';
import { extractTemplateVars, dateParsers } from './lib/stringTemplate';
@ -450,8 +452,7 @@ export class Backend {
// make sure to serialize the file
if (file.url?.startsWith('blob:')) {
const blob = await fetch(file.url as string).then(res => res.blob());
const options = file.name.match(/.svg$/) ? { type: 'image/svg+xml' } : {};
return { ...file, file: new File([blob], file.name, options) };
return { ...file, file: blobToFileObj(file.name, blob) };
}
return file;
}),
@ -492,10 +493,12 @@ export class Backend {
});
const entryWithFormat = this.entryWithFormat(collection)(entry);
if (collection.has('media_folder') && !integration) {
entry.mediaFiles = await this.implementation.getMedia(
selectMediaFolder(state.config, collection, fromJS(entryWithFormat)),
);
const mediaFolders = selectMediaFolders(state, collection, fromJS(entryWithFormat));
if (mediaFolders.length > 0 && !integration) {
entry.mediaFiles = [];
for (const folder of mediaFolders) {
entry.mediaFiles = [...entry.mediaFiles, ...(await this.implementation.getMedia(folder))];
}
} else {
entry.mediaFiles = state.mediaLibrary.get('files') || [];
}
@ -697,6 +700,7 @@ export class Backend {
collection,
entryDraft.get('entry').set('path', path),
oldPath,
asset.folder,
);
asset.path = newPath;
});

View File

@ -83,14 +83,15 @@ const CardImage = styled.div`
height: 150px;
`;
const CardImageAsset = ({ getAsset, image }) => {
return <Asset path={image} getAsset={getAsset} component={CardImage} />;
const CardImageAsset = ({ getAsset, image, folder }) => {
return <Asset folder={folder} path={image} getAsset={getAsset} component={CardImage} />;
};
const EntryCard = ({
path,
summary,
image,
imageFolder,
collectionLabel,
viewStyle = VIEW_STYLE_LIST,
boundGetAsset,
@ -114,7 +115,9 @@ const EntryCard = ({
{collectionLabel ? <CollectionLabel>{collectionLabel}</CollectionLabel> : null}
<CardHeading>{summary}</CardHeading>
</CardBody>
{image ? <CardImageAsset getAsset={boundGetAsset} image={image} /> : null}
{image ? (
<CardImageAsset getAsset={boundGetAsset} image={image} folder={imageFolder} />
) : null}
</GridCardLink>
</GridCard>
);
@ -140,12 +143,16 @@ const mapStateToProps = (state, ownProps) => {
summary,
path: `/collections/${collection.get('name')}/entries/${entry.get('slug')}`,
image,
imageFolder: collection
.get('fields')
?.find(f => f.get('name') === inferedFields.imageField && f.get('widget') === 'image')
?.get('media_folder'),
};
};
const mapDispatchToProps = {
boundGetAsset: (collection, entry) => (dispatch, getState) => path => {
return getAsset({ collection, entry, path })(dispatch, getState);
boundGetAsset: (collection, entry) => (dispatch, getState) => (path, folder) => {
return getAsset({ collection, entry, path, folder })(dispatch, getState);
},
};

View File

@ -515,8 +515,8 @@ const mapDispatchToProps = {
unpublishPublishedEntry,
deleteUnpublishedEntry,
logoutUser,
boundGetAsset: (collection, entry) => (dispatch, getState) => path => {
return getAsset({ collection, entry, path })(dispatch, getState);
boundGetAsset: (collection, entry) => (dispatch, getState) => (path, folder) => {
return getAsset({ collection, entry, path, folder })(dispatch, getState);
},
};

View File

@ -288,8 +288,8 @@ const mapDispatchToProps = {
},
clearSearch,
clearFieldErrors,
boundGetAsset: (collection, entry) => (dispatch, getState) => path => {
return getAsset({ collection, entry, path })(dispatch, getState);
boundGetAsset: (collection, entry) => (dispatch, getState) => (path, folder) => {
return getAsset({ collection, entry, path, folder })(dispatch, getState);
},
};

View File

@ -160,7 +160,7 @@ class MediaLibrary extends React.Component {
event.persist();
event.stopPropagation();
event.preventDefault();
const { persistMedia, privateUpload, config, t } = this.props;
const { persistMedia, privateUpload, config, t, mediaFolder } = this.props;
const { files: fileList } = event.dataTransfer || event.target;
const files = [...fileList];
const file = files[0];
@ -173,7 +173,7 @@ class MediaLibrary extends React.Component {
}),
);
} else {
await persistMedia(file, { privateUpload });
await persistMedia(file, { privateUpload, mediaFolder });
this.setState({ selectedFile: this.props.files[0] });
@ -190,8 +190,8 @@ class MediaLibrary extends React.Component {
handleInsert = () => {
const { selectedFile } = this.state;
const { path } = selectedFile;
const { insertMedia } = this.props;
insertMedia(path);
const { insertMedia, publicFolder } = this.props;
insertMedia(path, publicFolder);
this.handleClose();
};
@ -332,6 +332,8 @@ const mapStateToProps = state => {
page: mediaLibrary.get('page'),
hasNextPage: mediaLibrary.get('hasNextPage'),
isPaginating: mediaLibrary.get('isPaginating'),
mediaFolder: mediaLibrary.get('mediaFolder'),
publicFolder: mediaLibrary.get('publicFolder'),
};
return { ...mediaLibraryProps };
};

View File

@ -19,7 +19,7 @@ interface MediaLibrary {
const initializeMediaLibrary = once(async function initializeMediaLibrary(name, options) {
const lib = (getMediaLibrary(name) as unknown) as MediaLibrary;
const handleInsert = (url: string) => store.dispatch(insertMedia(url));
const handleInsert = (url: string) => store.dispatch(insertMedia(url, undefined));
const instance = await lib.init({ options, handleInsert });
store.dispatch(createMediaLibrary(instance));
});

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:

View File

@ -94,6 +94,8 @@ export type EntryField = StaticallyTypedRecord<{
widget: string;
name: string;
default: string | null;
media_folder?: string;
public_folder?: string;
}>;
export type EntryFields = List<EntryField>;
@ -108,6 +110,8 @@ export type CollectionFile = StaticallyTypedRecord<{
name: string;
fields: EntryFields;
label: string;
media_folder?: string;
public_folder?: string;
}>;
export type CollectionFiles = List<CollectionFile>;
@ -305,6 +309,8 @@ export interface MediaLibraryAction extends Action<string> {
forImage: boolean;
privateUpload: boolean;
config: Map<string, string>;
mediaFolder?: string;
publicFolder?: string;
} & { mediaPath: string | string[] } & { page: number } & {
files: MediaFile[];
page: number;

View File

@ -2,17 +2,20 @@ interface AssetProxyArgs {
path: string;
url?: string;
file?: File;
folder?: string;
}
export default class AssetProxy {
url: string;
fileObj?: File;
path: string;
folder?: string;
constructor({ url, file, path }: AssetProxyArgs) {
constructor({ url, file, path, folder }: AssetProxyArgs) {
this.url = url ? url : window.URL.createObjectURL(file);
this.fileObj = file;
this.path = path;
this.folder = folder;
}
toString(): string {
@ -35,6 +38,6 @@ export default class AssetProxy {
}
}
export function createAssetProxy({ url, file, path }: AssetProxyArgs): AssetProxy {
return new AssetProxy({ url, file, path });
export function createAssetProxy({ url, file, path, folder }: AssetProxyArgs): AssetProxy {
return new AssetProxy({ url, file, path, folder });
}