feat: field based media/public folders (#3208)
This commit is contained in:
parent
ee7445d49d
commit
97bc0c8dc4
@ -33,6 +33,7 @@ import {
|
|||||||
getPointerFileForMediaFileObj,
|
getPointerFileForMediaFileObj,
|
||||||
getLargeMediaFilteredMediaFiles,
|
getLargeMediaFilteredMediaFiles,
|
||||||
FetchError,
|
FetchError,
|
||||||
|
blobToFileObj,
|
||||||
} from 'netlify-cms-lib-util';
|
} from 'netlify-cms-lib-util';
|
||||||
import NetlifyAuthenticator from 'netlify-cms-lib-auth';
|
import NetlifyAuthenticator from 'netlify-cms-lib-auth';
|
||||||
import AuthenticationPage from './AuthenticationPage';
|
import AuthenticationPage from './AuthenticationPage';
|
||||||
@ -325,7 +326,7 @@ export default class BitbucketBackend implements Implementation {
|
|||||||
async getMediaFile(path: string) {
|
async getMediaFile(path: string) {
|
||||||
const name = basename(path);
|
const name = basename(path);
|
||||||
const blob = await getMediaAsBlob(path, null, this.api!.readFile.bind(this.api!));
|
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 url = URL.createObjectURL(fileObj);
|
||||||
const id = await getBlobSHA(fileObj);
|
const id = await getBlobSHA(fileObj);
|
||||||
|
|
||||||
@ -423,7 +424,7 @@ export default class BitbucketBackend implements Implementation {
|
|||||||
|
|
||||||
return getMediaAsBlob(file.path, null, readFile).then(blob => {
|
return getMediaAsBlob(file.path, null, readFile).then(blob => {
|
||||||
const name = basename(file.path);
|
const name = basename(file.path);
|
||||||
const fileObj = new File([blob], name);
|
const fileObj = blobToFileObj(name, blob);
|
||||||
return {
|
return {
|
||||||
id: file.path,
|
id: file.path,
|
||||||
displayURL: URL.createObjectURL(fileObj),
|
displayURL: URL.createObjectURL(fileObj),
|
||||||
|
@ -24,6 +24,7 @@ import {
|
|||||||
getPreviewStatus,
|
getPreviewStatus,
|
||||||
UnpublishedEntryMediaFile,
|
UnpublishedEntryMediaFile,
|
||||||
runWithLock,
|
runWithLock,
|
||||||
|
blobToFileObj,
|
||||||
} from 'netlify-cms-lib-util';
|
} from 'netlify-cms-lib-util';
|
||||||
import AuthenticationPage from './AuthenticationPage';
|
import AuthenticationPage from './AuthenticationPage';
|
||||||
import { UsersGetAuthenticatedResponse as GitHubUser } from '@octokit/rest';
|
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 blob = await getMediaAsBlob(path, null, this.api!.readFile.bind(this.api!));
|
||||||
|
|
||||||
const name = basename(path);
|
const name = basename(path);
|
||||||
const fileObj = new File([blob], name);
|
const fileObj = blobToFileObj(name, blob);
|
||||||
const url = URL.createObjectURL(fileObj);
|
const url = URL.createObjectURL(fileObj);
|
||||||
const id = await getBlobSHA(blob);
|
const id = await getBlobSHA(blob);
|
||||||
|
|
||||||
@ -388,7 +389,7 @@ export default class GitHub implements Implementation {
|
|||||||
|
|
||||||
return getMediaAsBlob(file.path, file.id, readFile).then(blob => {
|
return getMediaAsBlob(file.path, file.id, readFile).then(blob => {
|
||||||
const name = basename(file.path);
|
const name = basename(file.path);
|
||||||
const fileObj = new File([blob], name);
|
const fileObj = blobToFileObj(name, blob);
|
||||||
return {
|
return {
|
||||||
id: file.id,
|
id: file.id,
|
||||||
displayURL: URL.createObjectURL(fileObj),
|
displayURL: URL.createObjectURL(fileObj),
|
||||||
|
@ -25,10 +25,11 @@ import {
|
|||||||
asyncLock,
|
asyncLock,
|
||||||
AsyncLock,
|
AsyncLock,
|
||||||
runWithLock,
|
runWithLock,
|
||||||
|
getBlobSHA,
|
||||||
|
blobToFileObj,
|
||||||
} from 'netlify-cms-lib-util';
|
} from 'netlify-cms-lib-util';
|
||||||
import AuthenticationPage from './AuthenticationPage';
|
import AuthenticationPage from './AuthenticationPage';
|
||||||
import API, { API_NAME } from './API';
|
import API, { API_NAME } from './API';
|
||||||
import { getBlobSHA } from 'netlify-cms-lib-util/src';
|
|
||||||
|
|
||||||
const MAX_CONCURRENT_DOWNLOADS = 10;
|
const MAX_CONCURRENT_DOWNLOADS = 10;
|
||||||
|
|
||||||
@ -194,7 +195,7 @@ export default class GitLab implements Implementation {
|
|||||||
async getMediaFile(path: string) {
|
async getMediaFile(path: string) {
|
||||||
const name = basename(path);
|
const name = basename(path);
|
||||||
const blob = await getMediaAsBlob(path, null, this.api!.readFile.bind(this.api!));
|
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 url = URL.createObjectURL(fileObj);
|
||||||
const id = await getBlobSHA(blob);
|
const id = await getBlobSHA(blob);
|
||||||
|
|
||||||
@ -275,7 +276,7 @@ export default class GitLab implements Implementation {
|
|||||||
|
|
||||||
return getMediaAsBlob(file.path, null, readFile).then(blob => {
|
return getMediaAsBlob(file.path, null, readFile).then(blob => {
|
||||||
const name = basename(file.path);
|
const name = basename(file.path);
|
||||||
const fileObj = new File([blob], name);
|
const fileObj = blobToFileObj(name, blob);
|
||||||
return {
|
return {
|
||||||
id: file.path,
|
id: file.path,
|
||||||
displayURL: URL.createObjectURL(fileObj),
|
displayURL: URL.createObjectURL(fileObj),
|
||||||
|
@ -60,6 +60,7 @@ describe('media', () => {
|
|||||||
payload.collection,
|
payload.collection,
|
||||||
payload.entry,
|
payload.entry,
|
||||||
path,
|
path,
|
||||||
|
undefined,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -293,12 +293,19 @@ export function retrieveLocalBackup(collection: Collection, slug: string) {
|
|||||||
const assetProxies: AssetProxy[] = await Promise.all(
|
const assetProxies: AssetProxy[] = await Promise.all(
|
||||||
mediaFiles.map(file => {
|
mediaFiles.map(file => {
|
||||||
if (file.file || file.url) {
|
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 {
|
} else {
|
||||||
return getAsset({ collection, entry: fromJS(entry), path: file.path })(
|
return getAsset({
|
||||||
dispatch,
|
collection,
|
||||||
getState,
|
entry: fromJS(entry),
|
||||||
);
|
path: file.path,
|
||||||
|
folder: file.folder,
|
||||||
|
})(dispatch, getState);
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@ -554,7 +561,9 @@ export async function getMediaAssets({
|
|||||||
const assets = await Promise.all(
|
const assets = await Promise.all(
|
||||||
filesArray
|
filesArray
|
||||||
.filter(file => file.draft)
|
.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;
|
return assets;
|
||||||
|
@ -27,14 +27,14 @@ interface GetAssetArgs {
|
|||||||
collection: Collection;
|
collection: Collection;
|
||||||
entry: EntryMap;
|
entry: EntryMap;
|
||||||
path: string;
|
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) => {
|
return async (dispatch: ThunkDispatch<State, {}, AnyAction>, getState: () => State) => {
|
||||||
if (!path) return createAssetProxy({ path: '', file: new File([], 'empty') });
|
if (!path) return createAssetProxy({ path: '', file: new File([], 'empty') });
|
||||||
|
|
||||||
const state = getState();
|
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);
|
let asset = state.medias.get(resolvedPath);
|
||||||
if (asset) {
|
if (asset) {
|
||||||
|
@ -77,6 +77,8 @@ export function openMediaLibrary(
|
|||||||
config?: Map<string, unknown>;
|
config?: Map<string, unknown>;
|
||||||
allowMultiple?: boolean;
|
allowMultiple?: boolean;
|
||||||
forImage?: boolean;
|
forImage?: boolean;
|
||||||
|
mediaFolder?: string;
|
||||||
|
publicFolder?: string;
|
||||||
} = {},
|
} = {},
|
||||||
) {
|
) {
|
||||||
return (dispatch: ThunkDispatch<State, {}, AnyAction>, getState: () => State) => {
|
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) => {
|
return (dispatch: ThunkDispatch<State, {}, AnyAction>, getState: () => State) => {
|
||||||
const state = getState();
|
const state = getState();
|
||||||
const config = state.config;
|
const config = state.config;
|
||||||
@ -109,9 +111,17 @@ export function insertMedia(mediaPath: string | string[]) {
|
|||||||
const collectionName = state.entryDraft.getIn(['entry', 'collection']);
|
const collectionName = state.entryDraft.getIn(['entry', 'collection']);
|
||||||
const collection = state.collections.get(collectionName);
|
const collection = state.collections.get(collectionName);
|
||||||
if (Array.isArray(mediaPath)) {
|
if (Array.isArray(mediaPath)) {
|
||||||
mediaPath = mediaPath.map(path => selectMediaFilePublicPath(config, collection, path, entry));
|
mediaPath = mediaPath.map(path =>
|
||||||
|
selectMediaFilePublicPath(config, collection, path, entry, publicFolder),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
mediaPath = selectMediaFilePublicPath(config, collection, mediaPath as string, entry);
|
mediaPath = selectMediaFilePublicPath(
|
||||||
|
config,
|
||||||
|
collection,
|
||||||
|
mediaPath as string,
|
||||||
|
entry,
|
||||||
|
publicFolder,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
dispatch({ type: MEDIA_INSERT, payload: { mediaPath } });
|
dispatch({ type: MEDIA_INSERT, payload: { mediaPath } });
|
||||||
};
|
};
|
||||||
@ -191,12 +201,13 @@ function createMediaFileFromAsset({
|
|||||||
size: file.size,
|
size: file.size,
|
||||||
url: assetProxy.url,
|
url: assetProxy.url,
|
||||||
path: assetProxy.path,
|
path: assetProxy.path,
|
||||||
|
folder: assetProxy.folder,
|
||||||
};
|
};
|
||||||
return mediaFile;
|
return mediaFile;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function persistMedia(file: File, opts: MediaOptions = {}) {
|
export function persistMedia(file: File, opts: MediaOptions = {}) {
|
||||||
const { privateUpload } = opts;
|
const { privateUpload, mediaFolder } = opts;
|
||||||
return async (dispatch: ThunkDispatch<State, {}, AnyAction>, getState: () => State) => {
|
return async (dispatch: ThunkDispatch<State, {}, AnyAction>, getState: () => State) => {
|
||||||
const state = getState();
|
const state = getState();
|
||||||
const backend = currentBackend(state.config);
|
const backend = currentBackend(state.config);
|
||||||
@ -250,10 +261,11 @@ export function persistMedia(file: File, opts: MediaOptions = {}) {
|
|||||||
} else {
|
} else {
|
||||||
const entry = state.entryDraft.get('entry');
|
const entry = state.entryDraft.get('entry');
|
||||||
const collection = state.collections.get(entry?.get('collection'));
|
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({
|
assetProxy = createAssetProxy({
|
||||||
file,
|
file,
|
||||||
path,
|
path,
|
||||||
|
folder: mediaFolder,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -397,6 +409,7 @@ export function mediaLoading(page: number) {
|
|||||||
|
|
||||||
interface MediaOptions {
|
interface MediaOptions {
|
||||||
privateUpload?: boolean;
|
privateUpload?: boolean;
|
||||||
|
mediaFolder?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function mediaLoaded(files: ImplementationMediaFile[], opts: MediaOptions = {}) {
|
export function mediaLoaded(files: ImplementationMediaFile[], opts: MediaOptions = {}) {
|
||||||
|
@ -3,7 +3,7 @@ import { List, Map, fromJS } from 'immutable';
|
|||||||
import * as fuzzy from 'fuzzy';
|
import * as fuzzy from 'fuzzy';
|
||||||
import { resolveFormat } from './formats/formats';
|
import { resolveFormat } from './formats/formats';
|
||||||
import { selectUseWorkflow } from './reducers/config';
|
import { selectUseWorkflow } from './reducers/config';
|
||||||
import { selectMediaFilePath, selectMediaFolder, selectEntry } from './reducers/entries';
|
import { selectMediaFilePath, selectEntry } from './reducers/entries';
|
||||||
import { selectIntegration } from './reducers/integrations';
|
import { selectIntegration } from './reducers/integrations';
|
||||||
import {
|
import {
|
||||||
selectEntrySlug,
|
selectEntrySlug,
|
||||||
@ -13,6 +13,7 @@ import {
|
|||||||
selectAllowDeletion,
|
selectAllowDeletion,
|
||||||
selectFolderEntryExtension,
|
selectFolderEntryExtension,
|
||||||
selectInferedField,
|
selectInferedField,
|
||||||
|
selectMediaFolders,
|
||||||
} from './reducers/collections';
|
} from './reducers/collections';
|
||||||
import { createEntry, EntryValue } from './valueObjects/Entry';
|
import { createEntry, EntryValue } from './valueObjects/Entry';
|
||||||
import { sanitizeChar } from './lib/urlHelper';
|
import { sanitizeChar } from './lib/urlHelper';
|
||||||
@ -31,6 +32,7 @@ import {
|
|||||||
User,
|
User,
|
||||||
getPathDepth,
|
getPathDepth,
|
||||||
Config as ImplementationConfig,
|
Config as ImplementationConfig,
|
||||||
|
blobToFileObj,
|
||||||
} from 'netlify-cms-lib-util';
|
} from 'netlify-cms-lib-util';
|
||||||
import { status } from './constants/publishModes';
|
import { status } from './constants/publishModes';
|
||||||
import { extractTemplateVars, dateParsers } from './lib/stringTemplate';
|
import { extractTemplateVars, dateParsers } from './lib/stringTemplate';
|
||||||
@ -450,8 +452,7 @@ export class Backend {
|
|||||||
// make sure to serialize the file
|
// make sure to serialize the file
|
||||||
if (file.url?.startsWith('blob:')) {
|
if (file.url?.startsWith('blob:')) {
|
||||||
const blob = await fetch(file.url as string).then(res => res.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: blobToFileObj(file.name, blob) };
|
||||||
return { ...file, file: new File([blob], file.name, options) };
|
|
||||||
}
|
}
|
||||||
return file;
|
return file;
|
||||||
}),
|
}),
|
||||||
@ -492,10 +493,12 @@ export class Backend {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const entryWithFormat = this.entryWithFormat(collection)(entry);
|
const entryWithFormat = this.entryWithFormat(collection)(entry);
|
||||||
if (collection.has('media_folder') && !integration) {
|
const mediaFolders = selectMediaFolders(state, collection, fromJS(entryWithFormat));
|
||||||
entry.mediaFiles = await this.implementation.getMedia(
|
if (mediaFolders.length > 0 && !integration) {
|
||||||
selectMediaFolder(state.config, collection, fromJS(entryWithFormat)),
|
entry.mediaFiles = [];
|
||||||
);
|
for (const folder of mediaFolders) {
|
||||||
|
entry.mediaFiles = [...entry.mediaFiles, ...(await this.implementation.getMedia(folder))];
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
entry.mediaFiles = state.mediaLibrary.get('files') || [];
|
entry.mediaFiles = state.mediaLibrary.get('files') || [];
|
||||||
}
|
}
|
||||||
@ -697,6 +700,7 @@ export class Backend {
|
|||||||
collection,
|
collection,
|
||||||
entryDraft.get('entry').set('path', path),
|
entryDraft.get('entry').set('path', path),
|
||||||
oldPath,
|
oldPath,
|
||||||
|
asset.folder,
|
||||||
);
|
);
|
||||||
asset.path = newPath;
|
asset.path = newPath;
|
||||||
});
|
});
|
||||||
|
@ -83,14 +83,15 @@ const CardImage = styled.div`
|
|||||||
height: 150px;
|
height: 150px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const CardImageAsset = ({ getAsset, image }) => {
|
const CardImageAsset = ({ getAsset, image, folder }) => {
|
||||||
return <Asset path={image} getAsset={getAsset} component={CardImage} />;
|
return <Asset folder={folder} path={image} getAsset={getAsset} component={CardImage} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
const EntryCard = ({
|
const EntryCard = ({
|
||||||
path,
|
path,
|
||||||
summary,
|
summary,
|
||||||
image,
|
image,
|
||||||
|
imageFolder,
|
||||||
collectionLabel,
|
collectionLabel,
|
||||||
viewStyle = VIEW_STYLE_LIST,
|
viewStyle = VIEW_STYLE_LIST,
|
||||||
boundGetAsset,
|
boundGetAsset,
|
||||||
@ -114,7 +115,9 @@ const EntryCard = ({
|
|||||||
{collectionLabel ? <CollectionLabel>{collectionLabel}</CollectionLabel> : null}
|
{collectionLabel ? <CollectionLabel>{collectionLabel}</CollectionLabel> : null}
|
||||||
<CardHeading>{summary}</CardHeading>
|
<CardHeading>{summary}</CardHeading>
|
||||||
</CardBody>
|
</CardBody>
|
||||||
{image ? <CardImageAsset getAsset={boundGetAsset} image={image} /> : null}
|
{image ? (
|
||||||
|
<CardImageAsset getAsset={boundGetAsset} image={image} folder={imageFolder} />
|
||||||
|
) : null}
|
||||||
</GridCardLink>
|
</GridCardLink>
|
||||||
</GridCard>
|
</GridCard>
|
||||||
);
|
);
|
||||||
@ -140,12 +143,16 @@ const mapStateToProps = (state, ownProps) => {
|
|||||||
summary,
|
summary,
|
||||||
path: `/collections/${collection.get('name')}/entries/${entry.get('slug')}`,
|
path: `/collections/${collection.get('name')}/entries/${entry.get('slug')}`,
|
||||||
image,
|
image,
|
||||||
|
imageFolder: collection
|
||||||
|
.get('fields')
|
||||||
|
?.find(f => f.get('name') === inferedFields.imageField && f.get('widget') === 'image')
|
||||||
|
?.get('media_folder'),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
const mapDispatchToProps = {
|
||||||
boundGetAsset: (collection, entry) => (dispatch, getState) => path => {
|
boundGetAsset: (collection, entry) => (dispatch, getState) => (path, folder) => {
|
||||||
return getAsset({ collection, entry, path })(dispatch, getState);
|
return getAsset({ collection, entry, path, folder })(dispatch, getState);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -515,8 +515,8 @@ const mapDispatchToProps = {
|
|||||||
unpublishPublishedEntry,
|
unpublishPublishedEntry,
|
||||||
deleteUnpublishedEntry,
|
deleteUnpublishedEntry,
|
||||||
logoutUser,
|
logoutUser,
|
||||||
boundGetAsset: (collection, entry) => (dispatch, getState) => path => {
|
boundGetAsset: (collection, entry) => (dispatch, getState) => (path, folder) => {
|
||||||
return getAsset({ collection, entry, path })(dispatch, getState);
|
return getAsset({ collection, entry, path, folder })(dispatch, getState);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -288,8 +288,8 @@ const mapDispatchToProps = {
|
|||||||
},
|
},
|
||||||
clearSearch,
|
clearSearch,
|
||||||
clearFieldErrors,
|
clearFieldErrors,
|
||||||
boundGetAsset: (collection, entry) => (dispatch, getState) => path => {
|
boundGetAsset: (collection, entry) => (dispatch, getState) => (path, folder) => {
|
||||||
return getAsset({ collection, entry, path })(dispatch, getState);
|
return getAsset({ collection, entry, path, folder })(dispatch, getState);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -160,7 +160,7 @@ class MediaLibrary extends React.Component {
|
|||||||
event.persist();
|
event.persist();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
event.preventDefault();
|
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 } = event.dataTransfer || event.target;
|
||||||
const files = [...fileList];
|
const files = [...fileList];
|
||||||
const file = files[0];
|
const file = files[0];
|
||||||
@ -173,7 +173,7 @@ class MediaLibrary extends React.Component {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
await persistMedia(file, { privateUpload });
|
await persistMedia(file, { privateUpload, mediaFolder });
|
||||||
|
|
||||||
this.setState({ selectedFile: this.props.files[0] });
|
this.setState({ selectedFile: this.props.files[0] });
|
||||||
|
|
||||||
@ -190,8 +190,8 @@ class MediaLibrary extends React.Component {
|
|||||||
handleInsert = () => {
|
handleInsert = () => {
|
||||||
const { selectedFile } = this.state;
|
const { selectedFile } = this.state;
|
||||||
const { path } = selectedFile;
|
const { path } = selectedFile;
|
||||||
const { insertMedia } = this.props;
|
const { insertMedia, publicFolder } = this.props;
|
||||||
insertMedia(path);
|
insertMedia(path, publicFolder);
|
||||||
this.handleClose();
|
this.handleClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -332,6 +332,8 @@ const mapStateToProps = state => {
|
|||||||
page: mediaLibrary.get('page'),
|
page: mediaLibrary.get('page'),
|
||||||
hasNextPage: mediaLibrary.get('hasNextPage'),
|
hasNextPage: mediaLibrary.get('hasNextPage'),
|
||||||
isPaginating: mediaLibrary.get('isPaginating'),
|
isPaginating: mediaLibrary.get('isPaginating'),
|
||||||
|
mediaFolder: mediaLibrary.get('mediaFolder'),
|
||||||
|
publicFolder: mediaLibrary.get('publicFolder'),
|
||||||
};
|
};
|
||||||
return { ...mediaLibraryProps };
|
return { ...mediaLibraryProps };
|
||||||
};
|
};
|
||||||
|
@ -19,7 +19,7 @@ interface MediaLibrary {
|
|||||||
|
|
||||||
const initializeMediaLibrary = once(async function initializeMediaLibrary(name, options) {
|
const initializeMediaLibrary = once(async function initializeMediaLibrary(name, options) {
|
||||||
const lib = (getMediaLibrary(name) as unknown) as MediaLibrary;
|
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 });
|
const instance = await lib.init({ options, handleInsert });
|
||||||
store.dispatch(createMediaLibrary(instance));
|
store.dispatch(createMediaLibrary(instance));
|
||||||
});
|
});
|
||||||
|
@ -1,6 +1,12 @@
|
|||||||
import { OrderedMap, fromJS } from 'immutable';
|
import { OrderedMap, fromJS } from 'immutable';
|
||||||
import { configLoaded } from 'Actions/config';
|
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';
|
import { FILES, FOLDER } from 'Constants/collectionTypes';
|
||||||
|
|
||||||
describe('collections', () => {
|
describe('collections', () => {
|
||||||
@ -76,4 +82,159 @@ describe('collections', () => {
|
|||||||
).toBe('dir1/dir2/slug');
|
).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', () => {
|
describe('selectMediaFolder', () => {
|
||||||
it("should return global media folder when collection doesn't specify media_folder", () => {
|
it("should return global media folder when collection doesn't specify media_folder", () => {
|
||||||
expect(
|
expect(
|
||||||
selectMediaFolder(Map({ media_folder: 'static/media' }), Map({ name: 'posts' })),
|
selectMediaFolder(
|
||||||
|
Map({ media_folder: 'static/media' }),
|
||||||
|
Map({ name: 'posts' }),
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
),
|
||||||
).toEqual('static/media');
|
).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(
|
expect(
|
||||||
selectMediaFolder(
|
selectMediaFolder(
|
||||||
Map({ media_folder: 'static/media' }),
|
Map({ media_folder: 'static/media' }),
|
||||||
Map({ name: 'posts', folder: 'posts', media_folder: '' }),
|
Map({ name: 'posts', folder: 'posts', media_folder: '' }),
|
||||||
null,
|
undefined,
|
||||||
|
undefined,
|
||||||
),
|
),
|
||||||
).toEqual('posts/DRAFT_MEDIA_FILES');
|
).toEqual('posts/DRAFT_MEDIA_FILES');
|
||||||
});
|
});
|
||||||
@ -94,6 +100,7 @@ describe('entries', () => {
|
|||||||
Map({ media_folder: 'static/media' }),
|
Map({ media_folder: 'static/media' }),
|
||||||
Map({ name: 'posts', folder: 'posts', media_folder: '' }),
|
Map({ name: 'posts', folder: 'posts', media_folder: '' }),
|
||||||
Map({ path: 'posts/title/index.md' }),
|
Map({ path: 'posts/title/index.md' }),
|
||||||
|
undefined,
|
||||||
),
|
),
|
||||||
).toEqual('posts/title');
|
).toEqual('posts/title');
|
||||||
});
|
});
|
||||||
@ -104,11 +111,12 @@ describe('entries', () => {
|
|||||||
Map({ media_folder: 'static/media' }),
|
Map({ media_folder: 'static/media' }),
|
||||||
Map({ name: 'posts', folder: 'posts', media_folder: '../' }),
|
Map({ name: 'posts', folder: 'posts', media_folder: '../' }),
|
||||||
Map({ path: 'posts/title/index.md' }),
|
Map({ path: 'posts/title/index.md' }),
|
||||||
|
undefined,
|
||||||
),
|
),
|
||||||
).toEqual('posts/');
|
).toEqual('posts/');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return collection absolute media folder as is', () => {
|
it('should return collection absolute media folder without leading slash', () => {
|
||||||
expect(
|
expect(
|
||||||
selectMediaFolder(
|
selectMediaFolder(
|
||||||
Map({ media_folder: '/static/Images' }),
|
Map({ media_folder: '/static/Images' }),
|
||||||
@ -118,8 +126,9 @@ describe('entries', () => {
|
|||||||
media_folder: '/static/images/docs/getting-started',
|
media_folder: '/static/images/docs/getting-started',
|
||||||
}),
|
}),
|
||||||
Map({ path: 'src/docs/getting-started/with-github.md' }),
|
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', () => {
|
it('should compile relative media folder template', () => {
|
||||||
@ -145,6 +154,7 @@ describe('entries', () => {
|
|||||||
fromJS({ media_folder: 'static/media', slug: slugConfig }),
|
fromJS({ media_folder: 'static/media', slug: slugConfig }),
|
||||||
collection,
|
collection,
|
||||||
entry,
|
entry,
|
||||||
|
undefined,
|
||||||
),
|
),
|
||||||
).toEqual('static/media/hosting-and-deployment/deployment-with-nanobox');
|
).toEqual('static/media/hosting-and-deployment/deployment-with-nanobox');
|
||||||
});
|
});
|
||||||
@ -172,8 +182,85 @@ describe('entries', () => {
|
|||||||
fromJS({ media_folder: '/static/images', slug: slugConfig }),
|
fromJS({ media_folder: '/static/images', slug: slugConfig }),
|
||||||
collection,
|
collection,
|
||||||
entry,
|
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(
|
selectMediaFilePath(
|
||||||
Map({ media_folder: 'static/media' }),
|
Map({ media_folder: 'static/media' }),
|
||||||
Map({ name: 'posts', folder: 'posts' }),
|
Map({ name: 'posts', folder: 'posts' }),
|
||||||
null,
|
undefined,
|
||||||
'image.png',
|
'image.png',
|
||||||
|
undefined,
|
||||||
),
|
),
|
||||||
).toBe('static/media/image.png');
|
).toBe('static/media/image.png');
|
||||||
});
|
});
|
||||||
@ -200,8 +288,9 @@ describe('entries', () => {
|
|||||||
selectMediaFilePath(
|
selectMediaFilePath(
|
||||||
Map({ media_folder: 'static/media' }),
|
Map({ media_folder: 'static/media' }),
|
||||||
Map({ name: 'posts', folder: 'posts', media_folder: '' }),
|
Map({ name: 'posts', folder: 'posts', media_folder: '' }),
|
||||||
null,
|
undefined,
|
||||||
'image.png',
|
'image.png',
|
||||||
|
undefined,
|
||||||
),
|
),
|
||||||
).toBe('posts/DRAFT_MEDIA_FILES/image.png');
|
).toBe('posts/DRAFT_MEDIA_FILES/image.png');
|
||||||
});
|
});
|
||||||
@ -213,6 +302,19 @@ describe('entries', () => {
|
|||||||
Map({ name: 'posts', folder: 'posts', media_folder: '../../static/media/' }),
|
Map({ name: 'posts', folder: 'posts', media_folder: '../../static/media/' }),
|
||||||
Map({ path: 'posts/title/index.md' }),
|
Map({ path: 'posts/title/index.md' }),
|
||||||
'image.png',
|
'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');
|
).toBe('static/media/image.png');
|
||||||
});
|
});
|
||||||
@ -227,7 +329,13 @@ describe('entries', () => {
|
|||||||
|
|
||||||
it('should resolve path from public folder for collection with no media folder', () => {
|
it('should resolve path from public folder for collection with no media folder', () => {
|
||||||
expect(
|
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');
|
).toBe('/media/image.png');
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -237,6 +345,8 @@ describe('entries', () => {
|
|||||||
Map({ public_folder: '/media' }),
|
Map({ public_folder: '/media' }),
|
||||||
Map({ name: 'posts', folder: 'posts', public_folder: '' }),
|
Map({ name: 'posts', folder: 'posts', public_folder: '' }),
|
||||||
'image.png',
|
'image.png',
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
),
|
),
|
||||||
).toBe('image.png');
|
).toBe('image.png');
|
||||||
});
|
});
|
||||||
@ -247,11 +357,13 @@ describe('entries', () => {
|
|||||||
Map({ public_folder: '/media' }),
|
Map({ public_folder: '/media' }),
|
||||||
Map({ name: 'posts', folder: 'posts', public_folder: '../../static/media/' }),
|
Map({ name: 'posts', folder: 'posts', public_folder: '../../static/media/' }),
|
||||||
'image.png',
|
'image.png',
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
),
|
),
|
||||||
).toBe('../../static/media/image.png');
|
).toBe('../../static/media/image.png');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should compile public folder template', () => {
|
it('should compile collection public folder template', () => {
|
||||||
const slugConfig = {
|
const slugConfig = {
|
||||||
encoding: 'unicode',
|
encoding: 'unicode',
|
||||||
clean_accents: false,
|
clean_accents: false,
|
||||||
@ -275,8 +387,93 @@ describe('entries', () => {
|
|||||||
collection,
|
collection,
|
||||||
'image.png',
|
'image.png',
|
||||||
entry,
|
entry,
|
||||||
|
undefined,
|
||||||
),
|
),
|
||||||
).toEqual('/static/media/hosting-and-deployment/deployment-with-nanobox/image.png');
|
).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 { FILES, FOLDER } from '../constants/collectionTypes';
|
||||||
import { INFERABLE_FIELDS, IDENTIFIER_FIELDS } from '../constants/fieldInference';
|
import { INFERABLE_FIELDS, IDENTIFIER_FIELDS } from '../constants/fieldInference';
|
||||||
import { formatExtensions } from '../formats/formats';
|
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) => {
|
const collections = (state = null, action: CollectionsAction) => {
|
||||||
switch (action.type) {
|
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) =>
|
export const selectFields = (collection: Collection, slug: string) =>
|
||||||
selectors[collection.get('type')].fields(collection, slug);
|
selectors[collection.get('type')].fields(collection, slug);
|
||||||
export const selectFolderEntryExtension = (collection: Collection) =>
|
export const selectFolderEntryExtension = (collection: Collection) =>
|
||||||
|
@ -27,6 +27,7 @@ import {
|
|||||||
} from '../types/redux';
|
} from '../types/redux';
|
||||||
import { folderFormatter } from '../lib/formatters';
|
import { folderFormatter } from '../lib/formatters';
|
||||||
import { isAbsolutePath, basename } from 'netlify-cms-lib-util';
|
import { isAbsolutePath, basename } from 'netlify-cms-lib-util';
|
||||||
|
import { trimStart } from 'lodash';
|
||||||
|
|
||||||
let collection: string;
|
let collection: string;
|
||||||
let loadedEntries: EntryObject[];
|
let loadedEntries: EntryObject[];
|
||||||
@ -138,32 +139,67 @@ export const selectEntries = (state: Entries, collection: string) => {
|
|||||||
|
|
||||||
const DRAFT_MEDIA_FILES = 'DRAFT_MEDIA_FILES';
|
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 = (
|
export const selectMediaFolder = (
|
||||||
config: Config,
|
config: Config,
|
||||||
collection: Collection | null,
|
collection: Collection | null,
|
||||||
entryMap: EntryMap | undefined,
|
entryMap: EntryMap | undefined,
|
||||||
|
fieldMediaFolder: string | undefined,
|
||||||
) => {
|
) => {
|
||||||
let mediaFolder = config.get('media_folder');
|
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');
|
const entryPath = entryMap?.get('path');
|
||||||
if (entryPath) {
|
if (entryPath) {
|
||||||
const entryDir = dirname(entryPath);
|
const entryDir = dirname(entryPath);
|
||||||
const folder = folderFormatter(
|
const folder = folderFormatter(
|
||||||
collection.get('media_folder') as string,
|
customFolder,
|
||||||
entryMap as EntryMap,
|
entryMap as EntryMap,
|
||||||
collection,
|
collection!,
|
||||||
mediaFolder,
|
mediaFolder,
|
||||||
'media_folder',
|
'media_folder',
|
||||||
config.get('slug'),
|
config.get('slug'),
|
||||||
);
|
);
|
||||||
// return absolute paths as is
|
// return absolute paths as is without the leading '/'
|
||||||
if (folder.startsWith('/')) {
|
if (folder.startsWith('/')) {
|
||||||
return folder;
|
mediaFolder = join(trimStart(folder, '/'));
|
||||||
|
} else {
|
||||||
|
mediaFolder = join(entryDir, folder as string);
|
||||||
}
|
}
|
||||||
mediaFolder = join(entryDir, folder as string);
|
|
||||||
} else {
|
} 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,
|
collection: Collection | null,
|
||||||
entryMap: EntryMap | undefined,
|
entryMap: EntryMap | undefined,
|
||||||
mediaPath: string,
|
mediaPath: string,
|
||||||
|
fieldMediaFolder: string | undefined,
|
||||||
) => {
|
) => {
|
||||||
if (isAbsolutePath(mediaPath)) {
|
if (isAbsolutePath(mediaPath)) {
|
||||||
return mediaPath;
|
return mediaPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
const mediaFolder = selectMediaFolder(config, collection, entryMap);
|
const mediaFolder = selectMediaFolder(config, collection, entryMap, fieldMediaFolder);
|
||||||
|
|
||||||
return join(mediaFolder, basename(mediaPath));
|
return join(mediaFolder, basename(mediaPath));
|
||||||
};
|
};
|
||||||
@ -190,6 +227,7 @@ export const selectMediaFilePublicPath = (
|
|||||||
collection: Collection | null,
|
collection: Collection | null,
|
||||||
mediaPath: string,
|
mediaPath: string,
|
||||||
entryMap: EntryMap | undefined,
|
entryMap: EntryMap | undefined,
|
||||||
|
fieldPublicFolder: string | undefined,
|
||||||
) => {
|
) => {
|
||||||
if (isAbsolutePath(mediaPath)) {
|
if (isAbsolutePath(mediaPath)) {
|
||||||
return mediaPath;
|
return mediaPath;
|
||||||
@ -197,11 +235,18 @@ export const selectMediaFilePublicPath = (
|
|||||||
|
|
||||||
let publicFolder = config.get('public_folder');
|
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(
|
publicFolder = folderFormatter(
|
||||||
collection.get('public_folder') as string,
|
customFolder,
|
||||||
entryMap,
|
entryMap,
|
||||||
collection,
|
collection!,
|
||||||
publicFolder,
|
publicFolder,
|
||||||
'public_folder',
|
'public_folder',
|
||||||
config.get('slug'),
|
config.get('slug'),
|
||||||
|
@ -56,7 +56,14 @@ const mediaLibrary = (state = Map(defaultState), action: MediaLibraryAction) =>
|
|||||||
map.set('showMediaButton', action.payload.enableStandalone());
|
map.set('showMediaButton', action.payload.enableStandalone());
|
||||||
});
|
});
|
||||||
case MEDIA_LIBRARY_OPEN: {
|
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 libConfig = config || Map();
|
||||||
const privateUploadChanged = state.get('privateUpload') !== privateUpload;
|
const privateUploadChanged = state.get('privateUpload') !== privateUpload;
|
||||||
if (privateUploadChanged) {
|
if (privateUploadChanged) {
|
||||||
@ -77,6 +84,8 @@ const mediaLibrary = (state = Map(defaultState), action: MediaLibraryAction) =>
|
|||||||
map.set('canInsert', !!controlID);
|
map.set('canInsert', !!controlID);
|
||||||
map.set('privateUpload', privateUpload);
|
map.set('privateUpload', privateUpload);
|
||||||
map.set('config', libConfig);
|
map.set('config', libConfig);
|
||||||
|
map.set('mediaFolder', mediaFolder);
|
||||||
|
map.set('publicFolder', publicFolder);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
case MEDIA_LIBRARY_CLOSE:
|
case MEDIA_LIBRARY_CLOSE:
|
||||||
|
@ -94,6 +94,8 @@ export type EntryField = StaticallyTypedRecord<{
|
|||||||
widget: string;
|
widget: string;
|
||||||
name: string;
|
name: string;
|
||||||
default: string | null;
|
default: string | null;
|
||||||
|
media_folder?: string;
|
||||||
|
public_folder?: string;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
export type EntryFields = List<EntryField>;
|
export type EntryFields = List<EntryField>;
|
||||||
@ -108,6 +110,8 @@ export type CollectionFile = StaticallyTypedRecord<{
|
|||||||
name: string;
|
name: string;
|
||||||
fields: EntryFields;
|
fields: EntryFields;
|
||||||
label: string;
|
label: string;
|
||||||
|
media_folder?: string;
|
||||||
|
public_folder?: string;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
export type CollectionFiles = List<CollectionFile>;
|
export type CollectionFiles = List<CollectionFile>;
|
||||||
@ -305,6 +309,8 @@ export interface MediaLibraryAction extends Action<string> {
|
|||||||
forImage: boolean;
|
forImage: boolean;
|
||||||
privateUpload: boolean;
|
privateUpload: boolean;
|
||||||
config: Map<string, string>;
|
config: Map<string, string>;
|
||||||
|
mediaFolder?: string;
|
||||||
|
publicFolder?: string;
|
||||||
} & { mediaPath: string | string[] } & { page: number } & {
|
} & { mediaPath: string | string[] } & { page: number } & {
|
||||||
files: MediaFile[];
|
files: MediaFile[];
|
||||||
page: number;
|
page: number;
|
||||||
|
@ -2,17 +2,20 @@ interface AssetProxyArgs {
|
|||||||
path: string;
|
path: string;
|
||||||
url?: string;
|
url?: string;
|
||||||
file?: File;
|
file?: File;
|
||||||
|
folder?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class AssetProxy {
|
export default class AssetProxy {
|
||||||
url: string;
|
url: string;
|
||||||
fileObj?: File;
|
fileObj?: File;
|
||||||
path: string;
|
path: string;
|
||||||
|
folder?: string;
|
||||||
|
|
||||||
constructor({ url, file, path }: AssetProxyArgs) {
|
constructor({ url, file, path, folder }: AssetProxyArgs) {
|
||||||
this.url = url ? url : window.URL.createObjectURL(file);
|
this.url = url ? url : window.URL.createObjectURL(file);
|
||||||
this.fileObj = file;
|
this.fileObj = file;
|
||||||
this.path = path;
|
this.path = path;
|
||||||
|
this.folder = folder;
|
||||||
}
|
}
|
||||||
|
|
||||||
toString(): string {
|
toString(): string {
|
||||||
@ -35,6 +38,6 @@ export default class AssetProxy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createAssetProxy({ url, file, path }: AssetProxyArgs): AssetProxy {
|
export function createAssetProxy({ url, file, path, folder }: AssetProxyArgs): AssetProxy {
|
||||||
return new AssetProxy({ url, file, path });
|
return new AssetProxy({ url, file, path, folder });
|
||||||
}
|
}
|
||||||
|
@ -12,8 +12,10 @@ const image = {
|
|||||||
toBlock: ({ alt, image, title }) =>
|
toBlock: ({ alt, image, title }) =>
|
||||||
`}"` : ''})`,
|
`}"` : ''})`,
|
||||||
// eslint-disable-next-line react/display-name
|
// eslint-disable-next-line react/display-name
|
||||||
toPreview: async ({ alt, image, title }, getAsset) => {
|
toPreview: async ({ alt, image, title }, getAsset, fields) => {
|
||||||
const src = await getAsset(image);
|
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 || ''} />;
|
return <img src={src || ''} alt={alt || ''} title={title || ''} />;
|
||||||
},
|
},
|
||||||
pattern: /^!\[(.*)\]\((.*?)(\s"(.*)")?\)$/,
|
pattern: /^!\[(.*)\]\((.*?)(\s"(.*)")?\)$/,
|
||||||
|
@ -18,6 +18,7 @@ export interface ImplementationMediaFile {
|
|||||||
draft?: boolean;
|
draft?: boolean;
|
||||||
url?: string;
|
url?: string;
|
||||||
file?: File;
|
file?: File;
|
||||||
|
folder?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UnpublishedEntryMediaFile {
|
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) => {
|
export const getMediaAsBlob = async (path: string, id: string | null, readFile: ReadFile) => {
|
||||||
let blob: Blob;
|
let blob: Blob;
|
||||||
if (path.match(/.svg$/)) {
|
if (path.match(/.svg$/)) {
|
||||||
|
@ -36,6 +36,7 @@ import {
|
|||||||
runWithLock,
|
runWithLock,
|
||||||
Config as C,
|
Config as C,
|
||||||
UnpublishedEntryMediaFile as UEMF,
|
UnpublishedEntryMediaFile as UEMF,
|
||||||
|
blobToFileObj,
|
||||||
} from './implementation';
|
} from './implementation';
|
||||||
import {
|
import {
|
||||||
readFile,
|
readFile,
|
||||||
@ -136,6 +137,7 @@ export const NetlifyCmsLibUtil = {
|
|||||||
getPointerFileForMediaFileObj,
|
getPointerFileForMediaFileObj,
|
||||||
branchFromContentKey,
|
branchFromContentKey,
|
||||||
contentKeyFromBranch,
|
contentKeyFromBranch,
|
||||||
|
blobToFileObj,
|
||||||
};
|
};
|
||||||
export {
|
export {
|
||||||
APIError,
|
APIError,
|
||||||
@ -186,4 +188,5 @@ export {
|
|||||||
getPointerFileForMediaFileObj,
|
getPointerFileForMediaFileObj,
|
||||||
branchFromContentKey,
|
branchFromContentKey,
|
||||||
contentKeyFromBranch,
|
contentKeyFromBranch,
|
||||||
|
blobToFileObj,
|
||||||
};
|
};
|
||||||
|
@ -6,6 +6,7 @@ class Asset extends React.Component {
|
|||||||
path: PropTypes.string.isRequired,
|
path: PropTypes.string.isRequired,
|
||||||
getAsset: PropTypes.func.isRequired,
|
getAsset: PropTypes.func.isRequired,
|
||||||
component: PropTypes.elementType.isRequired,
|
component: PropTypes.elementType.isRequired,
|
||||||
|
folder: PropTypes.string,
|
||||||
};
|
};
|
||||||
|
|
||||||
subscribed = true;
|
subscribed = true;
|
||||||
@ -14,13 +15,12 @@ class Asset extends React.Component {
|
|||||||
value: null,
|
value: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
_fetchAsset() {
|
async _fetchAsset() {
|
||||||
const { getAsset, path } = this.props;
|
const { getAsset, path, folder } = this.props;
|
||||||
getAsset(path).then(value => {
|
const value = await getAsset(path, folder);
|
||||||
if (this.subscribed) {
|
if (this.subscribed) {
|
||||||
this.setState({ value });
|
this.setState({ value });
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
@ -32,7 +32,11 @@ class Asset extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
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();
|
this._fetchAsset();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -37,8 +37,8 @@ const Image = styled(({ value: src }) => <img src={src || ''} role="presentation
|
|||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const ImageAsset = ({ getAsset, value }) => {
|
const ImageAsset = ({ getAsset, value, folder }) => {
|
||||||
return <Asset path={value} getAsset={getAsset} component={Image} />;
|
return <Asset folder={folder} path={value} getAsset={getAsset} component={Image} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
const MultiImageWrapper = styled.div`
|
const MultiImageWrapper = styled.div`
|
||||||
@ -175,6 +175,8 @@ export default function withFileControl({ forImage } = {}) {
|
|||||||
value,
|
value,
|
||||||
allowMultiple: !!mediaLibraryFieldOptions.get('allow_multiple', true),
|
allowMultiple: !!mediaLibraryFieldOptions.get('allow_multiple', true),
|
||||||
config: mediaLibraryFieldOptions.get('config'),
|
config: mediaLibraryFieldOptions.get('config'),
|
||||||
|
mediaFolder: field.get('media_folder'),
|
||||||
|
publicFolder: field.get('public_folder'),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -218,13 +220,15 @@ export default function withFileControl({ forImage } = {}) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
renderImages = () => {
|
renderImages = () => {
|
||||||
const { getAsset, value } = this.props;
|
const { getAsset, value, field } = this.props;
|
||||||
|
const folder = field.get('media_folder');
|
||||||
|
|
||||||
if (isMultiple(value)) {
|
if (isMultiple(value)) {
|
||||||
return (
|
return (
|
||||||
<MultiImageWrapper>
|
<MultiImageWrapper>
|
||||||
{value.map(val => (
|
{value.map(val => (
|
||||||
<ImageWrapper key={val}>
|
<ImageWrapper key={val}>
|
||||||
<ImageAsset getAsset={getAsset} value={val} />
|
<ImageAsset getAsset={getAsset} value={val} folder={folder} />
|
||||||
</ImageWrapper>
|
</ImageWrapper>
|
||||||
))}
|
))}
|
||||||
</MultiImageWrapper>
|
</MultiImageWrapper>
|
||||||
@ -232,7 +236,7 @@ export default function withFileControl({ forImage } = {}) {
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<ImageWrapper>
|
<ImageWrapper>
|
||||||
<ImageAsset getAsset={getAsset} value={value} />
|
<ImageAsset getAsset={getAsset} value={value} folder={folder} />
|
||||||
</ImageWrapper>
|
</ImageWrapper>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -10,14 +10,23 @@ const StyledImage = styled(({ value: src }) => <img src={src || ''} role="presen
|
|||||||
height: auto;
|
height: auto;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledImageAsset = ({ getAsset, value }) => {
|
const StyledImageAsset = ({ getAsset, value, field }) => {
|
||||||
return <Asset path={value} getAsset={getAsset} component={StyledImage} />;
|
return (
|
||||||
|
<Asset
|
||||||
|
folder={field.get('media_folder')}
|
||||||
|
path={value}
|
||||||
|
getAsset={getAsset}
|
||||||
|
component={StyledImage}
|
||||||
|
/>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const ImagePreviewContent = props => {
|
const ImagePreviewContent = props => {
|
||||||
const { value, getAsset } = props;
|
const { value, getAsset, field } = props;
|
||||||
if (Array.isArray(value) || List.isList(value)) {
|
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} />;
|
return <StyledImageAsset {...props} />;
|
||||||
};
|
};
|
||||||
|
@ -49,6 +49,38 @@ const createSlateValue = (rawValue, { voidCodeBlock }) => {
|
|||||||
return Value.create({ document });
|
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 {
|
export default class Editor extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
@ -59,6 +91,8 @@ export default class Editor extends React.Component {
|
|||||||
this.codeBlockComponent || editorComponents.has('code-block')
|
this.codeBlockComponent || editorComponents.has('code-block')
|
||||||
? editorComponents
|
? editorComponents
|
||||||
: editorComponents.set('code-block', { label: 'Code Block', type: 'code-block' });
|
: editorComponents.set('code-block', { label: 'Code Block', type: 'code-block' });
|
||||||
|
|
||||||
|
mergeMediaConfig(this.editorComponents, this.props.field);
|
||||||
this.renderBlock = renderBlock({
|
this.renderBlock = renderBlock({
|
||||||
classNameWrapper: props.className,
|
classNameWrapper: props.className,
|
||||||
resolveWidget: props.resolveWidget,
|
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.
|
* Retrieve the shortcode preview component.
|
||||||
*/
|
*/
|
||||||
async function getPreview(plugin, shortcodeData) {
|
async function getPreview(plugin, shortcodeData) {
|
||||||
const { toPreview, widget } = plugin;
|
const { toPreview, widget, fields } = plugin;
|
||||||
if (toPreview) {
|
if (toPreview) {
|
||||||
return toPreview(shortcodeData, getAsset);
|
return toPreview(shortcodeData, getAsset, fields);
|
||||||
}
|
}
|
||||||
const preview = resolveWidget(widget);
|
const preview = resolveWidget(widget);
|
||||||
return React.createElement(preview.preview, {
|
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"
|
resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-5.0.0.tgz#3871cc0a6a002e8c3e5b3cf7f336264675f06b9d"
|
||||||
integrity sha1-OHHMCmoALow+Wzz38zYmRnXwa50=
|
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:
|
detect-newline@^2.1.0:
|
||||||
version "2.1.0"
|
version "2.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-2.1.0.tgz#f41f1c10be4b00e87b5f13da680759f2c5bfd3e2"
|
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"
|
run-node "^1.0.0"
|
||||||
slash "^3.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"
|
version "0.4.24"
|
||||||
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
|
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
|
||||||
integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==
|
integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==
|
||||||
@ -11802,15 +11797,6 @@ ncp@^2.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/ncp/-/ncp-2.0.0.tgz#195a21d6c46e361d2fb1281ba38b91e9df7bdbb3"
|
resolved "https://registry.yarnpkg.com/ncp/-/ncp-2.0.0.tgz#195a21d6c46e361d2fb1281ba38b91e9df7bdbb3"
|
||||||
integrity sha1-GVoh1sRuNh0vsSgbo4uR6d9727M=
|
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:
|
negotiator@0.6.2:
|
||||||
version "0.6.2"
|
version "0.6.2"
|
||||||
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb"
|
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"
|
string.prototype.trim "^1.1.2"
|
||||||
warning "^4.0.3"
|
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:
|
node-releases@^1.1.29, node-releases@^1.1.47:
|
||||||
version "1.1.47"
|
version "1.1.47"
|
||||||
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.47.tgz#c59ef739a1fd7ecbd9f0b7cf5b7871e8a8b591e4"
|
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"
|
semver "^5.6.0"
|
||||||
validate-npm-package-name "^3.0.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"
|
version "1.4.8"
|
||||||
resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-1.4.8.tgz#56ee6cc135b9f98ad3d51c1c95da22bbb9b2ef3e"
|
resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-1.4.8.tgz#56ee6cc135b9f98ad3d51c1c95da22bbb9b2ef3e"
|
||||||
integrity sha512-5+AZgwru5IevF5ZdnFglB5wNlHG1AOOuw28WhUq8/8emhBmLv6jX5by4WJCh7lW0uSYZYS6DXqIsyZVIXRZU9A==
|
integrity sha512-5+AZgwru5IevF5ZdnFglB5wNlHG1AOOuw28WhUq8/8emhBmLv6jX5by4WJCh7lW0uSYZYS6DXqIsyZVIXRZU9A==
|
||||||
@ -12158,7 +12128,7 @@ npm-run-path@^4.0.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
path-key "^3.0.0"
|
path-key "^3.0.0"
|
||||||
|
|
||||||
npmlog@^4.0.2, npmlog@^4.1.2:
|
npmlog@^4.1.2:
|
||||||
version "4.1.2"
|
version "4.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b"
|
resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b"
|
||||||
integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==
|
integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==
|
||||||
@ -13637,7 +13607,7 @@ rbush@2.0.2:
|
|||||||
dependencies:
|
dependencies:
|
||||||
quickselect "^1.0.1"
|
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"
|
version "1.2.8"
|
||||||
resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed"
|
resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed"
|
||||||
integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==
|
integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==
|
||||||
@ -14793,7 +14763,7 @@ rimraf@2.6.3:
|
|||||||
dependencies:
|
dependencies:
|
||||||
glob "^7.1.3"
|
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"
|
version "2.7.1"
|
||||||
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec"
|
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec"
|
||||||
integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==
|
integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==
|
||||||
@ -14984,7 +14954,7 @@ semver-diff@^2.0.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
semver "^5.0.3"
|
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"
|
version "5.7.1"
|
||||||
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
|
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
|
||||||
integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
|
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"
|
resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.3.tgz#a1fccc06b58db61fd7a45da2da44f5f3a3e67ba2"
|
||||||
integrity sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==
|
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"
|
version "4.4.13"
|
||||||
resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.13.tgz#43b364bc52888d555298637b10d60790254ab525"
|
resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.13.tgz#43b364bc52888d555298637b10d60790254ab525"
|
||||||
integrity sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA==
|
integrity sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA==
|
||||||
|
Loading…
x
Reference in New Issue
Block a user