feat: field based media/public folders (#3208)
This commit is contained in:
parent
ee7445d49d
commit
97bc0c8dc4
@ -33,6 +33,7 @@ import {
|
||||
getPointerFileForMediaFileObj,
|
||||
getLargeMediaFilteredMediaFiles,
|
||||
FetchError,
|
||||
blobToFileObj,
|
||||
} from 'netlify-cms-lib-util';
|
||||
import NetlifyAuthenticator from 'netlify-cms-lib-auth';
|
||||
import AuthenticationPage from './AuthenticationPage';
|
||||
@ -325,7 +326,7 @@ export default class BitbucketBackend implements Implementation {
|
||||
async getMediaFile(path: string) {
|
||||
const name = basename(path);
|
||||
const blob = await getMediaAsBlob(path, null, this.api!.readFile.bind(this.api!));
|
||||
const fileObj = new File([blob], name);
|
||||
const fileObj = blobToFileObj(name, blob);
|
||||
const url = URL.createObjectURL(fileObj);
|
||||
const id = await getBlobSHA(fileObj);
|
||||
|
||||
@ -423,7 +424,7 @@ export default class BitbucketBackend implements Implementation {
|
||||
|
||||
return getMediaAsBlob(file.path, null, readFile).then(blob => {
|
||||
const name = basename(file.path);
|
||||
const fileObj = new File([blob], name);
|
||||
const fileObj = blobToFileObj(name, blob);
|
||||
return {
|
||||
id: file.path,
|
||||
displayURL: URL.createObjectURL(fileObj),
|
||||
|
@ -24,6 +24,7 @@ import {
|
||||
getPreviewStatus,
|
||||
UnpublishedEntryMediaFile,
|
||||
runWithLock,
|
||||
blobToFileObj,
|
||||
} from 'netlify-cms-lib-util';
|
||||
import AuthenticationPage from './AuthenticationPage';
|
||||
import { UsersGetAuthenticatedResponse as GitHubUser } from '@octokit/rest';
|
||||
@ -324,7 +325,7 @@ export default class GitHub implements Implementation {
|
||||
const blob = await getMediaAsBlob(path, null, this.api!.readFile.bind(this.api!));
|
||||
|
||||
const name = basename(path);
|
||||
const fileObj = new File([blob], name);
|
||||
const fileObj = blobToFileObj(name, blob);
|
||||
const url = URL.createObjectURL(fileObj);
|
||||
const id = await getBlobSHA(blob);
|
||||
|
||||
@ -388,7 +389,7 @@ export default class GitHub implements Implementation {
|
||||
|
||||
return getMediaAsBlob(file.path, file.id, readFile).then(blob => {
|
||||
const name = basename(file.path);
|
||||
const fileObj = new File([blob], name);
|
||||
const fileObj = blobToFileObj(name, blob);
|
||||
return {
|
||||
id: file.id,
|
||||
displayURL: URL.createObjectURL(fileObj),
|
||||
|
@ -25,10 +25,11 @@ import {
|
||||
asyncLock,
|
||||
AsyncLock,
|
||||
runWithLock,
|
||||
getBlobSHA,
|
||||
blobToFileObj,
|
||||
} from 'netlify-cms-lib-util';
|
||||
import AuthenticationPage from './AuthenticationPage';
|
||||
import API, { API_NAME } from './API';
|
||||
import { getBlobSHA } from 'netlify-cms-lib-util/src';
|
||||
|
||||
const MAX_CONCURRENT_DOWNLOADS = 10;
|
||||
|
||||
@ -194,7 +195,7 @@ export default class GitLab implements Implementation {
|
||||
async getMediaFile(path: string) {
|
||||
const name = basename(path);
|
||||
const blob = await getMediaAsBlob(path, null, this.api!.readFile.bind(this.api!));
|
||||
const fileObj = new File([blob], name);
|
||||
const fileObj = blobToFileObj(name, blob);
|
||||
const url = URL.createObjectURL(fileObj);
|
||||
const id = await getBlobSHA(blob);
|
||||
|
||||
@ -275,7 +276,7 @@ export default class GitLab implements Implementation {
|
||||
|
||||
return getMediaAsBlob(file.path, null, readFile).then(blob => {
|
||||
const name = basename(file.path);
|
||||
const fileObj = new File([blob], name);
|
||||
const fileObj = blobToFileObj(name, blob);
|
||||
return {
|
||||
id: file.path,
|
||||
displayURL: URL.createObjectURL(fileObj),
|
||||
|
@ -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 });
|
||||
}
|
||||
|
@ -12,8 +12,10 @@ const image = {
|
||||
toBlock: ({ alt, image, title }) =>
|
||||
`}"` : ''})`,
|
||||
// eslint-disable-next-line react/display-name
|
||||
toPreview: async ({ alt, image, title }, getAsset) => {
|
||||
const src = await getAsset(image);
|
||||
toPreview: async ({ alt, image, title }, getAsset, fields) => {
|
||||
const imageField = fields?.find(f => f.get('widget') === 'image');
|
||||
const folder = imageField?.get('media_folder');
|
||||
const src = await getAsset(image, folder);
|
||||
return <img src={src || ''} alt={alt || ''} title={title || ''} />;
|
||||
},
|
||||
pattern: /^!\[(.*)\]\((.*?)(\s"(.*)")?\)$/,
|
||||
|
@ -18,6 +18,7 @@ export interface ImplementationMediaFile {
|
||||
draft?: boolean;
|
||||
url?: string;
|
||||
file?: File;
|
||||
folder?: string;
|
||||
}
|
||||
|
||||
export interface UnpublishedEntryMediaFile {
|
||||
@ -264,6 +265,11 @@ export const unpublishedEntries = async (
|
||||
}
|
||||
};
|
||||
|
||||
export const blobToFileObj = (name: string, blob: Blob) => {
|
||||
const options = name.match(/.svg$/) ? { type: 'image/svg+xml' } : {};
|
||||
return new File([blob], name, options);
|
||||
};
|
||||
|
||||
export const getMediaAsBlob = async (path: string, id: string | null, readFile: ReadFile) => {
|
||||
let blob: Blob;
|
||||
if (path.match(/.svg$/)) {
|
||||
|
@ -36,6 +36,7 @@ import {
|
||||
runWithLock,
|
||||
Config as C,
|
||||
UnpublishedEntryMediaFile as UEMF,
|
||||
blobToFileObj,
|
||||
} from './implementation';
|
||||
import {
|
||||
readFile,
|
||||
@ -136,6 +137,7 @@ export const NetlifyCmsLibUtil = {
|
||||
getPointerFileForMediaFileObj,
|
||||
branchFromContentKey,
|
||||
contentKeyFromBranch,
|
||||
blobToFileObj,
|
||||
};
|
||||
export {
|
||||
APIError,
|
||||
@ -186,4 +188,5 @@ export {
|
||||
getPointerFileForMediaFileObj,
|
||||
branchFromContentKey,
|
||||
contentKeyFromBranch,
|
||||
blobToFileObj,
|
||||
};
|
||||
|
@ -6,6 +6,7 @@ class Asset extends React.Component {
|
||||
path: PropTypes.string.isRequired,
|
||||
getAsset: PropTypes.func.isRequired,
|
||||
component: PropTypes.elementType.isRequired,
|
||||
folder: PropTypes.string,
|
||||
};
|
||||
|
||||
subscribed = true;
|
||||
@ -14,13 +15,12 @@ class Asset extends React.Component {
|
||||
value: null,
|
||||
};
|
||||
|
||||
_fetchAsset() {
|
||||
const { getAsset, path } = this.props;
|
||||
getAsset(path).then(value => {
|
||||
if (this.subscribed) {
|
||||
this.setState({ value });
|
||||
}
|
||||
});
|
||||
async _fetchAsset() {
|
||||
const { getAsset, path, folder } = this.props;
|
||||
const value = await getAsset(path, folder);
|
||||
if (this.subscribed) {
|
||||
this.setState({ value });
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
@ -32,7 +32,11 @@ class Asset extends React.Component {
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (prevProps.path !== this.props.path || prevProps.getAsset !== this.props.getAsset) {
|
||||
if (
|
||||
prevProps.path !== this.props.path ||
|
||||
prevProps.getAsset !== this.props.getAsset ||
|
||||
prevProps.folder !== this.props.folder
|
||||
) {
|
||||
this._fetchAsset();
|
||||
}
|
||||
}
|
||||
|
@ -37,8 +37,8 @@ const Image = styled(({ value: src }) => <img src={src || ''} role="presentation
|
||||
object-fit: contain;
|
||||
`;
|
||||
|
||||
const ImageAsset = ({ getAsset, value }) => {
|
||||
return <Asset path={value} getAsset={getAsset} component={Image} />;
|
||||
const ImageAsset = ({ getAsset, value, folder }) => {
|
||||
return <Asset folder={folder} path={value} getAsset={getAsset} component={Image} />;
|
||||
};
|
||||
|
||||
const MultiImageWrapper = styled.div`
|
||||
@ -175,6 +175,8 @@ export default function withFileControl({ forImage } = {}) {
|
||||
value,
|
||||
allowMultiple: !!mediaLibraryFieldOptions.get('allow_multiple', true),
|
||||
config: mediaLibraryFieldOptions.get('config'),
|
||||
mediaFolder: field.get('media_folder'),
|
||||
publicFolder: field.get('public_folder'),
|
||||
});
|
||||
};
|
||||
|
||||
@ -218,13 +220,15 @@ export default function withFileControl({ forImage } = {}) {
|
||||
};
|
||||
|
||||
renderImages = () => {
|
||||
const { getAsset, value } = this.props;
|
||||
const { getAsset, value, field } = this.props;
|
||||
const folder = field.get('media_folder');
|
||||
|
||||
if (isMultiple(value)) {
|
||||
return (
|
||||
<MultiImageWrapper>
|
||||
{value.map(val => (
|
||||
<ImageWrapper key={val}>
|
||||
<ImageAsset getAsset={getAsset} value={val} />
|
||||
<ImageAsset getAsset={getAsset} value={val} folder={folder} />
|
||||
</ImageWrapper>
|
||||
))}
|
||||
</MultiImageWrapper>
|
||||
@ -232,7 +236,7 @@ export default function withFileControl({ forImage } = {}) {
|
||||
}
|
||||
return (
|
||||
<ImageWrapper>
|
||||
<ImageAsset getAsset={getAsset} value={value} />
|
||||
<ImageAsset getAsset={getAsset} value={value} folder={folder} />
|
||||
</ImageWrapper>
|
||||
);
|
||||
};
|
||||
|
@ -10,14 +10,23 @@ const StyledImage = styled(({ value: src }) => <img src={src || ''} role="presen
|
||||
height: auto;
|
||||
`;
|
||||
|
||||
const StyledImageAsset = ({ getAsset, value }) => {
|
||||
return <Asset path={value} getAsset={getAsset} component={StyledImage} />;
|
||||
const StyledImageAsset = ({ getAsset, value, field }) => {
|
||||
return (
|
||||
<Asset
|
||||
folder={field.get('media_folder')}
|
||||
path={value}
|
||||
getAsset={getAsset}
|
||||
component={StyledImage}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const ImagePreviewContent = props => {
|
||||
const { value, getAsset } = props;
|
||||
const { value, getAsset, field } = props;
|
||||
if (Array.isArray(value) || List.isList(value)) {
|
||||
return value.map(val => <StyledImageAsset key={val} value={val} getAsset={getAsset} />);
|
||||
return value.map(val => (
|
||||
<StyledImageAsset key={val} value={val} getAsset={getAsset} field={field} />
|
||||
));
|
||||
}
|
||||
return <StyledImageAsset {...props} />;
|
||||
};
|
||||
|
@ -49,6 +49,38 @@ const createSlateValue = (rawValue, { voidCodeBlock }) => {
|
||||
return Value.create({ document });
|
||||
};
|
||||
|
||||
export const mergeMediaConfig = (editorComponents, field) => {
|
||||
// merge editor media library config to image components
|
||||
if (editorComponents.has('image')) {
|
||||
const imageComponent = editorComponents.get('image');
|
||||
const fields = imageComponent?.fields;
|
||||
|
||||
if (fields) {
|
||||
imageComponent.fields = fields.update(
|
||||
fields.findIndex(f => f.get('widget') === 'image'),
|
||||
f => {
|
||||
// merge `media_library` config
|
||||
if (field.has('media_library')) {
|
||||
f = f.set(
|
||||
'media_library',
|
||||
field.get('media_library').mergeDeep(f.get('media_library')),
|
||||
);
|
||||
}
|
||||
// merge 'media_folder'
|
||||
if (field.has('media_folder') && !f.has('media_folder')) {
|
||||
f = f.set('media_folder', field.get('media_folder'));
|
||||
}
|
||||
// merge 'public_folder'
|
||||
if (field.has('public_folder') && !f.has('public_folder')) {
|
||||
f = f.set('public_folder', field.get('public_folder'));
|
||||
}
|
||||
return f;
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default class Editor extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
@ -59,6 +91,8 @@ export default class Editor extends React.Component {
|
||||
this.codeBlockComponent || editorComponents.has('code-block')
|
||||
? editorComponents
|
||||
: editorComponents.set('code-block', { label: 'Code Block', type: 'code-block' });
|
||||
|
||||
mergeMediaConfig(this.editorComponents, this.props.field);
|
||||
this.renderBlock = renderBlock({
|
||||
classNameWrapper: props.className,
|
||||
resolveWidget: props.resolveWidget,
|
||||
|
@ -0,0 +1,55 @@
|
||||
import { Map, fromJS } from 'immutable';
|
||||
import { mergeMediaConfig } from '../VisualEditor';
|
||||
|
||||
describe('VisualEditor', () => {
|
||||
describe('mergeMediaConfig', () => {
|
||||
it('should copy editor media settings to image component', () => {
|
||||
const editorComponents = Map({
|
||||
image: {
|
||||
id: 'image',
|
||||
label: 'Image',
|
||||
type: 'shortcode',
|
||||
icon: 'exclamation-triangle',
|
||||
widget: 'object',
|
||||
pattern: {},
|
||||
fields: fromJS([
|
||||
{
|
||||
label: 'Image',
|
||||
name: 'image',
|
||||
widget: 'image',
|
||||
media_library: { allow_multiple: false },
|
||||
},
|
||||
{ label: 'Alt Text', name: 'alt' },
|
||||
{ label: 'Title', name: 'title' },
|
||||
]),
|
||||
},
|
||||
});
|
||||
|
||||
const field = fromJS({
|
||||
label: 'Body',
|
||||
name: 'body',
|
||||
widget: 'markdown',
|
||||
media_folder: '/{{media_folder}}/posts/images/widget/body',
|
||||
public_folder: '{{public_folder}}/posts/images/widget/body',
|
||||
media_library: { config: { max_file_size: 1234 } },
|
||||
});
|
||||
|
||||
mergeMediaConfig(editorComponents, field);
|
||||
|
||||
expect(editorComponents.get('image').fields).toEqual(
|
||||
fromJS([
|
||||
{
|
||||
label: 'Image',
|
||||
name: 'image',
|
||||
widget: 'image',
|
||||
media_library: { allow_multiple: false, config: { max_file_size: 1234 } },
|
||||
media_folder: '/{{media_folder}}/posts/images/widget/body',
|
||||
public_folder: '{{public_folder}}/posts/images/widget/body',
|
||||
},
|
||||
{ label: 'Alt Text', name: 'alt' },
|
||||
{ label: 'Title', name: 'title' },
|
||||
]),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
@ -53,9 +53,9 @@ export default function remarkToRehypeShortcodes({ plugins, getAsset, resolveWid
|
||||
* Retrieve the shortcode preview component.
|
||||
*/
|
||||
async function getPreview(plugin, shortcodeData) {
|
||||
const { toPreview, widget } = plugin;
|
||||
const { toPreview, widget, fields } = plugin;
|
||||
if (toPreview) {
|
||||
return toPreview(shortcodeData, getAsset);
|
||||
return toPreview(shortcodeData, getAsset, fields);
|
||||
}
|
||||
const preview = resolveWidget(widget);
|
||||
return React.createElement(preview.preview, {
|
||||
|
44
yarn.lock
44
yarn.lock
@ -6538,11 +6538,6 @@ detect-indent@^5.0.0:
|
||||
resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-5.0.0.tgz#3871cc0a6a002e8c3e5b3cf7f336264675f06b9d"
|
||||
integrity sha1-OHHMCmoALow+Wzz38zYmRnXwa50=
|
||||
|
||||
detect-libc@^1.0.2:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b"
|
||||
integrity sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=
|
||||
|
||||
detect-newline@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-2.1.0.tgz#f41f1c10be4b00e87b5f13da680759f2c5bfd3e2"
|
||||
@ -9106,7 +9101,7 @@ husky@^3.0.9:
|
||||
run-node "^1.0.0"
|
||||
slash "^3.0.0"
|
||||
|
||||
iconv-lite@0.4.24, iconv-lite@^0.4.24, iconv-lite@^0.4.4, iconv-lite@~0.4.13:
|
||||
iconv-lite@0.4.24, iconv-lite@^0.4.24, iconv-lite@~0.4.13:
|
||||
version "0.4.24"
|
||||
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
|
||||
integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==
|
||||
@ -11802,15 +11797,6 @@ ncp@^2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/ncp/-/ncp-2.0.0.tgz#195a21d6c46e361d2fb1281ba38b91e9df7bdbb3"
|
||||
integrity sha1-GVoh1sRuNh0vsSgbo4uR6d9727M=
|
||||
|
||||
needle@^2.2.1:
|
||||
version "2.4.0"
|
||||
resolved "https://registry.yarnpkg.com/needle/-/needle-2.4.0.tgz#6833e74975c444642590e15a750288c5f939b57c"
|
||||
integrity sha512-4Hnwzr3mi5L97hMYeNl8wRW/Onhy4nUKR/lVemJ8gJedxxUyBLm9kkrDColJvoSfwi0jCNhD+xCdOtiGDQiRZg==
|
||||
dependencies:
|
||||
debug "^3.2.6"
|
||||
iconv-lite "^0.4.4"
|
||||
sax "^1.2.4"
|
||||
|
||||
negotiator@0.6.2:
|
||||
version "0.6.2"
|
||||
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb"
|
||||
@ -11974,22 +11960,6 @@ node-polyglot@^2.3.0:
|
||||
string.prototype.trim "^1.1.2"
|
||||
warning "^4.0.3"
|
||||
|
||||
node-pre-gyp@*:
|
||||
version "0.14.0"
|
||||
resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.14.0.tgz#9a0596533b877289bcad4e143982ca3d904ddc83"
|
||||
integrity sha512-+CvDC7ZttU/sSt9rFjix/P05iS43qHCOOGzcr3Ry99bXG7VX953+vFyEuph/tfqoYu8dttBkE86JSKBO2OzcxA==
|
||||
dependencies:
|
||||
detect-libc "^1.0.2"
|
||||
mkdirp "^0.5.1"
|
||||
needle "^2.2.1"
|
||||
nopt "^4.0.1"
|
||||
npm-packlist "^1.1.6"
|
||||
npmlog "^4.0.2"
|
||||
rc "^1.2.7"
|
||||
rimraf "^2.6.1"
|
||||
semver "^5.3.0"
|
||||
tar "^4.4.2"
|
||||
|
||||
node-releases@^1.1.29, node-releases@^1.1.47:
|
||||
version "1.1.47"
|
||||
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.47.tgz#c59ef739a1fd7ecbd9f0b7cf5b7871e8a8b591e4"
|
||||
@ -12111,7 +12081,7 @@ npm-normalize-package-bin@^1.0.0, npm-normalize-package-bin@^1.0.1:
|
||||
semver "^5.6.0"
|
||||
validate-npm-package-name "^3.0.0"
|
||||
|
||||
npm-packlist@^1.1.6, npm-packlist@^1.4.4:
|
||||
npm-packlist@^1.4.4:
|
||||
version "1.4.8"
|
||||
resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-1.4.8.tgz#56ee6cc135b9f98ad3d51c1c95da22bbb9b2ef3e"
|
||||
integrity sha512-5+AZgwru5IevF5ZdnFglB5wNlHG1AOOuw28WhUq8/8emhBmLv6jX5by4WJCh7lW0uSYZYS6DXqIsyZVIXRZU9A==
|
||||
@ -12158,7 +12128,7 @@ npm-run-path@^4.0.0:
|
||||
dependencies:
|
||||
path-key "^3.0.0"
|
||||
|
||||
npmlog@^4.0.2, npmlog@^4.1.2:
|
||||
npmlog@^4.1.2:
|
||||
version "4.1.2"
|
||||
resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b"
|
||||
integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==
|
||||
@ -13637,7 +13607,7 @@ rbush@2.0.2:
|
||||
dependencies:
|
||||
quickselect "^1.0.1"
|
||||
|
||||
rc@^1.0.1, rc@^1.1.6, rc@^1.2.7:
|
||||
rc@^1.0.1, rc@^1.1.6:
|
||||
version "1.2.8"
|
||||
resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed"
|
||||
integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==
|
||||
@ -14793,7 +14763,7 @@ rimraf@2.6.3:
|
||||
dependencies:
|
||||
glob "^7.1.3"
|
||||
|
||||
rimraf@^2.2.8, rimraf@^2.5.2, rimraf@^2.5.4, rimraf@^2.6.1, rimraf@^2.6.2, rimraf@^2.6.3, rimraf@^2.7.1:
|
||||
rimraf@^2.2.8, rimraf@^2.5.2, rimraf@^2.5.4, rimraf@^2.6.2, rimraf@^2.6.3, rimraf@^2.7.1:
|
||||
version "2.7.1"
|
||||
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec"
|
||||
integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==
|
||||
@ -14984,7 +14954,7 @@ semver-diff@^2.0.0:
|
||||
dependencies:
|
||||
semver "^5.0.3"
|
||||
|
||||
"semver@2 || 3 || 4 || 5", "semver@2.x || 3.x || 4 || 5", semver@^5.0.3, semver@^5.1.0, semver@^5.3.0, semver@^5.4.1, semver@^5.5, semver@^5.5.0, semver@^5.5.1, semver@^5.6.0, semver@^5.7.0, semver@^5.7.1:
|
||||
"semver@2 || 3 || 4 || 5", "semver@2.x || 3.x || 4 || 5", semver@^5.0.3, semver@^5.1.0, semver@^5.4.1, semver@^5.5, semver@^5.5.0, semver@^5.5.1, semver@^5.6.0, semver@^5.7.0, semver@^5.7.1:
|
||||
version "5.7.1"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
|
||||
integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
|
||||
@ -16178,7 +16148,7 @@ tapable@^1.0.0, tapable@^1.1.3:
|
||||
resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.3.tgz#a1fccc06b58db61fd7a45da2da44f5f3a3e67ba2"
|
||||
integrity sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==
|
||||
|
||||
tar@^4.4.10, tar@^4.4.12, tar@^4.4.2, tar@^4.4.8:
|
||||
tar@^4.4.10, tar@^4.4.12, tar@^4.4.8:
|
||||
version "4.4.13"
|
||||
resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.13.tgz#43b364bc52888d555298637b10d60790254ab525"
|
||||
integrity sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA==
|
||||
|
Loading…
x
Reference in New Issue
Block a user