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;