From bb45c573cdd2ef132f46dfcf45030d722d235ff6 Mon Sep 17 00:00:00 2001 From: Christian Gastrell Date: Wed, 14 Aug 2024 16:51:03 -0300 Subject: [PATCH] VideoPress dashboard: improve upload error handler (#38769) * add debug calls, minor code improvements (map -> forEach, await/async functions, etc) * changelog entry * less debug on uploader lib * make onBeforeRequest async (as expected by tus client lib). Add onShouldRetry to abort retries on 400 errors (mostly unrecoverable for now) * add state action/reducer/selector for SET_VIDEO_UPLOADING_ERROR to keep errored videos separate and transient * include errored videos in all dashboard considerations and derived flags * add state action/reducer to clear upload errors * add error components for grid and row views * implement new error components on grid and list * remove unused parameters --------- Co-authored-by: Douglas --- .../fix-videopress-upload-error-handler | 4 + .../admin/components/admin-page/index.tsx | 20 +- .../admin/components/admin-page/libraries.tsx | 6 +- .../admin/components/video-card/error.tsx | 133 ++++++++++++ .../admin/components/video-grid/index.tsx | 23 +- .../admin/components/video-list/index.tsx | 5 +- .../admin/components/video-row/error.tsx | 199 ++++++++++++++++++ .../components/video-thumbnail/index.tsx | 12 +- .../video-thumbnail/style.module.scss | 4 + .../admin/components/video-thumbnail/types.ts | 5 + .../client/admin/hooks/use-videos/index.js | 2 + .../src/client/admin/types/index.ts | 1 + .../lib/resumable-file-uploader/index.ts | 36 +++- .../videopress/src/client/state/actions.js | 26 ++- .../videopress/src/client/state/constants.js | 3 + .../videopress/src/client/state/reducers.js | 38 ++++ .../videopress/src/client/state/selectors.js | 8 + 17 files changed, 492 insertions(+), 33 deletions(-) create mode 100644 projects/packages/videopress/changelog/fix-videopress-upload-error-handler create mode 100644 projects/packages/videopress/src/client/admin/components/video-card/error.tsx create mode 100644 projects/packages/videopress/src/client/admin/components/video-row/error.tsx 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 && ( +
+ + +
+ ) } +
+ + { isSm && isOpen && ( +
+ + +
+ ) } + + ); +}; + +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 && ( +
+ +
+ ) } + + { isSmall && ( +
+ +
+ ) } +
+ ) } +
+ + { 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;