From 14504073e089fc45af74991eeed67511601635e2 Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Wed, 15 Mar 2023 11:57:55 -0500 Subject: [PATCH 1/7] feat: Video selection page created The SelectImageModal component has been refactored so that it can also be used on the video selection page; and all its child components. Now this component is called SelectionModal and is used both for the image selector and in this new video selection screen. The assets api has been used to get the videos. --- src/editors/Selector.jsx | 34 +++ src/editors/Selector.test.jsx | 40 +++ src/editors/SelectorPage.jsx | 38 +++ src/editors/SelectorPage.test.jsx | 25 ++ .../__snapshots__/Selector.test.jsx.snap | 3 + .../__snapshots__/SelectorPage.test.jsx.snap | 45 ++++ .../__snapshots__/index.test.jsx.snap | 2 + .../SwitchToAdvancedEditorCard.test.jsx.snap | 1 + .../__snapshots__/index.test.jsx.snap | 92 +++++++ src/editors/containers/VideoGallery/hooks.js | 134 ++++++++++ src/editors/containers/VideoGallery/index.jsx | 64 +++++ .../containers/VideoGallery/index.test.jsx | 85 ++++++ .../containers/VideoGallery/messages.js | 117 ++++++++ src/editors/containers/VideoGallery/utils.js | 42 +++ .../sharedComponents/BaseModal/index.jsx | 5 +- .../__snapshots__/index.test.jsx.snap | 1 + .../SelectImageModal/SearchSort.jsx | 71 ----- .../__snapshots__/index.test.jsx.snap | 249 ++++++------------ .../SelectImageModal/hooks.js | 8 +- .../SelectImageModal/index.jsx | 107 ++------ .../SelectImageModal/index.test.jsx | 58 ++-- .../SelectImageModal/messages.js | 20 -- .../Gallery.jsx | 30 ++- .../Gallery.test.jsx | 8 +- .../GalleryCard.jsx | 35 ++- .../GalleryCard.test.jsx | 2 +- .../SelectionModal/SearchSort.jsx | 135 ++++++++++ .../SearchSort.test.jsx | 6 +- .../__snapshots__/Gallery.test.jsx.snap | 17 +- .../__snapshots__/GalleryCard.test.jsx.snap | 2 +- .../__snapshots__/SearchSort.test.jsx.snap | 4 +- .../__snapshots__/index.test.jsx.snap | 13 + .../sharedComponents/SelectionModal/index.jsx | 131 +++++++++ .../SelectionModal/index.test.jsx | 87 ++++++ .../SelectionModal/messages.js | 24 ++ .../__snapshots__/index.test.jsx.snap | 1 + src/index.jsx | 3 +- 37 files changed, 1317 insertions(+), 422 deletions(-) create mode 100644 src/editors/Selector.jsx create mode 100644 src/editors/Selector.test.jsx create mode 100644 src/editors/SelectorPage.jsx create mode 100644 src/editors/SelectorPage.test.jsx create mode 100644 src/editors/__snapshots__/Selector.test.jsx.snap create mode 100644 src/editors/__snapshots__/SelectorPage.test.jsx.snap create mode 100644 src/editors/containers/VideoGallery/__snapshots__/index.test.jsx.snap create mode 100644 src/editors/containers/VideoGallery/hooks.js create mode 100644 src/editors/containers/VideoGallery/index.jsx create mode 100644 src/editors/containers/VideoGallery/index.test.jsx create mode 100644 src/editors/containers/VideoGallery/messages.js create mode 100644 src/editors/containers/VideoGallery/utils.js delete mode 100644 src/editors/sharedComponents/ImageUploadModal/SelectImageModal/SearchSort.jsx rename src/editors/sharedComponents/{ImageUploadModal/SelectImageModal => SelectionModal}/Gallery.jsx (68%) rename src/editors/sharedComponents/{ImageUploadModal/SelectImageModal => SelectionModal}/Gallery.test.jsx (89%) rename src/editors/sharedComponents/{ImageUploadModal/SelectImageModal => SelectionModal}/GalleryCard.jsx (56%) rename src/editors/sharedComponents/{ImageUploadModal/SelectImageModal => SelectionModal}/GalleryCard.test.jsx (92%) create mode 100644 src/editors/sharedComponents/SelectionModal/SearchSort.jsx rename src/editors/sharedComponents/{ImageUploadModal/SelectImageModal => SelectionModal}/SearchSort.test.jsx (90%) rename src/editors/sharedComponents/{ImageUploadModal/SelectImageModal => SelectionModal}/__snapshots__/Gallery.test.jsx.snap (81%) rename src/editors/sharedComponents/{ImageUploadModal/SelectImageModal => SelectionModal}/__snapshots__/GalleryCard.test.jsx.snap (93%) rename src/editors/sharedComponents/{ImageUploadModal/SelectImageModal => SelectionModal}/__snapshots__/SearchSort.test.jsx.snap (98%) create mode 100644 src/editors/sharedComponents/SelectionModal/__snapshots__/index.test.jsx.snap create mode 100644 src/editors/sharedComponents/SelectionModal/index.jsx create mode 100644 src/editors/sharedComponents/SelectionModal/index.test.jsx create mode 100644 src/editors/sharedComponents/SelectionModal/messages.js diff --git a/src/editors/Selector.jsx b/src/editors/Selector.jsx new file mode 100644 index 000000000..9bf074ee5 --- /dev/null +++ b/src/editors/Selector.jsx @@ -0,0 +1,34 @@ +import React from 'react'; +import { useDispatch } from 'react-redux'; +import PropTypes from 'prop-types'; +import VideoGallery from './containers/VideoGallery'; +import * as hooks from './hooks'; + +export const Selector = ({ + learningContextId, + lmsEndpointUrl, + studioEndpointUrl, +}) => { + const dispatch = useDispatch(); + hooks.initializeApp({ + dispatch, + data: { + blockId: '', + blockType: 'video', + learningContextId, + lmsEndpointUrl, + studioEndpointUrl, + }, + }); + return ( + + ); +}; + +Selector.propTypes = { + learningContextId: PropTypes.string.isRequired, + lmsEndpointUrl: PropTypes.string.isRequired, + studioEndpointUrl: PropTypes.string.isRequired, +}; + +export default Selector; diff --git a/src/editors/Selector.test.jsx b/src/editors/Selector.test.jsx new file mode 100644 index 000000000..ffbaadce5 --- /dev/null +++ b/src/editors/Selector.test.jsx @@ -0,0 +1,40 @@ +import React from 'react'; +import { useDispatch } from 'react-redux'; +import { shallow } from 'enzyme'; +import * as hooks from './hooks'; +import Selector from './Selector'; + +jest.mock('./hooks', () => ({ + initializeApp: jest.fn(), +})); + +jest.mock('./containers/VideoGallery', () => 'VideoGallery'); + +const props = { + learningContextId: 'course-v1:edX+DemoX+Demo_Course', + lmsEndpointUrl: 'evenfakerurl.com', + studioEndpointUrl: 'fakeurl.com', +}; + +const initData = { + blockId: '', + blockType: 'video', + ...props, +}; + +describe('Editor', () => { + describe('render', () => { + test('rendering correctly with expected Input', () => { + expect(shallow()).toMatchSnapshot(); + }); + }); + describe('behavior', () => { + it('calls initializeApp hook with dispatch, and passed data', () => { + shallow(); + expect(hooks.initializeApp).toHaveBeenCalledWith({ + dispatch: useDispatch(), + data: initData, + }); + }); + }); +}); diff --git a/src/editors/SelectorPage.jsx b/src/editors/SelectorPage.jsx new file mode 100644 index 000000000..d3777b2c5 --- /dev/null +++ b/src/editors/SelectorPage.jsx @@ -0,0 +1,38 @@ +import React from 'react'; +import { Provider } from 'react-redux'; +import PropTypes from 'prop-types'; +import ErrorBoundary from './sharedComponents/ErrorBoundary'; +import { Selector } from './Selector'; +import store from './data/store'; + +const SelectorPage = ({ + courseId, + lmsEndpointUrl, + studioEndpointUrl, +}) => ( + + + + + +); + +SelectorPage.defaultProps = { + courseId: null, + lmsEndpointUrl: null, + studioEndpointUrl: null, +}; + +SelectorPage.propTypes = { + courseId: PropTypes.string, + lmsEndpointUrl: PropTypes.string, + studioEndpointUrl: PropTypes.string, +}; + +export default SelectorPage; diff --git a/src/editors/SelectorPage.test.jsx b/src/editors/SelectorPage.test.jsx new file mode 100644 index 000000000..9d278888d --- /dev/null +++ b/src/editors/SelectorPage.test.jsx @@ -0,0 +1,25 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import SelectorPage from './SelectorPage'; + +const props = { + courseId: 'course-v1:edX+DemoX+Demo_Course', + lmsEndpointUrl: 'evenfakerurl.com', + studioEndpointUrl: 'fakeurl.com', +}; + +jest.mock('react-redux', () => ({ + Provider: 'Provider', +})); +jest.mock('./Selector', () => 'Selector'); + +describe('Selector Page', () => { + describe('snapshots', () => { + test('rendering correctly with expected Input', () => { + expect(shallow()).toMatchSnapshot(); + }); + test('rendering with props to null', () => { + expect(shallow()).toMatchSnapshot(); + }); + }); +}); diff --git a/src/editors/__snapshots__/Selector.test.jsx.snap b/src/editors/__snapshots__/Selector.test.jsx.snap new file mode 100644 index 000000000..966902d43 --- /dev/null +++ b/src/editors/__snapshots__/Selector.test.jsx.snap @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Editor render rendering correctly with expected Input 1`] = ``; diff --git a/src/editors/__snapshots__/SelectorPage.test.jsx.snap b/src/editors/__snapshots__/SelectorPage.test.jsx.snap new file mode 100644 index 000000000..578eefd76 --- /dev/null +++ b/src/editors/__snapshots__/SelectorPage.test.jsx.snap @@ -0,0 +1,45 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Selector Page snapshots rendering correctly with expected Input 1`] = ` + + + + + +`; + +exports[`Selector Page snapshots rendering with props to null 1`] = ` + + + + + +`; diff --git a/src/editors/containers/EditorContainer/__snapshots__/index.test.jsx.snap b/src/editors/containers/EditorContainer/__snapshots__/index.test.jsx.snap index acc78abdb..25ebbcb12 100644 --- a/src/editors/containers/EditorContainer/__snapshots__/index.test.jsx.snap +++ b/src/editors/containers/EditorContainer/__snapshots__/index.test.jsx.snap @@ -25,6 +25,7 @@ exports[`EditorContainer component render snapshot: initialized. enable save and } footerAction={null} + isFullscreenScroll={true} isOpen={false} size="md" title="Exit the editor?" @@ -103,6 +104,7 @@ exports[`EditorContainer component render snapshot: not initialized. disable sav } footerAction={null} + isFullscreenScroll={true} isOpen={false} size="md" title="Exit the editor?" diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/__snapshots__/SwitchToAdvancedEditorCard.test.jsx.snap b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/__snapshots__/SwitchToAdvancedEditorCard.test.jsx.snap index defddbed5..2a0a1c3c4 100644 --- a/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/__snapshots__/SwitchToAdvancedEditorCard.test.jsx.snap +++ b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/__snapshots__/SwitchToAdvancedEditorCard.test.jsx.snap @@ -21,6 +21,7 @@ exports[`SwitchToAdvancedEditorCard snapshot snapshot: SwitchToAdvancedEditorCar } footerAction={null} + isFullscreenScroll={true} isOpen={false} size="md" title={ diff --git a/src/editors/containers/VideoGallery/__snapshots__/index.test.jsx.snap b/src/editors/containers/VideoGallery/__snapshots__/index.test.jsx.snap new file mode 100644 index 000000000..33dad4cc6 --- /dev/null +++ b/src/editors/containers/VideoGallery/__snapshots__/index.test.jsx.snap @@ -0,0 +1,92 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`VideoGallery component snapshot 1`] = ` +
+ +
+`; diff --git a/src/editors/containers/VideoGallery/hooks.js b/src/editors/containers/VideoGallery/hooks.js new file mode 100644 index 000000000..fa74d2d4c --- /dev/null +++ b/src/editors/containers/VideoGallery/hooks.js @@ -0,0 +1,134 @@ +import React from 'react'; +import * as module from './hooks'; +import messages from './messages'; +import { + filterKeys, + filterMessages, + sortKeys, + sortMessages, +} from './utils'; + +export const state = { + highlighted: (val) => React.useState(val), + searchString: (val) => React.useState(val), + showSelectVideoError: (val) => React.useState(val), + showSizeError: (val) => React.useState(val), + sortBy: (val) => React.useState(val), + filertBy: (val) => React.useState(val), + hideSelectedVideos: (val) => React.useState(val), +}; + +export const searchAndSortHooks = () => { + const [searchString, setSearchString] = module.state.searchString(''); + const [sortBy, setSortBy] = module.state.sortBy(sortKeys.dateNewest); + const [filterBy, setFilterBy] = module.state.filertBy(filterKeys.videoStatus); + const [hideSelectedVideos, setHideSelectedVideos] = module.state.hideSelectedVideos(false); + + return { + searchString, + onSearchChange: (e) => setSearchString(e.target.value), + clearSearchString: () => setSearchString(''), + sortBy, + onSortClick: (key) => () => setSortBy(key), + sortKeys, + sortMessages, + filterBy, + onFilterClick: (key) => () => setFilterBy(key), + filterKeys, + filterMessages, + showSwitch: true, + hideSelectedVideos, + switchMessage: messages.hideSelectedCourseVideosSwitchLabel, + onSwitchClick: () => setHideSelectedVideos(!hideSelectedVideos), + }; +}; + +export const videoListHooks = ({ videos }) => { + const [highlighted, setHighlighted] = module.state.highlighted(null); + const [ + showSelectVideoError, + setShowSelectVideoError, + ] = module.state.showSelectVideoError(false); + const [ + showSizeError, + setShowSizeError, + ] = module.state.showSizeError(false); + const filteredList = videos; // TODO missing filters and sort + return { + galleryError: { + show: showSelectVideoError, + set: () => setShowSelectVideoError(true), + dismiss: () => setShowSelectVideoError(false), + message: messages.selectVideoError, + }, + // TODO We need to update this message when implementing the video upload screen + inputError: { + show: showSizeError, + set: () => setShowSizeError(true), + dismiss: () => setShowSelectVideoError(false), + message: messages.fileSizeError, + }, + galleryProps: { + galleryIsEmpty: Object.keys(filteredList).length === 0, + searchIsEmpty: filteredList.length === 0, + displayList: filteredList, + highlighted, + onHighlightChange: (e) => setHighlighted(e.target.value), + emptyGalleryLabel: messages.emptyGalleryLabel, + showIdsOnCards: true, + height: '100%', + }, + selectBtnProps: { + onclick: () => { + // TODO Update this when implementing the selection feature + }, + }, + }; +}; + +export const fileInputHooks = () => { + // TODO [Update video] Implement this + const ref = React.useRef(); + const click = () => ref.current.click(); + + return { + click, + addFile: () => {}, + ref, + }; +}; + +export const filterAssets = ({ assets }) => { + let videos = []; + const assetsList = Object.values(assets); + if (assetsList.length > 0) { + videos = assetsList.filter(asset => asset?.contentType?.startsWith('video/')); + } + return videos; +}; + +export const videoHooks = ({ videos }) => { + const searchSortProps = module.searchAndSortHooks(); + const videoList = module.videoListHooks({ videos }); + const { + galleryError, + galleryProps, + inputError, + selectBtnProps, + } = videoList; + const fileInput = module.fileInputHooks(); + + return { + galleryError, + inputError, + fileInput, + galleryProps, + searchSortProps, + selectBtnProps, + }; +}; + +export default { + videoHooks, + filterAssets, +}; diff --git a/src/editors/containers/VideoGallery/index.jsx b/src/editors/containers/VideoGallery/index.jsx new file mode 100644 index 000000000..365e0e654 --- /dev/null +++ b/src/editors/containers/VideoGallery/index.jsx @@ -0,0 +1,64 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; +import { selectors } from '../../data/redux'; +import hooks from './hooks'; +import SelectionModal from '../../sharedComponents/SelectionModal'; +import { acceptedImgKeys } from './utils'; +import messages from './messages'; + +export const VideoGallery = ({ + // redux + assets, +}) => { + const videos = hooks.filterAssets({ assets }); + const { + galleryError, + inputError, + fileInput, + galleryProps, + searchSortProps, + selectBtnProps, + } = hooks.videoHooks({ videos }); + + const modalMessages = { + confirmMsg: messages.selectVideoButtonlabel, + titleMsg: messages.titleLabel, + uploadButtonMsg: messages.uploadButtonLabel, + fetchError: messages.fetchVideosError, + uploadError: messages.uploadVideoError, + }; + + return ( +
+ { /* TODO */ }, + size: 'fullscreen', + isFullscreenScroll: false, + galleryError, + inputError, + fileInput, + galleryProps, + searchSortProps, + selectBtnProps, + acceptedFiles: acceptedImgKeys, + modalMessages, + }} + /> +
+ ); +}; + +VideoGallery.propTypes = { + assets: PropTypes.shape({}).isRequired, +}; + +export const mapStateToProps = (state) => ({ + assets: selectors.app.assets(state), +}); + +export const mapDispatchToProps = {}; + +export default connect(mapStateToProps, mapDispatchToProps)(VideoGallery); diff --git a/src/editors/containers/VideoGallery/index.test.jsx b/src/editors/containers/VideoGallery/index.test.jsx new file mode 100644 index 000000000..7ced7e233 --- /dev/null +++ b/src/editors/containers/VideoGallery/index.test.jsx @@ -0,0 +1,85 @@ +import React from 'react'; +import { shallow } from 'enzyme'; + +import SelectionModal from '../../sharedComponents/SelectionModal'; +import hooks from './hooks'; +import * as module from '.'; + +jest.mock('../../sharedComponents/SelectionModal', () => 'SelectionModal'); + +jest.mock('./hooks', () => ({ + filterAssets: jest.fn(() => []), + videoHooks: jest.fn(() => ({ + galleryError: { + show: 'ShoWERror gAlLery', + set: jest.fn(), + dismiss: jest.fn(), + message: { + id: 'Gallery error id', + defaultMessage: 'Gallery error', + description: 'Gallery error', + }, + }, + inputError: { + show: 'ShoWERror inPUT', + set: jest.fn(), + dismiss: jest.fn(), + message: { + id: 'Input error id', + defaultMessage: 'Input error', + description: 'Input error', + }, + }, + fileInput: { + addFile: 'videoHooks.fileInput.addFile', + click: 'videoHooks.fileInput.click', + ref: 'videoHooks.fileInput.ref', + }, + galleryProps: { gallery: 'props' }, + searchSortProps: { search: 'sortProps' }, + selectBtnProps: { select: 'btnProps' }, + })), +})); + +jest.mock('../../data/redux', () => ({ + selectors: { + requests: { + isPending: (state, { requestKey }) => ({ isPending: { state, requestKey } }), + }, + }, +})); + +describe('VideoGallery', () => { + describe('component', () => { + const props = { + assets: { sOmEaSsET: { staTICUrl: '/assets/sOmEaSsET' } }, + }; + let el; + const videoHooks = hooks.videoHooks(); + beforeEach(() => { + el = shallow(); + }); + test('snapshot', () => { + expect(el).toMatchSnapshot(); + }); + it('provides confirm action, forwarding selectBtnProps from imgHooks', () => { + expect(el.find(SelectionModal).props().selectBtnProps).toEqual( + expect.objectContaining({ ...hooks.videoHooks().selectBtnProps }), + ); + }); + it('provides file upload button linked to fileInput.click', () => { + expect(el.find(SelectionModal).props().fileInput.click).toEqual( + videoHooks.fileInput.click, + ); + }); + it('provides a SearchSort component with searchSortProps from imgHooks', () => { + expect(el.find(SelectionModal).props().searchSortProps).toEqual(videoHooks.searchSortProps); + }); + it('provides a Gallery component with galleryProps from imgHooks', () => { + expect(el.find(SelectionModal).props().galleryProps).toEqual(videoHooks.galleryProps); + }); + it('provides a FileInput component with fileInput props from imgHooks', () => { + expect(el.find(SelectionModal).props().fileInput).toMatchObject(videoHooks.fileInput); + }); + }); +}); diff --git a/src/editors/containers/VideoGallery/messages.js b/src/editors/containers/VideoGallery/messages.js new file mode 100644 index 000000000..d351cd751 --- /dev/null +++ b/src/editors/containers/VideoGallery/messages.js @@ -0,0 +1,117 @@ +export const messages = { + // Gallery + emptyGalleryLabel: { + id: 'authoring.selectvideomodal.emptyGalleryLabel', + defaultMessage: + 'No videos found in your gallery. Please upload a video using the button below.', + description: 'Label for when video gallery is empty.', + }, + selectVideoButtonlabel: { + id: 'authoring.selectvideomodal.selectvideo.label', + defaultMessage: 'Select video', + description: 'Label for Select video button', + }, + titleLabel: { + id: 'authoring.selectvideomodal.title.label', + defaultMessage: 'Add video to your course', + description: 'Title for the select video modal', + }, + uploadButtonLabel: { + id: 'authoring.selectvideomodal.upload.label', + defaultMessage: 'Upload or embed a new video', + description: 'Label for upload button', + }, + + // Sort Dropdown + sortByDateNewest: { + id: 'authoring.selectvideomodal.sort.datenewest.label', + defaultMessage: 'By date added (newest)', + description: 'Dropdown label for sorting by date (newest)', + }, + sortByDateOldest: { + id: 'authoring.selectvideomodal.sort.dateoldest.label', + defaultMessage: 'By date added (oldest)', + description: 'Dropdown label for sorting by date (oldest)', + }, + sortByNameAscending: { + id: 'authoring.selectvideomodal.sort.nameascending.label', + defaultMessage: 'By name (ascending)', + description: 'Dropdown label for sorting by name (ascending)', + }, + sortByNameDescending: { + id: 'authoring.selectvideomodal.sort.namedescending.label', + defaultMessage: 'By name (descending)', + description: 'Dropdown label for sorting by name (descending)', + }, + sortByDurationShortest: { + id: 'authoring.selectvideomodal.sort.durationshortest.label', + defaultMessage: 'By duration (shortest)', + description: 'Dropdown label for sorting by duration (shortest)', + }, + sortByDurationLongest: { + id: 'authoring.selectvideomodal.sort.durationlongest.label', + defaultMessage: 'By duration (longest)', + description: 'Dropdown label for sorting by duration (longest)', + }, + + // Filter Dropdown + filterByVideoStatusNone: { + id: 'authoring.selectvideomodal.filter.videostatusnone.label', + defaultMessage: 'Video status', + description: 'Dropdown label for filter by video status (none)', + }, + filterByVideoStatusUploading: { + id: 'authoring.selectvideomodal.filter.videostatusuploading.label', + defaultMessage: 'Uploading', + description: 'Dropdown label for filter by video status (uploading)', + }, + filterByVideoStatusProcessing: { + id: 'authoring.selectvideomodal.filter.videostatusprocessing.label', + defaultMessage: 'Processing', + description: 'Dropdown label for filter by video status (processing)', + }, + filterByVideoStatusReady: { + id: 'authoring.selectvideomodal.filter.videostatusready.label', + defaultMessage: 'Ready', + description: 'Dropdown label for filter by video status (ready)', + }, + filterByVideoStatusFailed: { + id: 'authoring.selectvideomodal.filter.videostatusfailed.label', + defaultMessage: 'Failed', + description: 'Dropdown label for filter by video status (failed)', + }, + + // Hide switch + hideSelectedCourseVideosSwitchLabel: { + id: 'authoring.selectvideomodal.switch.hideselectedcoursevideos.label', + defaultMessage: 'Hide selected course videos', + description: 'Switch label for hide selected course videos', + }, + + // Errors + selectVideoError: { + id: 'authoring.selectvideomodal.error.selectVideoError', + defaultMessage: 'Select a video to continue.', + description: + 'Message presented to user when clicking Next without selecting a video', + }, + fileSizeError: { + id: 'authoring.selectvideomodal.error.fileSizeError', + defaultMessage: + 'Video must be 10 MB or less. Please resize image and try again.', + description: + 'Message presented to user when file size of video is larger than 10 MB', + }, + uploadVideoError: { + id: 'authoring.selectvideomodal.error.uploadVideoError', + defaultMessage: 'Failed to upload video. Please try again.', + description: 'Message presented to user when video fails to upload', + }, + fetchVideosError: { + id: 'authoring.selectvideomodal.error.fetchVideosError', + defaultMessage: 'Failed to obtain course videos. Please try again.', + description: 'Message presented to user when videos are not found', + }, +}; + +export default messages; diff --git a/src/editors/containers/VideoGallery/utils.js b/src/editors/containers/VideoGallery/utils.js new file mode 100644 index 000000000..eb137072d --- /dev/null +++ b/src/editors/containers/VideoGallery/utils.js @@ -0,0 +1,42 @@ +import { StrictDict, keyStore } from '../../utils'; +import messages from './messages'; + +const messageKeys = keyStore(messages); + +export const sortKeys = StrictDict({ + dateNewest: 'dateNewest', + dateOldest: 'dateOldest', + nameAscending: 'nameAscending', + nameDescending: 'nameDescending', + durationShortest: 'durationShortest', + durationLongest: 'durationLongest', +}); + +export const sortMessages = StrictDict({ + dateNewest: messages[messageKeys.sortByDateNewest], + dateOldest: messages[messageKeys.sortByDateOldest], + nameAscending: messages[messageKeys.sortByNameAscending], + nameDescending: messages[messageKeys.sortByNameDescending], + durationShortest: messages[messageKeys.sortByDurationShortest], + durationLongest: messages[messageKeys.sortByDurationLongest], +}); + +export const filterKeys = StrictDict({ + videoStatus: 'videoStatus', + uploading: 'uploading', + processing: 'processing', + ready: 'ready', + failed: 'failed', +}); + +export const filterMessages = StrictDict({ + videoStatus: messages[messageKeys.filterByVideoStatusNone], + uploading: messages[messageKeys.filterByVideoStatusUploading], + processing: messages[messageKeys.filterByVideoStatusProcessing], + ready: messages[messageKeys.filterByVideoStatusReady], + failed: messages[messageKeys.filterByVideoStatusFailed], +}); + +export const acceptedImgKeys = StrictDict({ + mp4: '.mp4', +}); diff --git a/src/editors/sharedComponents/BaseModal/index.jsx b/src/editors/sharedComponents/BaseModal/index.jsx index fd029e673..5f87c8167 100644 --- a/src/editors/sharedComponents/BaseModal/index.jsx +++ b/src/editors/sharedComponents/BaseModal/index.jsx @@ -17,6 +17,7 @@ export const BaseModal = ({ confirmAction, footerAction, size, + isFullscreenScroll, }) => ( @@ -51,6 +52,7 @@ export const BaseModal = ({ BaseModal.defaultProps = { footerAction: null, size: 'lg', + isFullscreenScroll: true, }; BaseModal.propTypes = { @@ -61,6 +63,7 @@ BaseModal.propTypes = { confirmAction: PropTypes.node.isRequired, footerAction: PropTypes.node, size: PropTypes.string, + isFullscreenScroll: PropTypes.bool, }; export default BaseModal; diff --git a/src/editors/sharedComponents/ImageUploadModal/ImageSettingsModal/__snapshots__/index.test.jsx.snap b/src/editors/sharedComponents/ImageUploadModal/ImageSettingsModal/__snapshots__/index.test.jsx.snap index b3091d827..29f2bd566 100644 --- a/src/editors/sharedComponents/ImageUploadModal/ImageSettingsModal/__snapshots__/index.test.jsx.snap +++ b/src/editors/sharedComponents/ImageUploadModal/ImageSettingsModal/__snapshots__/index.test.jsx.snap @@ -35,6 +35,7 @@ exports[`ImageSettingsModal render snapshot 1`] = ` } footerAction={null} + isFullscreenScroll={true} isOpen={false} size="lg" title="Image Settings" diff --git a/src/editors/sharedComponents/ImageUploadModal/SelectImageModal/SearchSort.jsx b/src/editors/sharedComponents/ImageUploadModal/SelectImageModal/SearchSort.jsx deleted file mode 100644 index a2b406be7..000000000 --- a/src/editors/sharedComponents/ImageUploadModal/SelectImageModal/SearchSort.jsx +++ /dev/null @@ -1,71 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; - -import { - ActionRow, Dropdown, Form, Icon, IconButton, -} from '@edx/paragon'; -import { Close, Search } from '@edx/paragon/icons'; -import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n'; - -import { sortKeys, sortMessages } from './utils'; -import messages from './messages'; - -export const SearchSort = ({ - searchString, - onSearchChange, - clearSearchString, - sortBy, - onSortClick, - // injected - intl, -}) => ( - - - - ) - : - } - value={searchString} - /> - - - - - - - - {Object.keys(sortKeys).map(key => ( - - - - ))} - - - -); - -SearchSort.propTypes = { - searchString: PropTypes.string.isRequired, - onSearchChange: PropTypes.func.isRequired, - clearSearchString: PropTypes.func.isRequired, - sortBy: PropTypes.string.isRequired, - onSortClick: PropTypes.func.isRequired, - // injected - intl: intlShape.isRequired, -}; - -export default injectIntl(SearchSort); diff --git a/src/editors/sharedComponents/ImageUploadModal/SelectImageModal/__snapshots__/index.test.jsx.snap b/src/editors/sharedComponents/ImageUploadModal/SelectImageModal/__snapshots__/index.test.jsx.snap index 797f148e3..6c4ef7516 100644 --- a/src/editors/sharedComponents/ImageUploadModal/SelectImageModal/__snapshots__/index.test.jsx.snap +++ b/src/editors/sharedComponents/ImageUploadModal/SelectImageModal/__snapshots__/index.test.jsx.snap @@ -1,189 +1,94 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`SelectImageModal component snapshot 1`] = ` - - - + - - + close={[MockFunction props.close]} + fileInput={ + Object { + "addFile": "imgHooks.fileInput.addFile", + "click": "imgHooks.fileInput.click", + "ref": "imgHooks.fileInput.ref", + } + } + galleryError={ + Object { + "dismiss": [MockFunction], + "message": Object { + "defaultMessage": "Gallery error", + "description": "Gallery error", + "id": "Gallery error id", + }, + "set": [MockFunction], + "show": "ShoWERror gAlLery", + } + } + galleryProps={ + Object { + "gallery": "props", + } + } + inputError={ + Object { + "dismiss": [MockFunction], + "message": Object { + "defaultMessage": "Input error", + "description": "Input error", + "id": "Input error id", + }, + "set": [MockFunction], + "show": "ShoWERror inPUT", + } } isOpen={true} - title="Add an image" -> - - - - - - - - - - - - - - -`; - -exports[`SelectImageModal component snapshot: uploaded image not loaded, show spinner 1`] = ` - - - } - footerAction={ - + searchSortProps={ + Object { + "search": "sortProps", + } } - isOpen={true} - title="Add an image" -> - - - - - - - - - - - - - - + } +/> `; diff --git a/src/editors/sharedComponents/ImageUploadModal/SelectImageModal/hooks.js b/src/editors/sharedComponents/ImageUploadModal/SelectImageModal/hooks.js index b8e813770..e5e2e95bb 100644 --- a/src/editors/sharedComponents/ImageUploadModal/SelectImageModal/hooks.js +++ b/src/editors/sharedComponents/ImageUploadModal/SelectImageModal/hooks.js @@ -3,7 +3,8 @@ import { useDispatch } from 'react-redux'; import { thunkActions } from '../../../data/redux'; import * as module from './hooks'; -import { sortFunctions, sortKeys } from './utils'; +import { sortFunctions, sortKeys, sortMessages } from './utils'; +import messages from './messages'; export const state = { highlighted: (val) => React.useState(val), @@ -22,6 +23,8 @@ export const searchAndSortHooks = () => { clearSearchString: () => setSearchString(''), sortBy, onSortClick: (key) => () => setSortBy(key), + sortKeys, + sortMessages, }; }; @@ -49,11 +52,13 @@ export const imgListHooks = ({ searchSortProps, setSelection, images }) => { show: showSelectImageError, set: () => setShowSelectImageError(true), dismiss: () => setShowSelectImageError(false), + message: messages.selectImageError, }, inputError: { show: showSizeError, set: () => setShowSizeError(true), dismiss: () => setShowSizeError(false), + message: messages.fileSizeError, }, images, galleryProps: { @@ -62,6 +67,7 @@ export const imgListHooks = ({ searchSortProps, setSelection, images }) => { displayList: list, highlighted, onHighlightChange: (e) => setHighlighted(e.target.value), + emptyGalleryLabel: messages.emptyGalleryLabel, }, // highlight by id selectBtnProps: { diff --git a/src/editors/sharedComponents/ImageUploadModal/SelectImageModal/index.jsx b/src/editors/sharedComponents/ImageUploadModal/SelectImageModal/index.jsx index e2ee598bf..116635f22 100644 --- a/src/editors/sharedComponents/ImageUploadModal/SelectImageModal/index.jsx +++ b/src/editors/sharedComponents/ImageUploadModal/SelectImageModal/index.jsx @@ -1,27 +1,8 @@ -import React from 'react'; -import { connect } from 'react-redux'; import PropTypes from 'prop-types'; - -import { Button, Stack, Spinner } from '@edx/paragon'; -import { Add } from '@edx/paragon/icons'; -import { - FormattedMessage, - injectIntl, - intlShape, -} from '@edx/frontend-platform/i18n'; -import { selectors } from '../../../data/redux'; -import { RequestKeys } from '../../../data/constants/requests'; -import { acceptedImgKeys } from './utils'; - import hooks from './hooks'; +import { acceptedImgKeys } from './utils'; +import SelectionModal from '../../SelectionModal'; import messages from './messages'; -import BaseModal from '../../BaseModal'; -import SearchSort from './SearchSort'; -import Gallery from './Gallery'; -import FileInput from '../../FileInput'; -import FetchErrorAlert from '../../ErrorAlerts/FetchErrorAlert'; -import UploadErrorAlert from '../../ErrorAlerts/UploadErrorAlert'; -import ErrorAlert from '../../ErrorAlerts/ErrorAlert'; export const SelectImageModal = ({ isOpen, @@ -29,10 +10,6 @@ export const SelectImageModal = ({ setSelection, clearSelection, images, - // injected - intl, - // redux - inputIsLoading, }) => { const { galleryError, @@ -43,53 +20,29 @@ export const SelectImageModal = ({ selectBtnProps, } = hooks.imgHooks({ setSelection, clearSelection, images }); - return ( - - - - )} - isOpen={isOpen} - footerAction={( - - )} - title={intl.formatMessage(messages.titleLabel)} - > - {/* Error Alerts */} - - - - - + const modalMessages = { + confirmMsg: messages.nextButtonLabel, + titleMsg: messages.titleLabel, + uploadButtonMsg: messages.uploadButtonLabel, + fetchError: messages.fetchImagesError, + uploadError: messages.uploadImageError, + }; - {/* User Feedback Alerts */} - - - - - - {!inputIsLoading ? : ( - - )} - - - + return ( + ); }; @@ -99,16 +52,6 @@ SelectImageModal.propTypes = { setSelection: PropTypes.func.isRequired, clearSelection: PropTypes.func.isRequired, images: PropTypes.arrayOf(PropTypes.string).isRequired, - // injected - intl: intlShape.isRequired, - // redux - inputIsLoading: PropTypes.bool.isRequired, }; -export const mapStateToProps = (state) => ({ - inputIsLoading: selectors.requests.isPending(state, { requestKey: RequestKeys.uploadAsset }), -}); - -export const mapDispatchToProps = {}; - -export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(SelectImageModal)); +export default SelectImageModal; diff --git a/src/editors/sharedComponents/ImageUploadModal/SelectImageModal/index.test.jsx b/src/editors/sharedComponents/ImageUploadModal/SelectImageModal/index.test.jsx index cc6cbf1b6..d834cfdfe 100644 --- a/src/editors/sharedComponents/ImageUploadModal/SelectImageModal/index.test.jsx +++ b/src/editors/sharedComponents/ImageUploadModal/SelectImageModal/index.test.jsx @@ -2,22 +2,11 @@ import React from 'react'; import { shallow } from 'enzyme'; import { formatMessage } from '../../../../testUtils'; -import { RequestKeys } from '../../../data/constants/requests'; -import { selectors } from '../../../data/redux'; -import BaseModal from '../../BaseModal'; -import FileInput from '../../FileInput'; -import Gallery from './Gallery'; -import SearchSort from './SearchSort'; +import SelectionModal from '../../SelectionModal'; import hooks from './hooks'; -import { SelectImageModal, mapStateToProps, mapDispatchToProps } from '.'; +import { SelectImageModal } from '.'; -jest.mock('../../BaseModal', () => 'BaseModal'); -jest.mock('../../FileInput', () => 'FileInput'); -jest.mock('./Gallery', () => 'Gallery'); -jest.mock('./SearchSort', () => 'SearchSort'); -jest.mock('../../ErrorAlerts/FetchErrorAlert', () => 'FetchErrorAlert'); -jest.mock('../../ErrorAlerts/UploadErrorAlert', () => 'UploadErrorAlert'); -jest.mock('../..//ErrorAlerts/ErrorAlert', () => 'ErrorAlert'); +jest.mock('../../SelectionModal', () => 'SelectionModal'); jest.mock('./hooks', () => ({ imgHooks: jest.fn(() => ({ @@ -25,11 +14,21 @@ jest.mock('./hooks', () => ({ show: 'ShoWERror gAlLery', set: jest.fn(), dismiss: jest.fn(), + message: { + id: 'Gallery error id', + defaultMessage: 'Gallery error', + description: 'Gallery error', + }, }, inputError: { show: 'ShoWERror inPUT', set: jest.fn(), dismiss: jest.fn(), + message: { + id: 'Input error id', + defaultMessage: 'Input error', + description: 'Input error', + }, }, fileInput: { addFile: 'imgHooks.fileInput.addFile', @@ -58,7 +57,6 @@ describe('SelectImageModal', () => { setSelection: jest.fn().mockName('props.setSelection'), clearSelection: jest.fn().mockName('props.clearSelection'), intl: { formatMessage }, - inputIsLoading: false, }; let el; const imgHooks = hooks.imgHooks(); @@ -68,42 +66,24 @@ describe('SelectImageModal', () => { test('snapshot', () => { expect(el).toMatchSnapshot(); }); - test('snapshot: uploaded image not loaded, show spinner', () => { - props.inputIsLoading = true; - expect(shallow()).toMatchSnapshot(); - props.inputIsLoading = false; - }); it('provides confirm action, forwarding selectBtnProps from imgHooks', () => { - expect(el.find(BaseModal).props().confirmAction.props).toEqual( - expect.objectContaining({ ...hooks.imgHooks().selectBtnProps, variant: 'primary' }), + expect(el.find(SelectionModal).props().selectBtnProps).toEqual( + expect.objectContaining({ ...hooks.imgHooks().selectBtnProps }), ); }); it('provides file upload button linked to fileInput.click', () => { - expect(el.find(BaseModal).props().footerAction.props.onClick).toEqual( + expect(el.find(SelectionModal).props().fileInput.click).toEqual( imgHooks.fileInput.click, ); }); it('provides a SearchSort component with searchSortProps from imgHooks', () => { - expect(el.find(SearchSort).props()).toEqual(imgHooks.searchSortProps); + expect(el.find(SelectionModal).props().searchSortProps).toEqual(imgHooks.searchSortProps); }); it('provides a Gallery component with galleryProps from imgHooks', () => { - expect(el.find(Gallery).props()).toEqual(imgHooks.galleryProps); + expect(el.find(SelectionModal).props().galleryProps).toEqual(imgHooks.galleryProps); }); it('provides a FileInput component with fileInput props from imgHooks', () => { - expect(el.find(FileInput).props()).toMatchObject({ fileInput: imgHooks.fileInput }); - }); - }); - describe('mapStateToProps', () => { - const testState = { some: 'testState' }; - test('loads inputIsLoading from requests.isPending selector for uploadAsset request', () => { - expect(mapStateToProps(testState).inputIsLoading).toEqual( - selectors.requests.isPending(testState, { requestKey: RequestKeys.uploadAsset }), - ); - }); - }); - describe('mapDispatchToProps', () => { - test('is empty', () => { - expect(mapDispatchToProps).toEqual({}); + expect(el.find(SelectionModal).props().fileInput).toMatchObject(imgHooks.fileInput); }); }); }); diff --git a/src/editors/sharedComponents/ImageUploadModal/SelectImageModal/messages.js b/src/editors/sharedComponents/ImageUploadModal/SelectImageModal/messages.js index d31b9d7b6..f39c30779 100644 --- a/src/editors/sharedComponents/ImageUploadModal/SelectImageModal/messages.js +++ b/src/editors/sharedComponents/ImageUploadModal/SelectImageModal/messages.js @@ -17,11 +17,6 @@ const messages = defineMessages({ defaultMessage: 'Add an image', description: 'Title for the select image modal', }, - searchPlaceholder: { - id: 'authoring.texteditor.selectimagemodal.search.placeholder', - defaultMessage: 'Search', - description: 'Placeholder text for search bar', - }, // Sort Dropdown sortByDateNewest: { @@ -46,27 +41,12 @@ const messages = defineMessages({ }, // Gallery - addedDate: { - id: 'authoring.texteditor.selectimagemodal.addedDate.label', - defaultMessage: 'Added {date} at {time}', - description: 'File date-added string', - }, - loading: { - id: 'authoring.texteditor.selectimagemodal.spinner.readertext', - defaultMessage: 'loading...', - description: 'Gallery loading spinner screen-reader text', - }, emptyGalleryLabel: { id: 'authoring.texteditor.selectimagemodal.emptyGalleryLabel', defaultMessage: 'No images found in your gallery. Please upload an image using the button below.', description: 'Label for when image gallery is empty.', }, - emptySearchLabel: { - id: 'authoring.texteditor.selectimagemodal.emptySearchLabel', - defaultMessage: 'No search results.', - description: 'Label for when search returns nothing.', - }, // Errors uploadImageError: { diff --git a/src/editors/sharedComponents/ImageUploadModal/SelectImageModal/Gallery.jsx b/src/editors/sharedComponents/SelectionModal/Gallery.jsx similarity index 68% rename from src/editors/sharedComponents/ImageUploadModal/SelectImageModal/Gallery.jsx rename to src/editors/sharedComponents/SelectionModal/Gallery.jsx index 99419a6a9..fb113f101 100644 --- a/src/editors/sharedComponents/ImageUploadModal/SelectImageModal/Gallery.jsx +++ b/src/editors/sharedComponents/SelectionModal/Gallery.jsx @@ -6,10 +6,15 @@ import { Scrollable, SelectableBox, Spinner, } from '@edx/paragon'; -import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n'; +import { + FormattedMessage, + injectIntl, + intlShape, + MessageDescriptor, +} from '@edx/frontend-platform/i18n'; -import { selectors } from '../../../data/redux'; -import { RequestKeys } from '../../../data/constants/requests'; +import { selectors } from '../../data/redux'; +import { RequestKeys } from '../../data/constants/requests'; import messages from './messages'; import GalleryCard from './GalleryCard'; @@ -20,6 +25,9 @@ export const Gallery = ({ displayList, highlighted, onHighlightChange, + emptyGalleryLabel, + showIdsOnCards, + height, // injected intl, // redux @@ -36,20 +44,20 @@ export const Gallery = ({ } if (galleryIsEmpty) { return ( -
- +
+
); } if (searchIsEmpty) { return ( -
+
); } return ( - +
- {displayList.map(img => )} + { displayList.map(asset => ) }
@@ -67,6 +75,9 @@ export const Gallery = ({ Gallery.defaultProps = { highlighted: '', + showIdsOnCards: false, + height: '375px', + emptyGalleryLabel: null, }; Gallery.propTypes = { galleryIsEmpty: PropTypes.bool.isRequired, @@ -74,6 +85,9 @@ Gallery.propTypes = { displayList: PropTypes.arrayOf(PropTypes.object).isRequired, highlighted: PropTypes.string, onHighlightChange: PropTypes.func.isRequired, + emptyGalleryLabel: MessageDescriptor, + showIdsOnCards: PropTypes.bool, + height: PropTypes.string, // injected intl: intlShape.isRequired, // redux diff --git a/src/editors/sharedComponents/ImageUploadModal/SelectImageModal/Gallery.test.jsx b/src/editors/sharedComponents/SelectionModal/Gallery.test.jsx similarity index 89% rename from src/editors/sharedComponents/ImageUploadModal/SelectImageModal/Gallery.test.jsx rename to src/editors/sharedComponents/SelectionModal/Gallery.test.jsx index 1227f08ea..f46445a27 100644 --- a/src/editors/sharedComponents/ImageUploadModal/SelectImageModal/Gallery.test.jsx +++ b/src/editors/sharedComponents/SelectionModal/Gallery.test.jsx @@ -1,12 +1,12 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { formatMessage } from '../../../../testUtils'; -import { RequestKeys } from '../../../data/constants/requests'; -import { selectors } from '../../../data/redux'; +import { formatMessage } from '../../../testUtils'; +import { RequestKeys } from '../../data/constants/requests'; +import { selectors } from '../../data/redux'; import { Gallery, mapStateToProps, mapDispatchToProps } from './Gallery'; -jest.mock('../../../data/redux', () => ({ +jest.mock('../../data/redux', () => ({ selectors: { requests: { isFinished: (state, { requestKey }) => ({ isFinished: { state, requestKey } }), diff --git a/src/editors/sharedComponents/ImageUploadModal/SelectImageModal/GalleryCard.jsx b/src/editors/sharedComponents/SelectionModal/GalleryCard.jsx similarity index 56% rename from src/editors/sharedComponents/ImageUploadModal/SelectImageModal/GalleryCard.jsx rename to src/editors/sharedComponents/SelectionModal/GalleryCard.jsx index c031fd32e..10d8e5aba 100644 --- a/src/editors/sharedComponents/ImageUploadModal/SelectImageModal/GalleryCard.jsx +++ b/src/editors/sharedComponents/SelectionModal/GalleryCard.jsx @@ -1,28 +1,42 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { Image, SelectableBox } from '@edx/paragon'; +import { + Button, + Icon, + Image, + SelectableBox, +} from '@edx/paragon'; import { FormattedMessage, FormattedDate, FormattedTime } from '@edx/frontend-platform/i18n'; +import { Link } from '@edx/paragon/icons'; import messages from './messages'; export const GalleryCard = ({ - img, + asset, + showId, }) => ( - +
-

{img.displayName}

+

{asset.displayName}

+ { showId && ( +

+ +

+ )}

, - time: , + date: , + time: , }} />

@@ -31,8 +45,12 @@ export const GalleryCard = ({ ); +GalleryCard.defaultProps = { + showId: false, +}; + GalleryCard.propTypes = { - img: PropTypes.shape({ + asset: PropTypes.shape({ contentType: PropTypes.string, displayName: PropTypes.string, externalUrl: PropTypes.string, @@ -43,6 +61,7 @@ GalleryCard.propTypes = { thumbnail: PropTypes.string, url: PropTypes.string, }).isRequired, + showId: PropTypes.bool, }; export default GalleryCard; diff --git a/src/editors/sharedComponents/ImageUploadModal/SelectImageModal/GalleryCard.test.jsx b/src/editors/sharedComponents/SelectionModal/GalleryCard.test.jsx similarity index 92% rename from src/editors/sharedComponents/ImageUploadModal/SelectImageModal/GalleryCard.test.jsx rename to src/editors/sharedComponents/SelectionModal/GalleryCard.test.jsx index 7769d71b4..cd8577d18 100644 --- a/src/editors/sharedComponents/ImageUploadModal/SelectImageModal/GalleryCard.test.jsx +++ b/src/editors/sharedComponents/SelectionModal/GalleryCard.test.jsx @@ -12,7 +12,7 @@ describe('GalleryCard component', () => { }; let el; beforeEach(() => { - el = shallow(); + el = shallow(); }); test(`snapshot: dateAdded=${img.dateAdded}`, () => { expect(el).toMatchSnapshot(); diff --git a/src/editors/sharedComponents/SelectionModal/SearchSort.jsx b/src/editors/sharedComponents/SelectionModal/SearchSort.jsx new file mode 100644 index 000000000..3a54af3ce --- /dev/null +++ b/src/editors/sharedComponents/SelectionModal/SearchSort.jsx @@ -0,0 +1,135 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import { + ActionRow, Dropdown, Form, Icon, IconButton, +} from '@edx/paragon'; +import { Close, Search } from '@edx/paragon/icons'; +import { + FormattedMessage, + injectIntl, + MessageDescriptor, + intlShape, +} from '@edx/frontend-platform/i18n'; + +import messages from './messages'; + +export const SearchSort = ({ + searchString, + onSearchChange, + clearSearchString, + sortBy, + onSortClick, + sortKeys, + sortMessages, + filterBy, + onFilterClick, + filterKeys, + filterMessages, + showSwitch, + switchMessage, + onSwitchClick, + // injected + intl, +}) => ( + + + + ) + : + } + value={searchString} + /> + + + { !showSwitch && } + + + + + + {Object.keys(sortKeys).map(key => ( + + + + ))} + + + + { filterKeys && filterMessages && ( + + + + + + {Object.keys(filterKeys).map(key => ( + + + + ))} + + + )} + + { showSwitch && ( + <> + + + + + + + + )} + + +); + +SearchSort.defaultProps = { + filterBy: '', + onFilterClick: null, + filterKeys: null, + filterMessages: null, + showSwitch: false, + switchMessage: null, + onSwitchClick: null, +}; + +SearchSort.propTypes = { + searchString: PropTypes.string.isRequired, + onSearchChange: PropTypes.func.isRequired, + clearSearchString: PropTypes.func.isRequired, + sortBy: PropTypes.string.isRequired, + onSortClick: PropTypes.func.isRequired, + sortKeys: PropTypes.shape({}).isRequired, + sortMessages: PropTypes.shape({}).isRequired, + filterBy: PropTypes.string, + onFilterClick: PropTypes.func, + filterKeys: PropTypes.shape({}), + filterMessages: PropTypes.shape({}), + showSwitch: PropTypes.bool, + switchMessage: MessageDescriptor, + onSwitchClick: PropTypes.func, + // injected + intl: intlShape.isRequired, +}; + +export default injectIntl(SearchSort); diff --git a/src/editors/sharedComponents/ImageUploadModal/SelectImageModal/SearchSort.test.jsx b/src/editors/sharedComponents/SelectionModal/SearchSort.test.jsx similarity index 90% rename from src/editors/sharedComponents/ImageUploadModal/SelectImageModal/SearchSort.test.jsx rename to src/editors/sharedComponents/SelectionModal/SearchSort.test.jsx index bb2b98676..dbb5e9de9 100644 --- a/src/editors/sharedComponents/ImageUploadModal/SelectImageModal/SearchSort.test.jsx +++ b/src/editors/sharedComponents/SelectionModal/SearchSort.test.jsx @@ -3,9 +3,9 @@ import { shallow } from 'enzyme'; import { Dropdown } from '@edx/paragon'; import { FormattedMessage } from '@edx/frontend-platform/i18n'; -import { formatMessage } from '../../../../testUtils'; +import { formatMessage } from '../../../testUtils'; -import { sortKeys, sortMessages } from './utils'; +import { sortKeys, sortMessages } from '../ImageUploadModal/SelectImageModal/utils'; import { SearchSort } from './SearchSort'; describe('SearchSort component', () => { @@ -14,6 +14,8 @@ describe('SearchSort component', () => { onSearchChange: jest.fn().mockName('props.onSearchChange'), clearSearchString: jest.fn().mockName('props.clearSearchString'), sortBy: sortKeys.dateOldest, + sortKeys, + sortMessages, onSortClick: jest.fn().mockName('props.onSortClick'), intl: { formatMessage }, }; diff --git a/src/editors/sharedComponents/ImageUploadModal/SelectImageModal/__snapshots__/Gallery.test.jsx.snap b/src/editors/sharedComponents/SelectionModal/__snapshots__/Gallery.test.jsx.snap similarity index 81% rename from src/editors/sharedComponents/ImageUploadModal/SelectImageModal/__snapshots__/Gallery.test.jsx.snap rename to src/editors/sharedComponents/SelectionModal/__snapshots__/Gallery.test.jsx.snap index 74efb5b18..92be690c8 100644 --- a/src/editors/sharedComponents/ImageUploadModal/SelectImageModal/__snapshots__/Gallery.test.jsx.snap +++ b/src/editors/sharedComponents/SelectionModal/__snapshots__/Gallery.test.jsx.snap @@ -9,11 +9,7 @@ exports[`TextEditor Image Gallery component component snapshot: loaded but no im } } > - +
`; @@ -29,7 +25,7 @@ exports[`TextEditor Image Gallery component component snapshot: loaded but searc
`; @@ -54,28 +50,31 @@ exports[`TextEditor Image Gallery component component snapshot: loaded, show gal value="props.highlighted" >
diff --git a/src/editors/sharedComponents/ImageUploadModal/SelectImageModal/__snapshots__/GalleryCard.test.jsx.snap b/src/editors/sharedComponents/SelectionModal/__snapshots__/GalleryCard.test.jsx.snap similarity index 93% rename from src/editors/sharedComponents/ImageUploadModal/SelectImageModal/__snapshots__/GalleryCard.test.jsx.snap rename to src/editors/sharedComponents/SelectionModal/__snapshots__/GalleryCard.test.jsx.snap index 51636fd3d..41f5ac1df 100644 --- a/src/editors/sharedComponents/ImageUploadModal/SelectImageModal/__snapshots__/GalleryCard.test.jsx.snap +++ b/src/editors/sharedComponents/SelectionModal/__snapshots__/GalleryCard.test.jsx.snap @@ -28,7 +28,7 @@ exports[`GalleryCard component snapshot: dateAdded=12345 1`] = ` + + +`; + +exports[`Selection Modal snapshots rendering with props to null 1`] = ` + + + +`; diff --git a/src/editors/sharedComponents/SelectionModal/index.jsx b/src/editors/sharedComponents/SelectionModal/index.jsx new file mode 100644 index 000000000..2b71dcbe8 --- /dev/null +++ b/src/editors/sharedComponents/SelectionModal/index.jsx @@ -0,0 +1,131 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import { Button, Stack } from '@edx/paragon'; +import { Add } from '@edx/paragon/icons'; +import { + FormattedMessage, + injectIntl, + MessageDescriptor, + intlShape, +} from '@edx/frontend-platform/i18n'; + +import BaseModal from '../BaseModal'; +import SearchSort from './SearchSort'; +import Gallery from './Gallery'; +import FileInput from '../FileInput'; +import ErrorAlert from '../ErrorAlerts/ErrorAlert'; +import FetchErrorAlert from '../ErrorAlerts/FetchErrorAlert'; +import UploadErrorAlert from '../ErrorAlerts/UploadErrorAlert'; + +export const SelectionModal = ({ + isOpen, + close, + size, + isFullscreenScroll, + galleryError, + inputError, + fileInput, + galleryProps, + searchSortProps, + selectBtnProps, + acceptedFiles, + modalMessages, + // injected + intl, +}) => { + const { + confirmMsg, + uploadButtonMsg, + titleMsg, + fetchError, + uploadError, + } = modalMessages; + return ( + + + + )} + isOpen={isOpen} + size={size} + isFullscreenScroll={isFullscreenScroll} + footerAction={( + + )} + title={intl.formatMessage(titleMsg)} + > + {/* Error Alerts */} + + + + + + + {/* User Feedback Alerts */} + + + + + + + + + + ); +}; + +SelectionModal.defaultProps = { + size: 'lg', + isFullscreenScroll: true, +}; + +SelectionModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + close: PropTypes.func.isRequired, + size: PropTypes.string, + isFullscreenScroll: PropTypes.bool, + galleryError: PropTypes.shape({ + dismiss: PropTypes.func.isRequired, + show: PropTypes.bool.isRequired, + set: PropTypes.func.isRequired, + message: MessageDescriptor, + }).isRequired, + inputError: PropTypes.shape({ + dismiss: PropTypes.func.isRequired, + show: PropTypes.bool.isRequired, + set: PropTypes.func.isRequired, + message: MessageDescriptor, + }).isRequired, + fileInput: PropTypes.shape({ + click: PropTypes.func.isRequired, + addFile: PropTypes.func.isRequired, + }).isRequired, + galleryProps: PropTypes.shape({}).isRequired, + searchSortProps: PropTypes.shape({}).isRequired, + selectBtnProps: PropTypes.shape({}).isRequired, + acceptedFiles: PropTypes.shape({}).isRequired, + modalMessages: PropTypes.shape({ + confirmMsg: MessageDescriptor, + uploadButtonMsg: MessageDescriptor, + titleMsg: MessageDescriptor, + fetchError: MessageDescriptor, + uploadError: MessageDescriptor, + }).isRequired, + // injected + intl: intlShape.isRequired, +}; + +export default injectIntl(SelectionModal); diff --git a/src/editors/sharedComponents/SelectionModal/index.test.jsx b/src/editors/sharedComponents/SelectionModal/index.test.jsx new file mode 100644 index 000000000..a77669a8c --- /dev/null +++ b/src/editors/sharedComponents/SelectionModal/index.test.jsx @@ -0,0 +1,87 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import { formatMessage } from '../../../testUtils'; +import SelectionModal from '.'; + +const props = { + isOpen: jest.fn(), + isClose: jest.fn(), + size: 'fullscreen', + isFullscreenScroll: false, + galleryError: { + show: 'ShoWERror gAlLery', + set: jest.fn(), + dismiss: jest.fn(), + message: { + id: 'Gallery error id', + defaultMessage: 'Gallery error', + description: 'Gallery error', + }, + }, + inputError: { + show: 'ShoWERror inPUT', + set: jest.fn(), + dismiss: jest.fn(), + message: { + id: 'Input error id', + defaultMessage: 'Input error', + description: 'Input error', + }, + }, + fileInput: { + addFile: 'imgHooks.fileInput.addFile', + click: 'imgHooks.fileInput.click', + ref: 'imgHooks.fileInput.ref', + }, + galleryProps: { gallery: 'props' }, + searchSortProps: { search: 'sortProps' }, + selectBtnProps: { select: 'btnProps' }, + acceptedFiles: { png: '.png' }, + modalMessages: { + confirmMsg: { + id: 'confirmMsg', + defaultMessage: 'confirmMsg', + description: 'confirmMsg', + }, + uploadButtonMsg: { + id: 'uploadButtonMsg', + defaultMessage: 'uploadButtonMsg', + description: 'uploadButtonMsg', + }, + titleMsg: { + id: 'titleMsg', + defaultMessage: 'titleMsg', + description: 'titleMsg', + }, + fetchError: { + id: 'fetchError', + defaultMessage: 'fetchError', + description: 'fetchError', + }, + uploadError: { + id: 'uploadError', + defaultMessage: 'uploadError', + description: 'uploadError', + }, + }, + intl: { formatMessage }, +}; + +jest.mock('../BaseModal', () => 'BaseModal'); +jest.mock('./SearchSort', () => 'SearchSort'); +jest.mock('./Gallery', () => 'Gallery'); +jest.mock('../FileInput', () => 'FileInput'); +jest.mock('../ErrorAlerts/ErrorAlert', () => 'ErrorAlert'); +jest.mock('../ErrorAlerts/FetchErrorAlert', () => 'FetchErrorAlert'); +jest.mock('../ErrorAlerts/UploadErrorAlert', () => 'UploadErrorAlert'); + +describe('Selection Modal', () => { + describe('snapshots', () => { + test('rendering correctly with expected Input', () => { + expect(shallow()).toMatchSnapshot(); + }); + test('rendering with props to null', () => { + expect(shallow()).toMatchSnapshot(); + }); + }); +}); diff --git a/src/editors/sharedComponents/SelectionModal/messages.js b/src/editors/sharedComponents/SelectionModal/messages.js new file mode 100644 index 000000000..10d18d0c2 --- /dev/null +++ b/src/editors/sharedComponents/SelectionModal/messages.js @@ -0,0 +1,24 @@ +export const messages = { + searchPlaceholder: { + id: 'authoring.selectionmodal.search.placeholder', + defaultMessage: 'Search', + description: 'Placeholder text for search bar', + }, + emptySearchLabel: { + id: 'authoring.selectionmodal.emptySearchLabel', + defaultMessage: 'No search results.', + description: 'Label for when search returns nothing.', + }, + loading: { + id: 'authoring.selectionmodal.spinner.readertext', + defaultMessage: 'loading...', + description: 'Gallery loading spinner screen-reader text', + }, + addedDate: { + id: 'authoring.selectionmodal.addedDate.label', + defaultMessage: 'Added {date} at {time}', + description: 'File date-added string', + }, +}; + +export default messages; diff --git a/src/editors/sharedComponents/SourceCodeModal/__snapshots__/index.test.jsx.snap b/src/editors/sharedComponents/SourceCodeModal/__snapshots__/index.test.jsx.snap index c7b019463..33662d621 100644 --- a/src/editors/sharedComponents/SourceCodeModal/__snapshots__/index.test.jsx.snap +++ b/src/editors/sharedComponents/SourceCodeModal/__snapshots__/index.test.jsx.snap @@ -24,6 +24,7 @@ exports[`SourceCodeModal renders as expected with default behavior 1`] = ` } footerAction={null} + isFullscreenScroll={true} isOpen={false} size="xl" title="Edit Source Code" diff --git a/src/index.jsx b/src/index.jsx index a92a3b872..c4b875116 100644 --- a/src/index.jsx +++ b/src/index.jsx @@ -1,6 +1,7 @@ import Placeholder from './Placeholder'; import messages from './i18n/index'; import EditorPage from './editors/EditorPage'; +import SelectorPage from './editors/SelectorPage'; -export { messages, EditorPage }; +export { messages, EditorPage, SelectorPage }; export default Placeholder; From b78e58cd2a647c5346c8450c630a24dd0cdf8d63 Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Fri, 17 Mar 2023 11:59:59 -0500 Subject: [PATCH 2/7] feat: New request hook to fetch videos --- .../components/HandoutWidget/index.jsx | 5 +++- .../components/HandoutWidget/index.test.jsx | 3 +++ .../__snapshots__/index.test.jsx.snap | 3 +++ src/editors/containers/VideoGallery/hooks.js | 19 +++++++++++---- src/editors/containers/VideoGallery/index.jsx | 21 ++++++++++++---- .../containers/VideoGallery/index.test.jsx | 11 ++++++--- src/editors/data/constants/requests.js | 2 ++ src/editors/data/redux/app/reducer.js | 2 ++ src/editors/data/redux/app/selectors.js | 1 + src/editors/data/redux/requests/reducer.js | 2 ++ src/editors/data/redux/thunkActions/app.js | 12 ++++++---- .../data/redux/thunkActions/app.test.js | 5 ++++ .../data/redux/thunkActions/requests.js | 13 ++++++++++ src/editors/data/services/cms/api.js | 3 +++ src/editors/data/services/cms/urls.js | 4 ++++ .../ErrorAlerts/FetchErrorAlert.jsx | 13 ++-------- .../ErrorAlerts/FetchErrorAlert.test.jsx | 12 +--------- .../ErrorAlerts/UploadErrorAlert.jsx | 13 ++-------- .../ErrorAlerts/UploadErrorAlert.test.jsx | 12 +--------- .../SelectImageModal/index.jsx | 24 ++++++++++++++++++- .../SelectionModal/Gallery.jsx | 19 +++------------ .../SelectionModal/Gallery.test.jsx | 17 +------------ .../SelectionModal/GalleryCard.jsx | 2 ++ .../sharedComponents/SelectionModal/index.jsx | 16 ++++++++++--- 24 files changed, 136 insertions(+), 98 deletions(-) diff --git a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/HandoutWidget/index.jsx b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/HandoutWidget/index.jsx index 760a3fde6..f958b9cea 100644 --- a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/HandoutWidget/index.jsx +++ b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/HandoutWidget/index.jsx @@ -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 = ({ > - + {handout ? ( @@ -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) => ({ diff --git a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/HandoutWidget/index.test.jsx b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/HandoutWidget/index.test.jsx index 96dba8257..4256f060c 100644 --- a/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/HandoutWidget/index.test.jsx +++ b/src/editors/containers/VideoEditor/components/VideoSettingsModal/components/HandoutWidget/index.test.jsx @@ -24,6 +24,9 @@ jest.mock('../../../../../../data/redux', () => ({ app: { isLibrary: jest.fn(args => ({ isLibrary: args })), }, + requests: { + isFailed: jest.fn(args => ({ isFailed: args })), + }, }, })); diff --git a/src/editors/containers/VideoGallery/__snapshots__/index.test.jsx.snap b/src/editors/containers/VideoGallery/__snapshots__/index.test.jsx.snap index 33dad4cc6..31dc76953 100644 --- a/src/editors/containers/VideoGallery/__snapshots__/index.test.jsx.snap +++ b/src/editors/containers/VideoGallery/__snapshots__/index.test.jsx.snap @@ -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 { diff --git a/src/editors/containers/VideoGallery/hooks.js b/src/editors/containers/VideoGallery/hooks.js index fa74d2d4c..dde1fe7df 100644 --- a/src/editors/containers/VideoGallery/hooks.js +++ b/src/editors/containers/VideoGallery/hooks.js @@ -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, }; diff --git a/src/editors/containers/VideoGallery/index.jsx b/src/editors/containers/VideoGallery/index.jsx index 365e0e654..0f1cced0b 100644 --- a/src/editors/containers/VideoGallery/index.jsx +++ b/src/editors/containers/VideoGallery/index.jsx @@ -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, }} />
@@ -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 = {}; diff --git a/src/editors/containers/VideoGallery/index.test.jsx b/src/editors/containers/VideoGallery/index.test.jsx index 7ced7e233..58469c424 100644 --- a/src/editors/containers/VideoGallery/index.test.jsx +++ b/src/editors/containers/VideoGallery/index.test.jsx @@ -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(); diff --git a/src/editors/data/constants/requests.js b/src/editors/data/constants/requests.js index 195bbf469..989661420 100644 --- a/src/editors/data/constants/requests.js +++ b/src/editors/data/constants/requests.js @@ -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', diff --git a/src/editors/data/redux/app/reducer.js b/src/editors/data/redux/app/reducer.js index cc73b2b79..59a189f40 100644 --- a/src/editors/data/redux/app/reducer.js +++ b/src/editors/data/redux/app/reducer.js @@ -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 }), }, }); diff --git a/src/editors/data/redux/app/selectors.js b/src/editors/data/redux/app/selectors.js index 4df9eceae..7c6a99d40 100644 --- a/src/editors/data/redux/app/selectors.js +++ b/src/editors/data/redux/app/selectors.js @@ -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( diff --git a/src/editors/data/redux/requests/reducer.js b/src/editors/data/redux/requests/reducer.js index 4f604cd12..15274560f 100644 --- a/src/editors/data/redux/requests/reducer.js +++ b/src/editors/data/redux/requests/reducer.js @@ -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 }, diff --git a/src/editors/data/redux/thunkActions/app.js b/src/editors/data/redux/thunkActions/app.js index aca044751..d080cb528 100644 --- a/src/editors/data/redux/thunkActions/app.js +++ b/src/editors/data/redux/thunkActions/app.js @@ -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, diff --git a/src/editors/data/redux/thunkActions/app.test.js b/src/editors/data/redux/thunkActions/app.test.js index 01373e9af..8a2621f6f 100644 --- a/src/editors/data/redux/thunkActions/app.test.js +++ b/src/editors/data/redux/thunkActions/app.test.js @@ -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; }); }); diff --git a/src/editors/data/redux/thunkActions/requests.js b/src/editors/data/redux/thunkActions/requests.js index c8014b5a4..efdec1292 100644 --- a/src/editors/data/redux/thunkActions/requests.js +++ b/src/editors/data/redux/thunkActions/requests.js @@ -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, diff --git a/src/editors/data/services/cms/api.js b/src/editors/data/services/cms/api.js index 141bd3b43..f0739b504 100644 --- a/src/editors/data/services/cms/api.js +++ b/src/editors/data/services/cms/api.js @@ -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 }), ), diff --git a/src/editors/data/services/cms/urls.js b/src/editors/data/services/cms/urls.js index c8642c741..c8d2e0618 100644 --- a/src/editors/data/services/cms/urls.js +++ b/src/editors/data/services/cms/urls.js @@ -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}` +); diff --git a/src/editors/sharedComponents/ErrorAlerts/FetchErrorAlert.jsx b/src/editors/sharedComponents/ErrorAlerts/FetchErrorAlert.jsx index 4f916f5aa..d098c7678 100644 --- a/src/editors/sharedComponents/ErrorAlerts/FetchErrorAlert.jsx +++ b/src/editors/sharedComponents/ErrorAlerts/FetchErrorAlert.jsx @@ -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 }) => ( ({ - isFetchError: selectors.requests.isFailed(state, { requestKey: RequestKeys.fetchAssets }), -}); -export const mapDispatchToProps = {}; -export default connect(mapStateToProps, mapDispatchToProps)(FetchErrorAlert); + +export default FetchErrorAlert; diff --git a/src/editors/sharedComponents/ErrorAlerts/FetchErrorAlert.test.jsx b/src/editors/sharedComponents/ErrorAlerts/FetchErrorAlert.test.jsx index ddae06cdd..ddb838388 100644 --- a/src/editors/sharedComponents/ErrorAlerts/FetchErrorAlert.test.jsx +++ b/src/editors/sharedComponents/ErrorAlerts/FetchErrorAlert.test.jsx @@ -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()).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 })); - }); - }); }); diff --git a/src/editors/sharedComponents/ErrorAlerts/UploadErrorAlert.jsx b/src/editors/sharedComponents/ErrorAlerts/UploadErrorAlert.jsx index 74f4e664a..9119c4627 100644 --- a/src/editors/sharedComponents/ErrorAlerts/UploadErrorAlert.jsx +++ b/src/editors/sharedComponents/ErrorAlerts/UploadErrorAlert.jsx @@ -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 }) => ( ({ - isUploadError: selectors.requests.isFailed(state, { requestKey: RequestKeys.uploadAsset }), -}); -export const mapDispatchToProps = {}; -export default connect(mapStateToProps, mapDispatchToProps)(UploadErrorAlert); + +export default UploadErrorAlert; diff --git a/src/editors/sharedComponents/ErrorAlerts/UploadErrorAlert.test.jsx b/src/editors/sharedComponents/ErrorAlerts/UploadErrorAlert.test.jsx index 8ae5d03ff..4365c6903 100644 --- a/src/editors/sharedComponents/ErrorAlerts/UploadErrorAlert.test.jsx +++ b/src/editors/sharedComponents/ErrorAlerts/UploadErrorAlert.test.jsx @@ -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()).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 })); - }); - }); }); diff --git a/src/editors/sharedComponents/ImageUploadModal/SelectImageModal/index.jsx b/src/editors/sharedComponents/ImageUploadModal/SelectImageModal/index.jsx index 116635f22..c56637a66 100644 --- a/src/editors/sharedComponents/ImageUploadModal/SelectImageModal/index.jsx +++ b/src/editors/sharedComponents/ImageUploadModal/SelectImageModal/index.jsx @@ -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); diff --git a/src/editors/sharedComponents/SelectionModal/Gallery.jsx b/src/editors/sharedComponents/SelectionModal/Gallery.jsx index fb113f101..26359b000 100644 --- a/src/editors/sharedComponents/SelectionModal/Gallery.jsx +++ b/src/editors/sharedComponents/SelectionModal/Gallery.jsx @@ -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); diff --git a/src/editors/sharedComponents/SelectionModal/Gallery.test.jsx b/src/editors/sharedComponents/SelectionModal/Gallery.test.jsx index f46445a27..f9904889b 100644 --- a/src/editors/sharedComponents/SelectionModal/Gallery.test.jsx +++ b/src/editors/sharedComponents/SelectionModal/Gallery.test.jsx @@ -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()).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({}); - }); - }); }); diff --git a/src/editors/sharedComponents/SelectionModal/GalleryCard.jsx b/src/editors/sharedComponents/SelectionModal/GalleryCard.jsx index 10d8e5aba..3bef3811a 100644 --- a/src/editors/sharedComponents/SelectionModal/GalleryCard.jsx +++ b/src/editors/sharedComponents/SelectionModal/GalleryCard.jsx @@ -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, }; diff --git a/src/editors/sharedComponents/SelectionModal/index.jsx b/src/editors/sharedComponents/SelectionModal/index.jsx index 2b71dcbe8..6afa0fb8c 100644 --- a/src/editors/sharedComponents/SelectionModal/index.jsx +++ b/src/editors/sharedComponents/SelectionModal/index.jsx @@ -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 ( {/* Error Alerts */} - - + + - + @@ -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, }; From 3b69958427f1b4ef36aaf9b610b5d9f9dc097d4b Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Fri, 17 Mar 2023 14:45:08 -0500 Subject: [PATCH 3/7] feat: Search and filters added --- src/editors/containers/VideoGallery/hooks.js | 43 +++++- .../containers/VideoGallery/hooks.test.js | 134 ++++++++++++++++++ src/editors/containers/VideoGallery/utils.js | 21 +++ 3 files changed, 195 insertions(+), 3 deletions(-) create mode 100644 src/editors/containers/VideoGallery/hooks.test.js diff --git a/src/editors/containers/VideoGallery/hooks.js b/src/editors/containers/VideoGallery/hooks.js index dde1fe7df..835c313a7 100644 --- a/src/editors/containers/VideoGallery/hooks.js +++ b/src/editors/containers/VideoGallery/hooks.js @@ -6,6 +6,7 @@ import { filterMessages, sortKeys, sortMessages, + sortFunctions, } from './utils'; export const state = { @@ -43,7 +44,43 @@ export const searchAndSortHooks = () => { }; }; -export const videoListHooks = ({ videos }) => { +export const filterListBySearch = ({ searchString, videoList }) => ( + videoList.filter(({ displayName }) => displayName.toLowerCase().includes(searchString.toLowerCase())) +); + +export const filterListByStatus = ({ statusFilter, videoList }) => { + if (statusFilter === filterKeys.videoStatus) { + return videoList; + } + return videoList.filter(({ status }) => status === statusFilter); +}; + +export const filterListByHideSelectedCourse = ({ videoList }) => ( + // TODO Missing to implement this + videoList +); + +export const filterList = ({ + sortBy, + filterBy, + searchString, + videos, +}) => { + let filteredList = module.filterListBySearch({ + searchString, + videoList: videos, + }); + filteredList = module.filterListByStatus({ + statusFilter: filterBy, + videoList: filteredList, + }); + filteredList = module.filterListByHideSelectedCourse({ + videoList: filteredList, + }); + return filteredList.sort(sortFunctions[sortBy in sortKeys ? sortKeys[sortBy] : sortKeys.dateNewest]); +}; + +export const videoListHooks = ({ searchSortProps, videos }) => { const [highlighted, setHighlighted] = module.state.highlighted(null); const [ showSelectVideoError, @@ -53,7 +90,7 @@ export const videoListHooks = ({ videos }) => { showSizeError, setShowSizeError, ] = module.state.showSizeError(false); - const filteredList = videos; // TODO missing filters and sort + const filteredList = module.filterList({ ...searchSortProps, videos }); return { galleryError: { show: showSelectVideoError, @@ -118,7 +155,7 @@ export const buildVideos = ({ rawVideos }) => { export const videoHooks = ({ videos }) => { const searchSortProps = module.searchAndSortHooks(); - const videoList = module.videoListHooks({ videos }); + const videoList = module.videoListHooks({ searchSortProps, videos }); const { galleryError, galleryProps, diff --git a/src/editors/containers/VideoGallery/hooks.test.js b/src/editors/containers/VideoGallery/hooks.test.js new file mode 100644 index 000000000..8508404fa --- /dev/null +++ b/src/editors/containers/VideoGallery/hooks.test.js @@ -0,0 +1,134 @@ +import * as hooks from './hooks'; +import { filterKeys, sortKeys } from './utils'; +import { MockUseState } from '../../../testUtils'; +import { keyStore } from '../../utils'; + +jest.mock('react', () => ({ + ...jest.requireActual('react'), + useRef: jest.fn(val => ({ current: val })), + useEffect: jest.fn(), + useCallback: (cb, prereqs) => ({ cb, prereqs }), +})); + +jest.mock('react-redux', () => { + const dispatchFn = jest.fn(); + return { + ...jest.requireActual('react-redux'), + dispatch: dispatchFn, + useDispatch: jest.fn(() => dispatchFn), + }; +}); + +const state = new MockUseState(hooks); +const hookKeys = keyStore(hooks); +let hook; +const testValue = 'testVALUEVALID'; + +describe('VideoGallery hooks', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + describe('using state', () => { + beforeEach(() => { state.mock(); }); + afterEach(() => { state.restore(); }); + + describe('searchAndSortHooks', () => { + beforeEach(() => { + hook = hooks.searchAndSortHooks(); + }); + it('returns searchString value, initialized to an empty string', () => { + expect(state.stateVals.searchString).toEqual(hook.searchString); + expect(state.stateVals.searchString).toEqual(''); + }); + it('returns highlighted value, initialized to dateNewest', () => { + expect(state.stateVals.sortBy).toEqual(hook.sortBy); + expect(state.stateVals.sortBy).toEqual(sortKeys.dateNewest); + }); + test('onSearchChange sets searchString with event target value', () => { + hook.onSearchChange({ target: { value: testValue } }); + expect(state.setState.searchString).toHaveBeenCalledWith(testValue); + }); + test('clearSearchString sets search string to empty string', () => { + hook.clearSearchString(); + expect(state.setState.searchString).toHaveBeenCalledWith(''); + }); + test('onSortClick takes a key and returns callback to set sortBY to that key', () => { + hook.onSortClick(testValue); + expect(state.setState.sortBy).not.toHaveBeenCalled(); + hook.onSortClick(testValue)(); + expect(state.setState.sortBy).toHaveBeenCalledWith(testValue); + }); + }); + describe('filterListBySearch', () => { + const matching = [ + 'test', + 'TEst', + 'eeees', + 'essSSSS', + ]; + const notMatching = ['bad', 'other', 'bad stuff']; + const searchString = 'eS'; + test('returns list filtered lowercase by displayName', () => { + const filter = jest.fn(cb => ({ filter: cb })); + hook = hooks.filterListBySearch({ searchString, videoList: { filter } }); + expect(filter).toHaveBeenCalled(); + const [[filterCb]] = filter.mock.calls; + matching.forEach(val => expect(filterCb({ displayName: val })).toEqual(true)); + notMatching.forEach(val => expect(filterCb({ displayName: val })).toEqual(false)); + }); + }); + describe('imgListHooks outputs', () => { + const props = { + searchSortProps: { searchString: 'Es', sortBy: sortKeys.dateNewest, filterBy: filterKeys.videoStatus }, + videos: [ + { + displayName: 'sOmEuiMAge', + staTICUrl: '/assets/sOmEuiMAge', + id: 'sOmEuiMAgeURl', + }, + ], + }; + const filterList = (args) => ({ filterList: args }); + const load = () => { + jest.spyOn(hooks, hookKeys.filterList).mockImplementationOnce(filterList); + hook = hooks.videoListHooks(props); + }; + beforeEach(() => { + load(); + }); + describe('galleryProps', () => { + it('returns highlighted value, initialized to null', () => { + expect(hook.galleryProps.highlighted).toEqual(state.stateVals.highlighted); + expect(state.stateVals.highlighted).toEqual(null); + }); + test('onHighlightChange sets highlighted with event target value', () => { + hook.galleryProps.onHighlightChange({ target: { value: testValue } }); + expect(state.setState.highlighted).toHaveBeenCalledWith(testValue); + }); + test('displayList returns filterListhook called with searchSortProps and videos', () => { + expect(hook.galleryProps.displayList).toEqual(filterList({ + ...props.searchSortProps, + videos: props.videos, + })); + }); + }); + describe('galleryError', () => { + test('show is initialized to false and returns properly', () => { + const show = 'sHOWSelectiRROr'; + expect(hook.galleryError.show).toEqual(false); + state.mockVal(state.keys.showSelectVideoError, show); + hook = hooks.videoListHooks(props); + expect(hook.galleryError.show).toEqual(show); + }); + test('set sets showSelectVideoError to true', () => { + hook.galleryError.set(); + expect(state.setState.showSelectVideoError).toHaveBeenCalledWith(true); + }); + test('dismiss sets showSelectVideoError to false', () => { + hook.galleryError.dismiss(); + expect(state.setState.showSelectVideoError).toHaveBeenCalledWith(false); + }); + }); + }); + }); +}); diff --git a/src/editors/containers/VideoGallery/utils.js b/src/editors/containers/VideoGallery/utils.js index eb137072d..989a84e80 100644 --- a/src/editors/containers/VideoGallery/utils.js +++ b/src/editors/containers/VideoGallery/utils.js @@ -37,6 +37,27 @@ export const filterMessages = StrictDict({ failed: messages[messageKeys.filterByVideoStatusFailed], }); +export const sortFunctions = StrictDict({ + dateNewest: (a, b) => b.dateAdded - a.dateAdded, + dateOldest: (a, b) => a.dateAdded - b.dateAdded, + nameAscending: (a, b) => { + const nameA = a.displayName.toLowerCase(); + const nameB = b.displayName.toLowerCase(); + if (nameA < nameB) { return -1; } + if (nameB < nameA) { return 1; } + return b.dateAdded - a.dateAdded; + }, + nameDescending: (a, b) => { + const nameA = a.displayName.toLowerCase(); + const nameB = b.displayName.toLowerCase(); + if (nameA < nameB) { return 1; } + if (nameB < nameA) { return -1; } + return b.dateAdded - a.dateAdded; + }, + durationShortest: (a, b) => a.duration - b.duration, + durationLongest: (a, b) => b.duration - a.duration, +}); + export const acceptedImgKeys = StrictDict({ mp4: '.mp4', }); From 71efe876d33806e27f4933019cd373fe0475551a Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Sun, 19 Mar 2023 15:41:43 -0500 Subject: [PATCH 4/7] style: adding more styles on the selection modal --- package-lock.json | 34 +++++++++++ package.json | 1 + .../{Selector.jsx => VideoSelector.jsx} | 6 +- ...lector.test.jsx => VideoSelector.test.jsx} | 8 +-- ...SelectorPage.jsx => VideoSelectorPage.jsx} | 12 ++-- ...ge.test.jsx => VideoSelectorPage.test.jsx} | 10 ++-- .../__snapshots__/Selector.test.jsx.snap | 3 - .../__snapshots__/VideoSelector.test.jsx.snap | 3 + ...x.snap => VideoSelectorPage.test.jsx.snap} | 4 +- .../__snapshots__/index.test.jsx.snap | 4 ++ .../AnswerWidget/AnswerOption.test.jsx | 3 + .../QuestionWidget/index.test.jsx | 5 ++ .../ShowAnswerCard.test.jsx | 3 + .../SwitchToAdvancedEditorCard.test.jsx.snap | 2 + .../containers/TextEditor/index.test.jsx | 5 ++ src/editors/containers/VideoGallery/hooks.js | 30 +++++++--- .../__snapshots__/index.test.jsx.snap | 12 +++- .../sharedComponents/BaseModal/index.jsx | 11 +++- .../__snapshots__/index.test.jsx.snap | 2 + .../SelectionModal/Gallery.jsx | 10 ++-- .../SelectionModal/GalleryCard.jsx | 60 ++++++++++++------- .../SelectionModal/SearchSort.jsx | 4 +- .../__snapshots__/Gallery.test.jsx.snap | 3 + .../__snapshots__/GalleryCard.test.jsx.snap | 34 +++++++++-- .../sharedComponents/SelectionModal/index.jsx | 24 ++++---- .../__snapshots__/index.test.jsx.snap | 2 + src/editors/utils/formatDuration.js | 18 ++++++ src/editors/utils/index.js | 1 + src/index.jsx | 4 +- 29 files changed, 233 insertions(+), 85 deletions(-) rename src/editors/{Selector.jsx => VideoSelector.jsx} (88%) rename src/editors/{Selector.test.jsx => VideoSelector.test.jsx} (81%) rename src/editors/{SelectorPage.jsx => VideoSelectorPage.jsx} (76%) rename src/editors/{SelectorPage.test.jsx => VideoSelectorPage.test.jsx} (60%) delete mode 100644 src/editors/__snapshots__/Selector.test.jsx.snap create mode 100644 src/editors/__snapshots__/VideoSelector.test.jsx.snap rename src/editors/__snapshots__/{SelectorPage.test.jsx.snap => VideoSelectorPage.test.jsx.snap} (84%) create mode 100644 src/editors/utils/formatDuration.js diff --git a/package-lock.json b/package-lock.json index a6109891e..94e32a4a4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ "codemirror": "^6.0.0", "fast-xml-parser": "^4.0.10", "lodash-es": "^4.17.21", + "moment-shortformat": "^2.1.0", "react-redux": "^7.2.8", "react-responsive": "8.2.0", "react-transition-group": "4.4.2", @@ -26149,6 +26150,27 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/moment": { + "version": "2.29.4", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", + "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==", + "peer": true, + "engines": { + "node": "*" + } + }, + "node_modules/moment-shortformat": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/moment-shortformat/-/moment-shortformat-2.1.0.tgz", + "integrity": "sha512-TBh8jH4cVQOnZU+fQXkyCgj74ti//0CTdQd5sQLDTZuHLMaUP3uFEhbfb3yrrLYNVzgoiUhyiIMf0rDdn5iTJg==", + "deprecated": "Package is no longer maintained", + "engines": { + "node": ">= 4" + }, + "peerDependencies": { + "moment": "^2.4.0" + } + }, "node_modules/moo": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/moo/-/moo-0.5.2.tgz", @@ -54618,6 +54640,18 @@ } } }, + "moment": { + "version": "2.29.4", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", + "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==", + "peer": true + }, + "moment-shortformat": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/moment-shortformat/-/moment-shortformat-2.1.0.tgz", + "integrity": "sha512-TBh8jH4cVQOnZU+fQXkyCgj74ti//0CTdQd5sQLDTZuHLMaUP3uFEhbfb3yrrLYNVzgoiUhyiIMf0rDdn5iTJg==", + "requires": {} + }, "moo": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/moo/-/moo-0.5.2.tgz", diff --git a/package.json b/package.json index 5be2b045c..2c8ac4800 100644 --- a/package.json +++ b/package.json @@ -69,6 +69,7 @@ "codemirror": "^6.0.0", "fast-xml-parser": "^4.0.10", "lodash-es": "^4.17.21", + "moment-shortformat": "^2.1.0", "react-redux": "^7.2.8", "react-responsive": "8.2.0", "react-transition-group": "4.4.2", diff --git a/src/editors/Selector.jsx b/src/editors/VideoSelector.jsx similarity index 88% rename from src/editors/Selector.jsx rename to src/editors/VideoSelector.jsx index 9bf074ee5..ae5961f73 100644 --- a/src/editors/Selector.jsx +++ b/src/editors/VideoSelector.jsx @@ -4,7 +4,7 @@ import PropTypes from 'prop-types'; import VideoGallery from './containers/VideoGallery'; import * as hooks from './hooks'; -export const Selector = ({ +export const VideoSelector = ({ learningContextId, lmsEndpointUrl, studioEndpointUrl, @@ -25,10 +25,10 @@ export const Selector = ({ ); }; -Selector.propTypes = { +VideoSelector.propTypes = { learningContextId: PropTypes.string.isRequired, lmsEndpointUrl: PropTypes.string.isRequired, studioEndpointUrl: PropTypes.string.isRequired, }; -export default Selector; +export default VideoSelector; diff --git a/src/editors/Selector.test.jsx b/src/editors/VideoSelector.test.jsx similarity index 81% rename from src/editors/Selector.test.jsx rename to src/editors/VideoSelector.test.jsx index ffbaadce5..fa518a0cb 100644 --- a/src/editors/Selector.test.jsx +++ b/src/editors/VideoSelector.test.jsx @@ -2,7 +2,7 @@ import React from 'react'; import { useDispatch } from 'react-redux'; import { shallow } from 'enzyme'; import * as hooks from './hooks'; -import Selector from './Selector'; +import VideoSelector from './VideoSelector'; jest.mock('./hooks', () => ({ initializeApp: jest.fn(), @@ -22,15 +22,15 @@ const initData = { ...props, }; -describe('Editor', () => { +describe('Video Selector', () => { describe('render', () => { test('rendering correctly with expected Input', () => { - expect(shallow()).toMatchSnapshot(); + expect(shallow()).toMatchSnapshot(); }); }); describe('behavior', () => { it('calls initializeApp hook with dispatch, and passed data', () => { - shallow(); + shallow(); expect(hooks.initializeApp).toHaveBeenCalledWith({ dispatch: useDispatch(), data: initData, diff --git a/src/editors/SelectorPage.jsx b/src/editors/VideoSelectorPage.jsx similarity index 76% rename from src/editors/SelectorPage.jsx rename to src/editors/VideoSelectorPage.jsx index d3777b2c5..371cfa5e4 100644 --- a/src/editors/SelectorPage.jsx +++ b/src/editors/VideoSelectorPage.jsx @@ -2,17 +2,17 @@ import React from 'react'; import { Provider } from 'react-redux'; import PropTypes from 'prop-types'; import ErrorBoundary from './sharedComponents/ErrorBoundary'; -import { Selector } from './Selector'; +import { VideoSelector } from './VideoSelector'; import store from './data/store'; -const SelectorPage = ({ +const VideoSelectorPage = ({ courseId, lmsEndpointUrl, studioEndpointUrl, }) => ( - ); -SelectorPage.defaultProps = { +VideoSelectorPage.defaultProps = { courseId: null, lmsEndpointUrl: null, studioEndpointUrl: null, }; -SelectorPage.propTypes = { +VideoSelectorPage.propTypes = { courseId: PropTypes.string, lmsEndpointUrl: PropTypes.string, studioEndpointUrl: PropTypes.string, }; -export default SelectorPage; +export default VideoSelectorPage; diff --git a/src/editors/SelectorPage.test.jsx b/src/editors/VideoSelectorPage.test.jsx similarity index 60% rename from src/editors/SelectorPage.test.jsx rename to src/editors/VideoSelectorPage.test.jsx index 9d278888d..8cab51d13 100644 --- a/src/editors/SelectorPage.test.jsx +++ b/src/editors/VideoSelectorPage.test.jsx @@ -1,6 +1,6 @@ import React from 'react'; import { shallow } from 'enzyme'; -import SelectorPage from './SelectorPage'; +import VideoSelectorPage from './VideoSelectorPage'; const props = { courseId: 'course-v1:edX+DemoX+Demo_Course', @@ -11,15 +11,15 @@ const props = { jest.mock('react-redux', () => ({ Provider: 'Provider', })); -jest.mock('./Selector', () => 'Selector'); +jest.mock('./VideoSelector', () => 'VideoSelector'); -describe('Selector Page', () => { +describe('Video Selector Page', () => { describe('snapshots', () => { test('rendering correctly with expected Input', () => { - expect(shallow()).toMatchSnapshot(); + expect(shallow()).toMatchSnapshot(); }); test('rendering with props to null', () => { - expect(shallow()).toMatchSnapshot(); + expect(shallow()).toMatchSnapshot(); }); }); }); diff --git a/src/editors/__snapshots__/Selector.test.jsx.snap b/src/editors/__snapshots__/Selector.test.jsx.snap deleted file mode 100644 index 966902d43..000000000 --- a/src/editors/__snapshots__/Selector.test.jsx.snap +++ /dev/null @@ -1,3 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Editor render rendering correctly with expected Input 1`] = ``; diff --git a/src/editors/__snapshots__/VideoSelector.test.jsx.snap b/src/editors/__snapshots__/VideoSelector.test.jsx.snap new file mode 100644 index 000000000..d067c4a41 --- /dev/null +++ b/src/editors/__snapshots__/VideoSelector.test.jsx.snap @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Video Selector render rendering correctly with expected Input 1`] = ``; diff --git a/src/editors/__snapshots__/SelectorPage.test.jsx.snap b/src/editors/__snapshots__/VideoSelectorPage.test.jsx.snap similarity index 84% rename from src/editors/__snapshots__/SelectorPage.test.jsx.snap rename to src/editors/__snapshots__/VideoSelectorPage.test.jsx.snap index 578eefd76..390794300 100644 --- a/src/editors/__snapshots__/SelectorPage.test.jsx.snap +++ b/src/editors/__snapshots__/VideoSelectorPage.test.jsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Selector Page snapshots rendering correctly with expected Input 1`] = ` +exports[`Video Selector Page snapshots rendering correctly with expected Input 1`] = ` `; -exports[`Selector Page snapshots rendering with props to null 1`] = ` +exports[`Video Selector Page snapshots rendering with props to null 1`] = ` } footerAction={null} + headerComponent={null} isFullscreenScroll={true} isOpen={false} size="md" @@ -84,6 +86,7 @@ exports[`EditorContainer component render snapshot: not initialized. disable sav className="position-relative zindex-0" > } footerAction={null} + headerComponent={null} isFullscreenScroll={true} isOpen={false} size="md" diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/AnswerOption.test.jsx b/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/AnswerOption.test.jsx index 3b039975d..5df664cea 100644 --- a/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/AnswerOption.test.jsx +++ b/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/AnswerOption.test.jsx @@ -10,6 +10,9 @@ jest.mock('../../../../../data/redux', () => ({ problemType: jest.fn(state => ({ problemType: state })), }, }, + thunkActions: { + video: jest.fn(), + }, })); describe('AnswerOption', () => { diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/QuestionWidget/index.test.jsx b/src/editors/containers/ProblemEditor/components/EditProblemView/QuestionWidget/index.test.jsx index 561602f53..80b288a7b 100644 --- a/src/editors/containers/ProblemEditor/components/EditProblemView/QuestionWidget/index.test.jsx +++ b/src/editors/containers/ProblemEditor/components/EditProblemView/QuestionWidget/index.test.jsx @@ -21,6 +21,11 @@ jest.mock('../../../../../data/redux', () => ({ question: jest.fn(state => ({ question: state })), }, }, + thunkActions: { + video: { + importTranscript: jest.fn(), + }, + }, })); jest.mock('../../../../../sharedComponents/TinyMceWidget/hooks', () => ({ diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/ShowAnswerCard.test.jsx b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/ShowAnswerCard.test.jsx index 81f58d130..36b4ff0b4 100644 --- a/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/ShowAnswerCard.test.jsx +++ b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/ShowAnswerCard.test.jsx @@ -16,6 +16,9 @@ jest.mock('../../../../../../data/redux', () => ({ learningContextId: jest.fn(state => ({ learningContextId: state })), }, }, + thunkActions: { + video: jest.fn(), + }, })); describe('ShowAnswerCard', () => { diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/__snapshots__/SwitchToAdvancedEditorCard.test.jsx.snap b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/__snapshots__/SwitchToAdvancedEditorCard.test.jsx.snap index 2a0a1c3c4..124838e2d 100644 --- a/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/__snapshots__/SwitchToAdvancedEditorCard.test.jsx.snap +++ b/src/editors/containers/ProblemEditor/components/EditProblemView/SettingsWidget/settingsComponents/__snapshots__/SwitchToAdvancedEditorCard.test.jsx.snap @@ -5,6 +5,7 @@ exports[`SwitchToAdvancedEditorCard snapshot snapshot: SwitchToAdvancedEditorCar className="border border-light-700 shadow-none" > } footerAction={null} + headerComponent={null} isFullscreenScroll={true} isOpen={false} size="md" diff --git a/src/editors/containers/TextEditor/index.test.jsx b/src/editors/containers/TextEditor/index.test.jsx index 2ee14c75a..196f5028b 100644 --- a/src/editors/containers/TextEditor/index.test.jsx +++ b/src/editors/containers/TextEditor/index.test.jsx @@ -61,6 +61,11 @@ jest.mock('../../data/redux', () => ({ isFinished: jest.fn((state, params) => ({ isFailed: { state, params } })), }, }, + thunkActions: { + video: { + importTranscript: jest.fn(), + }, + }, })); describe('TextEditor', () => { diff --git a/src/editors/containers/VideoGallery/hooks.js b/src/editors/containers/VideoGallery/hooks.js index 835c313a7..84104711b 100644 --- a/src/editors/containers/VideoGallery/hooks.js +++ b/src/editors/containers/VideoGallery/hooks.js @@ -139,20 +139,34 @@ export const buildVideos = ({ rawVideos }) => { let videos = []; 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, + videos = videoList.map(video => ({ + id: video.edx_video_id, + displayName: video.client_video_id, + externalUrl: video.course_video_image_url, + dateAdded: video.created, locked: false, - thumbnail: asset.course_video_image_url, - status: asset.status, - duration: asset.duration, + thumbnail: video.course_video_image_url, + status: video.status, + statusBadgeVariant: module.getstatusBadgeVariant({ status: video.status }), + duration: video.duration, + transcripts: video.transcripts, })); } return videos; }; +export const getstatusBadgeVariant = ({ status }) => { + switch (status) { + case filterKeys.failed: + return 'danger'; + case filterKeys.uploading: + case filterKeys.processing: + return 'light'; + default: + return null; + } +}; + export const videoHooks = ({ videos }) => { const searchSortProps = module.searchAndSortHooks(); const videoList = module.videoListHooks({ searchSortProps, videos }); diff --git a/src/editors/sharedComponents/BaseModal/__snapshots__/index.test.jsx.snap b/src/editors/sharedComponents/BaseModal/__snapshots__/index.test.jsx.snap index e15c4ccdc..00d2ed5d9 100644 --- a/src/editors/sharedComponents/BaseModal/__snapshots__/index.test.jsx.snap +++ b/src/editors/sharedComponents/BaseModal/__snapshots__/index.test.jsx.snap @@ -10,12 +10,20 @@ exports[`BaseModal ImageUploadModal template component snapshot 1`] = ` size="lg" variant="default" > - + props.title node - + props.children node diff --git a/src/editors/sharedComponents/BaseModal/index.jsx b/src/editors/sharedComponents/BaseModal/index.jsx index 5f87c8167..66521fcb7 100644 --- a/src/editors/sharedComponents/BaseModal/index.jsx +++ b/src/editors/sharedComponents/BaseModal/index.jsx @@ -14,10 +14,12 @@ export const BaseModal = ({ close, title, children, + headerComponent, confirmAction, footerAction, size, isFullscreenScroll, + bodyStyle, }) => ( - + {title} + {headerComponent} - + {children} @@ -51,8 +54,10 @@ export const BaseModal = ({ BaseModal.defaultProps = { footerAction: null, + headerComponent: null, size: 'lg', isFullscreenScroll: true, + bodyStyle: null, }; BaseModal.propTypes = { @@ -62,8 +67,10 @@ BaseModal.propTypes = { children: PropTypes.node.isRequired, confirmAction: PropTypes.node.isRequired, footerAction: PropTypes.node, + headerComponent: PropTypes.node, size: PropTypes.string, isFullscreenScroll: PropTypes.bool, + bodyStyle: PropTypes.shape({}), }; export default BaseModal; diff --git a/src/editors/sharedComponents/ImageUploadModal/ImageSettingsModal/__snapshots__/index.test.jsx.snap b/src/editors/sharedComponents/ImageUploadModal/ImageSettingsModal/__snapshots__/index.test.jsx.snap index 29f2bd566..c0459220a 100644 --- a/src/editors/sharedComponents/ImageUploadModal/ImageSettingsModal/__snapshots__/index.test.jsx.snap +++ b/src/editors/sharedComponents/ImageUploadModal/ImageSettingsModal/__snapshots__/index.test.jsx.snap @@ -2,6 +2,7 @@ exports[`ImageSettingsModal render snapshot 1`] = ` } footerAction={null} + headerComponent={null} isFullscreenScroll={true} isOpen={false} size="lg" diff --git a/src/editors/sharedComponents/SelectionModal/Gallery.jsx b/src/editors/sharedComponents/SelectionModal/Gallery.jsx index 26359b000..69eafb5c2 100644 --- a/src/editors/sharedComponents/SelectionModal/Gallery.jsx +++ b/src/editors/sharedComponents/SelectionModal/Gallery.jsx @@ -9,7 +9,6 @@ import { FormattedMessage, injectIntl, intlShape, - MessageDescriptor, } from '@edx/frontend-platform/i18n'; import messages from './messages'; @@ -39,20 +38,20 @@ export const Gallery = ({ } if (galleryIsEmpty) { return ( -
+
); } if (searchIsEmpty) { return ( -
+
); } return ( - +
( - +
- -
-

{asset.displayName}

- { showId && ( -

- -

+
+ + { asset.status && asset.statusBadgeVariant && ( + + {asset.status} + )} -

+ { asset.duration >= 0 && ( + + {formatDuration(asset.duration)} + + )} +

+
+

{asset.displayName}

+ { asset.transcripts && ( +
+ +
+ )} +

); -GalleryCard.defaultProps = { - showId: false, -}; - GalleryCard.propTypes = { asset: PropTypes.shape({ contentType: PropTypes.string, @@ -62,8 +75,9 @@ GalleryCard.propTypes = { url: PropTypes.string, duration: PropTypes.number, status: PropTypes.string, + statusBadgeVariant: PropTypes.string, + transcripts: PropTypes.array, }).isRequired, - showId: PropTypes.bool, }; export default GalleryCard; diff --git a/src/editors/sharedComponents/SelectionModal/SearchSort.jsx b/src/editors/sharedComponents/SelectionModal/SearchSort.jsx index 3a54af3ce..11a5f9145 100644 --- a/src/editors/sharedComponents/SelectionModal/SearchSort.jsx +++ b/src/editors/sharedComponents/SelectionModal/SearchSort.jsx @@ -8,7 +8,6 @@ import { Close, Search } from '@edx/paragon/icons'; import { FormattedMessage, injectIntl, - MessageDescriptor, intlShape, } from '@edx/frontend-platform/i18n'; @@ -109,7 +108,6 @@ SearchSort.defaultProps = { filterKeys: null, filterMessages: null, showSwitch: false, - switchMessage: null, onSwitchClick: null, }; @@ -126,7 +124,7 @@ SearchSort.propTypes = { filterKeys: PropTypes.shape({}), filterMessages: PropTypes.shape({}), showSwitch: PropTypes.bool, - switchMessage: MessageDescriptor, + switchMessage: PropTypes.shape({}).isRequired, onSwitchClick: PropTypes.func, // injected intl: intlShape.isRequired, diff --git a/src/editors/sharedComponents/SelectionModal/__snapshots__/Gallery.test.jsx.snap b/src/editors/sharedComponents/SelectionModal/__snapshots__/Gallery.test.jsx.snap index 92be690c8..380b58307 100644 --- a/src/editors/sharedComponents/SelectionModal/__snapshots__/Gallery.test.jsx.snap +++ b/src/editors/sharedComponents/SelectionModal/__snapshots__/Gallery.test.jsx.snap @@ -6,6 +6,7 @@ exports[`TextEditor Image Gallery component component snapshot: loaded but no im style={ Object { "height": "375px", + "margin": "0 -1.5rem", } } > @@ -19,6 +20,7 @@ exports[`TextEditor Image Gallery component component snapshot: loaded but searc style={ Object { "height": "375px", + "margin": "0 -1.5rem", } } > @@ -36,6 +38,7 @@ exports[`TextEditor Image Gallery component component snapshot: loaded, show gal style={ Object { "height": "375px", + "margin": "0 -1.5rem", } } > diff --git a/src/editors/sharedComponents/SelectionModal/__snapshots__/GalleryCard.test.jsx.snap b/src/editors/sharedComponents/SelectionModal/__snapshots__/GalleryCard.test.jsx.snap index 41f5ac1df..684f7189c 100644 --- a/src/editors/sharedComponents/SelectionModal/__snapshots__/GalleryCard.test.jsx.snap +++ b/src/editors/sharedComponents/SelectionModal/__snapshots__/GalleryCard.test.jsx.snap @@ -4,27 +4,49 @@ exports[`GalleryCard component snapshot: dateAdded=12345 1`] = `

- + > + +

props.img.displayName

-

+

)} title={intl.formatMessage(titleMsg)} + bodyStyle={{ background: '#EBEBEB' }} + headerComponent={( +

+ +
+ )} > {/* Error Alerts */} @@ -85,8 +90,7 @@ export const SelectionModal = ({ > - - + @@ -108,13 +112,13 @@ SelectionModal.propTypes = { dismiss: PropTypes.func.isRequired, show: PropTypes.bool.isRequired, set: PropTypes.func.isRequired, - message: MessageDescriptor, + message: PropTypes.shape({}).isRequired, }).isRequired, inputError: PropTypes.shape({ dismiss: PropTypes.func.isRequired, show: PropTypes.bool.isRequired, set: PropTypes.func.isRequired, - message: MessageDescriptor, + message: PropTypes.shape({}).isRequired, }).isRequired, fileInput: PropTypes.shape({ click: PropTypes.func.isRequired, @@ -125,11 +129,11 @@ SelectionModal.propTypes = { selectBtnProps: PropTypes.shape({}).isRequired, acceptedFiles: PropTypes.shape({}).isRequired, modalMessages: PropTypes.shape({ - confirmMsg: MessageDescriptor, - uploadButtonMsg: MessageDescriptor, - titleMsg: MessageDescriptor, - fetchError: MessageDescriptor, - uploadError: MessageDescriptor, + confirmMsg: PropTypes.shape({}).isRequired, + uploadButtonMsg: PropTypes.shape({}).isRequired, + titleMsg: PropTypes.shape({}).isRequired, + fetchError: PropTypes.shape({}).isRequired, + uploadError: PropTypes.shape({}).isRequired, }).isRequired, isLoaded: PropTypes.bool.isRequired, isFetchError: PropTypes.bool.isRequired, diff --git a/src/editors/sharedComponents/SourceCodeModal/__snapshots__/index.test.jsx.snap b/src/editors/sharedComponents/SourceCodeModal/__snapshots__/index.test.jsx.snap index 33662d621..2676d51a9 100644 --- a/src/editors/sharedComponents/SourceCodeModal/__snapshots__/index.test.jsx.snap +++ b/src/editors/sharedComponents/SourceCodeModal/__snapshots__/index.test.jsx.snap @@ -2,6 +2,7 @@ exports[`SourceCodeModal renders as expected with default behavior 1`] = ` } footerAction={null} + headerComponent={null} isFullscreenScroll={true} isOpen={false} size="xl" diff --git a/src/editors/utils/formatDuration.js b/src/editors/utils/formatDuration.js new file mode 100644 index 000000000..83008b7d7 --- /dev/null +++ b/src/editors/utils/formatDuration.js @@ -0,0 +1,18 @@ +import * as moment from 'moment-shortformat'; + +const formatDuration = (duration) => { + const d = moment.duration(duration, 'seconds'); + if (d.hours > 0) { + return ( + `${d.hours().toString().padStart(2, '0')}:` + + `${d.minutes().toString().padStart(2, '0')}:` + + `${d.seconds().toString().padStart(2, '0')}` + ); + } + return ( + `${d.minutes().toString().padStart(2, '0')}:` + + `${d.seconds().toString().padStart(2, '0')}` + ); +}; + +export default formatDuration; diff --git a/src/editors/utils/index.js b/src/editors/utils/index.js index b765c1382..14ecc749c 100644 --- a/src/editors/utils/index.js +++ b/src/editors/utils/index.js @@ -3,3 +3,4 @@ export { default as StrictDict } from './StrictDict'; export { default as keyStore } from './keyStore'; export { default as camelizeKeys } from './camelizeKeys'; export { default as removeItemOnce } from './removeOnce'; +export { default as formatDuration } from './formatDuration'; diff --git a/src/index.jsx b/src/index.jsx index c4b875116..847deec60 100644 --- a/src/index.jsx +++ b/src/index.jsx @@ -1,7 +1,7 @@ import Placeholder from './Placeholder'; import messages from './i18n/index'; import EditorPage from './editors/EditorPage'; -import SelectorPage from './editors/SelectorPage'; +import VideoSelectorPage from './editors/VideoSelectorPage'; -export { messages, EditorPage, SelectorPage }; +export { messages, EditorPage, VideoSelectorPage }; export default Placeholder; From b3bced98752d81df5d74c1ee1fba9693fa76772a Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Fri, 31 Mar 2023 13:49:52 -0500 Subject: [PATCH 5/7] feat: New screens for loading, no videos and error states. Also tests added --- package-lock.json | 961 +++++++++++++++--- package.json | 2 + .../ExplanationWidget/index.test.jsx | 5 + .../containers/VideoGallery/hooks.test.js | 123 ++- .../containers/VideoGallery/messages.js | 2 +- .../containers/VideoGallery/utils.test.js | 62 ++ src/editors/data/redux/app/reducer.test.js | 1 + src/editors/data/redux/app/selectors.test.js | 1 + .../data/redux/thunkActions/app.test.js | 9 + .../data/redux/thunkActions/requests.test.js | 27 + src/editors/data/services/cms/api.test.js | 24 + src/editors/data/services/cms/urls.test.js | 14 + .../ErrorAlerts/ErrorAlert.jsx | 4 +- .../SelectionModal/Gallery.jsx | 24 +- .../SelectionModal/Gallery.test.jsx | 4 + .../SelectionModal/SearchSort.test.jsx | 78 +- .../__snapshots__/Gallery.test.jsx.snap | 21 +- .../__snapshots__/SearchSort.test.jsx.snap | 289 +++++- .../__snapshots__/index.test.jsx.snap | 13 - .../sharedComponents/SelectionModal/index.jsx | 12 +- .../SelectionModal/index.test.jsx | 18 +- src/editors/utils/formatDuration.js | 2 +- src/editors/utils/formatDuration.test.js | 12 + 23 files changed, 1517 insertions(+), 191 deletions(-) create mode 100644 src/editors/containers/VideoGallery/utils.test.js delete mode 100644 src/editors/sharedComponents/SelectionModal/__snapshots__/index.test.jsx.snap create mode 100644 src/editors/utils/formatDuration.test.js diff --git a/package-lock.json b/package-lock.json index 94e32a4a4..b73ae471c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ "codemirror": "^6.0.0", "fast-xml-parser": "^4.0.10", "lodash-es": "^4.17.21", + "moment": "^2.29.4", "moment-shortformat": "^2.1.0", "react-redux": "^7.2.8", "react-responsive": "8.2.0", @@ -42,6 +43,7 @@ "@edx/paragon": "^20.28.0", "@edx/reactifex": "^2.1.1", "@testing-library/dom": "^8.13.0", + "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^12.1.1", "@testing-library/user-event": "^13.5.0", "codecov": "3.8.3", @@ -67,6 +69,12 @@ "react-dom": "^16.14.0" } }, + "node_modules/@adobe/css-tools": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.2.0.tgz", + "integrity": "sha512-E09FiIft46CmH5Qnjb0wsW54/YQd69LsxeKUOWawmws1XWvyFGURnAChH0mlr7YPFR1ofwvUQfcL0J3lMxXqPA==", + "dev": true + }, "node_modules/@babel/cli": { "version": "7.16.0", "resolved": "https://registry.npmjs.org/@babel/cli/-/cli-7.16.0.tgz", @@ -5018,6 +5026,47 @@ "node": ">=8" } }, + "node_modules/@jest/expect-utils": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.5.0.tgz", + "integrity": "sha512-fmKzsidoXQT2KwnrwE0SQq3uj8Z763vzR8LnLBwC2qYWEFpjX8daRsk6rHUM1QvNlEW/UJXNXm59ztmJJWs2Mg==", + "dev": true, + "dependencies": { + "jest-get-type": "^29.4.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/schemas": { + "version": "29.4.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.4.3.tgz", + "integrity": "sha512-VLYKXQmtmuEz6IxJsrZwzG9NvtkQsWNnWMsKxqWNu3+CnfzJQhp0WDDKWLVV9hLKr0l3SLLFRqcYHjhtyuDVxg==", + "dev": true, + "dependencies": { + "@sinclair/typebox": "^0.25.16" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.5.0.tgz", + "integrity": "sha512-qbu7kN6czmVRc3xWFQcAN03RAUamgppVUdXrvl1Wr3jlNF93o9mJbGcDWrwGB6ht44u7efB1qCFgVQmca24Uog==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.4.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/@lezer/common": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.0.2.tgz", @@ -5417,6 +5466,12 @@ "@babel/runtime": "^7.9.2" } }, + "node_modules/@sinclair/typebox": { + "version": "0.25.24", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.25.24.tgz", + "integrity": "sha512-XJfwUVUKDHF5ugKwIcxEgc9k8b7HbznCp6eUfWgu710hMPNIO4aw4/zB5RogDQz8nd6gyCDpU9O/m6qYEWY6yQ==", + "dev": true + }, "node_modules/@svgr/babel-plugin-remove-jsx-attribute": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-6.5.0.tgz", @@ -6405,6 +6460,41 @@ "node": ">=12" } }, + "node_modules/@testing-library/jest-dom": { + "version": "5.16.5", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-5.16.5.tgz", + "integrity": "sha512-N5ixQ2qKpi5OLYfwQmUb/5mSV9LneAcaUfp32pn4yCnpb8r/Yz0pXFPck21dIicKmi+ta5WRAknkZCfA8refMA==", + "dev": true, + "dependencies": { + "@adobe/css-tools": "^4.0.1", + "@babel/runtime": "^7.9.2", + "@types/testing-library__jest-dom": "^5.9.1", + "aria-query": "^5.0.0", + "chalk": "^3.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.5.6", + "lodash": "^4.17.15", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=8", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/@testing-library/react": { "version": "12.1.1", "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-12.1.1.tgz", @@ -6544,6 +6634,57 @@ "@types/istanbul-lib-coverage": "*" } }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz", + "integrity": "sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "29.5.1", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.1.tgz", + "integrity": "sha512-tEuVcHrpaixS36w7hpsfLBLpjtMRJUE09/MHXn923LOVojDwyC14cWcfc0rDs0VEfUyYmt/+iX1kxxp+gZMcaQ==", + "dev": true, + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, + "node_modules/@types/jest/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@types/jest/node_modules/pretty-format": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.5.0.tgz", + "integrity": "sha512-V2mGkI31qdttvTFX7Mt4efOqHXqJWMu4/r66Xh3Z3BwZaPfPJgp6/gbwoujRpPUtfEF6AUUWx3Jim3GCw5g/Qw==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.4.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@types/jest/node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + }, "node_modules/@types/json-schema": { "version": "7.0.11", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", @@ -6616,6 +6757,21 @@ "integrity": "sha512-K5K+yml8LTo9bWJI/rECfIPrGgxdpeNbj+d53lwN4QjW1MCwlkhUms+gtdzigTeUyBr09+u8BwOIY3MXvHdcsA==", "dev": true }, + "node_modules/@types/stack-utils": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz", + "integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==", + "dev": true + }, + "node_modules/@types/testing-library__jest-dom": { + "version": "5.14.5", + "resolved": "https://registry.npmjs.org/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.14.5.tgz", + "integrity": "sha512-SBwbxYoyPIvxHbeHxTZX2Pe/74F/tX2/D3mMvzabdeJ25bBojfW0TyB8BHrbq/9zaaKICJZjLP+8r6AeZMFCuQ==", + "dev": true, + "dependencies": { + "@types/jest": "*" + } + }, "node_modules/@types/uglify-js": { "version": "3.17.1", "resolved": "https://registry.npmjs.org/@types/uglify-js/-/uglify-js-3.17.1.tgz", @@ -6645,6 +6801,15 @@ "node": ">= 8" } }, + "node_modules/@types/yargs": { + "version": "17.0.24", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.24.tgz", + "integrity": "sha512-6i0aC7jV6QzQB8ne1joVZ0eSFIstHsCrobmOtghM11yGlH0j43FKL2UhWdELkyps0zuf7qVTUVCCR+tgSlyLLw==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, "node_modules/@types/yargs-parser": { "version": "21.0.0", "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.0.tgz", @@ -6895,18 +7060,6 @@ "node": ">= 8" } }, - "node_modules/anymatch/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/aproba": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", @@ -8465,15 +8618,6 @@ "@types/node": "*" } }, - "node_modules/babel-jest/node_modules/@types/istanbul-reports": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz", - "integrity": "sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw==", - "dev": true, - "dependencies": { - "@types/istanbul-lib-report": "*" - } - }, "node_modules/babel-jest/node_modules/@types/yargs": { "version": "15.0.14", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.14.tgz", @@ -11443,6 +11587,12 @@ "resolved": "https://registry.npmjs.org/css-mediaquery/-/css-mediaquery-0.1.2.tgz", "integrity": "sha512-COtn4EROW5dBGlE/4PiKnh6rZpAPxDeFLaEEwt4i10jpDMFt2EhQGS79QmmrO+iKCHv0PU/HrOWEhijFd1x99Q==" }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true + }, "node_modules/cssnano": { "version": "5.0.12", "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-5.0.12.tgz", @@ -12383,6 +12533,15 @@ "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", "dev": true }, + "node_modules/diff-sequences": { + "version": "29.4.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.4.3.tgz", + "integrity": "sha512-ofrBgwpPhCD85kMKtE9RYFFq6OC1A89oW2vvgWZNCwxrUpRUILopY7lsYyMDSjc8g6U6aiO0Qubg6r4Wgt5ZnA==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -14267,6 +14426,22 @@ "node": ">= 0.6" } }, + "node_modules/expect": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.5.0.tgz", + "integrity": "sha512-yM7xqUrCO2JdpFo4XpM82t+PJBFybdqoQuJLDGeDX2ij8NZzqRHyu3Hp188/JX7SWqud+7t4MUdvcgGBICMHZg==", + "dev": true, + "dependencies": { + "@jest/expect-utils": "^29.5.0", + "jest-get-type": "^29.4.3", + "jest-matcher-utils": "^29.5.0", + "jest-message-util": "^29.5.0", + "jest-util": "^29.5.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/ext-list": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/ext-list/-/ext-list-2.2.2.tgz", @@ -22394,6 +22569,170 @@ "node": ">= 10.14.2" } }, + "node_modules/jest-diff": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.5.0.tgz", + "integrity": "sha512-LtxijLLZBduXnHSniy0WMdaHjmQnt3g5sa16W4p0HqukYTTsyTW3GD1q41TyGl5YFXj/5B2U6dlh5FM1LIMgxw==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.4.3", + "jest-get-type": "^29.4.3", + "pretty-format": "^29.5.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-diff/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-diff/node_modules/pretty-format": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.5.0.tgz", + "integrity": "sha512-V2mGkI31qdttvTFX7Mt4efOqHXqJWMu4/r66Xh3Z3BwZaPfPJgp6/gbwoujRpPUtfEF6AUUWx3Jim3GCw5g/Qw==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.4.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-diff/node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + }, + "node_modules/jest-get-type": { + "version": "29.4.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.4.3.tgz", + "integrity": "sha512-J5Xez4nRRMjk8emnTpWrlkyb9pfRQQanDrvWHhsR1+VUfbwxi30eVcZFlcdGInRibU4G5LwHXpI7IRHU0CY+gg==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.5.0.tgz", + "integrity": "sha512-lecRtgm/rjIK0CQ7LPQwzCs2VwW6WAahA55YBuI+xqmhm7LAaxokSB8C97yJeYyT+HvQkH741StzpU41wohhWw==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.5.0", + "jest-get-type": "^29.4.3", + "pretty-format": "^29.5.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-matcher-utils/node_modules/pretty-format": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.5.0.tgz", + "integrity": "sha512-V2mGkI31qdttvTFX7Mt4efOqHXqJWMu4/r66Xh3Z3BwZaPfPJgp6/gbwoujRpPUtfEF6AUUWx3Jim3GCw5g/Qw==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.4.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils/node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + }, + "node_modules/jest-message-util": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.5.0.tgz", + "integrity": "sha512-Kijeg9Dag6CKtIDA7O21zNTACqD5MD/8HfIV8pdD94vFyFuer52SigdC3IQMhab3vACxXMiFk+yMHNdbqtyTGA==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.5.0", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.5.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-message-util/node_modules/pretty-format": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.5.0.tgz", + "integrity": "sha512-V2mGkI31qdttvTFX7Mt4efOqHXqJWMu4/r66Xh3Z3BwZaPfPJgp6/gbwoujRpPUtfEF6AUUWx3Jim3GCw5g/Qw==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.4.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util/node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + }, + "node_modules/jest-message-util/node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/jest-pnp-resolver": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", @@ -22411,6 +22750,38 @@ } } }, + "node_modules/jest-util": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.5.0.tgz", + "integrity": "sha512-RYMgG/MTadOr5t8KdhejfvUU82MxsCu5MF6KuDUHl+NuwzUt+Sm6jJWxTJVrDR1j5M/gJVCPKQEpWXY+yIQ6lQ==", + "dev": true, + "dependencies": { + "@jest/types": "^29.5.0", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-util/node_modules/ci-info": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.8.0.tgz", + "integrity": "sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "engines": { + "node": ">=8" + } + }, "node_modules/jest/node_modules/@babel/generator": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.20.5.tgz", @@ -22873,15 +23244,6 @@ "@types/node": "*" } }, - "node_modules/jest/node_modules/@types/istanbul-reports": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz", - "integrity": "sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw==", - "dev": true, - "dependencies": { - "@types/istanbul-lib-report": "*" - } - }, "node_modules/jest/node_modules/@types/normalize-package-data": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz", @@ -22894,12 +23256,6 @@ "integrity": "sha512-ri0UmynRRvZiiUJdiz38MmIblKK+oH30MztdBVR95dv/Ubw6neWSb8u1XpRb72L4qsZOhz+L+z9JD40SJmfWow==", "dev": true }, - "node_modules/jest/node_modules/@types/stack-utils": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz", - "integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==", - "dev": true - }, "node_modules/jest/node_modules/@types/yargs": { "version": "15.0.14", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.14.tgz", @@ -23231,15 +23587,6 @@ "node": ">=8" } }, - "node_modules/jest/node_modules/escape-string-regexp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", - "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/jest/node_modules/escodegen": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.0.0.tgz", @@ -25136,18 +25483,6 @@ "source-map": "^0.6.0" } }, - "node_modules/jest/node_modules/stack-utils": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", - "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", - "dev": true, - "dependencies": { - "escape-string-regexp": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/jest/node_modules/static-extend": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", @@ -25956,18 +26291,6 @@ "node": ">=8" } }, - "node_modules/micromatch/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -26009,6 +26332,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/mini-css-extract-plugin": { "version": "1.6.2", "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-1.6.2.tgz", @@ -26154,7 +26486,6 @@ "version": "2.29.4", "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==", - "peer": true, "engines": { "node": "*" } @@ -27834,6 +28165,18 @@ "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", "dev": true }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/pify": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", @@ -29204,16 +29547,17 @@ "node": ">=8.10.0" } }, - "node_modules/readdirp/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", "dev": true, - "engines": { - "node": ">=8.6" + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "engines": { + "node": ">=8" } }, "node_modules/redux": { @@ -30256,6 +30600,27 @@ "deprecated": "Modern JS already guarantees Array#sort() is a stable sort, so this library is deprecated. See the compatibility table on MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#browser_compatibility", "dev": true }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -30751,6 +31116,18 @@ "node": ">=6" } }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -34569,6 +34946,12 @@ } }, "dependencies": { + "@adobe/css-tools": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.2.0.tgz", + "integrity": "sha512-E09FiIft46CmH5Qnjb0wsW54/YQd69LsxeKUOWawmws1XWvyFGURnAChH0mlr7YPFR1ofwvUQfcL0J3lMxXqPA==", + "dev": true + }, "@babel/cli": { "version": "7.16.0", "resolved": "https://registry.npmjs.org/@babel/cli/-/cli-7.16.0.tgz", @@ -38218,6 +38601,38 @@ "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", "dev": true }, + "@jest/expect-utils": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.5.0.tgz", + "integrity": "sha512-fmKzsidoXQT2KwnrwE0SQq3uj8Z763vzR8LnLBwC2qYWEFpjX8daRsk6rHUM1QvNlEW/UJXNXm59ztmJJWs2Mg==", + "dev": true, + "requires": { + "jest-get-type": "^29.4.3" + } + }, + "@jest/schemas": { + "version": "29.4.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.4.3.tgz", + "integrity": "sha512-VLYKXQmtmuEz6IxJsrZwzG9NvtkQsWNnWMsKxqWNu3+CnfzJQhp0WDDKWLVV9hLKr0l3SLLFRqcYHjhtyuDVxg==", + "dev": true, + "requires": { + "@sinclair/typebox": "^0.25.16" + } + }, + "@jest/types": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.5.0.tgz", + "integrity": "sha512-qbu7kN6czmVRc3xWFQcAN03RAUamgppVUdXrvl1Wr3jlNF93o9mJbGcDWrwGB6ht44u7efB1qCFgVQmca24Uog==", + "dev": true, + "requires": { + "@jest/schemas": "^29.4.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + } + }, "@lezer/common": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.0.2.tgz", @@ -38538,6 +38953,12 @@ } } }, + "@sinclair/typebox": { + "version": "0.25.24", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.25.24.tgz", + "integrity": "sha512-XJfwUVUKDHF5ugKwIcxEgc9k8b7HbznCp6eUfWgu710hMPNIO4aw4/zB5RogDQz8nd6gyCDpU9O/m6qYEWY6yQ==", + "dev": true + }, "@svgr/babel-plugin-remove-jsx-attribute": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-6.5.0.tgz", @@ -39210,6 +39631,35 @@ "pretty-format": "^27.0.2" } }, + "@testing-library/jest-dom": { + "version": "5.16.5", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-5.16.5.tgz", + "integrity": "sha512-N5ixQ2qKpi5OLYfwQmUb/5mSV9LneAcaUfp32pn4yCnpb8r/Yz0pXFPck21dIicKmi+ta5WRAknkZCfA8refMA==", + "dev": true, + "requires": { + "@adobe/css-tools": "^4.0.1", + "@babel/runtime": "^7.9.2", + "@types/testing-library__jest-dom": "^5.9.1", + "aria-query": "^5.0.0", + "chalk": "^3.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.5.6", + "lodash": "^4.17.15", + "redent": "^3.0.0" + }, + "dependencies": { + "chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + } + } + }, "@testing-library/react": { "version": "12.1.1", "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-12.1.1.tgz", @@ -39331,6 +39781,50 @@ "@types/istanbul-lib-coverage": "*" } }, + "@types/istanbul-reports": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz", + "integrity": "sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw==", + "dev": true, + "requires": { + "@types/istanbul-lib-report": "*" + } + }, + "@types/jest": { + "version": "29.5.1", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.1.tgz", + "integrity": "sha512-tEuVcHrpaixS36w7hpsfLBLpjtMRJUE09/MHXn923LOVojDwyC14cWcfc0rDs0VEfUyYmt/+iX1kxxp+gZMcaQ==", + "dev": true, + "requires": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true + }, + "pretty-format": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.5.0.tgz", + "integrity": "sha512-V2mGkI31qdttvTFX7Mt4efOqHXqJWMu4/r66Xh3Z3BwZaPfPJgp6/gbwoujRpPUtfEF6AUUWx3Jim3GCw5g/Qw==", + "dev": true, + "requires": { + "@jest/schemas": "^29.4.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + } + }, + "react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + } + } + }, "@types/json-schema": { "version": "7.0.11", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", @@ -39403,6 +39897,21 @@ "integrity": "sha512-K5K+yml8LTo9bWJI/rECfIPrGgxdpeNbj+d53lwN4QjW1MCwlkhUms+gtdzigTeUyBr09+u8BwOIY3MXvHdcsA==", "dev": true }, + "@types/stack-utils": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz", + "integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==", + "dev": true + }, + "@types/testing-library__jest-dom": { + "version": "5.14.5", + "resolved": "https://registry.npmjs.org/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.14.5.tgz", + "integrity": "sha512-SBwbxYoyPIvxHbeHxTZX2Pe/74F/tX2/D3mMvzabdeJ25bBojfW0TyB8BHrbq/9zaaKICJZjLP+8r6AeZMFCuQ==", + "dev": true, + "requires": { + "@types/jest": "*" + } + }, "@types/uglify-js": { "version": "3.17.1", "resolved": "https://registry.npmjs.org/@types/uglify-js/-/uglify-js-3.17.1.tgz", @@ -39431,6 +39940,15 @@ } } }, + "@types/yargs": { + "version": "17.0.24", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.24.tgz", + "integrity": "sha512-6i0aC7jV6QzQB8ne1joVZ0eSFIstHsCrobmOtghM11yGlH0j43FKL2UhWdELkyps0zuf7qVTUVCCR+tgSlyLLw==", + "dev": true, + "requires": { + "@types/yargs-parser": "*" + } + }, "@types/yargs-parser": { "version": "21.0.0", "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.0.tgz", @@ -39631,14 +40149,6 @@ "requires": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" - }, - "dependencies": { - "picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true - } } }, "aproba": { @@ -40772,15 +41282,6 @@ "@types/node": "*" } }, - "@types/istanbul-reports": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz", - "integrity": "sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw==", - "dev": true, - "requires": { - "@types/istanbul-lib-report": "*" - } - }, "@types/yargs": { "version": "15.0.14", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.14.tgz", @@ -43125,6 +43626,12 @@ "resolved": "https://registry.npmjs.org/css-mediaquery/-/css-mediaquery-0.1.2.tgz", "integrity": "sha512-COtn4EROW5dBGlE/4PiKnh6rZpAPxDeFLaEEwt4i10jpDMFt2EhQGS79QmmrO+iKCHv0PU/HrOWEhijFd1x99Q==" }, + "css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true + }, "cssnano": { "version": "5.0.12", "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-5.0.12.tgz", @@ -43767,6 +44274,12 @@ "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", "dev": true }, + "diff-sequences": { + "version": "29.4.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.4.3.tgz", + "integrity": "sha512-ofrBgwpPhCD85kMKtE9RYFFq6OC1A89oW2vvgWZNCwxrUpRUILopY7lsYyMDSjc8g6U6aiO0Qubg6r4Wgt5ZnA==", + "dev": true + }, "dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -45221,6 +45734,19 @@ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", "dev": true }, + "expect": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.5.0.tgz", + "integrity": "sha512-yM7xqUrCO2JdpFo4XpM82t+PJBFybdqoQuJLDGeDX2ij8NZzqRHyu3Hp188/JX7SWqud+7t4MUdvcgGBICMHZg==", + "dev": true, + "requires": { + "@jest/expect-utils": "^29.5.0", + "jest-get-type": "^29.4.3", + "jest-matcher-utils": "^29.5.0", + "jest-message-util": "^29.5.0", + "jest-util": "^29.5.0" + } + }, "ext-list": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/ext-list/-/ext-list-2.2.2.tgz", @@ -52013,15 +52539,6 @@ "@types/node": "*" } }, - "@types/istanbul-reports": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz", - "integrity": "sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw==", - "dev": true, - "requires": { - "@types/istanbul-lib-report": "*" - } - }, "@types/normalize-package-data": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz", @@ -52034,12 +52551,6 @@ "integrity": "sha512-ri0UmynRRvZiiUJdiz38MmIblKK+oH30MztdBVR95dv/Ubw6neWSb8u1XpRb72L4qsZOhz+L+z9JD40SJmfWow==", "dev": true }, - "@types/stack-utils": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz", - "integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==", - "dev": true - }, "@types/yargs": { "version": "15.0.14", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.14.tgz", @@ -52311,12 +52822,6 @@ } } }, - "escape-string-regexp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", - "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", - "dev": true - }, "escodegen": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.0.0.tgz", @@ -53821,15 +54326,6 @@ "source-map": "^0.6.0" } }, - "stack-utils": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", - "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", - "dev": true, - "requires": { - "escape-string-regexp": "^2.0.0" - } - }, "static-extend": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", @@ -54117,6 +54613,134 @@ } } }, + "jest-diff": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.5.0.tgz", + "integrity": "sha512-LtxijLLZBduXnHSniy0WMdaHjmQnt3g5sa16W4p0HqukYTTsyTW3GD1q41TyGl5YFXj/5B2U6dlh5FM1LIMgxw==", + "dev": true, + "requires": { + "chalk": "^4.0.0", + "diff-sequences": "^29.4.3", + "jest-get-type": "^29.4.3", + "pretty-format": "^29.5.0" + }, + "dependencies": { + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true + }, + "pretty-format": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.5.0.tgz", + "integrity": "sha512-V2mGkI31qdttvTFX7Mt4efOqHXqJWMu4/r66Xh3Z3BwZaPfPJgp6/gbwoujRpPUtfEF6AUUWx3Jim3GCw5g/Qw==", + "dev": true, + "requires": { + "@jest/schemas": "^29.4.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + } + }, + "react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + } + } + }, + "jest-get-type": { + "version": "29.4.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.4.3.tgz", + "integrity": "sha512-J5Xez4nRRMjk8emnTpWrlkyb9pfRQQanDrvWHhsR1+VUfbwxi30eVcZFlcdGInRibU4G5LwHXpI7IRHU0CY+gg==", + "dev": true + }, + "jest-matcher-utils": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.5.0.tgz", + "integrity": "sha512-lecRtgm/rjIK0CQ7LPQwzCs2VwW6WAahA55YBuI+xqmhm7LAaxokSB8C97yJeYyT+HvQkH741StzpU41wohhWw==", + "dev": true, + "requires": { + "chalk": "^4.0.0", + "jest-diff": "^29.5.0", + "jest-get-type": "^29.4.3", + "pretty-format": "^29.5.0" + }, + "dependencies": { + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true + }, + "pretty-format": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.5.0.tgz", + "integrity": "sha512-V2mGkI31qdttvTFX7Mt4efOqHXqJWMu4/r66Xh3Z3BwZaPfPJgp6/gbwoujRpPUtfEF6AUUWx3Jim3GCw5g/Qw==", + "dev": true, + "requires": { + "@jest/schemas": "^29.4.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + } + }, + "react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + } + } + }, + "jest-message-util": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.5.0.tgz", + "integrity": "sha512-Kijeg9Dag6CKtIDA7O21zNTACqD5MD/8HfIV8pdD94vFyFuer52SigdC3IQMhab3vACxXMiFk+yMHNdbqtyTGA==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.5.0", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.5.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "dependencies": { + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true + }, + "pretty-format": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.5.0.tgz", + "integrity": "sha512-V2mGkI31qdttvTFX7Mt4efOqHXqJWMu4/r66Xh3Z3BwZaPfPJgp6/gbwoujRpPUtfEF6AUUWx3Jim3GCw5g/Qw==", + "dev": true, + "requires": { + "@jest/schemas": "^29.4.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + } + }, + "react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + } + } + }, "jest-pnp-resolver": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", @@ -54124,6 +54748,28 @@ "dev": true, "requires": {} }, + "jest-util": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.5.0.tgz", + "integrity": "sha512-RYMgG/MTadOr5t8KdhejfvUU82MxsCu5MF6KuDUHl+NuwzUt+Sm6jJWxTJVrDR1j5M/gJVCPKQEpWXY+yIQ6lQ==", + "dev": true, + "requires": { + "@jest/types": "^29.5.0", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "dependencies": { + "ci-info": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.8.0.tgz", + "integrity": "sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==", + "dev": true + } + } + }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -54500,12 +55146,6 @@ "requires": { "fill-range": "^7.0.1" } - }, - "picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true } } }, @@ -54535,6 +55175,12 @@ "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz", "integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==" }, + "min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true + }, "mini-css-extract-plugin": { "version": "1.6.2", "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-1.6.2.tgz", @@ -54643,8 +55289,7 @@ "moment": { "version": "2.29.4", "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", - "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==", - "peer": true + "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==" }, "moment-shortformat": { "version": "2.1.0", @@ -55881,6 +56526,12 @@ "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", "dev": true }, + "picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true + }, "pify": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", @@ -56942,14 +57593,16 @@ "dev": true, "requires": { "picomatch": "^2.2.1" - }, - "dependencies": { - "picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true - } + } + }, + "redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "requires": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" } }, "redux": { @@ -57744,6 +58397,23 @@ "integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==", "dev": true }, + "stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "requires": { + "escape-string-regexp": "^2.0.0" + }, + "dependencies": { + "escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true + } + } + }, "statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -58107,6 +58777,15 @@ "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", "dev": true }, + "strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "requires": { + "min-indent": "^1.0.0" + } + }, "strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", diff --git a/package.json b/package.json index 2c8ac4800..828bfd45a 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "@edx/paragon": "^20.28.0", "@edx/reactifex": "^2.1.1", "@testing-library/dom": "^8.13.0", + "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^12.1.1", "@testing-library/user-event": "^13.5.0", "codecov": "3.8.3", @@ -69,6 +70,7 @@ "codemirror": "^6.0.0", "fast-xml-parser": "^4.0.10", "lodash-es": "^4.17.21", + "moment": "^2.29.4", "moment-shortformat": "^2.1.0", "react-redux": "^7.2.8", "react-responsive": "8.2.0", diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/ExplanationWidget/index.test.jsx b/src/editors/containers/ProblemEditor/components/EditProblemView/ExplanationWidget/index.test.jsx index cef3d4ed5..178da1e86 100644 --- a/src/editors/containers/ProblemEditor/components/EditProblemView/ExplanationWidget/index.test.jsx +++ b/src/editors/containers/ProblemEditor/components/EditProblemView/ExplanationWidget/index.test.jsx @@ -11,6 +11,11 @@ jest.mock('../../../../../data/redux', () => ({ settings: jest.fn(state => ({ question: state })), }, }, + thunkActions: { + video: { + importTranscript: jest.fn(), + }, + }, })); jest.mock('../../../../../sharedComponents/TinyMceWidget/hooks', () => ({ diff --git a/src/editors/containers/VideoGallery/hooks.test.js b/src/editors/containers/VideoGallery/hooks.test.js index 8508404fa..43d0b77f0 100644 --- a/src/editors/containers/VideoGallery/hooks.test.js +++ b/src/editors/containers/VideoGallery/hooks.test.js @@ -28,6 +28,15 @@ describe('VideoGallery hooks', () => { beforeEach(() => { jest.clearAllMocks(); }); + describe('state hooks', () => { + state.testGetter(state.keys.highlighted); + state.testGetter(state.keys.searchString); + state.testGetter(state.keys.showSelectVideoError); + state.testGetter(state.keys.showSizeError); + state.testGetter(state.keys.sortBy); + state.testGetter(state.keys.filertBy); + state.testGetter(state.keys.hideSelectedVideos); + }); describe('using state', () => { beforeEach(() => { state.mock(); }); afterEach(() => { state.restore(); }); @@ -77,9 +86,79 @@ describe('VideoGallery hooks', () => { notMatching.forEach(val => expect(filterCb({ displayName: val })).toEqual(false)); }); }); - describe('imgListHooks outputs', () => { + describe('buildVideos', () => { + const rawVideos = [ + { + edx_video_id: 'id_1', + client_video_id: 'client_id_1', + course_video_image_url: 'course_video_image_url_1', + created: 'created_1', + status: 'status_1', + duration: 1, + transcripts: [], + }, + { + edx_video_id: 'id_2', + client_video_id: 'client_id_2', + course_video_image_url: 'course_video_image_url_2', + created: 'created_2', + status: 'status_2', + duration: 2, + transcripts: [], + }, + ]; + const expectedValues = [ + { + id: 'id_1', + displayName: 'client_id_1', + externalUrl: 'course_video_image_url_1', + dateAdded: 'created_1', + locked: false, + thumbnail: 'course_video_image_url_1', + status: 'status_1', + statusBadgeVariant: null, + duration: 1, + transcripts: [], + }, + { + id: 'id_2', + displayName: 'client_id_2', + externalUrl: 'course_video_image_url_2', + dateAdded: 'created_2', + locked: false, + thumbnail: 'course_video_image_url_2', + status: 'status_2', + statusBadgeVariant: null, + duration: 2, + transcripts: [], + }, + ]; + test('return the expected values', () => { + const values = hooks.buildVideos({ rawVideos }); + expect(values).toEqual(expectedValues); + }); + }); + describe('getstatusBadgeVariant', () => { + test('return the expected values', () => { + let value = hooks.getstatusBadgeVariant({ status: filterKeys.failed }); + expect(value).toEqual('danger'); + value = hooks.getstatusBadgeVariant({ status: filterKeys.uploading }); + expect(value).toEqual('light'); + value = hooks.getstatusBadgeVariant({ status: filterKeys.processing }); + expect(value).toEqual('light'); + value = hooks.getstatusBadgeVariant({ status: filterKeys.videoStatus }); + expect(value).toBeNull(); + value = hooks.getstatusBadgeVariant({ status: filterKeys.ready }); + expect(value).toBeNull(); + }); + }); + describe('videoListHooks outputs', () => { const props = { - searchSortProps: { searchString: 'Es', sortBy: sortKeys.dateNewest, filterBy: filterKeys.videoStatus }, + searchSortProps: { + searchString: 'Es', + sortBy: sortKeys.dateNewest, + filterBy: filterKeys.videoStatus, + }, videos: [ { displayName: 'sOmEuiMAge', @@ -131,4 +210,44 @@ describe('VideoGallery hooks', () => { }); }); }); + describe('videoHooks', () => { + const videoListHooks = { + galleryProps: 'some gallery props', + selectBtnProps: 'some select btn props', + }; + const searchAndSortHooks = { search: 'props' }; + const fileInputHooks = { file: 'input hooks' }; + const videos = { video: { staTICUrl: '/assets/sOmEuiMAge' } }; + const spies = {}; + beforeEach(() => { + spies.videoList = jest.spyOn(hooks, hookKeys.videoListHooks) + .mockReturnValueOnce(videoListHooks); + spies.search = jest.spyOn(hooks, hookKeys.searchAndSortHooks) + .mockReturnValueOnce(searchAndSortHooks); + spies.file = jest.spyOn(hooks, hookKeys.fileInputHooks) + .mockReturnValueOnce(fileInputHooks); + hook = hooks.videoHooks({ videos }); + }); + it('forwards fileInputHooks as fileInput', () => { + expect(hook.fileInput).toEqual(fileInputHooks); + expect(spies.file.mock.calls.length).toEqual(1); + expect(spies.file).toHaveBeenCalled(); + }); + it('initializes videoListHooks', () => { + expect(spies.videoList.mock.calls.length).toEqual(1); + expect(spies.videoList).toHaveBeenCalledWith({ + searchSortProps: searchAndSortHooks, + videos, + }); + }); + it('forwards searchAndSortHooks as searchSortProps', () => { + expect(hook.searchSortProps).toEqual(searchAndSortHooks); + expect(spies.search.mock.calls.length).toEqual(1); + expect(spies.search).toHaveBeenCalled(); + }); + it('forwards galleryProps and selectBtnProps from the video list hooks', () => { + expect(hook.galleryProps).toEqual(videoListHooks.galleryProps); + expect(hook.selectBtnProps).toEqual(videoListHooks.selectBtnProps); + }); + }); }); diff --git a/src/editors/containers/VideoGallery/messages.js b/src/editors/containers/VideoGallery/messages.js index d351cd751..a846b3956 100644 --- a/src/editors/containers/VideoGallery/messages.js +++ b/src/editors/containers/VideoGallery/messages.js @@ -3,7 +3,7 @@ export const messages = { emptyGalleryLabel: { id: 'authoring.selectvideomodal.emptyGalleryLabel', defaultMessage: - 'No videos found in your gallery. Please upload a video using the button below.', + 'No results found.', description: 'Label for when video gallery is empty.', }, selectVideoButtonlabel: { diff --git a/src/editors/containers/VideoGallery/utils.test.js b/src/editors/containers/VideoGallery/utils.test.js new file mode 100644 index 000000000..ef9818833 --- /dev/null +++ b/src/editors/containers/VideoGallery/utils.test.js @@ -0,0 +1,62 @@ +import { sortFunctions } from './utils'; + +describe('VideGallery utils', () => { + describe('sortFunctions', () => { + const dateA = { + dateAdded: new Date('2023-03-30'), + }; + const dateB = { + dateAdded: new Date('2023-03-31'), + }; + const nameA = { + displayName: 'This is the Name A', + dateAdded: new Date('2023-03-30'), + }; + const nameB = { + displayName: 'Hello World', + dateAdded: new Date('2023-03-30'), + }; + const nameC = { + displayName: 'Hello World', + dateAdded: new Date('2023-03-31'), + }; + const durationA = { + duration: 10, + }; + const durationB = { + duration: 100, + }; + test('correct functionality of dateNewest', () => { + expect(sortFunctions.dateNewest(dateA, dateB)).toBeGreaterThan(0); + expect(sortFunctions.dateNewest(dateB, dateA)).toBeLessThan(0); + expect(sortFunctions.dateNewest(dateA, dateA)).toEqual(0); + }); + test('correct functionality of dateOldest', () => { + expect(sortFunctions.dateOldest(dateA, dateB)).toBeLessThan(0); + expect(sortFunctions.dateOldest(dateB, dateA)).toBeGreaterThan(0); + expect(sortFunctions.dateOldest(dateA, dateA)).toEqual(0); + }); + test('correct functionality of nameAscending', () => { + expect(sortFunctions.nameAscending(nameA, nameB)).toEqual(1); + expect(sortFunctions.nameAscending(nameB, nameA)).toEqual(-1); + expect(sortFunctions.nameAscending(nameA, nameA)).toEqual(0); + expect(sortFunctions.nameAscending(nameB, nameC)).toBeGreaterThan(0); + }); + test('correct functionality of nameDescending', () => { + expect(sortFunctions.nameDescending(nameA, nameB)).toEqual(-1); + expect(sortFunctions.nameDescending(nameB, nameA)).toEqual(1); + expect(sortFunctions.nameDescending(nameA, nameA)).toEqual(0); + expect(sortFunctions.nameDescending(nameB, nameC)).toBeGreaterThan(0); + }); + test('correct functionality of durationShortest', () => { + expect(sortFunctions.durationShortest(durationA, durationB)).toBeLessThan(0); + expect(sortFunctions.durationShortest(durationB, durationA)).toBeGreaterThan(0); + expect(sortFunctions.durationShortest(durationA, durationA)).toEqual(0); + }); + test('correct functionality of durationLongest', () => { + expect(sortFunctions.durationLongest(durationA, durationB)).toBeGreaterThan(0); + expect(sortFunctions.durationLongest(durationB, durationA)).toBeLessThan(0); + expect(sortFunctions.durationLongest(durationA, durationA)).toEqual(0); + }); + }); +}); diff --git a/src/editors/data/redux/app/reducer.test.js b/src/editors/data/redux/app/reducer.test.js index 09b1bccdb..f23e8c14c 100644 --- a/src/editors/data/redux/app/reducer.test.js +++ b/src/editors/data/redux/app/reducer.test.js @@ -48,6 +48,7 @@ describe('app reducer', () => { ['setBlockTitle', 'blockTitle'], ['setSaveResponse', 'saveResponse'], ['setAssets', 'assets'], + ['setVideos', 'videos'], ['setCourseDetails', 'courseDetails'], ].map(args => setterTest(...args)); describe('setBlockValue', () => { diff --git a/src/editors/data/redux/app/selectors.test.js b/src/editors/data/redux/app/selectors.test.js index d2fb44f28..ac7cc400a 100644 --- a/src/editors/data/redux/app/selectors.test.js +++ b/src/editors/data/redux/app/selectors.test.js @@ -48,6 +48,7 @@ describe('app selectors unit tests', () => { simpleKeys.blockTitle, simpleKeys.studioView, simpleKeys.assets, + simpleKeys.videos, ].map(testSimpleSelector); }); }); diff --git a/src/editors/data/redux/thunkActions/app.test.js b/src/editors/data/redux/thunkActions/app.test.js index 8a2621f6f..f6ee54837 100644 --- a/src/editors/data/redux/thunkActions/app.test.js +++ b/src/editors/data/redux/thunkActions/app.test.js @@ -166,6 +166,15 @@ describe('app thunkActions', () => { expect(dispatch).toHaveBeenCalledWith(actions.app.setAssets(response)); }); }); + describe('fetchVideos', () => { + it('dispatches fetchVideos action with setVideos for onSuccess param', () => { + const response = { data: { videos: 'testRESPONSE' } }; + thunkActions.fetchVideos()(dispatch); + const [[dispatchCall]] = dispatch.mock.calls; + dispatchCall.fetchVideos.onSuccess(response); + expect(dispatch).toHaveBeenCalledWith(actions.app.setVideos(response.data.videos)); + }); + }); describe('uploadImage', () => { const setSelection = jest.fn(); beforeEach(() => { diff --git a/src/editors/data/redux/thunkActions/requests.test.js b/src/editors/data/redux/thunkActions/requests.test.js index ea77e5d81..e2b3c548a 100644 --- a/src/editors/data/redux/thunkActions/requests.test.js +++ b/src/editors/data/redux/thunkActions/requests.test.js @@ -27,6 +27,7 @@ jest.mock('../../services/cms/api', () => ({ fetchCourseDetails: (args) => args, saveBlock: (args) => args, fetchAssets: ({ id, url }) => ({ id, url }), + fetchVideos: ({ id, url }) => ({ id, url }), uploadAsset: (args) => args, loadImages: jest.fn(), uploadThumbnail: (args) => args, @@ -265,6 +266,32 @@ describe('requests thunkActions module', () => { expect(loadImages).toHaveBeenCalledWith({ fetchAssets: expectedArgs }); }); }); + describe('fetchVideos', () => { + const expectedArgs = { + studioEndpointUrl: selectors.app.studioEndpointUrl(testState), + learningContextId: selectors.app.learningContextId(testState), + }; + let fetchVideos; + let dispatchedAction; + beforeEach(() => { + fetchVideos = jest.fn((args) => new Promise((resolve) => { + resolve({ data: { videos: { fetchVideos: args } } }); + })); + jest.spyOn(api, apiKeys.fetchVideos).mockImplementationOnce(fetchVideos); + requests.fetchVideos({ ...fetchParams, onSuccess, onFailure })(dispatch, () => testState); + [[dispatchedAction]] = dispatch.mock.calls; + }); + it('dispatches networkRequest', () => { + expect(dispatchedAction.networkRequest).not.toEqual(undefined); + }); + test('forwards onSuccess and onFailure', () => { + expect(dispatchedAction.networkRequest.onSuccess).toEqual(onSuccess); + expect(dispatchedAction.networkRequest.onFailure).toEqual(onFailure); + }); + test('api.fetchVideos promise called with studioEndpointUrl and learningContextId', () => { + expect(fetchVideos).toHaveBeenCalledWith(expectedArgs); + }); + }); describe('saveBlock', () => { const content = 'SoME HtMl CoNtent As String'; testNetworkRequestAction({ diff --git a/src/editors/data/services/cms/api.test.js b/src/editors/data/services/cms/api.test.js index b2a97fa7f..6e94bb8e0 100644 --- a/src/editors/data/services/cms/api.test.js +++ b/src/editors/data/services/cms/api.test.js @@ -21,8 +21,11 @@ jest.mock('./urls', () => ({ allowThumbnailUpload: jest.fn().mockName('urls.allowThumbnailUpload'), thumbnailUpload: jest.fn().mockName('urls.thumbnailUpload'), checkTranscriptsForImport: jest.fn().mockName('urls.checkTranscriptsForImport'), + courseDetailsUrl: jest.fn().mockName('urls.courseDetailsUrl'), + courseAdvanceSettings: jest.fn().mockName('urls.courseAdvanceSettings'), replaceTranscript: jest.fn().mockName('urls.replaceTranscript'), videoFeatures: jest.fn().mockName('urls.videoFeatures'), + courseVideos: jest.fn().mockName('urls.courseVideos'), })); jest.mock('./utils', () => ({ @@ -73,6 +76,27 @@ describe('cms api', () => { }); }); + describe('fetchCourseDetails', () => { + it('should call get with url.courseDetailsUrl', () => { + apiMethods.fetchCourseDetails({ learningContextId, studioEndpointUrl }); + expect(get).toHaveBeenCalledWith(urls.courseDetailsUrl({ studioEndpointUrl, learningContextId })); + }); + }); + + describe('fetchVideos', () => { + it('should call get with url.courseVideos', () => { + apiMethods.fetchVideos({ learningContextId, studioEndpointUrl }); + expect(get).toHaveBeenCalledWith(urls.courseVideos({ studioEndpointUrl, learningContextId })); + }); + }); + + describe('fetchAdvancedSettings', () => { + it('should call get with url.courseAdvanceSettings', () => { + apiMethods.fetchAdvancedSettings({ learningContextId, studioEndpointUrl }); + expect(get).toHaveBeenCalledWith(urls.courseAdvanceSettings({ studioEndpointUrl, learningContextId })); + }); + }); + describe('normalizeContent', () => { test('return value for blockType: html', () => { const content = 'Im baby palo santo ugh celiac fashion axe. La croix lo-fi venmo whatever. Beard man braid migas single-origin coffee forage ramps.'; diff --git a/src/editors/data/services/cms/urls.test.js b/src/editors/data/services/cms/urls.test.js index 9f5282a37..47dbd5cff 100644 --- a/src/editors/data/services/cms/urls.test.js +++ b/src/editors/data/services/cms/urls.test.js @@ -13,7 +13,9 @@ import { courseDetailsUrl, checkTranscriptsForImport, replaceTranscript, + courseAdvanceSettings, videoFeatures, + courseVideos, } from './urls'; describe('cms url methods', () => { @@ -125,10 +127,22 @@ describe('cms url methods', () => { .toEqual(`${studioEndpointUrl}/transcripts/replace?data=${parameters}`); }); }); + describe('courseAdvanceSettings', () => { + it('returns url with studioEndpointUrl and learningContextId', () => { + expect(courseAdvanceSettings({ studioEndpointUrl, learningContextId })) + .toEqual(`${studioEndpointUrl}/api/contentstore/v0/advanced_settings/${learningContextId}`); + }); + }); describe('videoFeatures', () => { it('returns url with studioEndpointUrl and learningContextId', () => { expect(videoFeatures({ studioEndpointUrl, learningContextId })) .toEqual(`${studioEndpointUrl}/video_features/${learningContextId}`); }); }); + describe('courseVideos', () => { + it('returns url with studioEndpointUrl and learningContextId', () => { + expect(courseVideos({ studioEndpointUrl, learningContextId })) + .toEqual(`${studioEndpointUrl}/videos/${learningContextId}`); + }); + }); }); diff --git a/src/editors/sharedComponents/ErrorAlerts/ErrorAlert.jsx b/src/editors/sharedComponents/ErrorAlerts/ErrorAlert.jsx index fe73eb053..40a813e89 100644 --- a/src/editors/sharedComponents/ErrorAlerts/ErrorAlert.jsx +++ b/src/editors/sharedComponents/ErrorAlerts/ErrorAlert.jsx @@ -2,7 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { Alert } from '@edx/paragon'; -import { Outline } from '@edx/paragon/icons'; +import { Error } from '@edx/paragon/icons'; import { FormattedMessage } from '@edx/frontend-platform/i18n'; import messages from './messages'; @@ -42,7 +42,7 @@ export const ErrorAlert = ({ return ( diff --git a/src/editors/sharedComponents/SelectionModal/Gallery.jsx b/src/editors/sharedComponents/SelectionModal/Gallery.jsx index 69eafb5c2..e497a40c2 100644 --- a/src/editors/sharedComponents/SelectionModal/Gallery.jsx +++ b/src/editors/sharedComponents/SelectionModal/Gallery.jsx @@ -15,6 +15,7 @@ import messages from './messages'; import GalleryCard from './GalleryCard'; export const Gallery = ({ + show, galleryIsEmpty, searchIsEmpty, displayList, @@ -27,13 +28,24 @@ export const Gallery = ({ // injected intl, }) => { + if (!show) { + return null; + } if (!isLoaded) { return ( - +
+ +
); } if (galleryIsEmpty) { @@ -71,8 +83,10 @@ Gallery.defaultProps = { highlighted: '', showIdsOnCards: false, height: '375px', + show: true, }; Gallery.propTypes = { + show: PropTypes.bool, isLoaded: PropTypes.bool.isRequired, galleryIsEmpty: PropTypes.bool.isRequired, searchIsEmpty: PropTypes.bool.isRequired, diff --git a/src/editors/sharedComponents/SelectionModal/Gallery.test.jsx b/src/editors/sharedComponents/SelectionModal/Gallery.test.jsx index f9904889b..eafa533df 100644 --- a/src/editors/sharedComponents/SelectionModal/Gallery.test.jsx +++ b/src/editors/sharedComponents/SelectionModal/Gallery.test.jsx @@ -37,5 +37,9 @@ describe('TextEditor Image Gallery component', () => { test('snapshot: loaded, show gallery', () => { expect(shallow()).toMatchSnapshot(); }); + test('snapshot: not shot gallery', () => { + const wrapper = shallow(); + expect(wrapper.type()).toBeNull(); + }); }); }); diff --git a/src/editors/sharedComponents/SelectionModal/SearchSort.test.jsx b/src/editors/sharedComponents/SelectionModal/SearchSort.test.jsx index dbb5e9de9..ccf8d36c8 100644 --- a/src/editors/sharedComponents/SelectionModal/SearchSort.test.jsx +++ b/src/editors/sharedComponents/SelectionModal/SearchSort.test.jsx @@ -6,20 +6,21 @@ import { FormattedMessage } from '@edx/frontend-platform/i18n'; import { formatMessage } from '../../../testUtils'; import { sortKeys, sortMessages } from '../ImageUploadModal/SelectImageModal/utils'; +import { filterKeys, filterMessages } from '../../containers/VideoGallery/utils'; import { SearchSort } from './SearchSort'; describe('SearchSort component', () => { - const props = { - searchString: 'props.searchString', - onSearchChange: jest.fn().mockName('props.onSearchChange'), - clearSearchString: jest.fn().mockName('props.clearSearchString'), - sortBy: sortKeys.dateOldest, - sortKeys, - sortMessages, - onSortClick: jest.fn().mockName('props.onSortClick'), - intl: { formatMessage }, - }; - describe('snapshots', () => { + describe('snapshots without filterKeys', () => { + const props = { + searchString: 'props.searchString', + onSearchChange: jest.fn().mockName('props.onSearchChange'), + clearSearchString: jest.fn().mockName('props.clearSearchString'), + sortBy: sortKeys.dateOldest, + sortKeys, + sortMessages, + onSortClick: jest.fn().mockName('props.onSortClick'), + intl: { formatMessage }, + }; test('with search string (close button)', () => { expect(shallow()).toMatchSnapshot(); }); @@ -42,4 +43,59 @@ describe('SearchSort component', () => { )).toEqual(true); }); }); + describe('snapshots with filterKeys', () => { + const props = { + searchString: 'props.searchString', + onSearchChange: jest.fn().mockName('props.onSearchChange'), + clearSearchString: jest.fn().mockName('props.clearSearchString'), + sortBy: sortKeys.dateOldest, + sortKeys, + sortMessages, + filterKeys, + filterMessages, + showSwitch: true, + onSortClick: jest.fn().mockName('props.onSortClick'), + onFilterClick: jest.fn().mockName('props.onFilterClick'), + intl: { formatMessage }, + }; + test('with search string (close button)', () => { + expect(shallow()).toMatchSnapshot(); + }); + test('without search string (search icon)', () => { + expect(shallow()).toMatchSnapshot(); + }); + test('adds a sort option for each sortKey', () => { + const el = shallow(); + expect(el.find(Dropdown).containsMatchingElement( + , + )).toEqual(true); + expect(el.find(Dropdown).containsMatchingElement( + , + )).toEqual(true); + expect(el.find(Dropdown).containsMatchingElement( + , + )).toEqual(true); + expect(el.find(Dropdown).containsMatchingElement( + , + )).toEqual(true); + }); + test('adds a filter option for each filterKet', () => { + const el = shallow(); + expect(el.find(Dropdown).containsMatchingElement( + , + )).toEqual(true); + expect(el.find(Dropdown).containsMatchingElement( + , + )).toEqual(true); + expect(el.find(Dropdown).containsMatchingElement( + , + )).toEqual(true); + expect(el.find(Dropdown).containsMatchingElement( + , + )).toEqual(true); + expect(el.find(Dropdown).containsMatchingElement( + , + )).toEqual(true); + }); + }); }); diff --git a/src/editors/sharedComponents/SelectionModal/__snapshots__/Gallery.test.jsx.snap b/src/editors/sharedComponents/SelectionModal/__snapshots__/Gallery.test.jsx.snap index 380b58307..9c2eacf95 100644 --- a/src/editors/sharedComponents/SelectionModal/__snapshots__/Gallery.test.jsx.snap +++ b/src/editors/sharedComponents/SelectionModal/__snapshots__/Gallery.test.jsx.snap @@ -85,9 +85,20 @@ exports[`TextEditor Image Gallery component component snapshot: loaded, show gal `; exports[`TextEditor Image Gallery component component snapshot: not loaded, show spinner 1`] = ` - +
+ +
`; diff --git a/src/editors/sharedComponents/SelectionModal/__snapshots__/SearchSort.test.jsx.snap b/src/editors/sharedComponents/SelectionModal/__snapshots__/SearchSort.test.jsx.snap index a48329e45..1f3156ab8 100644 --- a/src/editors/sharedComponents/SelectionModal/__snapshots__/SearchSort.test.jsx.snap +++ b/src/editors/sharedComponents/SelectionModal/__snapshots__/SearchSort.test.jsx.snap @@ -1,6 +1,291 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`SearchSort component snapshots with search string (close button) 1`] = ` +exports[`SearchSort component snapshots with filterKeys with search string (close button) 1`] = ` + + + + } + value="props.searchString" + /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +`; + +exports[`SearchSort component snapshots with filterKeys without search string (search icon) 1`] = ` + + + } + value="" + /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +`; + +exports[`SearchSort component snapshots without filterKeys with search string (close button) 1`] = ` `; -exports[`SearchSort component snapshots without search string (search icon) 1`] = ` +exports[`SearchSort component snapshots without filterKeys without search string (search icon) 1`] = ` - - -`; - -exports[`Selection Modal snapshots rendering with props to null 1`] = ` - - - -`; diff --git a/src/editors/sharedComponents/SelectionModal/index.jsx b/src/editors/sharedComponents/SelectionModal/index.jsx index b0d868383..19218edd3 100644 --- a/src/editors/sharedComponents/SelectionModal/index.jsx +++ b/src/editors/sharedComponents/SelectionModal/index.jsx @@ -43,8 +43,18 @@ export const SelectionModal = ({ fetchError, uploadError, } = modalMessages; + + let background = '#FFFFFF'; + let showGallery = true; + if (isLoaded && !isFetchError && !isUploadError && !inputError.show) { + background = '#EBEBEB'; + } else if (isLoaded) { + showGallery = false; + } + const galleryPropsValues = { isLoaded, + show: showGallery, ...galleryProps, }; return ( @@ -64,7 +74,7 @@ export const SelectionModal = ({ )} title={intl.formatMessage(titleMsg)} - bodyStyle={{ background: '#EBEBEB' }} + bodyStyle={{ background, padding: '24px' }} headerComponent={(
diff --git a/src/editors/sharedComponents/SelectionModal/index.test.jsx b/src/editors/sharedComponents/SelectionModal/index.test.jsx index a77669a8c..2f9389059 100644 --- a/src/editors/sharedComponents/SelectionModal/index.test.jsx +++ b/src/editors/sharedComponents/SelectionModal/index.test.jsx @@ -1,7 +1,9 @@ import React from 'react'; -import { shallow } from 'enzyme'; +import { IntlProvider } from '@edx/frontend-platform/i18n'; +import { render, screen } from '@testing-library/react'; import { formatMessage } from '../../../testUtils'; import SelectionModal from '.'; +import '@testing-library/jest-dom'; const props = { isOpen: jest.fn(), @@ -69,7 +71,7 @@ const props = { jest.mock('../BaseModal', () => 'BaseModal'); jest.mock('./SearchSort', () => 'SearchSort'); -jest.mock('./Gallery', () => 'Gallery'); +jest.mock('./Gallery', () => () => 'Gallery'); jest.mock('../FileInput', () => 'FileInput'); jest.mock('../ErrorAlerts/ErrorAlert', () => 'ErrorAlert'); jest.mock('../ErrorAlerts/FetchErrorAlert', () => 'FetchErrorAlert'); @@ -77,11 +79,13 @@ jest.mock('../ErrorAlerts/UploadErrorAlert', () => 'UploadErrorAlert'); describe('Selection Modal', () => { describe('snapshots', () => { - test('rendering correctly with expected Input', () => { - expect(shallow()).toMatchSnapshot(); - }); - test('rendering with props to null', () => { - expect(shallow()).toMatchSnapshot(); + test('rendering correctly with expected Input', async () => { + render( + + + , + ); + expect(screen.getByText('Gallery')).toBeInTheDocument(); }); }); }); diff --git a/src/editors/utils/formatDuration.js b/src/editors/utils/formatDuration.js index 83008b7d7..8458f51fc 100644 --- a/src/editors/utils/formatDuration.js +++ b/src/editors/utils/formatDuration.js @@ -2,7 +2,7 @@ import * as moment from 'moment-shortformat'; const formatDuration = (duration) => { const d = moment.duration(duration, 'seconds'); - if (d.hours > 0) { + if (d.hours() > 0) { return ( `${d.hours().toString().padStart(2, '0')}:` + `${d.minutes().toString().padStart(2, '0')}:` diff --git a/src/editors/utils/formatDuration.test.js b/src/editors/utils/formatDuration.test.js new file mode 100644 index 000000000..6720130d7 --- /dev/null +++ b/src/editors/utils/formatDuration.test.js @@ -0,0 +1,12 @@ +import formatDuration from './formatDuration'; + +describe('formatDuration', () => { + test.each([ + [60, '01:00'], + [35, '00:35'], + [60 * 10 + 15, '10:15'], + [60 * 60 + 60 * 15 + 13, '01:15:13'], + ])('correct functionality of formatDuration with duration as %p', (duration, expected) => { + expect(formatDuration(duration)).toEqual(expected); + }); +}); From 36e56588cbd7030675b1cc1b7ab636ac983e9632 Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Fri, 31 Mar 2023 15:32:14 -0500 Subject: [PATCH 6/7] test: Adding test for VideoGallery and SelectionModal --- .../__snapshots__/index.test.jsx.snap | 95 ------------- src/editors/containers/VideoGallery/hooks.js | 22 +-- .../containers/VideoGallery/hooks.test.js | 46 +++---- src/editors/containers/VideoGallery/index.jsx | 2 +- .../containers/VideoGallery/index.test.jsx | 17 +-- .../SelectionModal/index.test.jsx | 126 +++++++++++++++--- 6 files changed, 152 insertions(+), 156 deletions(-) delete mode 100644 src/editors/containers/VideoGallery/__snapshots__/index.test.jsx.snap diff --git a/src/editors/containers/VideoGallery/__snapshots__/index.test.jsx.snap b/src/editors/containers/VideoGallery/__snapshots__/index.test.jsx.snap deleted file mode 100644 index 31dc76953..000000000 --- a/src/editors/containers/VideoGallery/__snapshots__/index.test.jsx.snap +++ /dev/null @@ -1,95 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`VideoGallery component snapshot 1`] = ` -
- -
-`; diff --git a/src/editors/containers/VideoGallery/hooks.js b/src/editors/containers/VideoGallery/hooks.js index 84104711b..2eabcfc28 100644 --- a/src/editors/containers/VideoGallery/hooks.js +++ b/src/editors/containers/VideoGallery/hooks.js @@ -19,7 +19,7 @@ export const state = { hideSelectedVideos: (val) => React.useState(val), }; -export const searchAndSortHooks = () => { +export const searchAndSortProps = () => { const [searchString, setSearchString] = module.state.searchString(''); const [sortBy, setSortBy] = module.state.sortBy(sortKeys.dateNewest); const [filterBy, setFilterBy] = module.state.filertBy(filterKeys.videoStatus); @@ -80,7 +80,7 @@ export const filterList = ({ return filteredList.sort(sortFunctions[sortBy in sortKeys ? sortKeys[sortBy] : sortKeys.dateNewest]); }; -export const videoListHooks = ({ searchSortProps, videos }) => { +export const videoListProps = ({ searchSortProps, videos }) => { const [highlighted, setHighlighted] = module.state.highlighted(null); const [ showSelectVideoError, @@ -123,7 +123,7 @@ export const videoListHooks = ({ searchSortProps, videos }) => { }; }; -export const fileInputHooks = () => { +export const fileInputProps = () => { // TODO [Update video] Implement this const ref = React.useRef(); const click = () => ref.current.click(); @@ -137,9 +137,9 @@ export const fileInputHooks = () => { export const buildVideos = ({ rawVideos }) => { let videos = []; - const videoList = Object.values(rawVideos); - if (videoList.length > 0) { - videos = videoList.map(video => ({ + const rawVideoList = Object.values(rawVideos); + if (rawVideoList.length > 0) { + videos = rawVideoList.map(video => ({ id: video.edx_video_id, displayName: video.client_video_id, externalUrl: video.course_video_image_url, @@ -167,16 +167,16 @@ export const getstatusBadgeVariant = ({ status }) => { } }; -export const videoHooks = ({ videos }) => { - const searchSortProps = module.searchAndSortHooks(); - const videoList = module.videoListHooks({ searchSortProps, videos }); +export const videoProps = ({ videos }) => { + const searchSortProps = module.searchAndSortProps(); + const videoList = module.videoListProps({ searchSortProps, videos }); const { galleryError, galleryProps, inputError, selectBtnProps, } = videoList; - const fileInput = module.fileInputHooks(); + const fileInput = module.fileInputProps(); return { galleryError, @@ -189,6 +189,6 @@ export const videoHooks = ({ videos }) => { }; export default { - videoHooks, + videoProps, buildVideos, }; diff --git a/src/editors/containers/VideoGallery/hooks.test.js b/src/editors/containers/VideoGallery/hooks.test.js index 43d0b77f0..d3ad83e97 100644 --- a/src/editors/containers/VideoGallery/hooks.test.js +++ b/src/editors/containers/VideoGallery/hooks.test.js @@ -41,9 +41,9 @@ describe('VideoGallery hooks', () => { beforeEach(() => { state.mock(); }); afterEach(() => { state.restore(); }); - describe('searchAndSortHooks', () => { + describe('searchAndSortProps', () => { beforeEach(() => { - hook = hooks.searchAndSortHooks(); + hook = hooks.searchAndSortProps(); }); it('returns searchString value, initialized to an empty string', () => { expect(state.stateVals.searchString).toEqual(hook.searchString); @@ -152,7 +152,7 @@ describe('VideoGallery hooks', () => { expect(value).toBeNull(); }); }); - describe('videoListHooks outputs', () => { + describe('videoListProps outputs', () => { const props = { searchSortProps: { searchString: 'Es', @@ -170,7 +170,7 @@ describe('VideoGallery hooks', () => { const filterList = (args) => ({ filterList: args }); const load = () => { jest.spyOn(hooks, hookKeys.filterList).mockImplementationOnce(filterList); - hook = hooks.videoListHooks(props); + hook = hooks.videoListProps(props); }; beforeEach(() => { load(); @@ -196,7 +196,7 @@ describe('VideoGallery hooks', () => { const show = 'sHOWSelectiRROr'; expect(hook.galleryError.show).toEqual(false); state.mockVal(state.keys.showSelectVideoError, show); - hook = hooks.videoListHooks(props); + hook = hooks.videoListProps(props); expect(hook.galleryError.show).toEqual(show); }); test('set sets showSelectVideoError to true', () => { @@ -210,44 +210,44 @@ describe('VideoGallery hooks', () => { }); }); }); - describe('videoHooks', () => { - const videoListHooks = { + describe('videoProps', () => { + const videoList = { galleryProps: 'some gallery props', selectBtnProps: 'some select btn props', }; - const searchAndSortHooks = { search: 'props' }; - const fileInputHooks = { file: 'input hooks' }; + const searchAndSortProps = { search: 'props' }; + const fileInput = { file: 'input hooks' }; const videos = { video: { staTICUrl: '/assets/sOmEuiMAge' } }; const spies = {}; beforeEach(() => { - spies.videoList = jest.spyOn(hooks, hookKeys.videoListHooks) - .mockReturnValueOnce(videoListHooks); - spies.search = jest.spyOn(hooks, hookKeys.searchAndSortHooks) - .mockReturnValueOnce(searchAndSortHooks); - spies.file = jest.spyOn(hooks, hookKeys.fileInputHooks) - .mockReturnValueOnce(fileInputHooks); - hook = hooks.videoHooks({ videos }); + spies.videoList = jest.spyOn(hooks, hookKeys.videoListProps) + .mockReturnValueOnce(videoList); + spies.search = jest.spyOn(hooks, hookKeys.searchAndSortProps) + .mockReturnValueOnce(searchAndSortProps); + spies.file = jest.spyOn(hooks, hookKeys.fileInputProps) + .mockReturnValueOnce(fileInput); + hook = hooks.videoProps({ videos }); }); - it('forwards fileInputHooks as fileInput', () => { - expect(hook.fileInput).toEqual(fileInputHooks); + it('forwards fileInput as fileInput', () => { + expect(hook.fileInput).toEqual(fileInput); expect(spies.file.mock.calls.length).toEqual(1); expect(spies.file).toHaveBeenCalled(); }); - it('initializes videoListHooks', () => { + it('initializes videoList', () => { expect(spies.videoList.mock.calls.length).toEqual(1); expect(spies.videoList).toHaveBeenCalledWith({ - searchSortProps: searchAndSortHooks, + searchSortProps: searchAndSortProps, videos, }); }); it('forwards searchAndSortHooks as searchSortProps', () => { - expect(hook.searchSortProps).toEqual(searchAndSortHooks); + expect(hook.searchSortProps).toEqual(searchAndSortProps); expect(spies.search.mock.calls.length).toEqual(1); expect(spies.search).toHaveBeenCalled(); }); it('forwards galleryProps and selectBtnProps from the video list hooks', () => { - expect(hook.galleryProps).toEqual(videoListHooks.galleryProps); - expect(hook.selectBtnProps).toEqual(videoListHooks.selectBtnProps); + expect(hook.galleryProps).toEqual(videoList.galleryProps); + expect(hook.selectBtnProps).toEqual(videoList.selectBtnProps); }); }); }); diff --git a/src/editors/containers/VideoGallery/index.jsx b/src/editors/containers/VideoGallery/index.jsx index 0f1cced0b..8ea788349 100644 --- a/src/editors/containers/VideoGallery/index.jsx +++ b/src/editors/containers/VideoGallery/index.jsx @@ -23,7 +23,7 @@ export const VideoGallery = ({ galleryProps, searchSortProps, selectBtnProps, - } = hooks.videoHooks({ videos }); + } = hooks.videoProps({ videos }); const modalMessages = { confirmMsg: messages.selectVideoButtonlabel, diff --git a/src/editors/containers/VideoGallery/index.test.jsx b/src/editors/containers/VideoGallery/index.test.jsx index 58469c424..31d3896ba 100644 --- a/src/editors/containers/VideoGallery/index.test.jsx +++ b/src/editors/containers/VideoGallery/index.test.jsx @@ -9,7 +9,7 @@ jest.mock('../../sharedComponents/SelectionModal', () => 'SelectionModal'); jest.mock('./hooks', () => ({ buildVideos: jest.fn(() => []), - videoHooks: jest.fn(() => ({ + videoProps: jest.fn(() => ({ galleryError: { show: 'ShoWERror gAlLery', set: jest.fn(), @@ -60,31 +60,28 @@ describe('VideoGallery', () => { isUploadError: false, }; let el; - const videoHooks = hooks.videoHooks(); + const videoProps = hooks.videoProps(); beforeEach(() => { el = shallow(); }); - test('snapshot', () => { - expect(el).toMatchSnapshot(); - }); it('provides confirm action, forwarding selectBtnProps from imgHooks', () => { expect(el.find(SelectionModal).props().selectBtnProps).toEqual( - expect.objectContaining({ ...hooks.videoHooks().selectBtnProps }), + expect.objectContaining({ ...hooks.videoProps().selectBtnProps }), ); }); it('provides file upload button linked to fileInput.click', () => { expect(el.find(SelectionModal).props().fileInput.click).toEqual( - videoHooks.fileInput.click, + videoProps.fileInput.click, ); }); it('provides a SearchSort component with searchSortProps from imgHooks', () => { - expect(el.find(SelectionModal).props().searchSortProps).toEqual(videoHooks.searchSortProps); + expect(el.find(SelectionModal).props().searchSortProps).toEqual(videoProps.searchSortProps); }); it('provides a Gallery component with galleryProps from imgHooks', () => { - expect(el.find(SelectionModal).props().galleryProps).toEqual(videoHooks.galleryProps); + expect(el.find(SelectionModal).props().galleryProps).toEqual(videoProps.galleryProps); }); it('provides a FileInput component with fileInput props from imgHooks', () => { - expect(el.find(SelectionModal).props().fileInput).toMatchObject(videoHooks.fileInput); + expect(el.find(SelectionModal).props().fileInput).toMatchObject(videoProps.fileInput); }); }); }); diff --git a/src/editors/sharedComponents/SelectionModal/index.test.jsx b/src/editors/sharedComponents/SelectionModal/index.test.jsx index 2f9389059..164204194 100644 --- a/src/editors/sharedComponents/SelectionModal/index.test.jsx +++ b/src/editors/sharedComponents/SelectionModal/index.test.jsx @@ -11,7 +11,7 @@ const props = { size: 'fullscreen', isFullscreenScroll: false, galleryError: { - show: 'ShoWERror gAlLery', + show: false, set: jest.fn(), dismiss: jest.fn(), message: { @@ -21,7 +21,7 @@ const props = { }, }, inputError: { - show: 'ShoWERror inPUT', + show: false, set: jest.fn(), dismiss: jest.fn(), message: { @@ -66,26 +66,120 @@ const props = { description: 'uploadError', }, }, + isLoaded: true, + isFetchError: false, + isUploadError: false, intl: { formatMessage }, }; +const mockGalleryFn = jest.fn(); +const mockFileInputFn = jest.fn(); +const mockFetchErrorAlertFn = jest.fn(); +const mockUploadErrorAlertFn = jest.fn(); + jest.mock('../BaseModal', () => 'BaseModal'); jest.mock('./SearchSort', () => 'SearchSort'); -jest.mock('./Gallery', () => () => 'Gallery'); -jest.mock('../FileInput', () => 'FileInput'); -jest.mock('../ErrorAlerts/ErrorAlert', () => 'ErrorAlert'); -jest.mock('../ErrorAlerts/FetchErrorAlert', () => 'FetchErrorAlert'); -jest.mock('../ErrorAlerts/UploadErrorAlert', () => 'UploadErrorAlert'); +jest.mock('./Gallery', () => (componentProps) => { + mockGalleryFn(componentProps); + return (
Gallery
); +}); +jest.mock('../FileInput', () => (componentProps) => { + mockFileInputFn(componentProps); + return (
FileInput
); +}); +jest.mock('../ErrorAlerts/ErrorAlert', () => () => (
ErrorAlert
)); +jest.mock('../ErrorAlerts/FetchErrorAlert', () => (componentProps) => { + mockFetchErrorAlertFn(componentProps); + return (
FetchErrorAlert
); +}); +jest.mock('../ErrorAlerts/UploadErrorAlert', () => (componentProps) => { + mockUploadErrorAlertFn(componentProps); + return (
UploadErrorAlert
); +}); describe('Selection Modal', () => { - describe('snapshots', () => { - test('rendering correctly with expected Input', async () => { - render( - - - , - ); - expect(screen.getByText('Gallery')).toBeInTheDocument(); - }); + beforeEach(() => { + jest.clearAllMocks(); + }); + test('rendering correctly with expected Input', async () => { + render( + + + , + ); + expect(screen.getByText('Gallery')).toBeInTheDocument(); + expect(screen.getByText('FileInput')).toBeInTheDocument(); + expect(screen.getByText('FetchErrorAlert')).toBeInTheDocument(); + expect(screen.getByText('UploadErrorAlert')).toBeInTheDocument(); + + expect(mockGalleryFn).toHaveBeenCalledWith( + expect.objectContaining({ + ...props.galleryProps, + isLoaded: props.isLoaded, + show: true, + }), + ); + expect(mockFetchErrorAlertFn).toHaveBeenCalledWith( + expect.objectContaining({ + isFetchError: props.isFetchError, + message: props.modalMessages.fetchError, + }), + ); + expect(mockUploadErrorAlertFn).toHaveBeenCalledWith( + expect.objectContaining({ + isUploadError: props.isUploadError, + message: props.modalMessages.uploadError, + }), + ); + expect(mockFileInputFn).toHaveBeenCalledWith( + expect.objectContaining({ + acceptedFiles: '.png', + fileInput: props.fileInput, + }), + ); + }); + test('rendering correctly with errors', () => { + render( + + + , + ); + expect(screen.getByText('Gallery')).toBeInTheDocument(); + expect(screen.getByText('FileInput')).toBeInTheDocument(); + expect(screen.getByText('FetchErrorAlert')).toBeInTheDocument(); + expect(screen.getByText('UploadErrorAlert')).toBeInTheDocument(); + + expect(mockFetchErrorAlertFn).toHaveBeenCalledWith( + expect.objectContaining({ + isFetchError: true, + message: props.modalMessages.fetchError, + }), + ); + expect(mockGalleryFn).toHaveBeenCalledWith( + expect.objectContaining({ + ...props.galleryProps, + isLoaded: props.isLoaded, + show: false, + }), + ); + }); + test('rendering correctly with loading', () => { + render( + + + , + ); + expect(screen.getByText('Gallery')).toBeInTheDocument(); + expect(screen.getByText('FileInput')).toBeInTheDocument(); + expect(screen.getByText('FetchErrorAlert')).toBeInTheDocument(); + expect(screen.getByText('UploadErrorAlert')).toBeInTheDocument(); + + expect(mockGalleryFn).toHaveBeenCalledWith( + expect.objectContaining({ + ...props.galleryProps, + isLoaded: false, + show: true, + }), + ); }); }); From 5695f92127334130908d5af85598b08bca2f8c9e Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Wed, 5 Apr 2023 16:35:29 -0500 Subject: [PATCH 7/7] chore: Padding on gallery fixed --- src/editors/sharedComponents/SelectionModal/index.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/editors/sharedComponents/SelectionModal/index.jsx b/src/editors/sharedComponents/SelectionModal/index.jsx index 19218edd3..3e8592288 100644 --- a/src/editors/sharedComponents/SelectionModal/index.jsx +++ b/src/editors/sharedComponents/SelectionModal/index.jsx @@ -74,7 +74,7 @@ export const SelectionModal = ({ )} title={intl.formatMessage(titleMsg)} - bodyStyle={{ background, padding: '24px' }} + bodyStyle={{ background, padding: '9px 24px' }} headerComponent={(