diff --git a/projects/packages/videopress/changelog/fix-videopress-upload-error-handler b/projects/packages/videopress/changelog/fix-videopress-upload-error-handler
new file mode 100644
index 0000000000000..401323095c692
--- /dev/null
+++ b/projects/packages/videopress/changelog/fix-videopress-upload-error-handler
@@ -0,0 +1,4 @@
+Significance: minor
+Type: changed
+
+VideoPress: fix upload error handler to be able to hint the user something's gone wrong
diff --git a/projects/packages/videopress/src/client/admin/components/admin-page/index.tsx b/projects/packages/videopress/src/client/admin/components/admin-page/index.tsx
index 914c33ed28a97..7178449b89919 100644
--- a/projects/packages/videopress/src/client/admin/components/admin-page/index.tsx
+++ b/projects/packages/videopress/src/client/admin/components/admin-page/index.tsx
@@ -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();
@@ -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[] ) => {
@@ -144,7 +154,7 @@ const useDashboardVideos = () => {
handleFilesUpload,
handleLocalVideoUpload,
loading: isFetching,
- uploading: uploading?.length > 0,
+ uploading: uploading?.length > 0 || uploadErrors?.length > 0,
hasVideoPressPurchase,
};
};
diff --git a/projects/packages/videopress/src/client/admin/components/admin-page/libraries.tsx b/projects/packages/videopress/src/client/admin/components/admin-page/libraries.tsx
index 1acfb8fd020a9..adfb95ad11e0a 100644
--- a/projects/packages/videopress/src/client/admin/components/admin-page/libraries.tsx
+++ b/projects/packages/videopress/src/client/admin/components/admin-page/libraries.tsx
@@ -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(
diff --git a/projects/packages/videopress/src/client/admin/components/video-card/error.tsx b/projects/packages/videopress/src/client/admin/components/video-card/error.tsx
new file mode 100644
index 0000000000000..30b259c82c4eb
--- /dev/null
+++ b/projects/packages/videopress/src/client/admin/components/video-card/error.tsx
@@ -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 (
+ <>
+
setIsOpen( wasOpen => ! wasOpen ) } ) }
+ >
+ { ! isSm &&
}
+
+
+
+
+ { isSm && (
+
+ { isOpen && }
+ { ! isOpen && }
+
+ ) }
+
+
+ { title }
+
+
+
+ { showError && (
+
+
+ { __(
+ "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'
+ ) }
+
+
+ ) }
+
+ { ! isSm && (
+
+
+ { __( 'Upload Error!', 'jetpack-videopress-pkg' ) }
+
+
+
+
+ ) }
+
+
+ { isSm && isOpen && (
+
+
+ { __( 'Upload Error!', 'jetpack-videopress-pkg' ) }
+
+
+
+
+ ) }
+ >
+ );
+};
+
+export default VideoCardError;
diff --git a/projects/packages/videopress/src/client/admin/components/video-grid/index.tsx b/projects/packages/videopress/src/client/admin/components/video-grid/index.tsx
index 042d8545e702c..5d3af2d6b1e7c 100644
--- a/projects/packages/videopress/src/client/admin/components/video-grid/index.tsx
+++ b/projects/packages/videopress/src/client/admin/components/video-grid/index.tsx
@@ -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';
@@ -29,15 +30,19 @@ const VideoGrid = ( { videos, count = 6, onVideoDetailsClick, loading }: VideoGr
{ gridVideos.map( ( video, index ) => {
return (
-
+ { video.error ? (
+
+ ) : (
+
+ ) }
);
} ) }
diff --git a/projects/packages/videopress/src/client/admin/components/video-list/index.tsx b/projects/packages/videopress/src/client/admin/components/video-list/index.tsx
index a4caea8ecdbae..75cfabba669a9 100644
--- a/projects/packages/videopress/src/client/admin/components/video-list/index.tsx
+++ b/projects/packages/videopress/src/client/admin/components/video-list/index.tsx
@@ -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
@@ -74,7 +75,9 @@ const VideoList = ( {
const isPrivate =
VIDEO_PRIVACY_LEVELS[ video.privacySetting ] === VIDEO_PRIVACY_LEVEL_PRIVATE;
- return (
+ return video.error ? (
+
+ ) : (
{
+ const textRef = useRef( null );
+ const checkboxRef = useRef( null );
+
+ const [ isSmall ] = useBreakpointMatch( 'sm' );
+ const [ keyPressed, setKeyDown ] = useState( false );
+ const [ expanded, setExpanded ] = useState( false );
+ const [ anchor, setAnchor ] = useState( null );
+ const [ showError, setShowError ] = useState( false );
+
+ const { dismissErroredVideo } = useDispatch( STORE_ID );
+
+ const uploadDateFormatted = dateI18n( 'M j, Y', new Date(), null );
+ const isEllipsisActive = textRef?.current?.offsetWidth < textRef?.current?.scrollWidth;
+
+ const showTitleLabel = ! isSmall && isEllipsisActive;
+ const showBottom = ! isSmall || ( isSmall && expanded );
+
+ const wrapperAriaLabel = sprintf(
+ /* translators: 1 Video title, 2 Video duration, 3 Video upload date */
+ __(
+ 'Video Upload Error: Video upload, titled %1$s, failed. Please try again or visit the troubleshooting docs at %2$s.',
+ 'jetpack-videopress-pkg'
+ ),
+ title,
+ getRedirectUrl( 'jetpack-videopress-dashboard-troubleshoot' )
+ );
+
+ const troubleshootUrl = getRedirectUrl( 'jetpack-videopress-dashboard-troubleshoot' );
+
+ const isSpaceOrEnter = code => code === 'Space' || code === 'Enter';
+
+ const onActionClick = () => {
+ setShowError( true );
+ };
+
+ const closeErrorHint = () => {
+ setShowError( false );
+ };
+
+ const handleClickWithStopPropagation = callback => event => {
+ event.stopPropagation();
+ callback?.( event );
+ };
+
+ const handleInfoWrapperClick = e => {
+ if ( isSmall ) {
+ setExpanded( current => ! current );
+ } else {
+ handleClick( e );
+ }
+ };
+
+ const handleClick = e => {
+ if ( e.target !== checkboxRef.current ) {
+ checkboxRef?.current?.click();
+ }
+ };
+
+ const handleKeyDown = e => {
+ if ( isSpaceOrEnter( e?.code ) ) {
+ setKeyDown( true );
+ }
+ };
+
+ const handleKeyUp = e => {
+ if ( isSpaceOrEnter( e?.code ) ) {
+ setKeyDown( false );
+ handleClick( e );
+ }
+ };
+
+ const handleDismiss = () => dismissErroredVideo( id );
+
+ return (
+
+
+
+
+
+
+
+
+ { showTitleLabel && (
+
+ { title }
+
+ ) }
+
+
+ { title }
+
+
+ { isSmall && { uploadDateFormatted } }
+
+
+ { isSmall &&
}
+
+
+ { showBottom && (
+
+ { ! isSmall && (
+
+
+ { __( 'Upload Error!', 'jetpack-videopress-pkg' ) }
+
+
+
+ ) }
+
+ { isSmall && (
+
+
+ { __( 'Upload Error!', 'jetpack-videopress-pkg' ) }
+
+
+
+ ) }
+
+ ) }
+
+
+ { showError && (
+
+
+ { __(
+ "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'
+ ) }
+
+
+ ) }
+
+ );
+};
+
+export default VideoRowError;
diff --git a/projects/packages/videopress/src/client/admin/components/video-thumbnail/index.tsx b/projects/packages/videopress/src/client/admin/components/video-thumbnail/index.tsx
index a5f6b171aaf3e..c0f728d216c0b 100644
--- a/projects/packages/videopress/src/client/admin/components/video-thumbnail/index.tsx
+++ b/projects/packages/videopress/src/client/admin/components/video-thumbnail/index.tsx
@@ -12,7 +12,7 @@ import {
import { Dropdown } from '@wordpress/components';
import { gmdateI18n } from '@wordpress/date';
import { __, sprintf } from '@wordpress/i18n';
-import { Icon, edit, cloud, image, media, video } from '@wordpress/icons';
+import { Icon, edit, cloud, image, media, video, warning } from '@wordpress/icons';
import clsx from 'clsx';
import { forwardRef } from 'react';
/**
@@ -153,6 +153,12 @@ const ProcessingThumbnail = ( { isRow = false }: { isRow?: boolean } ) => (
);
+const ErrorThumbnail = ( { isRow } ) => (
+
+
+
+);
+
/**
* React component to display video thumbnail.
*
@@ -177,6 +183,7 @@ const VideoThumbnail = forwardRef< HTMLDivElement, VideoThumbnailProps >(
onUploadImage,
uploadProgress,
isRow = false,
+ hasError = false,
},
ref
) => {
@@ -185,6 +192,7 @@ const VideoThumbnail = forwardRef< HTMLDivElement, VideoThumbnailProps >(
// Mapping thumbnail (Ordered by priority)
let thumbnail = defaultThumbnail;
+
thumbnail = loading ? : thumbnail;
thumbnail = uploading ? (
@@ -193,6 +201,8 @@ const VideoThumbnail = forwardRef< HTMLDivElement, VideoThumbnailProps >(
);
thumbnail = processing ? : thumbnail;
+ thumbnail = hasError ? : thumbnail;
+
thumbnail =
typeof thumbnail === 'string' && thumbnail !== '' ? (
diff --git a/projects/packages/videopress/src/client/admin/components/video-thumbnail/style.module.scss b/projects/packages/videopress/src/client/admin/components/video-thumbnail/style.module.scss
index a6973c23d8abb..459ec96043ede 100644
--- a/projects/packages/videopress/src/client/admin/components/video-thumbnail/style.module.scss
+++ b/projects/packages/videopress/src/client/admin/components/video-thumbnail/style.module.scss
@@ -43,6 +43,10 @@
align-items: center;
justify-content: center;
fill: var( --jp-gray-50 );
+
+ &.thumbnail-error {
+ fill: var( --jp-yellow-20 );
+ }
}
}
diff --git a/projects/packages/videopress/src/client/admin/components/video-thumbnail/types.ts b/projects/packages/videopress/src/client/admin/components/video-thumbnail/types.ts
index 016d1add98715..dcc92168971c0 100644
--- a/projects/packages/videopress/src/client/admin/components/video-thumbnail/types.ts
+++ b/projects/packages/videopress/src/client/admin/components/video-thumbnail/types.ts
@@ -82,4 +82,9 @@ export type VideoThumbnailProps = VideoThumbnailDropdownProps & {
* True if the thumbnail is used on a video row.
*/
isRow?: boolean;
+
+ /**
+ * True if the video has an error.
+ */
+ hasError?: boolean;
};
diff --git a/projects/packages/videopress/src/client/admin/hooks/use-videos/index.js b/projects/packages/videopress/src/client/admin/hooks/use-videos/index.js
index 9c352b9f16c10..7f9e876bff5b4 100644
--- a/projects/packages/videopress/src/client/admin/hooks/use-videos/index.js
+++ b/projects/packages/videopress/src/client/admin/hooks/use-videos/index.js
@@ -32,6 +32,7 @@ export default function useVideos() {
const pagination = useSelect( select => select( STORE_ID ).getPagination() );
const storageUsed = useSelect( select => select( STORE_ID ).getStorageUsed(), [] );
const filter = useSelect( select => select( STORE_ID ).getVideosFilter() );
+ const uploadErrors = useSelect( select => select( STORE_ID ).getUploadErrorVideos() );
return {
items,
@@ -48,6 +49,7 @@ export default function useVideos() {
...query,
...pagination,
...storageUsed,
+ uploadErrors,
// Handlers
setPage: page => dispatch( STORE_ID ).setVideosQuery( { page } ),
diff --git a/projects/packages/videopress/src/client/admin/types/index.ts b/projects/packages/videopress/src/client/admin/types/index.ts
index b93b716dfaa01..929ebe4cab781 100644
--- a/projects/packages/videopress/src/client/admin/types/index.ts
+++ b/projects/packages/videopress/src/client/admin/types/index.ts
@@ -169,6 +169,7 @@ export type VideoPressVideo = {
thumbnail?: string;
uploading?: boolean;
plays?: number; // Not provided yet
+ error?: string;
};
export type LocalVideo = {
diff --git a/projects/packages/videopress/src/client/lib/resumable-file-uploader/index.ts b/projects/packages/videopress/src/client/lib/resumable-file-uploader/index.ts
index c544666d5c8a1..5802ec657ceb0 100644
--- a/projects/packages/videopress/src/client/lib/resumable-file-uploader/index.ts
+++ b/projects/packages/videopress/src/client/lib/resumable-file-uploader/index.ts
@@ -1,6 +1,7 @@
/**
* External dependencies
*/
+import debugFactory from 'debug';
import * as tus from 'tus-js-client';
/**
* Internal dependencies
@@ -10,6 +11,8 @@ import getMediaToken from '../get-media-token';
import { VideoMediaProps } from './types';
import type { MediaTokenProps } from '../../lib/get-media-token/types';
+const debug = debugFactory( 'videopress:resumable-file-uploader' );
+
const jwtsForKeys = {};
declare module 'tus-js-client' {
@@ -54,12 +57,10 @@ const resumableFileUploader = ( {
onError,
}: UploadVideoArguments ) => {
const upload = new tus.Upload( file, {
- onError: onError,
+ onError,
onProgress,
endpoint: tokenData.url,
removeFingerprintOnSuccess: true,
- withCredentials: false,
- autoRetry: true,
overridePatchMethod: false,
chunkSize: 10000000, // 10 Mb.
metadata: {
@@ -67,9 +68,22 @@ const resumableFileUploader = ( {
filetype: file.type,
},
retryDelays: [ 0, 1000, 3000, 5000, 10000 ],
- onBeforeRequest: function ( req ) {
+ onShouldRetry: function ( err: tus.DetailedError ) {
+ const status = err.originalResponse ? err.originalResponse.getStatus() : 0;
+ // Do not retry if the status is a 400.
+ if ( status === 400 ) {
+ debug( 'cleanup retry due to 400 error' );
+ localStorage.removeItem( upload._urlStorageKey );
+ return false;
+ }
+
+ // For any other status code, we retry.
+ return true;
+ },
+ onBeforeRequest: async function ( req: VPUploadHttpRequest ) {
// make ALL requests be either POST or GET to honor the public-api.wordpress.com "contract".
const method = req._method;
+
if ( [ 'HEAD', 'OPTIONS' ].indexOf( method ) >= 0 ) {
req._method = 'GET';
req.setHeader( 'X-HTTP-Method-Override', method );
@@ -82,7 +96,7 @@ const resumableFileUploader = ( {
req._xhr.open( req._method, req._url, true );
// Set the headers again, reopening the xhr resets them.
- Object.keys( req._headers ).map( function ( headerName ) {
+ Object.keys( req._headers ).forEach( function ( headerName ) {
req.setHeader( headerName, req._headers[ headerName ] );
} );
@@ -100,19 +114,19 @@ const resumableFileUploader = ( {
if ( jwtsForKeys[ maybeUploadkey ] ) {
req.setHeader( 'x-videopress-upload-token', jwtsForKeys[ maybeUploadkey ] );
} else if ( 'HEAD' === method ) {
- return getMediaToken( 'upload-jwt' ).then( responseData => {
+ const responseData = await getMediaToken( 'upload-jwt' );
+ if ( responseData?.token ) {
jwtsForKeys[ maybeUploadkey ] = responseData.token;
req.setHeader( 'x-videopress-upload-token', responseData.token );
- return req;
- } );
+ }
}
}
-
- return Promise.resolve( req );
},
- onAfterResponse: function ( req, res ) {
+ onAfterResponse: async function ( req, res ) {
// Why is this not showing the x-headers?
if ( res.getStatus() >= 400 ) {
+ // Return, do nothing, it's handed to invoker's onError.
+ debug( 'upload error' );
return;
}
diff --git a/projects/packages/videopress/src/client/state/actions.js b/projects/packages/videopress/src/client/state/actions.js
index 650c5fb8bde1a..572d9f1878eca 100644
--- a/projects/packages/videopress/src/client/state/actions.js
+++ b/projects/packages/videopress/src/client/state/actions.js
@@ -2,7 +2,9 @@
* External dependencies
*/
import apiFetch from '@wordpress/api-fetch';
+import { __ } from '@wordpress/i18n';
import { addQueryArgs } from '@wordpress/url';
+import debugFactory from 'debug';
/**
* Internal dependencies
*/
@@ -33,6 +35,7 @@ import {
VIDEO_PRIVACY_LEVELS,
WP_REST_API_MEDIA_ENDPOINT,
SET_VIDEO_UPLOADING,
+ SET_VIDEO_UPLOADING_ERROR,
SET_VIDEO_PROCESSING,
SET_VIDEO_UPLOADED,
SET_IS_FETCHING_PURCHASES,
@@ -53,10 +56,13 @@ import {
UPDATE_PAGINATION_AFTER_DELETE,
FLUSH_DELETED_VIDEOS,
UPDATE_VIDEO_IS_PRIVATE,
+ DISMISS_ERRORED_VIDEO,
} from './constants';
import { mapVideoFromWPV2MediaEndpoint } from './utils/map-videos';
import { videoIsPrivate } from './utils/video-is-private';
+const debug = debugFactory( 'videopress:actions' );
+
/**
* Utility function to pool the video data until poster is ready.
*/
@@ -265,15 +271,14 @@ const uploadVideo =
async ( { dispatch } ) => {
const tempId = uid();
- // @todo: implement progress and error handler
- const noop = () => {};
-
+ debug( 'Uploading video' );
dispatch( { type: SET_VIDEO_UPLOADING, id: tempId, title: file?.name } );
// @todo: this should be stored in the state
const tokenData = await getMediaToken( 'upload-jwt' );
const onSuccess = async data => {
+ debug( 'Video uploaded', data );
dispatch( { type: SET_VIDEO_PROCESSING, id: tempId, data } );
const video = await pollingUploadedVideoData( data );
dispatch( { type: SET_VIDEO_UPLOADED, video } );
@@ -283,10 +288,18 @@ const uploadVideo =
dispatch( { type: SET_VIDEO_UPLOAD_PROGRESS, id: tempId, bytesSent, bytesTotal } );
};
+ const onError = err => {
+ debug( 'Upload error', err );
+ const error =
+ err?.originalResponse?.getHeader( 'x-videopress-upload-error' ) ||
+ __( 'Upload error', 'jetpack-videopress-pkg' );
+ dispatch( { type: SET_VIDEO_UPLOADING_ERROR, id: tempId, error } );
+ };
+
fileUploader( {
tokenData,
file,
- onError: noop,
+ onError,
onProgress,
onSuccess,
} );
@@ -476,6 +489,10 @@ const dismissFirstVideoPopover = () => {
return { type: DISMISS_FIRST_VIDEO_POPOVER };
};
+const dismissErroredVideo = id => {
+ return { type: DISMISS_ERRORED_VIDEO, id };
+};
+
const actions = {
setIsFetchingVideos,
setFetchVideosError,
@@ -521,6 +538,7 @@ const actions = {
updateVideoPressSettings,
updateVideoIsPrivate,
+ dismissErroredVideo,
};
export { actions as default };
diff --git a/projects/packages/videopress/src/client/state/constants.js b/projects/packages/videopress/src/client/state/constants.js
index 43b7ee37a6840..931dfa1417ee2 100644
--- a/projects/packages/videopress/src/client/state/constants.js
+++ b/projects/packages/videopress/src/client/state/constants.js
@@ -44,6 +44,7 @@ export const FLUSH_DELETED_VIDEOS = 'FLUSH_DELETED_VIDEOS';
export const UPDATE_PAGINATION_AFTER_DELETE = 'UPDATE_PAGINATION_AFTER_DELETE';
export const SET_VIDEO_UPLOADING = 'SET_VIDEO_UPLOADING';
+export const SET_VIDEO_UPLOADING_ERROR = 'SET_VIDEO_UPLOADING_ERROR';
export const SET_VIDEO_PROCESSING = 'SET_VIDEO_PROCESSING';
export const SET_VIDEO_UPLOADED = 'SET_VIDEO_UPLOADED';
export const SET_VIDEO_UPLOAD_PROGRESS = 'SET_VIDEO_UPLOAD_PROGRESS';
@@ -65,6 +66,8 @@ export const SET_VIDEOPRESS_SETTINGS = 'SET_VIDEOPRESS_SETTINGS';
export const UPDATE_VIDEO_IS_PRIVATE = 'UPDATE_VIDEO_IS_PRIVATE';
+export const DISMISS_ERRORED_VIDEO = 'DISMISS_ERRORED_VIDEO';
+
/*
* Accepted file extensions
*/
diff --git a/projects/packages/videopress/src/client/state/reducers.js b/projects/packages/videopress/src/client/state/reducers.js
index 9cc36efbc436b..f5b5c1f7ad9d3 100644
--- a/projects/packages/videopress/src/client/state/reducers.js
+++ b/projects/packages/videopress/src/client/state/reducers.js
@@ -20,6 +20,7 @@ import {
REMOVE_VIDEO,
DELETE_VIDEO,
SET_VIDEO_UPLOADING,
+ SET_VIDEO_UPLOADING_ERROR,
SET_VIDEO_PROCESSING,
SET_VIDEO_UPLOADED,
SET_IS_FETCHING_PURCHASES,
@@ -44,6 +45,7 @@ import {
FLUSH_DELETED_VIDEOS,
UPDATE_PAGINATION_AFTER_DELETE,
UPDATE_VIDEO_IS_PRIVATE,
+ DISMISS_ERRORED_VIDEO,
} from './constants';
/**
@@ -424,6 +426,42 @@ const videos = ( state, action ) => {
};
}
+ case SET_VIDEO_UPLOADING_ERROR: {
+ const { id, error } = action;
+ const currentMeta = state?._meta || {};
+ const currentMetaItems = currentMeta?.items || {};
+
+ return {
+ ...state,
+ _meta: {
+ ...currentMeta,
+ items: {
+ ...currentMetaItems,
+ [ id ]: {
+ ...currentMetaItems[ id ],
+ uploading: false,
+ error,
+ },
+ },
+ },
+ };
+ }
+
+ case DISMISS_ERRORED_VIDEO: {
+ const { id } = action;
+ const currentMeta = state?._meta || {};
+ const currentMetaItems = currentMeta?.items || {};
+ delete currentMetaItems[ id ];
+
+ return {
+ ...state,
+ _meta: {
+ ...currentMeta,
+ items: { ...currentMetaItems },
+ },
+ };
+ }
+
case SET_VIDEO_PROCESSING: {
const { id, data } = action;
const query = state?.query ?? getDefaultQuery();
diff --git a/projects/packages/videopress/src/client/state/selectors.js b/projects/packages/videopress/src/client/state/selectors.js
index 51b2e7dde4cbc..81aebcc16c316 100644
--- a/projects/packages/videopress/src/client/state/selectors.js
+++ b/projects/packages/videopress/src/client/state/selectors.js
@@ -9,6 +9,13 @@ export const getUploadingVideos = state => {
.filter( item => item.uploading );
};
+export const getUploadErrorVideos = state => {
+ const items = state?.videos?._meta?.items || {};
+ return Object.keys( items || {} )
+ .map( id => ( { ...items[ id ], id } ) )
+ .filter( item => !! item.error );
+};
+
export const getVideosQuery = state => {
return state?.videos?.query;
};
@@ -154,6 +161,7 @@ const selectors = {
isFetchingPlaybackToken,
getVideoPressSettings,
+ getUploadErrorVideos,
};
export default selectors;