fix(gitlab): fetch media library images through API (#1433)

This commit is contained in:
Benaiah Mischenko
2018-08-22 12:28:52 -07:00
committed by Shawn Erquhart
parent a4ba66e1a6
commit 83d2adc0be
11 changed files with 225 additions and 35 deletions

View File

@ -21,6 +21,9 @@ export const MEDIA_PERSIST_FAILURE = 'MEDIA_PERSIST_FAILURE';
export const MEDIA_DELETE_REQUEST = 'MEDIA_DELETE_REQUEST';
export const MEDIA_DELETE_SUCCESS = 'MEDIA_DELETE_SUCCESS';
export const MEDIA_DELETE_FAILURE = 'MEDIA_DELETE_FAILURE';
export const MEDIA_DISPLAY_URL_REQUEST = 'MEDIA_DISPLAY_URL_REQUEST';
export const MEDIA_DISPLAY_URL_SUCCESS = 'MEDIA_DISPLAY_URL_SUCCESS';
export const MEDIA_DISPLAY_URL_FAILURE = 'MEDIA_DISPLAY_URL_FAILURE';
export function openMediaLibrary(payload) {
return { type: MEDIA_LIBRARY_OPEN, payload };
@ -169,6 +172,24 @@ export function deleteMedia(file, opts = {}) {
};
}
export function loadMediaDisplayURL(file) {
return async dispatch => {
const { getBlobPromise, id } = file;
if (id && getBlobPromise) {
try {
dispatch(mediaDisplayURLRequest(id));
const blob = await getBlobPromise();
const newURL = window.URL.createObjectURL(blob);
dispatch(mediaDisplayURLSuccess(id, newURL));
return newURL;
} catch (err) {
dispatch(mediaDisplayURLFailure(id, err));
}
}
};
}
export function mediaLoading(page) {
return {
type: MEDIA_LOAD_REQUEST,
@ -221,3 +242,21 @@ export function mediaDeleteFailed(error, opts = {}) {
const { privateUpload } = opts;
return { type: MEDIA_DELETE_FAILURE, payload: { privateUpload } };
}
export function mediaDisplayURLRequest(key) {
return { type: MEDIA_DISPLAY_URL_REQUEST, payload: { key } };
}
export function mediaDisplayURLSuccess(key, url) {
return {
type: MEDIA_DISPLAY_URL_SUCCESS,
payload: { key, url },
};
}
export function mediaDisplayURLFailure(key, err) {
return {
type: MEDIA_DISPLAY_URL_FAILURE,
payload: { key, err },
};
}

View File

@ -1,6 +1,7 @@
import React from 'react';
import { connect } from 'react-redux';
import { orderBy, map } from 'lodash';
import { Map } from 'immutable';
import fuzzy from 'fuzzy';
import { resolvePath, fileExtension } from 'netlify-cms-lib-util';
import {
@ -8,6 +9,7 @@ import {
persistMedia as persistMediaAction,
deleteMedia as deleteMediaAction,
insertMedia as insertMediaAction,
loadMediaDisplayURL as loadMediaDisplayURLAction,
closeMediaLibrary as closeMediaLibraryAction,
} from 'Actions/mediaLibrary';
import MediaLibraryModal from './MediaLibraryModal';
@ -53,6 +55,30 @@ class MediaLibrary extends React.Component {
}
}
getDisplayURL = file => {
const { isVisible, loadMediaDisplayURL, displayURLs } = this.props;
if (!isVisible) {
return '';
}
if (file && file.url) {
return file.url;
}
const { url, isFetching } = displayURLs.get(file.id, Map()).toObject();
if (url && url !== '') {
return url;
}
if (!isFetching) {
loadMediaDisplayURL(file);
}
return '';
};
/**
* Filter an array of file data to include only images.
*/
@ -69,16 +95,18 @@ class MediaLibrary extends React.Component {
toTableData = files => {
const tableData =
files &&
files.map(({ key, name, size, queryOrder, url, urlIsPublicPath }) => {
files.map(({ key, name, id, size, queryOrder, url, urlIsPublicPath, getBlobPromise }) => {
const ext = fileExtension(name).toLowerCase();
return {
key,
id,
name,
type: ext.toUpperCase(),
size,
queryOrder,
url,
urlIsPublicPath,
getBlobPromise,
isImage: IMAGE_EXTENSIONS.includes(ext),
isViewableImage: IMAGE_EXTENSIONS_VIEWABLE.includes(ext),
};
@ -251,6 +279,7 @@ class MediaLibrary extends React.Component {
setScrollContainerRef={ref => (this.scrollContainerRef = ref)}
handleAssetClick={this.handleAssetClick}
handleLoadMore={this.handleLoadMore}
getDisplayURL={this.getDisplayURL}
/>
);
}
@ -265,6 +294,7 @@ const mapStateToProps = state => {
isVisible: mediaLibrary.get('isVisible'),
canInsert: mediaLibrary.get('canInsert'),
files: mediaLibrary.get('files'),
displayURLs: mediaLibrary.get('displayURLs'),
dynamicSearch: mediaLibrary.get('dynamicSearch'),
dynamicSearchActive: mediaLibrary.get('dynamicSearchActive'),
dynamicSearchQuery: mediaLibrary.get('dynamicSearchQuery'),
@ -285,6 +315,7 @@ const mapDispatchToProps = {
persistMedia: persistMediaAction,
deleteMedia: deleteMediaAction,
insertMedia: insertMediaAction,
loadMediaDisplayURL: loadMediaDisplayURLAction,
closeMediaLibrary: closeMediaLibraryAction,
};

View File

@ -36,7 +36,7 @@ const CardText = styled.p`
line-height: 1.3 !important;
`;
const MediaLibraryCard = ({ isSelected, imageUrl, text, onClick, width, margin, isPrivate }) => (
const MediaLibraryCard = ({ isSelected, displayURL, text, onClick, width, margin, isPrivate }) => (
<Card
isSelected={isSelected}
onClick={onClick}
@ -45,14 +45,14 @@ const MediaLibraryCard = ({ isSelected, imageUrl, text, onClick, width, margin,
tabIndex="-1"
isPrivate={isPrivate}
>
<div>{imageUrl ? <CardImage src={imageUrl} /> : <CardImagePlaceholder />}</div>
<div>{displayURL ? <CardImage src={displayURL} /> : <CardImagePlaceholder />}</div>
<CardText>{text}</CardText>
</Card>
);
MediaLibraryCard.propTypes = {
isSelected: PropTypes.bool,
imageUrl: PropTypes.string,
displayURL: PropTypes.string,
text: PropTypes.string.isRequired,
onClick: PropTypes.func.isRequired,
width: PropTypes.string.isRequired,

View File

@ -33,6 +33,7 @@ const MediaLibraryCardGrid = ({
cardWidth,
cardMargin,
isPrivate,
getDisplayURL,
}) => (
<CardGridContainer innerRef={setScrollContainerRef}>
<CardGrid>
@ -40,12 +41,12 @@ const MediaLibraryCardGrid = ({
<MediaLibraryCard
key={file.key}
isSelected={isSelectedFile(file)}
imageUrl={file.isViewableImage && file.url}
text={file.name}
onClick={() => onAssetClick(file)}
width={cardWidth}
margin={cardMargin}
isPrivate={isPrivate}
displayURL={getDisplayURL(file)}
/>
))}
{!canLoadMore ? null : <Waypoint onEnter={onLoadMore} />}
@ -74,6 +75,7 @@ MediaLibraryCardGrid.propTypes = {
paginatingMessage: PropTypes.string,
cardWidth: PropTypes.string.isRequired,
cardMargin: PropTypes.string.isRequired,
getDisplayURL: PropTypes.func.isRequired,
isPrivate: PropTypes.bool,
};

View File

@ -0,0 +1,64 @@
import React from 'react';
import styled from 'react-emotion';
const CardImage = styled.img`
width: 100%;
height: 160px;
object-fit: cover;
border-radius: 2px 2px 0 0;
`;
const CardImagePlaceholder = CardImage.withComponent(`div`);
export default class MediaLibraryCardImage extends React.Component {
state = {
imageURL: '',
isFetching: false,
};
loadImage() {
const { image, getCachedImageURLByID, cacheImageURLByID } = this.props;
const { imageURL: existingImageURL, isFetching } = this.state;
if (existingImageURL !== '' || isFetching) {
return;
}
if (getCachedImageURLByID && image.key) {
const imageURL = getCachedImageURLByID(image.key);
if (imageURL) {
this.setState({ imageURL });
return;
}
}
if (image.url) {
this.setState({ imageURL: image.url });
if (image.key && cacheImageURLByID) {
cacheImageURLByID(image.key, image.url);
}
return;
}
if (image.getBlobPromise) {
this.setState({ isFetching: true });
image.getBlobPromise().then(blob => {
const imageURL = window.URL.createObjectURL(blob);
this.setState({ imageURL, isFetching: false });
if (image.key && cacheImageURLByID) {
cacheImageURLByID(image.key, imageURL);
}
});
}
}
render() {
const { imageURL, isFetching } = this.state;
if (imageURL === '' && !isFetching) {
this.loadImage();
}
return imageURL === '' ? <CardImagePlaceholder /> : <CardImage src={imageURL} />;
}
}

View File

@ -92,6 +92,7 @@ const MediaLibraryModal = ({
setScrollContainerRef,
handleAssetClick,
handleLoadMore,
getDisplayURL,
}) => {
const filteredFiles = forImage ? handleFilter(files) : files;
const queriedFiles = !dynamicSearch && query ? handleQuery(query, filteredFiles) : filteredFiles;
@ -156,6 +157,7 @@ const MediaLibraryModal = ({
cardWidth={cardWidth}
cardMargin={cardMargin}
isPrivate={privateUpload}
getDisplayURL={getDisplayURL}
/>
</StyledModal>
);
@ -197,6 +199,7 @@ MediaLibraryModal.propTypes = {
setScrollContainerRef: PropTypes.func.isRequired,
handleAssetClick: PropTypes.func.isRequired,
handleLoadMore: PropTypes.func.isRequired,
getDisplayURL: PropTypes.func.isRequired,
};
export default MediaLibraryModal;

View File

@ -15,11 +15,18 @@ import {
MEDIA_DELETE_REQUEST,
MEDIA_DELETE_SUCCESS,
MEDIA_DELETE_FAILURE,
MEDIA_DISPLAY_URL_REQUEST,
MEDIA_DISPLAY_URL_SUCCESS,
MEDIA_DISPLAY_URL_FAILURE,
} from 'Actions/mediaLibrary';
const mediaLibrary = (state = Map({ isVisible: false, controlMedia: Map() }), action) => {
const mediaLibrary = (
state = Map({ isVisible: false, controlMedia: Map(), displayURLs: Map() }),
action,
) => {
const privateUploadChanged =
state.get('privateUpload') !== get(action, ['payload', 'privateUpload']);
let displayURLPath;
switch (action.type) {
case MEDIA_LIBRARY_OPEN: {
const { controlID, forImage, privateUpload } = action.payload || {};
@ -108,13 +115,14 @@ const mediaLibrary = (state = Map({ isVisible: false, controlMedia: Map() }), ac
case MEDIA_DELETE_REQUEST:
return state.set('isDeleting', true);
case MEDIA_DELETE_SUCCESS: {
const { key } = action.payload.file;
const { id, key } = action.payload.file;
if (privateUploadChanged) {
return state;
}
return state.withMutations(map => {
const updatedFiles = map.get('files').filter(file => file.key !== key);
map.set('files', updatedFiles);
map.deleteIn(['displayURLs', id]);
map.set('isDeleting', false);
});
}
@ -123,6 +131,23 @@ const mediaLibrary = (state = Map({ isVisible: false, controlMedia: Map() }), ac
return state;
}
return state.set('isDeleting', false);
case MEDIA_DISPLAY_URL_REQUEST:
return state.setIn(['displayURLs', action.payload.key, 'isFetching'], true);
case MEDIA_DISPLAY_URL_SUCCESS:
displayURLPath = ['displayURLs', action.payload.key];
return state
.setIn([...displayURLPath, 'isFetching'], false)
.setIn([...displayURLPath, 'url'], action.payload.url);
case MEDIA_DISPLAY_URL_FAILURE:
displayURLPath = ['displayURLs', action.payload.key];
return state
.setIn([...displayURLPath, 'isFetching'], false)
.setIn([...displayURLPath, 'err'], action.payload.err)
.deleteIn([...displayURLPath, 'url']);
default:
return state;
}