feat: video gallery thumbnail fallback (#412)
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { Image } from '@edx/paragon';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { selectors } from '../../data/redux';
|
||||
import hooks from './hooks';
|
||||
@@ -6,6 +7,7 @@ import SelectionModal from '../../sharedComponents/SelectionModal';
|
||||
import { acceptedImgKeys } from './utils';
|
||||
import messages from './messages';
|
||||
import { RequestKeys } from '../../data/constants/requests';
|
||||
import videoThumbnail from '../../data/images/videoThumbnail.svg';
|
||||
|
||||
export const VideoGallery = () => {
|
||||
const rawVideos = useSelector(selectors.app.videos);
|
||||
@@ -45,6 +47,14 @@ export const VideoGallery = () => {
|
||||
uploadError: messages.uploadVideoError,
|
||||
};
|
||||
|
||||
const thumbnailFallback = (
|
||||
<Image
|
||||
thumbnail
|
||||
className="px-6 py-4.5 h-100"
|
||||
src={videoThumbnail}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<SelectionModal
|
||||
{...{
|
||||
@@ -55,7 +65,10 @@ export const VideoGallery = () => {
|
||||
galleryError,
|
||||
inputError,
|
||||
fileInput,
|
||||
galleryProps,
|
||||
galleryProps: {
|
||||
...galleryProps,
|
||||
thumbnailFallback,
|
||||
},
|
||||
searchSortProps,
|
||||
selectBtnProps,
|
||||
acceptedFiles: acceptedImgKeys,
|
||||
|
||||
@@ -23,6 +23,7 @@ export const Gallery = ({
|
||||
showIdsOnCards,
|
||||
height,
|
||||
isLoaded,
|
||||
thumbnailFallback,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
if (!isLoaded) {
|
||||
@@ -66,7 +67,14 @@ export const Gallery = ({
|
||||
type="radio"
|
||||
value={highlighted}
|
||||
>
|
||||
{ displayList.map(asset => <GalleryCard key={asset.id} asset={asset} showId={showIdsOnCards} />) }
|
||||
{ displayList.map(asset => (
|
||||
<GalleryCard
|
||||
key={asset.id}
|
||||
asset={asset}
|
||||
showId={showIdsOnCards}
|
||||
thumbnailFallback={thumbnailFallback}
|
||||
/>
|
||||
)) }
|
||||
</SelectableBox.Set>
|
||||
</div>
|
||||
</Scrollable>
|
||||
@@ -78,6 +86,7 @@ Gallery.defaultProps = {
|
||||
showIdsOnCards: false,
|
||||
height: '375px',
|
||||
show: true,
|
||||
thumbnailFallback: undefined,
|
||||
};
|
||||
Gallery.propTypes = {
|
||||
show: PropTypes.bool,
|
||||
@@ -90,6 +99,7 @@ Gallery.propTypes = {
|
||||
emptyGalleryLabel: PropTypes.shape({}).isRequired,
|
||||
showIdsOnCards: PropTypes.bool,
|
||||
height: PropTypes.string,
|
||||
thumbnailFallback: PropTypes.element,
|
||||
};
|
||||
|
||||
export default Gallery;
|
||||
|
||||
@@ -14,67 +14,81 @@ import LanguageNamesWidget from '../../containers/VideoEditor/components/VideoSe
|
||||
|
||||
export const GalleryCard = ({
|
||||
asset,
|
||||
}) => (
|
||||
<SelectableBox
|
||||
className="card bg-white shadow-none border-0 py-0"
|
||||
key={asset.externalUrl}
|
||||
type="radio"
|
||||
value={asset.id}
|
||||
>
|
||||
<div className="card-div d-flex flex-row flex-nowrap align-items-center">
|
||||
<div
|
||||
className="position-relative"
|
||||
style={{
|
||||
width: '200px',
|
||||
height: '100px',
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
style={{ border: 'none', width: '200px', height: '100px' }}
|
||||
src={asset.externalUrl}
|
||||
/>
|
||||
{ asset.status && asset.statusBadgeVariant && (
|
||||
<Badge variant={asset.statusBadgeVariant} style={{ position: 'absolute', left: '6px', top: '6px' }}>
|
||||
{asset.status}
|
||||
</Badge>
|
||||
)}
|
||||
{ asset.duration >= 0 && (
|
||||
<Badge
|
||||
variant="dark"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
right: '6px',
|
||||
bottom: '6px',
|
||||
backgroundColor: 'black',
|
||||
}}
|
||||
>
|
||||
{formatDuration(asset.duration)}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<div className="card-text px-3 py-2" style={{ marginTop: '10px' }}>
|
||||
<h3 className="text-primary-500">{asset.displayName}</h3>
|
||||
{ asset.transcripts && (
|
||||
<div style={{ margin: '0 0 5px 0' }}>
|
||||
<LanguageNamesWidget
|
||||
transcripts={asset.transcripts}
|
||||
thumbnailFallback,
|
||||
}) => {
|
||||
const [thumbnailError, setThumbnailError] = React.useState(false);
|
||||
return (
|
||||
<SelectableBox
|
||||
className="card bg-white shadow-none border-0 py-0"
|
||||
key={asset.externalUrl}
|
||||
type="radio"
|
||||
value={asset.id}
|
||||
>
|
||||
<div className="card-div d-flex flex-row flex-nowrap align-items-center">
|
||||
<div
|
||||
className="position-relative"
|
||||
style={{
|
||||
width: '200px',
|
||||
height: '100px',
|
||||
}}
|
||||
>
|
||||
{(thumbnailError && thumbnailFallback) ? (
|
||||
<div style={{ width: '200px', height: '100px' }}>
|
||||
{ thumbnailFallback }
|
||||
</div>
|
||||
) : (
|
||||
<Image
|
||||
style={{ border: 'none', width: '200px', height: '100px' }}
|
||||
src={asset.externalUrl}
|
||||
onError={thumbnailFallback && (() => setThumbnailError(true))}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<p className="text-gray-500" style={{ fontSize: '11px' }}>
|
||||
<FormattedMessage
|
||||
{...messages.addedDate}
|
||||
values={{
|
||||
date: <FormattedDate value={asset.dateAdded} />,
|
||||
time: <FormattedTime value={asset.dateAdded} />,
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
)}
|
||||
{ asset.status && asset.statusBadgeVariant && (
|
||||
<Badge variant={asset.statusBadgeVariant} style={{ position: 'absolute', left: '6px', top: '6px' }}>
|
||||
{asset.status}
|
||||
</Badge>
|
||||
)}
|
||||
{ asset.duration >= 0 && (
|
||||
<Badge
|
||||
variant="dark"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
right: '6px',
|
||||
bottom: '6px',
|
||||
backgroundColor: 'black',
|
||||
}}
|
||||
>
|
||||
{formatDuration(asset.duration)}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<div className="card-text px-3 py-2" style={{ marginTop: '10px' }}>
|
||||
<h3 className="text-primary-500">{asset.displayName}</h3>
|
||||
{ asset.transcripts && (
|
||||
<div style={{ margin: '0 0 5px 0' }}>
|
||||
<LanguageNamesWidget
|
||||
transcripts={asset.transcripts}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<p className="text-gray-500" style={{ fontSize: '11px' }}>
|
||||
<FormattedMessage
|
||||
{...messages.addedDate}
|
||||
values={{
|
||||
date: <FormattedDate value={asset.dateAdded} />,
|
||||
time: <FormattedTime value={asset.dateAdded} />,
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SelectableBox>
|
||||
);
|
||||
</SelectableBox>
|
||||
);
|
||||
};
|
||||
|
||||
GalleryCard.defaultProps = {
|
||||
thumbnailFallback: undefined,
|
||||
};
|
||||
GalleryCard.propTypes = {
|
||||
asset: PropTypes.shape({
|
||||
contentType: PropTypes.string,
|
||||
@@ -91,6 +105,7 @@ GalleryCard.propTypes = {
|
||||
statusBadgeVariant: PropTypes.string,
|
||||
transcripts: PropTypes.arrayOf(PropTypes.string),
|
||||
}).isRequired,
|
||||
thumbnailFallback: PropTypes.element,
|
||||
};
|
||||
|
||||
export default GalleryCard;
|
||||
|
||||
@@ -10,6 +10,7 @@ describe('GalleryCard component', () => {
|
||||
displayName: 'props.img.displayName',
|
||||
dateAdded: 12345,
|
||||
};
|
||||
const thumbnailFallback = (<span>Image failed to load</span>);
|
||||
let el;
|
||||
beforeEach(() => {
|
||||
el = shallow(<GalleryCard asset={asset} />);
|
||||
@@ -20,6 +21,15 @@ describe('GalleryCard component', () => {
|
||||
it('loads Image with src from image external url', () => {
|
||||
expect(el.find(Image).props().src).toEqual(asset.externalUrl);
|
||||
});
|
||||
it('snapshot with thumbnail fallback and load error', () => {
|
||||
el = shallow(<GalleryCard asset={asset} thumbnailFallback={thumbnailFallback} />);
|
||||
el.find(Image).props().onError();
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
it('snapshot with thumbnail fallback and no error', () => {
|
||||
el = shallow(<GalleryCard asset={asset} thumbnailFallback={thumbnailFallback} />);
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
it('snapshot with status badge', () => {
|
||||
el = shallow(<GalleryCard asset={{ ...asset, status: 'failed', statusBadgeVariant: 'danger' }} />);
|
||||
expect(el).toMatchSnapshot();
|
||||
|
||||
@@ -253,6 +253,151 @@ exports[`GalleryCard component snapshot with status badge 1`] = `
|
||||
</SelectableBox>
|
||||
`;
|
||||
|
||||
exports[`GalleryCard component snapshot with thumbnail fallback and load error 1`] = `
|
||||
<SelectableBox
|
||||
className="card bg-white shadow-none border-0 py-0"
|
||||
key="props.img.externalUrl"
|
||||
type="radio"
|
||||
>
|
||||
<div
|
||||
className="card-div d-flex flex-row flex-nowrap align-items-center"
|
||||
>
|
||||
<div
|
||||
className="position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": "100px",
|
||||
"width": "200px",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"height": "100px",
|
||||
"width": "200px",
|
||||
}
|
||||
}
|
||||
>
|
||||
<span>
|
||||
Image failed to load
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="card-text px-3 py-2"
|
||||
style={
|
||||
Object {
|
||||
"marginTop": "10px",
|
||||
}
|
||||
}
|
||||
>
|
||||
<h3
|
||||
className="text-primary-500"
|
||||
>
|
||||
props.img.displayName
|
||||
</h3>
|
||||
<p
|
||||
className="text-gray-500"
|
||||
style={
|
||||
Object {
|
||||
"fontSize": "11px",
|
||||
}
|
||||
}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Added {date} at {time}"
|
||||
description="File date-added string"
|
||||
id="authoring.selectionmodal.addedDate.label"
|
||||
values={
|
||||
Object {
|
||||
"date": <FormattedDate
|
||||
value={12345}
|
||||
/>,
|
||||
"time": <FormattedTime
|
||||
value={12345}
|
||||
/>,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</SelectableBox>
|
||||
`;
|
||||
|
||||
exports[`GalleryCard component snapshot with thumbnail fallback and no error 1`] = `
|
||||
<SelectableBox
|
||||
className="card bg-white shadow-none border-0 py-0"
|
||||
key="props.img.externalUrl"
|
||||
type="radio"
|
||||
>
|
||||
<div
|
||||
className="card-div d-flex flex-row flex-nowrap align-items-center"
|
||||
>
|
||||
<div
|
||||
className="position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": "100px",
|
||||
"width": "200px",
|
||||
}
|
||||
}
|
||||
>
|
||||
<Image
|
||||
onError={[Function]}
|
||||
src="props.img.externalUrl"
|
||||
style={
|
||||
Object {
|
||||
"border": "none",
|
||||
"height": "100px",
|
||||
"width": "200px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="card-text px-3 py-2"
|
||||
style={
|
||||
Object {
|
||||
"marginTop": "10px",
|
||||
}
|
||||
}
|
||||
>
|
||||
<h3
|
||||
className="text-primary-500"
|
||||
>
|
||||
props.img.displayName
|
||||
</h3>
|
||||
<p
|
||||
className="text-gray-500"
|
||||
style={
|
||||
Object {
|
||||
"fontSize": "11px",
|
||||
}
|
||||
}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Added {date} at {time}"
|
||||
description="File date-added string"
|
||||
id="authoring.selectionmodal.addedDate.label"
|
||||
values={
|
||||
Object {
|
||||
"date": <FormattedDate
|
||||
value={12345}
|
||||
/>,
|
||||
"time": <FormattedTime
|
||||
value={12345}
|
||||
/>,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</SelectableBox>
|
||||
`;
|
||||
|
||||
exports[`GalleryCard component snapshot: dateAdded=12345 1`] = `
|
||||
<SelectableBox
|
||||
className="card bg-white shadow-none border-0 py-0"
|
||||
|
||||
Reference in New Issue
Block a user