From 9c9d3c8fdfa522cc53120be0afdde9eb8bf03e19 Mon Sep 17 00:00:00 2001 From: Ben Warzeski Date: Tue, 1 Mar 2022 11:17:03 -0500 Subject: [PATCH] feat: image upload skeleton (#22) --- .../ImageUpload/ImageUploadModal.jsx | 51 ------ .../containers/TextEditor/TextEditor.jsx | 12 +- .../TextEditor/components/BaseModal.jsx | 52 +++++++ .../components/ImageSettingsModal/index.jsx | 147 ++++++++++++++++++ .../components/ImageUploadModal.jsx | 47 ++++++ .../components/SelectImageModal.jsx | 66 ++++++++ src/editors/data/constants/mockData.js | 47 ++++++ src/editors/data/redux/app/selectors.js | 12 +- src/editors/data/redux/app/selectors.test.js | 3 + .../data/redux/thunkActions/.requests.js.swp | Bin 12288 -> 0 bytes src/editors/data/redux/thunkActions/app.js | 7 + 11 files changed, 385 insertions(+), 59 deletions(-) delete mode 100644 src/editors/containers/TextEditor/ImageUpload/ImageUploadModal.jsx create mode 100644 src/editors/containers/TextEditor/components/BaseModal.jsx create mode 100644 src/editors/containers/TextEditor/components/ImageSettingsModal/index.jsx create mode 100644 src/editors/containers/TextEditor/components/ImageUploadModal.jsx create mode 100644 src/editors/containers/TextEditor/components/SelectImageModal.jsx create mode 100644 src/editors/data/constants/mockData.js delete mode 100644 src/editors/data/redux/thunkActions/.requests.js.swp 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 21b96922787b18654af8d58e8f378aef30b4d982..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12288 zcmeI2&x;&I6vr#B@mE}xh&jZ|(;PCp%=TF!ij zXEUy&AQ-`$h#(S)7*Oz{pk9Ipg@6&1Me`2`9u$R~^q>&%TisPXvopIUf(H|8;lp(I zyn0pjtyk~$uvMQtGJAmT_jWM+-ND%V58qY)Y~ybuo!1$w^LWV@&vV&~^tbE&@$jjM znaQb{eUp#OJa%N?0k4$=erwqL#Xg6%g-F$;2x<+H#EQq^L@TOK1o^eXXFE+|9xIvK z@Gw$|khRcPeogQ&J5f`MS=`(ks3?gg9v#cLRMLA+4)^MwI0c*ng#x2&=hTi#eZB48 zd+5&1b50nifK$LJ;1qBQI0c*nP64NYQ{aE3fJ!&8moVS!%$%P!pBn}~&8_=z3OEIv z0!{&^fK$LJ;1qBQI0c*nP64NYQ{XC8zz-Q)!H@lg5ey#x|F^&YFWk=9_uy@C3`~Q) z;BK%LYyn@~#@Hz^4SpGA>`U+pm;%e1D7^2_8oW?Bw#-n z0hexJ>`(9q_#Lc(GvFxL0ycwhZ)WU6@G_VOTfxsaG4=zv0KNfVgY)1VI1Lo2f}d_= z>_>1Kya49Fec<8^jC~G11s{NS!8_m-cndrZc7k6w;;aOC56ponunAne9__(Na1hLZ zOVD-&d({;;yZ)gk38UKuN`rOLu6c_oFsabcXrXi;#3i?WzSB} zFN|dBWAQ5OB+Odei@91#MAO*AlUYGXB2{nEX%AJF$?9a+4$DS+9-xYKS|z(g`Ev|w zyawpO)E9o6mX9{FIC!Q@Nxa{WTAAQ9(RbbEYB33^QsF`*qVS-5LN9AOIg|wfmoj%^ zuS+M0EgZv=d|f{hu^BIP?WA-UhlL+BdDu5?=uCx6kIJuALTZuIe7OMgR(oplj>l@_ z<5w8$W}ycIZQb?9HvAU?epM6|Te+jRFCy;mBPq8nHu^t}q_SxdNoMn3l>ua<#D+mR zwSh3`3#e5h>&S~M?o$01KR~9VOwqn>6abO1F_~=UAtlF^o{cU{I-8Y&H>8eK zzEE~pMl*<`H5sUDwwm>1(WNwr&A{}H8HK*=xhmg>ifX#L*5;A@pRSNk6mJ_{J2@0u zox1pO9B#JH`YW@os67GD!rYM3wG{=Iq7coaM#P0~n>0vAseTto1W8lT@l;n>yR_O+ zTS*k*Sw((JCgh{`r%Lu#f&4h^Es#mDl8~kAoN%VQg~GlRsm0uLlBqOP+HaoCVb<%8 z>K!}ke6MJ~5~sW+dO)}O*fuR8*eFsINmOKn5f7`y;CU#kn2GkB&p3As6NUNdN%^I6 zp0;^X;PsXZ@ (dispatch) => { })); }; +export const fetchImages = ({ onSuccess }) => () => { + // get images + onSuccess(mockData.mockImageData); +}; + export default StrictDict({ fetchBlock, fetchUnit, initialize, saveBlock, + fetchImages, });