feat: video gallery thumbnail fallback (#412)

This commit is contained in:
Artur Gaspar
2023-12-04 11:58:40 -03:00
committed by GitHub
parent 1ddaf9a662
commit 1c9771b332
5 changed files with 253 additions and 60 deletions

View File

@@ -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,

View File

@@ -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;

View File

@@ -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;

View File

@@ -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();

View File

@@ -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"