feat: New request hook to fetch videos

This commit is contained in:
XnpioChV
2023-03-17 11:59:59 -05:00
parent 14504073e0
commit b78e58cd2a
24 changed files with 136 additions and 98 deletions

View File

@@ -26,6 +26,7 @@ import { ErrorAlert } from '../../../../../../sharedComponents/ErrorAlerts/Error
import { UploadErrorAlert } from '../../../../../../sharedComponents/ErrorAlerts/UploadErrorAlert';
import CollapsibleFormWidget from '../CollapsibleFormWidget';
import { ErrorContext } from '../../../../hooks';
import { RequestKeys } from '../../../../../../data/constants/requests';
/**
* Collapsible Form widget controlling video handouts
@@ -38,6 +39,7 @@ export const HandoutWidget = ({
handout,
getHandoutDownloadUrl,
updateField,
isUploadError,
}) => {
const [error] = React.useContext(ErrorContext).handout;
const { fileSizeError } = hooks.fileSizeError();
@@ -59,7 +61,7 @@ export const HandoutWidget = ({
>
<FormattedMessage {...messages.fileSizeError} />
</ErrorAlert>
<UploadErrorAlert message={messages.uploadHandoutError} />
<UploadErrorAlert isUploadError={isUploadError} message={messages.uploadHandoutError} />
<FileInput fileInput={fileInput} />
{handout ? (
<Stack gap={3}>
@@ -125,6 +127,7 @@ export const mapStateToProps = (state) => ({
isLibrary: selectors.app.isLibrary(state),
handout: selectors.video.handout(state),
getHandoutDownloadUrl: selectors.video.getHandoutDownloadUrl(state),
isUploadError: selectors.requests.isFailed(state, { requestKey: RequestKeys.uploadAsset }),
});
export const mapDispatchToProps = (dispatch) => ({

View File

@@ -24,6 +24,9 @@ jest.mock('../../../../../../data/redux', () => ({
app: {
isLibrary: jest.fn(args => ({ isLibrary: args })),
},
requests: {
isFailed: jest.fn(args => ({ isFailed: args })),
},
},
}));

View File

@@ -45,8 +45,11 @@ exports[`VideoGallery component snapshot 1`] = `
"show": "ShoWERror inPUT",
}
}
isFetchError={false}
isFullscreenScroll={false}
isLoaded={false}
isOpen={true}
isUploadError={false}
modalMessages={
Object {
"confirmMsg": Object {

View File

@@ -98,11 +98,20 @@ export const fileInputHooks = () => {
};
};
export const filterAssets = ({ assets }) => {
export const buildVideos = ({ rawVideos }) => {
let videos = [];
const assetsList = Object.values(assets);
if (assetsList.length > 0) {
videos = assetsList.filter(asset => asset?.contentType?.startsWith('video/'));
const videoList = Object.values(rawVideos);
if (videoList.length > 0) {
videos = videoList.map(asset => ({
id: asset.edx_video_id,
displayName: asset.client_video_id,
externalUrl: asset.course_video_image_url,
dateAdded: asset.created,
locked: false,
thumbnail: asset.course_video_image_url,
status: asset.status,
duration: asset.duration,
}));
}
return videos;
};
@@ -130,5 +139,5 @@ export const videoHooks = ({ videos }) => {
export default {
videoHooks,
filterAssets,
buildVideos,
};

View File

@@ -6,12 +6,16 @@ import hooks from './hooks';
import SelectionModal from '../../sharedComponents/SelectionModal';
import { acceptedImgKeys } from './utils';
import messages from './messages';
import { RequestKeys } from '../../data/constants/requests';
export const VideoGallery = ({
// redux
assets,
rawVideos,
isLoaded,
isFetchError,
isUploadError,
}) => {
const videos = hooks.filterAssets({ assets });
const videos = hooks.buildVideos({ rawVideos });
const {
galleryError,
inputError,
@@ -45,6 +49,9 @@ export const VideoGallery = ({
selectBtnProps,
acceptedFiles: acceptedImgKeys,
modalMessages,
isLoaded,
isUploadError,
isFetchError,
}}
/>
</div>
@@ -52,11 +59,17 @@ export const VideoGallery = ({
};
VideoGallery.propTypes = {
assets: PropTypes.shape({}).isRequired,
rawVideos: PropTypes.shape({}).isRequired,
isLoaded: PropTypes.bool.isRequired,
isFetchError: PropTypes.bool.isRequired,
isUploadError: PropTypes.bool.isRequired,
};
export const mapStateToProps = (state) => ({
assets: selectors.app.assets(state),
rawVideos: selectors.app.videos(state),
isLoaded: selectors.requests.isFinished(state, { requestKey: RequestKeys.fetchVideos }),
isFetchError: selectors.requests.isFailed(state, { requestKey: RequestKeys.fetchVideos }),
isUploadError: selectors.requests.isFailed(state, { requestKey: RequestKeys.uploadVideo }),
});
export const mapDispatchToProps = {};

View File

@@ -8,7 +8,7 @@ import * as module from '.';
jest.mock('../../sharedComponents/SelectionModal', () => 'SelectionModal');
jest.mock('./hooks', () => ({
filterAssets: jest.fn(() => []),
buildVideos: jest.fn(() => []),
videoHooks: jest.fn(() => ({
galleryError: {
show: 'ShoWERror gAlLery',
@@ -44,7 +44,9 @@ jest.mock('./hooks', () => ({
jest.mock('../../data/redux', () => ({
selectors: {
requests: {
isPending: (state, { requestKey }) => ({ isPending: { state, requestKey } }),
isLoaded: (state, { requestKey }) => ({ isLoaded: { state, requestKey } }),
isFetchError: (state, { requestKey }) => ({ isFetchError: { state, requestKey } }),
isUploadError: (state, { requestKey }) => ({ isUploadError: { state, requestKey } }),
},
},
}));
@@ -52,7 +54,10 @@ jest.mock('../../data/redux', () => ({
describe('VideoGallery', () => {
describe('component', () => {
const props = {
assets: { sOmEaSsET: { staTICUrl: '/assets/sOmEaSsET' } },
rawVideos: { sOmEaSsET: { staTICUrl: '/video/sOmEaSsET' } },
isLoaded: false,
isFetchError: false,
isUploadError: false,
};
let el;
const videoHooks = hooks.videoHooks();

View File

@@ -9,12 +9,14 @@ export const RequestStates = StrictDict({
export const RequestKeys = StrictDict({
fetchAssets: 'fetchAssets',
fetchVideos: 'fetchVideos',
fetchBlock: 'fetchBlock',
fetchImages: 'fetchImages',
fetchUnit: 'fetchUnit',
fetchStudioView: 'fetchStudioView',
saveBlock: 'saveBlock',
uploadAsset: 'uploadAsset',
uploadVideo: 'uploadVideo',
allowThumbnailUpload: 'allowThumbnailUpload',
uploadThumbnail: 'uploadThumbnail',
uploadTranscript: 'uploadTranscript',

View File

@@ -16,6 +16,7 @@ const initialState = {
studioEndpointUrl: null,
lmsEndpointUrl: null,
assets: {},
videos: {},
courseDetails: {},
};
@@ -45,6 +46,7 @@ const app = createSlice({
setSaveResponse: (state, { payload }) => ({ ...state, saveResponse: payload }),
initializeEditor: (state) => ({ ...state, editorInitialized: true }),
setAssets: (state, { payload }) => ({ ...state, assets: payload }),
setVideos: (state, { payload }) => ({ ...state, videos: payload }),
setCourseDetails: (state, { payload }) => ({ ...state, courseDetails: payload }),
},
});

View File

@@ -22,6 +22,7 @@ export const simpleSelectors = {
unitUrl: mkSimpleSelector(app => app.unitUrl),
blockTitle: mkSimpleSelector(app => app.blockTitle),
assets: mkSimpleSelector(app => app.assets),
videos: mkSimpleSelector(app => app.videos),
};
export const returnUrl = createSelector(

View File

@@ -16,6 +16,8 @@ const initialState = {
[RequestKeys.deleteTranscript]: { status: RequestStates.inactive },
[RequestKeys.fetchCourseDetails]: { status: RequestStates.inactive },
[RequestKeys.fetchAssets]: { status: RequestStates.inactive },
[RequestKeys.fetchVideos]: { status: RequestStates.inactive },
[RequestKeys.uploadVideo]: { status: RequestStates.inactive },
[RequestKeys.checkTranscriptsForImport]: { status: RequestStates.inactive },
[RequestKeys.importTranscript]: { status: RequestStates.inactive },
[RequestKeys.fetchVideoFeatures]: { status: RequestStates.inactive },

View File

@@ -31,6 +31,12 @@ export const fetchAssets = () => (dispatch) => {
}));
};
export const fetchVideos = () => (dispatch) => {
dispatch(requests.fetchVideos({
onSuccess: (response) => dispatch(actions.app.setVideos(response.data.videos)),
}));
};
export const fetchCourseDetails = () => (dispatch) => {
dispatch(requests.fetchCourseDetails({
onSuccess: (response) => dispatch(actions.app.setCourseDetails(response)),
@@ -50,6 +56,7 @@ export const initialize = (data) => (dispatch) => {
dispatch(module.fetchUnit());
dispatch(module.fetchStudioView());
dispatch(module.fetchAssets());
dispatch(module.fetchVideos());
dispatch(module.fetchCourseDetails());
};
@@ -74,11 +81,6 @@ export const uploadImage = ({ file, setSelection }) => (dispatch) => {
}));
};
export const fetchVideos = ({ onSuccess }) => (dispatch) => {
dispatch(requests.fetchAssets({ onSuccess }));
// onSuccess(mockData.mockVideoData);
};
export default StrictDict({
fetchBlock,
fetchCourseDetails,

View File

@@ -9,6 +9,7 @@ jest.mock('./requests', () => ({
uploadAsset: (args) => ({ uploadAsset: args }),
fetchStudioView: (args) => ({ fetchStudioView: args }),
fetchAssets: (args) => ({ fetchAssets: args }),
fetchVideos: (args) => ({ fetchVideos: args }),
fetchCourseDetails: (args) => ({ fetchCourseDetails: args }),
}));
@@ -105,12 +106,14 @@ describe('app thunkActions', () => {
fetchUnit,
fetchStudioView,
fetchAssets,
fetchVideos,
fetchCourseDetails,
} = thunkActions;
thunkActions.fetchBlock = () => 'fetchBlock';
thunkActions.fetchUnit = () => 'fetchUnit';
thunkActions.fetchStudioView = () => 'fetchStudioView';
thunkActions.fetchAssets = () => 'fetchAssets';
thunkActions.fetchVideos = () => 'fetchVideos';
thunkActions.fetchCourseDetails = () => 'fetchCourseDetails';
thunkActions.initialize(testValue)(dispatch);
expect(dispatch.mock.calls).toEqual([
@@ -119,12 +122,14 @@ describe('app thunkActions', () => {
[thunkActions.fetchUnit()],
[thunkActions.fetchStudioView()],
[thunkActions.fetchAssets()],
[thunkActions.fetchVideos()],
[thunkActions.fetchCourseDetails()],
]);
thunkActions.fetchBlock = fetchBlock;
thunkActions.fetchUnit = fetchUnit;
thunkActions.fetchStudioView = fetchStudioView;
thunkActions.fetchAssets = fetchAssets;
thunkActions.fetchVideos = fetchVideos;
thunkActions.fetchCourseDetails = fetchCourseDetails;
});
});

View File

@@ -136,6 +136,18 @@ export const fetchAssets = ({ ...rest }) => (dispatch, getState) => {
}));
};
export const fetchVideos = ({ ...rest }) => (dispatch, getState) => {
dispatch(module.networkRequest({
requestKey: RequestKeys.fetchVideos,
promise: api
.fetchVideos({
studioEndpointUrl: selectors.app.studioEndpointUrl(getState()),
learningContextId: selectors.app.learningContextId(getState()),
}),
...rest,
}));
};
export const allowThumbnailUpload = ({ ...rest }) => (dispatch, getState) => {
dispatch(module.networkRequest({
requestKey: RequestKeys.allowThumbnailUpload,
@@ -289,6 +301,7 @@ export default StrictDict({
fetchUnit,
saveBlock,
fetchAssets,
fetchVideos,
uploadAsset,
allowThumbnailUpload,
uploadThumbnail,

View File

@@ -18,6 +18,9 @@ export const apiMethods = {
fetchAssets: ({ learningContextId, studioEndpointUrl }) => get(
urls.courseAssets({ studioEndpointUrl, learningContextId }),
),
fetchVideos: ({ studioEndpointUrl, learningContextId }) => get(
urls.courseVideos({ studioEndpointUrl, learningContextId }),
),
fetchCourseDetails: ({ studioEndpointUrl, learningContextId }) => get(
urls.courseDetailsUrl({ studioEndpointUrl, learningContextId }),
),

View File

@@ -66,3 +66,7 @@ export const courseAdvanceSettings = ({ studioEndpointUrl, learningContextId })
export const videoFeatures = ({ studioEndpointUrl, learningContextId }) => (
`${studioEndpointUrl}/video_features/${learningContextId}`
);
export const courseVideos = ({ studioEndpointUrl, learningContextId }) => (
`${studioEndpointUrl}/videos/${learningContextId}`
);

View File

@@ -1,17 +1,12 @@
import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import ErrorAlert from './ErrorAlert';
import { selectors } from '../../data/redux';
import { RequestKeys } from '../../data/constants/requests';
export const FetchErrorAlert = ({
message,
// redux
isFetchError,
// inject
}) => (
<ErrorAlert
isError={isFetchError}
@@ -28,12 +23,8 @@ FetchErrorAlert.propTypes = {
defaultMessage: PropTypes.string,
description: PropTypes.string,
}).isRequired,
// redux
isFetchError: PropTypes.bool.isRequired,
};
export const mapStateToProps = (state) => ({
isFetchError: selectors.requests.isFailed(state, { requestKey: RequestKeys.fetchAssets }),
});
export const mapDispatchToProps = {};
export default connect(mapStateToProps, mapDispatchToProps)(FetchErrorAlert);
export default FetchErrorAlert;

View File

@@ -1,8 +1,6 @@
import React from 'react';
import { shallow } from 'enzyme';
import { FetchErrorAlert, mapStateToProps } from './FetchErrorAlert';
import { selectors } from '../../data/redux';
import { RequestKeys } from '../../data/constants/requests';
import { FetchErrorAlert } from './FetchErrorAlert';
jest.mock('../../data/redux', () => ({
selectors: {
@@ -18,12 +16,4 @@ describe('FetchErrorAlert', () => {
expect(shallow(<FetchErrorAlert isFetchError />)).toMatchSnapshot();
});
});
describe('mapStateToProps', () => {
const testState = { A: 'pple', B: 'anana', C: 'ucumber' };
test('isFetchError from requests.isFinished', () => {
expect(
mapStateToProps(testState).isFetchError,
).toEqual(selectors.requests.isFailed(testState, { requestKey: RequestKeys.fetchAssets }));
});
});
});

View File

@@ -1,17 +1,12 @@
import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import ErrorAlert from './ErrorAlert';
import { selectors } from '../../data/redux';
import { RequestKeys } from '../../data/constants/requests';
export const UploadErrorAlert = ({
message,
// redux
isUploadError,
// inject
}) => (
<ErrorAlert
isError={isUploadError}
@@ -28,11 +23,7 @@ UploadErrorAlert.propTypes = {
defaultMessage: PropTypes.string,
description: PropTypes.string,
}).isRequired,
// redux
isUploadError: PropTypes.bool.isRequired,
};
export const mapStateToProps = (state) => ({
isUploadError: selectors.requests.isFailed(state, { requestKey: RequestKeys.uploadAsset }),
});
export const mapDispatchToProps = {};
export default connect(mapStateToProps, mapDispatchToProps)(UploadErrorAlert);
export default UploadErrorAlert;

View File

@@ -1,8 +1,6 @@
import React from 'react';
import { shallow } from 'enzyme';
import { UploadErrorAlert, mapStateToProps } from './UploadErrorAlert';
import { selectors } from '../../data/redux';
import { RequestKeys } from '../../data/constants/requests';
import { UploadErrorAlert } from './UploadErrorAlert';
jest.mock('../../data/redux', () => ({
selectors: {
@@ -18,12 +16,4 @@ describe('UploadErrorAlert', () => {
expect(shallow(<UploadErrorAlert isUploadError />)).toMatchSnapshot();
});
});
describe('mapStateToProps', () => {
const testState = { A: 'pple', B: 'anana', C: 'ucumber' };
test('isUploadError from requests.isFinished', () => {
expect(
mapStateToProps(testState).isUploadError,
).toEqual(selectors.requests.isFailed(testState, { requestKey: RequestKeys.uploadAsset }));
});
});
});

View File

@@ -1,8 +1,11 @@
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import hooks from './hooks';
import { acceptedImgKeys } from './utils';
import SelectionModal from '../../SelectionModal';
import messages from './messages';
import { RequestKeys } from '../../../data/constants/requests';
import { selectors } from '../../../data/redux';
export const SelectImageModal = ({
isOpen,
@@ -10,6 +13,10 @@ export const SelectImageModal = ({
setSelection,
clearSelection,
images,
// redux
isLoaded,
isFetchError,
isUploadError,
}) => {
const {
galleryError,
@@ -41,6 +48,9 @@ export const SelectImageModal = ({
selectBtnProps,
acceptedFiles: acceptedImgKeys,
modalMessages,
isLoaded,
isFetchError,
isUploadError,
}}
/>
);
@@ -52,6 +62,18 @@ SelectImageModal.propTypes = {
setSelection: PropTypes.func.isRequired,
clearSelection: PropTypes.func.isRequired,
images: PropTypes.arrayOf(PropTypes.string).isRequired,
// redux
isLoaded: PropTypes.bool.isRequired,
isFetchError: PropTypes.bool.isRequired,
isUploadError: PropTypes.bool.isRequired,
};
export default SelectImageModal;
export const mapStateToProps = (state) => ({
isLoaded: selectors.requests.isFinished(state, { requestKey: RequestKeys.fetchAssets }),
isFetchError: selectors.requests.isFailed(state, { requestKey: RequestKeys.fetchAssets }),
isUploadError: selectors.requests.isFailed(state, { requestKey: RequestKeys.uploadAsset }),
});
export const mapDispatchToProps = {};
export default connect(mapStateToProps, mapDispatchToProps)(SelectImageModal);

View File

@@ -1,5 +1,4 @@
import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import {
@@ -13,9 +12,6 @@ import {
MessageDescriptor,
} from '@edx/frontend-platform/i18n';
import { selectors } from '../../data/redux';
import { RequestKeys } from '../../data/constants/requests';
import messages from './messages';
import GalleryCard from './GalleryCard';
@@ -28,10 +24,9 @@ export const Gallery = ({
emptyGalleryLabel,
showIdsOnCards,
height,
isLoaded,
// injected
intl,
// redux
isLoaded,
}) => {
if (!isLoaded) {
return (
@@ -80,6 +75,7 @@ Gallery.defaultProps = {
emptyGalleryLabel: null,
};
Gallery.propTypes = {
isLoaded: PropTypes.bool.isRequired,
galleryIsEmpty: PropTypes.bool.isRequired,
searchIsEmpty: PropTypes.bool.isRequired,
displayList: PropTypes.arrayOf(PropTypes.object).isRequired,
@@ -90,15 +86,6 @@ Gallery.propTypes = {
height: PropTypes.string,
// injected
intl: intlShape.isRequired,
// redux
isLoaded: PropTypes.bool.isRequired,
};
const requestKey = RequestKeys.fetchAssets;
export const mapStateToProps = (state) => ({
isLoaded: selectors.requests.isFinished(state, { requestKey }),
});
export const mapDispatchToProps = {};
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(Gallery));
export default injectIntl(Gallery);

View File

@@ -2,9 +2,7 @@ import React from 'react';
import { shallow } from 'enzyme';
import { formatMessage } from '../../../testUtils';
import { RequestKeys } from '../../data/constants/requests';
import { selectors } from '../../data/redux';
import { Gallery, mapStateToProps, mapDispatchToProps } from './Gallery';
import { Gallery } from './Gallery';
jest.mock('../../data/redux', () => ({
selectors: {
@@ -40,17 +38,4 @@ describe('TextEditor Image Gallery component', () => {
expect(shallow(<Gallery {...props} />)).toMatchSnapshot();
});
});
describe('mapStateToProps', () => {
const testState = { some: 'testState' };
test('loads isLoaded from requests.isFinished selector for fetchAssets request', () => {
expect(mapStateToProps(testState).isLoaded).toEqual(
selectors.requests.isFinished(testState, { requestKey: RequestKeys.fetchAssets }),
);
});
});
describe('mapDispatchToProps', () => {
test('is empty', () => {
expect(mapDispatchToProps).toEqual({});
});
});
});

View File

@@ -60,6 +60,8 @@ GalleryCard.propTypes = {
portableUrl: PropTypes.string,
thumbnail: PropTypes.string,
url: PropTypes.string,
duration: PropTypes.number,
status: PropTypes.string,
}).isRequired,
showId: PropTypes.bool,
};

View File

@@ -31,6 +31,9 @@ export const SelectionModal = ({
selectBtnProps,
acceptedFiles,
modalMessages,
isLoaded,
isFetchError,
isUploadError,
// injected
intl,
}) => {
@@ -41,6 +44,10 @@ export const SelectionModal = ({
fetchError,
uploadError,
} = modalMessages;
const galleryPropsValues = {
isLoaded,
...galleryProps,
};
return (
<BaseModal
close={close}
@@ -60,8 +67,8 @@ export const SelectionModal = ({
title={intl.formatMessage(titleMsg)}
>
{/* Error Alerts */}
<FetchErrorAlert message={fetchError} />
<UploadErrorAlert message={uploadError} />
<FetchErrorAlert isFetchError={isFetchError} message={fetchError} />
<UploadErrorAlert isUploadError={isUploadError} message={uploadError} />
<ErrorAlert
dismissError={inputError.dismiss}
hideHeading
@@ -80,7 +87,7 @@ export const SelectionModal = ({
</ErrorAlert>
<Stack gap={3}>
<SearchSort {...searchSortProps} />
<Gallery {...galleryProps} />
<Gallery {...galleryPropsValues} />
<FileInput fileInput={fileInput} acceptedFiles={Object.values(acceptedFiles).join()} />
</Stack>
</BaseModal>
@@ -124,6 +131,9 @@ SelectionModal.propTypes = {
fetchError: MessageDescriptor,
uploadError: MessageDescriptor,
}).isRequired,
isLoaded: PropTypes.bool.isRequired,
isFetchError: PropTypes.bool.isRequired,
isUploadError: PropTypes.bool.isRequired,
// injected
intl: intlShape.isRequired,
};