Skip to content

Commit

Permalink
Fix: show specific field media files in library, cascade folder templ…
Browse files Browse the repository at this point in the history
…ates (#3252)

* feat: cascade & compose media folders - initial commit

* refactor: code cleanup

* fix: pass field instead of folder to getAsset

* fix: only show field media files in library

* test: fix medial library selector test

* fix: fallback to original path when asset not found

* fix: only show field media files in media library

* fix: properly handle empty strings in field folders
  • Loading branch information
erezrokah committed Feb 14, 2020
1 parent 8d67de0 commit 02ef201
Show file tree
Hide file tree
Showing 22 changed files with 565 additions and 243 deletions.
21 changes: 21 additions & 0 deletions packages/netlify-cms-core/src/actions/__tests__/media.js
Original file line number Diff line number Diff line change
Expand Up @@ -105,5 +105,26 @@ describe('media', () => {
});
expect(result).toEqual(emptyAsset);
});

it('should return asset with original path on load error', () => {
const path = 'static/media/image.png';
const store = mockStore({
medias: Map({ [path]: { error: true } }),
});

selectMediaFilePath.mockReturnValue(path);
const payload = { path };

const result = store.dispatch(getAsset(payload));
const actions = store.getActions();

const asset = new AssetProxy({ url: path, path });
expect(actions).toHaveLength(1);
expect(actions[0]).toEqual({
type: ADD_ASSET,
payload: asset,
});
expect(result).toEqual(asset);
});
});
});
13 changes: 8 additions & 5 deletions packages/netlify-cms-core/src/actions/entries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -297,14 +297,14 @@ export function retrieveLocalBackup(collection: Collection, slug: string) {
path: file.path,
file: file.file,
url: file.url,
folder: file.folder,
field: file.field,
});
} else {
return getAsset({
collection,
entry: fromJS(entry),
path: file.path,
folder: file.folder,
field: file.field,
})(dispatch, getState);
}
}),
Expand Down Expand Up @@ -557,12 +557,15 @@ export async function getMediaAssets({
entry: EntryMap;
dispatch: Dispatch;
}) {
const filesArray = entry.get('mediaFiles').toJS() as MediaFile[];
const filesArray = entry.get('mediaFiles').toArray();
const assets = await Promise.all(
filesArray
.filter(file => file.draft)
.filter(file => file.get('draft'))
.map(file =>
getAsset({ collection, entry, path: file.path, folder: file.folder })(dispatch, getState),
getAsset({ collection, entry, path: file.get('path'), field: file.get('field') })(
dispatch,
getState,
),
),
);

Expand Down
25 changes: 16 additions & 9 deletions packages/netlify-cms-core/src/actions/media.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import AssetProxy, { createAssetProxy } from '../valueObjects/AssetProxy';
import { Collection, State, EntryMap } from '../types/redux';
import { Collection, State, EntryMap, EntryField } from '../types/redux';
import { ThunkDispatch } from 'redux-thunk';
import { AnyAction } from 'redux';
import { isAbsolutePath } from 'netlify-cms-lib-util';
Expand Down Expand Up @@ -67,7 +67,7 @@ interface GetAssetArgs {
collection: Collection;
entry: EntryMap;
path: string;
folder?: string;
field?: EntryField;
}

const emptyAsset = createAssetProxy({
Expand All @@ -82,26 +82,27 @@ export function boundGetAsset(
collection: Collection,
entry: EntryMap,
) {
const bound = (path: string, folder: string) => {
const asset = dispatch(getAsset({ collection, entry, path, folder }));
const bound = (path: string, field: EntryField) => {
const asset = dispatch(getAsset({ collection, entry, path, field }));
return asset;
};

return bound;
}

export function getAsset({ collection, entry, path, folder }: GetAssetArgs) {
export function getAsset({ collection, entry, path, field }: GetAssetArgs) {
return (dispatch: ThunkDispatch<State, {}, AnyAction>, getState: () => State) => {
if (!path) return emptyAsset;

const state = getState();
const resolvedPath = selectMediaFilePath(state.config, collection, entry, path, folder);
const resolvedPath = selectMediaFilePath(state.config, collection, entry, path, field);

let { asset, isLoading, error } = state.medias.get(resolvedPath) || {};
if (isLoading) {
return emptyAsset;
}
if (asset && !error) {

if (asset) {
// There is already an AssetProxy in memory for this path. Use it.
return asset;
}
Expand All @@ -111,8 +112,14 @@ export function getAsset({ collection, entry, path, folder }: GetAssetArgs) {
asset = createAssetProxy({ path: resolvedPath, url: path });
dispatch(addAsset(asset));
} else {
dispatch(loadAsset(resolvedPath));
asset = emptyAsset;
if (error) {
// on load error default back to original path
asset = createAssetProxy({ path, url: path });
dispatch(addAsset(asset));
} else {
dispatch(loadAsset(resolvedPath));
asset = emptyAsset;
}
}

return asset;
Expand Down
40 changes: 18 additions & 22 deletions packages/netlify-cms-core/src/actions/mediaLibrary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,13 @@ import { getIntegrationProvider } from '../integrations';
import { addAsset, removeAsset } from './media';
import { addDraftEntryMediaFile, removeDraftEntryMediaFile } from './entries';
import { sanitizeSlug } from '../lib/urlHelper';
import { State, MediaFile, DisplayURLState, MediaLibraryInstance } from '../types/redux';
import {
State,
MediaFile,
DisplayURLState,
MediaLibraryInstance,
EntryField,
} from '../types/redux';
import { AnyAction } from 'redux';
import { ThunkDispatch } from 'redux-thunk';
import { waitUntilWithTimeout } from './waitUntil';
Expand Down Expand Up @@ -103,7 +109,7 @@ export function closeMediaLibrary() {
};
}

export function insertMedia(mediaPath: string | string[], publicFolder: string | undefined) {
export function insertMedia(mediaPath: string | string[], field: EntryField | undefined) {
return (dispatch: ThunkDispatch<State, {}, AnyAction>, getState: () => State) => {
const state = getState();
const config = state.config;
Expand All @@ -112,16 +118,10 @@ export function insertMedia(mediaPath: string | string[], publicFolder: string |
const collection = state.collections.get(collectionName);
if (Array.isArray(mediaPath)) {
mediaPath = mediaPath.map(path =>
selectMediaFilePublicPath(config, collection, path, entry, publicFolder),
selectMediaFilePublicPath(config, collection, path, entry, field),
);
} else {
mediaPath = selectMediaFilePublicPath(
config,
collection,
mediaPath as string,
entry,
publicFolder,
);
mediaPath = selectMediaFilePublicPath(config, collection, mediaPath as string, entry, field);
}
dispatch({ type: MEDIA_INSERT, payload: { mediaPath } });
};
Expand Down Expand Up @@ -201,18 +201,18 @@ function createMediaFileFromAsset({
size: file.size,
url: assetProxy.url,
path: assetProxy.path,
folder: assetProxy.folder,
field: assetProxy.field,
};
return mediaFile;
}

export function persistMedia(file: File, opts: MediaOptions = {}) {
const { privateUpload, mediaFolder } = opts;
const { privateUpload, field } = opts;
return async (dispatch: ThunkDispatch<State, {}, AnyAction>, getState: () => State) => {
const state = getState();
const backend = currentBackend(state.config);
const integration = selectIntegration(state, null, 'assetStore');
const files: MediaFile[] = selectMediaFiles(state);
const files: MediaFile[] = selectMediaFiles(state, field);
const fileName = sanitizeSlug(file.name.toLowerCase(), state.config.get('slug'));
const existingFile = files.find(existingFile => existingFile.name.toLowerCase() === fileName);

Expand Down Expand Up @@ -261,11 +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, mediaFolder);
const path = selectMediaFilePath(state.config, collection, entry, file.name, field);
assetProxy = createAssetProxy({
file,
path,
folder: mediaFolder,
field,
});
}

Expand Down Expand Up @@ -358,12 +358,8 @@ export function deleteMedia(file: MediaFile, opts: MediaOptions = {}) {

export async function getMediaFile(state: State, path: string) {
const backend = currentBackend(state.config);
try {
const { url } = await backend.getMediaFile(path);
return { url };
} catch (e) {
return { url: path };
}
const { url } = await backend.getMediaFile(path);
return { url };
}

export function loadMediaDisplayURL(file: MediaFile) {
Expand Down Expand Up @@ -409,7 +405,7 @@ export function mediaLoading(page: number) {

interface MediaOptions {
privateUpload?: boolean;
mediaFolder?: string;
field?: EntryField;
}

export function mediaLoaded(files: ImplementationMediaFile[], opts: MediaOptions = {}) {
Expand Down
23 changes: 17 additions & 6 deletions packages/netlify-cms-core/src/backend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ import {
Implementation as BackendImplementation,
DisplayURL,
ImplementationEntry,
ImplementationMediaFile,
Credentials,
User,
getPathDepth,
Expand All @@ -45,6 +44,7 @@ import {
EntryDraft,
CollectionFile,
State,
EntryField,
} from './types/redux';
import AssetProxy from './valueObjects/AssetProxy';
import { FOLDER, FILES } from './constants/collectionTypes';
Expand Down Expand Up @@ -104,10 +104,22 @@ interface BackendOptions {
config?: Config;
}

export interface MediaFile {
name: string;
id: string;
size?: number;
displayURL?: DisplayURL;
path: string;
draft?: boolean;
url?: string;
file?: File;
field?: EntryField;
}

interface BackupEntry {
raw: string;
path: string;
mediaFiles: ImplementationMediaFile[];
mediaFiles: MediaFile[];
}

interface PersistArgs {
Expand Down Expand Up @@ -444,11 +456,11 @@ export class Backend {
return;
}

const mediaFiles = await Promise.all<ImplementationMediaFile>(
const mediaFiles = await Promise.all<MediaFile>(
entry
.get('mediaFiles')
.toJS()
.map(async (file: ImplementationMediaFile) => {
.map(async (file: MediaFile) => {
// make sure to serialize the file
if (file.url?.startsWith('blob:')) {
const blob = await fetch(file.url as string).then(res => res.blob());
Expand Down Expand Up @@ -485,7 +497,6 @@ export class Backend {
const integration = selectIntegration(state.integrations, null, 'assetStore');

const loadedEntry = await this.implementation.getEntry(path);

const entry = createEntry(collection.get('name'), slug, loadedEntry.file.path, {
raw: loadedEntry.data,
label,
Expand Down Expand Up @@ -700,7 +711,7 @@ export class Backend {
collection,
entryDraft.get('entry').set('path', path),
oldPath,
asset.folder,
asset.field,
);
asset.path = newPath;
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ const EntryCard = ({
path,
summary,
image,
imageFolder,
imageField,
collectionLabel,
viewStyle = VIEW_STYLE_LIST,
getAsset,
Expand All @@ -105,7 +105,7 @@ const EntryCard = ({
);
}

const asset = getAsset(image, imageFolder);
const asset = getAsset(image, imageField);
const src = asset.toString();

if (viewStyle === VIEW_STYLE_GRID) {
Expand Down Expand Up @@ -148,8 +148,7 @@ const mapStateToProps = (state, ownProps) => {
image,
imageFolder: collection
.get('fields')
?.find(f => f.get('name') === inferedFields.imageField && f.get('widget') === 'image')
?.get('media_folder'),
?.find(f => f.get('name') === inferedFields.imageField && f.get('widget') === 'image'),
isLoadingAsset,
};
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ class MediaLibrary extends React.Component {
event.persist();
event.stopPropagation();
event.preventDefault();
const { persistMedia, privateUpload, config, t, mediaFolder } = this.props;
const { persistMedia, privateUpload, config, t, field } = this.props;
const { files: fileList } = event.dataTransfer || event.target;
const files = [...fileList];
const file = files[0];
Expand All @@ -173,7 +173,7 @@ class MediaLibrary extends React.Component {
}),
);
} else {
await persistMedia(file, { privateUpload, mediaFolder });
await persistMedia(file, { privateUpload, field });

this.setState({ selectedFile: this.props.files[0] });

Expand All @@ -190,8 +190,8 @@ class MediaLibrary extends React.Component {
handleInsert = () => {
const { selectedFile } = this.state;
const { path } = selectedFile;
const { insertMedia, publicFolder } = this.props;
insertMedia(path, publicFolder);
const { insertMedia, field } = this.props;
insertMedia(path, field);
this.handleClose();
};

Expand Down Expand Up @@ -315,10 +315,11 @@ class MediaLibrary extends React.Component {

const mapStateToProps = state => {
const { mediaLibrary } = state;
const field = mediaLibrary.get('field');
const mediaLibraryProps = {
isVisible: mediaLibrary.get('isVisible'),
canInsert: mediaLibrary.get('canInsert'),
files: selectMediaFiles(state),
files: selectMediaFiles(state, field),
displayURLs: mediaLibrary.get('displayURLs'),
dynamicSearch: mediaLibrary.get('dynamicSearch'),
dynamicSearchActive: mediaLibrary.get('dynamicSearchActive'),
Expand All @@ -332,8 +333,7 @@ const mapStateToProps = state => {
page: mediaLibrary.get('page'),
hasNextPage: mediaLibrary.get('hasNextPage'),
isPaginating: mediaLibrary.get('isPaginating'),
mediaFolder: mediaLibrary.get('mediaFolder'),
publicFolder: mediaLibrary.get('publicFolder'),
field,
};
return { ...mediaLibraryProps };
};
Expand Down
2 changes: 2 additions & 0 deletions packages/netlify-cms-core/src/lib/formatters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,7 @@ export const folderFormatter = (
if (!entry || !entry.get('data')) {
return folderTemplate;
}

let fields = (entry.get('data') as Map<string, string>).set(folderKey, defaultFolder);
fields = addFileTemplateFields(entry.get('path'), fields);

Expand All @@ -232,5 +233,6 @@ export const folderFormatter = (
fields,
(value: string) => (value === defaultFolder ? defaultFolder : processSegment(value)),
);

return mediaFolder;
};

0 comments on commit 02ef201

Please sign in to comment.