feat: add media lib virtualization (#3381)

This commit is contained in:
Erez Rokah
2020-03-09 08:45:42 +01:00
committed by GitHub
parent 9c0f618148
commit 92e76011e7
10 changed files with 219 additions and 194 deletions

View File

@ -56,7 +56,9 @@
"react-sortable-hoc": "^1.0.0",
"react-split-pane": "^0.1.85",
"react-topbar-progress-indicator": "^2.0.0",
"react-virtualized-auto-sizer": "^1.0.2",
"react-waypoint": "^8.1.0",
"react-window": "^1.8.5",
"redux": "^4.0.1",
"redux-notifications": "^4.0.1",
"redux-optimist": "^1.0.0",

View File

@ -8,7 +8,7 @@ const IMAGE_HEIGHT = 160;
const Card = styled.div`
width: ${props => props.width};
height: 240px;
height: ${props => props.height};
margin: ${props => props.margin};
border: ${borders.textField};
border-color: ${props => props.isSelected && colors.active};
@ -71,6 +71,7 @@ class MediaLibraryCard extends React.Component {
onClick,
draftText,
width,
height,
margin,
isPrivate,
type,
@ -83,6 +84,7 @@ class MediaLibraryCard extends React.Component {
isSelected={isSelected}
onClick={onClick}
width={width}
height={height}
margin={margin}
tabIndex="-1"
isPrivate={isPrivate}
@ -114,6 +116,7 @@ MediaLibraryCard.propTypes = {
onClick: PropTypes.func.isRequired,
draftText: PropTypes.string.isRequired,
width: PropTypes.string.isRequired,
height: PropTypes.string.isRequired,
margin: PropTypes.string.isRequired,
isPrivate: PropTypes.bool,
type: PropTypes.string,

View File

@ -5,6 +5,143 @@ import Waypoint from 'react-waypoint';
import MediaLibraryCard from './MediaLibraryCard';
import { Map } from 'immutable';
import { colors } from 'netlify-cms-ui-default';
import { FixedSizeGrid as Grid } from 'react-window';
import AutoSizer from 'react-virtualized-auto-sizer';
const CardWrapper = props => {
const {
rowIndex,
columnIndex,
style,
data: {
mediaItems,
isSelectedFile,
onAssetClick,
cardDraftText,
cardWidth,
cardHeight,
isPrivate,
displayURLs,
loadDisplayURL,
columnCount,
gutter,
},
} = props;
const index = rowIndex * columnCount + columnIndex;
if (index >= mediaItems.length) {
return null;
}
const file = mediaItems[index];
return (
<div
style={{
...style,
left: style.left + gutter * columnIndex,
top: style.top + gutter,
width: style.width - gutter,
height: style.height - gutter,
}}
>
<MediaLibraryCard
key={file.key}
isSelected={isSelectedFile(file)}
text={file.name}
onClick={() => onAssetClick(file)}
isDraft={file.draft}
draftText={cardDraftText}
width={cardWidth}
height={cardHeight}
margin={'0px'}
isPrivate={isPrivate}
displayURL={displayURLs.get(file.id, file.url ? Map({ url: file.url }) : Map())}
loadDisplayURL={() => loadDisplayURL(file)}
type={file.type}
isViewableImage={file.isViewableImage}
/>
</div>
);
};
const VirtualizedGrid = props => {
const { mediaItems, setScrollContainerRef } = props;
return (
<CardGridContainer ref={setScrollContainerRef}>
<AutoSizer>
{({ height, width }) => {
const cardWidth = parseInt(props.cardWidth, 10);
const cardHeight = parseInt(props.cardHeight, 10);
const gutter = parseInt(props.cardMargin, 10);
const columnWidth = cardWidth + gutter;
const rowHeight = cardHeight + gutter;
const columnCount = Math.floor(width / columnWidth);
const rowCount = Math.ceil(mediaItems.length / columnCount);
return (
<Grid
columnCount={columnCount}
columnWidth={columnWidth}
rowCount={rowCount}
rowHeight={rowHeight}
width={width}
height={height}
itemData={{ ...props, gutter, columnCount }}
>
{CardWrapper}
</Grid>
);
}}
</AutoSizer>
</CardGridContainer>
);
};
const PaginatedGrid = ({
setScrollContainerRef,
mediaItems,
isSelectedFile,
onAssetClick,
cardDraftText,
cardWidth,
cardHeight,
cardMargin,
isPrivate,
displayURLs,
loadDisplayURL,
canLoadMore,
onLoadMore,
isPaginating,
paginatingMessage,
}) => {
return (
<CardGridContainer ref={setScrollContainerRef}>
<CardGrid>
{mediaItems.map(file => (
<MediaLibraryCard
key={file.key}
isSelected={isSelectedFile(file)}
text={file.name}
onClick={() => onAssetClick(file)}
isDraft={file.draft}
draftText={cardDraftText}
width={cardWidth}
height={cardHeight}
margin={cardMargin}
isPrivate={isPrivate}
displayURL={displayURLs.get(file.id, file.url ? Map({ url: file.url }) : Map())}
loadDisplayURL={() => loadDisplayURL(file)}
type={file.type}
isViewableImage={file.isViewableImage}
/>
))}
{!canLoadMore ? null : <Waypoint onEnter={onLoadMore} />}
</CardGrid>
{!isPaginating ? null : (
<PaginatingMessage isPrivate={isPrivate}>{paginatingMessage}</PaginatingMessage>
)}
</CardGridContainer>
);
};
const CardGridContainer = styled.div`
overflow-y: auto;
@ -23,48 +160,13 @@ const PaginatingMessage = styled.h1`
color: ${props => props.isPrivate && colors.textFieldBorder};
`;
const MediaLibraryCardGrid = ({
setScrollContainerRef,
mediaItems,
isSelectedFile,
onAssetClick,
canLoadMore,
onLoadMore,
isPaginating,
paginatingMessage,
cardDraftText,
cardWidth,
cardMargin,
isPrivate,
displayURLs,
loadDisplayURL,
}) => (
<CardGridContainer ref={setScrollContainerRef}>
<CardGrid>
{mediaItems.map(file => (
<MediaLibraryCard
key={file.key}
isSelected={isSelectedFile(file)}
text={file.name}
onClick={() => onAssetClick(file)}
isDraft={file.draft}
draftText={cardDraftText}
width={cardWidth}
margin={cardMargin}
isPrivate={isPrivate}
displayURL={displayURLs.get(file.id, file.url ? Map({ url: file.url }) : Map())}
loadDisplayURL={() => loadDisplayURL(file)}
type={file.type}
isViewableImage={file.isViewableImage}
/>
))}
{!canLoadMore ? null : <Waypoint onEnter={onLoadMore} />}
</CardGrid>
{!isPaginating ? null : (
<PaginatingMessage isPrivate={isPrivate}>{paginatingMessage}</PaginatingMessage>
)}
</CardGridContainer>
);
const MediaLibraryCardGrid = props => {
const { canLoadMore, isPaginating } = props;
if (canLoadMore || isPaginating) {
return <PaginatedGrid {...props} />;
}
return <VirtualizedGrid {...props} />;
};
MediaLibraryCardGrid.propTypes = {
setScrollContainerRef: PropTypes.func.isRequired,

View File

@ -17,6 +17,7 @@ import { colors } from 'netlify-cms-ui-default';
* widths per breakpoint.
*/
const cardWidth = `280px`;
const cardHeight = `240px`;
const cardMargin = `10px`;
/**
@ -172,6 +173,7 @@ const MediaLibraryModal = ({
paginatingMessage={t('mediaLibrary.mediaLibraryModal.loading')}
cardDraftText={t('mediaLibrary.mediaLibraryCard.draft')}
cardWidth={cardWidth}
cardHeight={cardHeight}
cardMargin={cardMargin}
isPrivate={privateUpload}
loadDisplayURL={loadDisplayURL}

View File

@ -10,6 +10,7 @@ describe('MediaLibraryCard', () => {
onClick: jest.fn(),
draftText: 'Draft',
width: '100px',
height: '240px',
margin: '10px',
isViewableImage: true,
loadDisplayURL: jest.fn(),

View File

@ -52,6 +52,7 @@ exports[`MediaLibraryCard should match snapshot for draft image 1`] = `
<div
class="emotion-8 emotion-9"
height="240px"
tabindex="-1"
width="100px"
>
@ -122,6 +123,7 @@ exports[`MediaLibraryCard should match snapshot for non draft image 1`] = `
<div
class="emotion-6 emotion-7"
height="240px"
tabindex="-1"
width="100px"
>
@ -188,6 +190,7 @@ exports[`MediaLibraryCard should match snapshot for non viewable image 1`] = `
<div
class="emotion-6 emotion-7"
height="240px"
tabindex="-1"
width="100px"
>