Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

VideoPress dashboard: improve upload error handler #38769

Merged
merged 11 commits into from
Aug 14, 2024
Merged
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: minor
Type: changed

VideoPress: fix upload error handler to be able to hint the user something's gone wrong
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,17 @@ import styles from './styles.module.scss';

const useDashboardVideos = () => {
const { uploadVideo, uploadVideoFromLibrary, setVideosQuery } = useDispatch( STORE_ID );
const { items, uploading, uploadedVideoCount, isFetching, search, page, itemsPerPage, total } =
useVideos();
const {
items,
uploadErrors,
uploading,
uploadedVideoCount,
isFetching,
search,
page,
itemsPerPage,
total,
} = useVideos();
const { items: localVideos, uploadedLocalVideoCount } = useLocalVideos();
const { hasVideoPressPurchase } = usePlan();

Expand Down Expand Up @@ -105,9 +114,10 @@ const useDashboardVideos = () => {
}, [ totalOfPages, page, pageFromSearchParam, search, searchFromSearchParam, tempPage.current ] );

// Do not show uploading videos if not in the first page or searching
let videos = page > 1 || Boolean( search ) ? items : [ ...uploading, ...items ];
let videos = page > 1 || Boolean( search ) ? items : [ ...uploadErrors, ...uploading, ...items ];

const hasVideos = uploadedVideoCount > 0 || isFetching || uploading?.length > 0;
const hasVideos =
uploadedVideoCount > 0 || isFetching || uploading?.length > 0 || uploadErrors?.length > 0;
const hasLocalVideos = uploadedLocalVideoCount > 0;

const handleFilesUpload = ( files: File[] ) => {
Expand Down Expand Up @@ -144,7 +154,7 @@ const useDashboardVideos = () => {
handleFilesUpload,
handleLocalVideoUpload,
loading: isFetching,
uploading: uploading?.length > 0,
uploading: uploading?.length > 0 || uploadErrors?.length > 0,
hasVideoPressPurchase,
};
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,10 +125,12 @@ export const VideoPressLibrary = ( { videos, totalVideos, loading }: VideoLibrar
const history = useHistory();
const { search } = useVideos();
const videosToDisplay = [
// First comes the videos that are uploading
// First comes video upload errors
...videos.filter( video => video.error ),
// Then comes the videos that are uploading
...videos.filter( video => video.uploading ),
// Then the videos that are not uploading, at most 6
...videos.filter( video => ! video.uploading ).slice( 0, 6 ),
...videos.filter( video => ! video.uploading && ! video.error ).slice( 0, 6 ),
];

const libraryTypeFromLocalStorage = localStorage.getItem(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
/**
* External dependencies
*/
import {
Button,
Title,
useBreakpointMatch,
ActionPopover,
getRedirectUrl,
Text,
} from '@automattic/jetpack-components';
import { useDispatch } from '@wordpress/data';
import { __ } from '@wordpress/i18n';
import { Icon, chevronDown, chevronUp, trash } from '@wordpress/icons';
import clsx from 'clsx';
import { useState } from 'react';
/**
* Internal dependencies
*/
import { STORE_ID } from '../../../state';
import VideoThumbnail from '../video-thumbnail';
import styles from './style.module.scss';
import { VideoCardProps } from './types';
/**
* Types
*/
import type React from 'react';

/**
* Video Card Error component
*
* @param {VideoCardProps} props - Component props.
* @returns {React.ReactNode} - VideoCardError react component.
*/
export const VideoCardError = ( { title, id }: VideoCardProps ) => {
const { dismissErroredVideo } = useDispatch( STORE_ID );
const isBlank = ! title;

const [ anchor, setAnchor ] = useState( null );
const [ isSm ] = useBreakpointMatch( 'sm' );
const [ isOpen, setIsOpen ] = useState( false );
const [ showError, setShowError ] = useState( false );
const disabled = false;

const handleDismiss = () => dismissErroredVideo( id );

const handleErrorHint = () => setShowError( true );

const troubleshootUrl = getRedirectUrl( 'jetpack-videopress-dashboard-troubleshoot' );

const closeErrorHint = () => setShowError( false );

return (
<>
<div
className={ clsx( styles[ 'video-card__wrapper' ], {
[ styles[ 'is-blank' ] ]: isBlank,
[ styles.disabled ]: isSm,
} ) }
{ ...( isSm && ! disabled && { onClick: () => setIsOpen( wasOpen => ! wasOpen ) } ) }
>
{ ! isSm && <div className={ styles[ 'video-card__background' ] } /> }

<VideoThumbnail
className={ styles[ 'video-card__thumbnail' ] }
ref={ setAnchor }
hasError={ true }
/>

<div className={ styles[ 'video-card__title-section' ] }>
{ isSm && (
<div className={ styles.chevron }>
{ isOpen && <Icon icon={ chevronUp } /> }
{ ! isOpen && <Icon icon={ chevronDown } /> }
</div>
) }

<Title className={ styles[ 'video-card__title' ] } mb={ 0 } size="small">
{ title }
</Title>
</div>

{ showError && (
<ActionPopover
title={ __( 'Error', 'jetpack-videopress-pkg' ) }
buttonContent={ __( 'Visit the docs', 'jetpack-videopress-pkg' ) }
buttonHref={ troubleshootUrl }
buttonExternalLink
anchor={ anchor }
onClose={ closeErrorHint }
onClick={ closeErrorHint }
noArrow={ false }
className={ styles[ 'action-popover' ] }
>
<Text>
{ __(
"There's been an error uploading your video. Try uploading the video again, if the error persists, visit our documentation to troubleshoot the issue or contact support.",
'jetpack-videopress-pkg'
) }
</Text>
</ActionPopover>
) }

{ ! isSm && (
<div
className={ clsx(
styles[ 'video-card__quick-actions-section' ],
styles[ 'is-blank' ]
) }
>
<Button variant="primary" size="small" onClick={ handleErrorHint }>
{ __( 'Upload Error!', 'jetpack-videopress-pkg' ) }
</Button>

<Button size="small" variant="tertiary" icon={ trash } onClick={ handleDismiss } />
</div>
) }
</div>

{ isSm && isOpen && (
<div className={ clsx( styles[ 'video-card__quick-actions-section' ], styles.small ) }>
<Button variant="primary" size="small" onClick={ handleErrorHint }>
{ __( 'Upload Error!', 'jetpack-videopress-pkg' ) }
</Button>

<Button size="small" variant="tertiary" icon={ trash } onClick={ handleDismiss } />
</div>
) }
</>
);
};

export default VideoCardError;
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { Container, Col } from '@automattic/jetpack-components';
* Internal dependencies
*/
import VideoCard from '../video-card';
import VideoCardError from '../video-card/error';
import styles from './style.module.scss';
import { VideoGridProps } from './types';
import type React from 'react';
Expand All @@ -29,15 +30,19 @@ const VideoGrid = ( { videos, count = 6, onVideoDetailsClick, loading }: VideoGr
{ gridVideos.map( ( video, index ) => {
return (
<Col key={ video?.guid ?? video?.id } sm={ 4 } md={ 4 } lg={ 4 }>
<VideoCard
id={ video?.id }
title={ video.title }
thumbnail={ video?.posterImage } // TODO: we should use thumbnail when the API is ready https://github.com/Automattic/jetpack/issues/26319
duration={ video.duration }
plays={ video.plays }
onVideoDetailsClick={ handleClickWithIndex( index, onVideoDetailsClick ) }
loading={ loading }
/>
{ video.error ? (
<VideoCardError id={ video?.id } title={ video.title } />
) : (
<VideoCard
id={ video?.id }
title={ video.title }
thumbnail={ video?.posterImage } // TODO: we should use thumbnail when the API is ready https://github.com/Automattic/jetpack/issues/26319
duration={ video.duration }
plays={ video.plays }
onVideoDetailsClick={ handleClickWithIndex( index, onVideoDetailsClick ) }
loading={ loading }
/>
) }
</Col>
);
} ) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { usePlan } from '../../hooks/use-plan';
import useVideos from '../../hooks/use-videos';
import Checkbox from '../checkbox';
import ConnectVideoRow, { LocalVideoRow, Stats } from '../video-row';
import VideoRowError from '../video-row/error';
import styles from './style.module.scss';
/**
* Types
Expand Down Expand Up @@ -74,7 +75,9 @@ const VideoList = ( {
const isPrivate =
VIDEO_PRIVACY_LEVELS[ video.privacySetting ] === VIDEO_PRIVACY_LEVEL_PRIVATE;

return (
return video.error ? (
<VideoRowError key={ video?.guid ?? video?.id } id={ video?.id } title={ video?.title } />
) : (
<ConnectVideoRow
key={ video?.guid ?? video?.id }
id={ video?.id }
Expand Down
Loading
Loading