From 14504073e089fc45af74991eeed67511601635e2 Mon Sep 17 00:00:00 2001 From: XnpioChV Date: Wed, 15 Mar 2023 11:57:55 -0500 Subject: [PATCH] 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;