feat: field based media/public folders (#3208)
This commit is contained in:
@ -60,6 +60,7 @@ describe('media', () => {
|
||||
payload.collection,
|
||||
payload.entry,
|
||||
path,
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@ -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;
|
||||
|
@ -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) {
|
||||
|
@ -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 = {}) {
|
||||
|
@ -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;
|
||||
});
|
||||
|
@ -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);
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -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);
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -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);
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -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 };
|
||||
};
|
||||
|
@ -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));
|
||||
});
|
||||
|
@ -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']);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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) =>
|
||||
|
@ -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'),
|
||||
|
@ -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:
|
||||
|
@ -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;
|
||||
|
@ -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 });
|
||||
}
|
||||
|
Reference in New Issue
Block a user