diff --git a/src/editors/containers/TextEditor/ImageUpload/ImageUploadModal.jsx b/src/editors/containers/TextEditor/ImageUpload/ImageUploadModal.jsx deleted file mode 100644 index 00cb118d5..000000000 --- a/src/editors/containers/TextEditor/ImageUpload/ImageUploadModal.jsx +++ /dev/null @@ -1,51 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { ModalDialog, ActionRow, Button } from '@edx/paragon'; - -const ImageUploadModal = ({ isOpen, close }) => ( - - - - Im a dialog box - - - -

- Im baby palo santo ugh celiac fashion axe. - La croix lo-fi venmo whatever. - Beard man braid migas single-origin coffee forage ramps. - Tumeric messenger bag bicycle rights wayfarers, try-hard cronut blue bottle health goth. - Sriracha tumblr cardigan, cloud bread succulents tumeric copper mug marfa semiotics woke next - level organic roof party +1 try-hard. -

-
- - - - Cancel - - - - -
-); - -ImageUploadModal.propTypes = { - isOpen: PropTypes.bool, - close: PropTypes.func, -}; -ImageUploadModal.defaultProps = { - isOpen: false, - close: () => {}, -}; -export default ImageUploadModal; diff --git a/src/editors/containers/TextEditor/TextEditor.jsx b/src/editors/containers/TextEditor/TextEditor.jsx index bb8727e9f..fad3a82bf 100644 --- a/src/editors/containers/TextEditor/TextEditor.jsx +++ b/src/editors/containers/TextEditor/TextEditor.jsx @@ -30,7 +30,7 @@ import { nullMethod, } from './hooks'; import messages from './messages'; -import ImageUploadModal from './ImageUpload/ImageUploadModal'; +import ImageUploadModal from './components/ImageUploadModal'; export const TextEditor = ({ setEditorRef, @@ -60,9 +60,13 @@ export const TextEditor = ({ ) : ( - )} diff --git a/src/editors/containers/TextEditor/components/BaseModal.jsx b/src/editors/containers/TextEditor/components/BaseModal.jsx new file mode 100644 index 000000000..69fb007c4 --- /dev/null +++ b/src/editors/containers/TextEditor/components/BaseModal.jsx @@ -0,0 +1,52 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import { + ActionRow, + ModalDialog, +} from '@edx/paragon'; + +export const BaseModal = ({ + isOpen, + close, + title, + children, + confirmAction, +}) => ( + + + + {title} + + + + {children} + + + + + Cancel + + {confirmAction} + + + +); + +BaseModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + close: PropTypes.func.isRequired, + title: PropTypes.node.isRequired, + children: PropTypes.node.isRequired, + confirmAction: PropTypes.node.isRequired, +}; + +export default BaseModal; diff --git a/src/editors/containers/TextEditor/components/ImageSettingsModal/index.jsx b/src/editors/containers/TextEditor/components/ImageSettingsModal/index.jsx new file mode 100644 index 000000000..3f88946b0 --- /dev/null +++ b/src/editors/containers/TextEditor/components/ImageSettingsModal/index.jsx @@ -0,0 +1,147 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { + Button, + Form, + Image, +} from '@edx/paragon'; + +import BaseModal from '../BaseModal'; +import * as module from '.'; + +export const hooks = { + dimensions: () => { + const [baseDimensions, setBaseDimensions] = React.useState(null); + const [dimensions, setDimensions] = React.useState(null); + const initialize = ({ height, width }) => { + setBaseDimensions({ height, width }); + setDimensions({ height, width }); + }; + const reset = () => setDimensions(baseDimensions); + const setWidth = (width) => setDimensions({ ...dimensions, width }); + const setHeight = (height) => setDimensions({ ...dimensions, height }); + return { + value: dimensions, + initialize, + reset, + setHeight, + setWidth, + }; + }, + altText: () => { + const [altText, setAltText] = React.useState(''); + const [isDecorative, setIsDecorative] = React.useState(false); + return { + value: altText, + set: setAltText, + isDecorative, + setIsDecorative, + }; + }, + onImgLoad: (initializeDimensions) => ({ target: img }) => { + initializeDimensions({ + height: img.naturalHeight, + width: img.naturalWidth, + }); + }, + onInputChange: (handleValue) => (e) => handleValue(e.target.value), + onCheckboxChange: (handleValue) => (e) => handleValue(e.target.checked), + onSave: ({ + saveToEditor, + dimensions, + altText, + isDecorative, + }) => saveToEditor({ + dimensions, + altText, + isDecorative, + }), +}; + +export const ImageSettingsModal = ({ + isOpen, + close, + selection, + saveToEditor, + returnToSelection, +}) => { + const dimensions = module.hooks.dimensions(); + const altText = module.hooks.altText(); + const onImgLoad = module.hooks.onImgLoad(dimensions.initialize); + const onSaveClick = module.hooks.onSave({ + saveToEditor, + dimensions: dimensions.value, + altText: altText.value, + isDecorative: altText.isDecorative, + }); + return ( + + Save + + )} + > + +
+ + + { dimensions.value && ( + + Image Dimensions + + + + )} + + Accessibility + + + This image is decorative (no alt text required). + + +
+ ); +}; + +ImageSettingsModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + close: PropTypes.func.isRequired, + selection: PropTypes.shape({ + url: PropTypes.string, + externalUrl: PropTypes.string, + }).isRequired, + saveToEditor: PropTypes.func.isRequired, + returnToSelection: PropTypes.func.isRequired, +}; +export default ImageSettingsModal; diff --git a/src/editors/containers/TextEditor/components/ImageUploadModal.jsx b/src/editors/containers/TextEditor/components/ImageUploadModal.jsx new file mode 100644 index 000000000..54ebc08c8 --- /dev/null +++ b/src/editors/containers/TextEditor/components/ImageUploadModal.jsx @@ -0,0 +1,47 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import ImageSettingsModal from './ImageSettingsModal'; +import SelectImageModal from './SelectImageModal'; + +const ImageUploadModal = ({ isOpen, close, editorRef }) => { + // selected image file reference data object. + // existance of this field determines which child modal is displayed + const [selection, setSelection] = React.useState(null); + const clearSelection = () => setSelection(null); + const saveToEditor = (settings) => { + console.log({ selection, settings }); + // tell editor ref to insert content at cursor location(); + }; + const closeAndReset = () => { + setSelection(null); + close(); + }; + if (selection) { + return ( + + ); + } + return (); +}; + +ImageUploadModal.defaultProps = { + editorRef: null, +}; +ImageUploadModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + close: PropTypes.func.isRequired, + editorRef: PropTypes.oneOfType([ + PropTypes.func, + PropTypes.shape({ current: PropTypes.any }), + ]), +}; +export default ImageUploadModal; diff --git a/src/editors/containers/TextEditor/components/SelectImageModal.jsx b/src/editors/containers/TextEditor/components/SelectImageModal.jsx new file mode 100644 index 000000000..2dc540c4d --- /dev/null +++ b/src/editors/containers/TextEditor/components/SelectImageModal.jsx @@ -0,0 +1,66 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import PropTypes from 'prop-types'; + +import { Button, Image } from '@edx/paragon'; + +import { thunkActions } from '../../../data/redux'; +import BaseModal from './BaseModal'; +import * as module from './SelectImageModal'; + +export const hooks = { + imageList: ({ fetchImages }) => { + const [images, setImages] = React.useState([]); + React.useEffect(() => { + fetchImages({ onSuccess: setImages }); + }, []); + return images; + }, + onSelectClick: ({ setSelection, images }) => () => setSelection(images[0]), +}; + +export const SelectImageModal = ({ + fetchImages, + isOpen, + close, + setSelection, +}) => { + const images = module.hooks.imageList({ fetchImages }); + const onSelectClick = module.hooks.onSelectClick({ + setSelection, + images, + }); + + return ( + Next} + > + {/* Content selection */} + {images.map( + img => ( +
+ +
+ ), + )} +
+ ); +}; + +SelectImageModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + close: PropTypes.func.isRequired, + setSelection: PropTypes.func.isRequired, + // redux + fetchImages: PropTypes.func.isRequired, +}; + +export const mapStateToProps = () => ({}); +export const mapDispatchToProps = { + fetchImages: thunkActions.app.fetchImages, +}; + +export default connect(mapStateToProps, mapDispatchToProps)(SelectImageModal); diff --git a/src/editors/data/constants/mockData.js b/src/editors/data/constants/mockData.js new file mode 100644 index 000000000..919fa6943 --- /dev/null +++ b/src/editors/data/constants/mockData.js @@ -0,0 +1,47 @@ +/* eslint-disable import/prefer-default-export */ +export const mockImageData = [ + { + displayName: 'shahrukh.jpg', + contentType: 'image/jpeg', + dateAdded: 'Jan 05, 2022 at 17:38 UTC', + url: '/asset-v1:edX+test101+2021_T1+type@asset+block@shahrukh.jpg', + externalUrl: 'https://courses.edx.org/asset-v1:edX+test101+2021_T1+type@asset+block@shahrukh.jpg', + portableUrl: '/static/shahrukh.jpg', + thumbnail: '/asset-v1:edX+test101+2021_T1+type@thumbnail+block@shahrukh.jpg', + locked: false, + id: 'asset-v1:edX+test101+2021_T1+type@asset+block@shahrukh.jpg', + }, + { + displayName: 'IMG_5899.jpg', + contentType: 'image/jpeg', + dateAdded: 'Nov 16, 2021 at 18:55 UTC', + url: '/asset-v1:edX+test101+2021_T1+type@asset+block@IMG_5899.jpg', + externalUrl: 'https://courses.edx.org/asset-v1:edX+test101+2021_T1+type@asset+block@IMG_5899.jpg', + portableUrl: '/static/IMG_5899.jpg', + thumbnail: '/asset-v1:edX+test101+2021_T1+type@thumbnail+block@IMG_5899.jpg', + locked: false, + id: 'asset-v1:edX+test101+2021_T1+type@asset+block@IMG_5899.jpg', + }, + { + displayName: 'ccexample.srt', + contentType: 'application/octet-stream', + dateAdded: 'Nov 01, 2021 at 15:42 UTC', + url: '/asset-v1:edX+test101+2021_T1+type@asset+block@ccexample.srt', + externalUrl: 'https://courses.edx.org/asset-v1:edX+test101+2021_T1+type@asset+block@ccexample.srt', + portableUrl: '/static/ccexample.srt', + thumbnail: null, + locked: false, + id: 'asset-v1:edX+test101+2021_T1+type@asset+block@ccexample.srt', + }, + { + displayName: 'Tennis Ball.jpeg', + contentType: 'image/jpeg', + dateAdded: 'Aug 04, 2021 at 16:52 UTC', + url: '/asset-v1:edX+test101+2021_T1+type@asset+block@Tennis_Ball.jpeg', + externalUrl: 'https://courses.edx.org/asset-v1:edX+test101+2021_T1+type@asset+block@Tennis_Ball.jpeg', + portableUrl: '/static/Tennis_Ball.jpeg', + thumbnail: '/asset-v1:edX+test101+2021_T1+type@thumbnail+block@Tennis_Ball-jpeg.jpg', + locked: false, + id: 'asset-v1:edX+test101+2021_T1+type@asset+block@Tennis_Ball.jpeg', + }, +]; diff --git a/src/editors/data/redux/app/selectors.js b/src/editors/data/redux/app/selectors.js index 256fcac3d..41ce6e83c 100644 --- a/src/editors/data/redux/app/selectors.js +++ b/src/editors/data/redux/app/selectors.js @@ -37,10 +37,14 @@ export const isInitialized = createSelector( export const typeHeader = createSelector( [module.simpleSelectors.blockType], - (blockType) => ((blockType === blockTypes.html) - ? 'Text' - : blockType[0].toUpperCase() + blockType.substring(1) - ), + (blockType) => { + if (blockType === null) { + return null; + } + return (blockType === blockTypes.html) + ? 'Text' + : blockType[0].toUpperCase() + blockType.substring(1); + }, ); export default { diff --git a/src/editors/data/redux/app/selectors.test.js b/src/editors/data/redux/app/selectors.test.js index edb28332f..832e3de5f 100644 --- a/src/editors/data/redux/app/selectors.test.js +++ b/src/editors/data/redux/app/selectors.test.js @@ -87,6 +87,9 @@ describe('app selectors unit tests', () => { it('is memoized based on blockType', () => { expect(selectors.typeHeader.preSelectors).toEqual([simpleSelectors.blockType]); }); + it('returns null if blockType is null', () => { + expect(selectors.typeHeader.cb(null)).toEqual(null); + }); it('returns Text if the blockType is html', () => { expect(selectors.typeHeader.cb('html')).toEqual('Text'); }); diff --git a/src/editors/data/redux/thunkActions/.requests.js.swp b/src/editors/data/redux/thunkActions/.requests.js.swp deleted file mode 100644 index 21b969227..000000000 Binary files a/src/editors/data/redux/thunkActions/.requests.js.swp and /dev/null differ diff --git a/src/editors/data/redux/thunkActions/app.js b/src/editors/data/redux/thunkActions/app.js index b1d4937e3..cf5805457 100644 --- a/src/editors/data/redux/thunkActions/app.js +++ b/src/editors/data/redux/thunkActions/app.js @@ -1,4 +1,5 @@ import { StrictDict } from '../../../utils'; +import mockData from '../../constants/mockData'; import { actions } from '..'; import * as requests from './requests'; import * as module from './app'; @@ -43,9 +44,15 @@ export const saveBlock = ({ content, returnToUnit }) => (dispatch) => { })); }; +export const fetchImages = ({ onSuccess }) => () => { + // get images + onSuccess(mockData.mockImageData); +}; + export default StrictDict({ fetchBlock, fetchUnit, initialize, saveBlock, + fetchImages, });