From 7caddb90a993876c2ff512267bf7340de45fdc70 Mon Sep 17 00:00:00 2001 From: Paul Kaplan <pkaplan@media.mit.edu> Date: Fri, 27 Apr 2018 11:42:27 -0400 Subject: [PATCH] Revert "Updates to preview modal for release (#1864)" This reverts commit 29d5619d936d30eea9ca601f6d052aad108ca0d3. --- src/components/gui/gui.jsx | 6 + src/components/import-modal/import-modal.css | 185 ++++++++++++++++++ src/components/import-modal/import-modal.jsx | 155 +++++++++++++++ .../preview-modal/preview-modal.css | 12 +- .../preview-modal/preview-modal.jsx | 72 +++---- src/containers/gui.jsx | 2 + src/containers/import-modal.jsx | 111 +++++++++++ src/containers/preview-modal.jsx | 17 +- src/reducers/modals.js | 11 ++ test/integration/project-loading.test.js | 35 ++++ 10 files changed, 561 insertions(+), 45 deletions(-) create mode 100644 src/components/import-modal/import-modal.css create mode 100644 src/components/import-modal/import-modal.jsx create mode 100644 src/containers/import-modal.jsx diff --git a/src/components/gui/gui.jsx b/src/components/gui/gui.jsx index 640e07fdd..1f7ccfb20 100644 --- a/src/components/gui/gui.jsx +++ b/src/components/gui/gui.jsx @@ -19,6 +19,7 @@ import Box from '../box/box.jsx'; import MenuBar from '../menu-bar/menu-bar.jsx'; import PreviewModal from '../../containers/preview-modal.jsx'; +import ImportModal from '../../containers/import-modal.jsx'; import WebGlModal from '../../containers/webgl-modal.jsx'; import TipsLibrary from '../../containers/tips-library.jsx'; import Cards from '../../containers/cards.jsx'; @@ -50,6 +51,7 @@ const GUIComponent = props => { cardsVisible, children, costumesTabVisible, + importInfoVisible, intl, loading, onExtensionButtonClick, @@ -93,6 +95,9 @@ const GUIComponent = props => { {loading ? ( <Loader /> ) : null} + {importInfoVisible ? ( + <ImportModal /> + ) : null} {isRendererSupported ? null : ( <WebGlModal /> )} @@ -225,6 +230,7 @@ GUIComponent.propTypes = { cardsVisible: PropTypes.bool, children: PropTypes.node, costumesTabVisible: PropTypes.bool, + importInfoVisible: PropTypes.bool, intl: intlShape.isRequired, loading: PropTypes.bool, onActivateCostumesTab: PropTypes.func, diff --git a/src/components/import-modal/import-modal.css b/src/components/import-modal/import-modal.css new file mode 100644 index 000000000..93480429b --- /dev/null +++ b/src/components/import-modal/import-modal.css @@ -0,0 +1,185 @@ +@import "../../css/colors.css"; +@import "../../css/units.css"; +@import "../../css/typography.css"; + +.modal-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 1000; + background-color: $ui-modal-overlay; +} + +.modal-content { + margin: 100px auto; + outline: none; + border: .25rem solid $ui-white-transparent; + padding: 0; + border-radius: $space; + user-select: none; + width: 500px; + color: $text-primary; + overflow: hidden; +} + +/* + TODO figure out how to remove filter altogether + since it is null... + Modal header has 3 items: + |x title filter| + + Use the same width for both side item containers, + so that title remains centered +*/ +$sides: 20rem; + +.header { + display: flex; + flex-direction: row; + flex-wrap: nowrap; + justify-content: flex-start; + height: $library-header-height; + + box-sizing: border-box; + width: 100%; + background-color: $pen-primary; +} + +.header-item { + display: flex; + align-items: center; + padding: 1rem; + text-decoration: none; + color: white; + user-select: none; +} + +.header-item-filter { + display: flex; + flex-basis: $sides; + justify-content: flex-end; +} + +.header-item-title { + flex-grow: 1; + flex-shrink: 0; + justify-content: center; + user-select: none; + letter-spacing: 0.4px; + cursor: default; +} + +.header-item-title h2 { + font-size: 1.25rem; +} + +.header-item-close { + display: flex; + flex-basis: $sides; + justify-content: flex-start; +} + +.body { + background: $ui-white; + padding: 1.5rem 2.25rem; + text-align: center; +} + +.input-row { + margin: 1.5rem 0; + font-weight: bolder; + text-align: right; + display: flex; + justify-content: center; + border: 1px solid; + border-radius: 0.25rem; + overflow: hidden; +} + +.ok-input-container { + border-color: $motion-primary; + box-shadow: 0 0 0 0.2rem $motion-transparent; +} + +.bad-input-container { + border-color: $data-primary; + box-shadow: 0 0 0 0.2rem hsla(30, 100%, 55%, 0.15); +} + +.input-row input { + width: 100%; + padding: 0 1rem; + height: 3rem; + color: $text-primary; + font-size: .875rem; + outline: none; + border: none; +} + +.input-row input::placeholder { + font-style: italic; + color: $text-primary-transparent; +} + +.input-row button { + padding: 0.5rem 2rem; + font-weight: bold; + font-size: .875rem; + cursor: pointer; + border: 0px solid $pen-primary; + outline: none; +} + +.input-row button.ok-button { + background: $pen-primary; + color: white; +} + +.error-row { + margin: 1.5rem 0; + text-align: center; + display: flex; + justify-content: center; + background: hsla(30, 100%, 55%, 0.25); + color: $data-primary; + border: 1px solid $data-primary; + border-radius: 0.25rem; +} + +.error-row p { + font-size: 0.875rem; + font-weight: bold; +} + +/* Confirmation buttons at the bottom of the modal */ +.button-row { + margin: 1.5rem 0; + font-weight: bolder; + text-align: right; + display: flex; + justify-content: center; +} + +.button-row button { + border: 1px solid $motion-primary; + border-radius: 0.25rem; + padding: 0.5rem 1.5rem; + background: white; + font-weight: bold; + font-size: .875rem; + cursor: pointer; + color: $motion-primary; +} + +.faq-link-text { + margin: 2rem 0 .5rem 0; + font-size: .875rem; + color: $text-primary; +} + +.faq-link { + color: $motion-primary; + text-decoration: none; +} diff --git a/src/components/import-modal/import-modal.jsx b/src/components/import-modal/import-modal.jsx new file mode 100644 index 000000000..ad42929e7 --- /dev/null +++ b/src/components/import-modal/import-modal.jsx @@ -0,0 +1,155 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import ReactModal from 'react-modal'; +import Box from '../box/box.jsx'; +import {defineMessages, injectIntl, intlShape, FormattedMessage} from 'react-intl'; +import classNames from 'classnames'; + +import CloseButton from '../close-button/close-button.jsx'; + +import styles from './import-modal.css'; + +const messages = defineMessages({ + title: { + id: 'gui.importInfo.title', + defaultMessage: 'View a Scratch 2.0 Project', + description: 'Scratch 2.0 import modal label - for accessibility' + }, + formDescription: { + defaultMessage: + 'Enter a link to one of your shared Scratch projects. Changes made in this 3.0 Preview will not be saved.', + description: 'Import project message', + id: 'gui.importInfo.message' + }, + invalidFormatError: { + id: 'gui.importInfo.invalidFormatError', + defaultMessage: 'Uh oh, that project link or id doesn\'t look quite right.', + description: 'Invalid project link or id message' + } +}); + +const ImportModal = ({intl, ...props}) => ( + <ReactModal + isOpen + className={styles.modalContent} + contentLabel={intl.formatMessage({...messages.title})} + overlayClassName={styles.modalOverlay} + onRequestClose={props.onCancel} + > + <Box> + <div className={styles.header}> + <div + className={classNames( + styles.headerItem, + styles.headerItemClose + )} + > + <CloseButton + buttonType="back" + size={CloseButton.SIZE_LARGE} + onClick={props.onGoBack} + /> + </div> + <div + className={classNames( + styles.headerItem, + styles.headerItemTitle + )} + > + <h2> + {intl.formatMessage({...messages.title})} + </h2> + </div> + <div className={classNames(styles.headerItem, styles.headerItemFilter)}> + {null} + </div> + </div> + </Box> + + <Box className={styles.body}> + <p> + {intl.formatMessage({...messages.formDescription})} + </p> + <Box + className={classNames(styles.inputRow, + (props.hasValidationError ? styles.badInputContainer : styles.okInputContainer)) + } + > + <input + autoFocus + placeholder={props.placeholder} + value={props.inputValue} + onChange={props.onChange} + onKeyPress={props.onKeyPress} + /> + <button + className={styles.okButton} + title="viewproject" + onClick={props.onViewProject} + > + <FormattedMessage + defaultMessage="View" + description="Label for button to load a scratch 2.0 project" + id="gui.importModal.viewproject" + /> + </button> + </Box> + {props.hasValidationError ? + <Box className={styles.errorRow}> + <p> + <FormattedMessage + {...messages[`${props.errorMessage}`]} + /> + </p> + </Box> : null + } + <Box className={styles.buttonRow}> + <button + onClick={props.onGoBack} + > + <FormattedMessage + defaultMessage="Go Back" + description="Label for button to back out of importing a project" + id="gui.importInfo.goback" + /> + </button> + </Box> + <Box className={styles.faqLinkText}> + <FormattedMessage + defaultMessage="To learn more, go to the {previewFaqLink}." + description="Invitation to try 3.0 preview" + id="gui.importInfo.previewfaq" + values={{ + previewFaqLink: ( + <a + className={styles.faqLink} + href="//scratch.mit.edu/preview-faq" + > + <FormattedMessage + defaultMessage="Preview FAQ" + description="link to Scratch 3.0 preview FAQ page" + id="gui.importInfo.previewfaqlink" + /> + </a> + ) + }} + /> + </Box> + </Box> + </ReactModal> +); + +ImportModal.propTypes = { + errorMessage: PropTypes.string.isRequired, + hasValidationError: PropTypes.bool.isRequired, + inputValue: PropTypes.string.isRequired, + intl: intlShape.isRequired, + onCancel: PropTypes.func.isRequired, + onChange: PropTypes.func.isRequired, + onGoBack: PropTypes.func.isRequired, + onKeyPress: PropTypes.func.isRequired, + onViewProject: PropTypes.func.isRequired, + placeholder: PropTypes.string +}; + +export default injectIntl(ImportModal); diff --git a/src/components/preview-modal/preview-modal.css b/src/components/preview-modal/preview-modal.css index a746c9178..837ed92cf 100644 --- a/src/components/preview-modal/preview-modal.css +++ b/src/components/preview-modal/preview-modal.css @@ -39,12 +39,6 @@ text-align: center; } -.disclaimer { - margin-top: 2.0rem; - border-top: 1px solid $text-primary; - padding-top: 1.0rem; -} - /* Confirmation buttons at the bottom of the modal */ .button-row { margin: 1.5rem 0; @@ -73,6 +67,12 @@ color: $motion-primary; } +.button-row button.view-project-button { + background: $pen-primary; + border-color: $pen-primary; + color: white; +} + .button-row button + button { margin-left: 0.5rem; } diff --git a/src/components/preview-modal/preview-modal.jsx b/src/components/preview-modal/preview-modal.jsx index 9e86e5434..5772b5185 100644 --- a/src/components/preview-modal/preview-modal.jsx +++ b/src/components/preview-modal/preview-modal.jsx @@ -35,45 +35,12 @@ const PreviewModal = ({intl, ...props}) => ( </h2> <p> <FormattedMessage - defaultMessage="We're excited for you to try the next generation of Scratch! - To learn more, go to the {previewFaqLink}." + defaultMessage="We're working on the next generation of Scratch. We're excited for you to try it!" description="Invitation to try 3.0 preview" - id="gui.previewInfo.previewfaq" - values={{ - previewFaqLink: ( - <a - className={styles.faqLink} - href="//scratch.mit.edu/preview-faq" - > - <FormattedMessage - defaultMessage="Preview FAQ" - description="link to Scratch 3.0 preview FAQ page" - id="gui.previewInfo.previewfaqlink" - /> - </a> - ) - }} + id="gui.previewInfo.invitation" /> </p> - <Box className={styles.disclaimer}> - <p> - <strong> - <FormattedMessage - defaultMessage="Changes to projects will not be saved." - description="Disclaimer for 3.0 preview" - id="gui.previewInfo.disclaimer" - /> - <br /> - <FormattedMessage - defaultMessage="This feature is coming soon!" - description="Notice that a feature is in progress" - id="gui.previewInfo.comingsoon" - /> - </strong> - </p> - </Box> - <Box className={styles.buttonRow}> <button className={styles.noButton} @@ -104,6 +71,38 @@ const PreviewModal = ({intl, ...props}) => ( }} /> </button> + <button + className={styles.viewProjectButton} + title="viewproject" + onClick={props.onViewProject} + > + <FormattedMessage + defaultMessage="View 2.0 Project" + description="Label for button to import a 2.0 project" + id="gui.previewModal.viewproject" + /> + </button> + </Box> + <Box className={styles.faqLinkText}> + <FormattedMessage + defaultMessage="To learn more, go to the {previewFaqLink}." + description="Invitation to try 3.0 preview" + id="gui.previewInfo.previewfaq" + values={{ + previewFaqLink: ( + <a + className={styles.faqLink} + href="//scratch.mit.edu/preview-faq" + > + <FormattedMessage + defaultMessage="Preview FAQ" + description="link to Scratch 3.0 preview FAQ page" + id="gui.previewInfo.previewfaqlink" + /> + </a> + ) + }} + /> </Box> </Box> </ReactModal> @@ -112,7 +111,8 @@ const PreviewModal = ({intl, ...props}) => ( PreviewModal.propTypes = { intl: intlShape.isRequired, onCancel: PropTypes.func.isRequired, - onTryIt: PropTypes.func.isRequired + onTryIt: PropTypes.func.isRequired, + onViewProject: PropTypes.func.isRequired }; export default injectIntl(PreviewModal); diff --git a/src/containers/gui.jsx b/src/containers/gui.jsx index a47467c0c..521837bff 100644 --- a/src/containers/gui.jsx +++ b/src/containers/gui.jsx @@ -86,6 +86,7 @@ class GUI extends React.Component { GUI.propTypes = { ...GUIComponent.propTypes, fetchingProject: PropTypes.bool, + importInfoVisible: PropTypes.bool, loadingStateVisible: PropTypes.bool, previewInfoVisible: PropTypes.bool, projectData: PropTypes.oneOfType([PropTypes.object, PropTypes.string]), @@ -99,6 +100,7 @@ const mapStateToProps = state => ({ blocksTabVisible: state.editorTab.activeTabIndex === BLOCKS_TAB_INDEX, cardsVisible: state.cards.visible, costumesTabVisible: state.editorTab.activeTabIndex === COSTUMES_TAB_INDEX, + importInfoVisible: state.modals.importInfo, loadingStateVisible: state.modals.loadingProject, previewInfoVisible: state.modals.previewInfo, soundsTabVisible: state.editorTab.activeTabIndex === SOUNDS_TAB_INDEX, diff --git a/src/containers/import-modal.jsx b/src/containers/import-modal.jsx new file mode 100644 index 000000000..0731098f1 --- /dev/null +++ b/src/containers/import-modal.jsx @@ -0,0 +1,111 @@ +import bindAll from 'lodash.bindall'; +import PropTypes from 'prop-types'; +import React from 'react'; +import {connect} from 'react-redux'; + +import ImportModalComponent from '../components/import-modal/import-modal.jsx'; + +import { + closeImportInfo, + openPreviewInfo +} from '../reducers/modals'; + +class ImportModal extends React.Component { + constructor (props) { + super(props); + bindAll(this, [ + 'handleViewProject', + 'handleCancel', + 'handleChange', + 'handleGoBack', + 'handleKeyPress' + ]); + + this.state = { + inputValue: '', + hasValidationError: false, + errorMessage: '' + }; + } + handleKeyPress (event) { + if (event.key === 'Enter') this.handleViewProject(); + } + handleViewProject () { + const inputValue = this.state.inputValue; + const projectId = this.validate(inputValue); + + if (projectId) { + const projectLink = document.createElement('a'); + document.body.appendChild(projectLink); + projectLink.href = `#${projectId}`; + projectLink.click(); + document.body.removeChild(projectLink); + this.props.onViewProject(); + } else { + this.setState({ + hasValidationError: true, + errorMessage: `invalidFormatError`}); + } + } + handleChange (e) { + this.setState({inputValue: e.target.value, hasValidationError: false}); + } + validate (input) { + const urlMatches = input.match(/^(?:https?:\/\/)?scratch\.mit\.edu\/projects\/(\d+)/); + if (urlMatches && urlMatches.length > 0) { + return urlMatches[1]; + } + const projectIdMatches = input.match(/^#?(\d+)$/); + if (projectIdMatches && projectIdMatches.length > 0) { + return projectIdMatches[1]; + } + return null; + } + handleCancel () { + this.props.onCancel(); + } + handleGoBack () { + this.props.onBack(); + } + render () { + return ( + <ImportModalComponent + errorMessage={this.state.errorMessage} + hasValidationError={this.state.hasValidationError} + inputValue={this.state.inputValue} + placeholder="scratch.mit.edu/projects/123456789" + onCancel={this.handleCancel} + onChange={this.handleChange} + onGoBack={this.handleGoBack} + onKeyPress={this.handleKeyPress} + onViewProject={this.handleViewProject} + /> + ); + } +} + +ImportModal.propTypes = { + onBack: PropTypes.func.isRequired, + onCancel: PropTypes.func.isRequired, + onViewProject: PropTypes.func +}; + +const mapStateToProps = () => ({}); + +const mapDispatchToProps = dispatch => ({ + onBack: () => { + dispatch(closeImportInfo()); + dispatch(openPreviewInfo()); + }, + onCancel: () => { + dispatch(closeImportInfo()); + }, + onViewProject: () => { + dispatch(closeImportInfo()); + } +}); + +export default connect( + mapStateToProps, + mapDispatchToProps +)(ImportModal); diff --git a/src/containers/preview-modal.jsx b/src/containers/preview-modal.jsx index 70fb3290b..3fa6c8f03 100644 --- a/src/containers/preview-modal.jsx +++ b/src/containers/preview-modal.jsx @@ -8,7 +8,8 @@ import PreviewModalComponent from '../components/preview-modal/preview-modal.jsx import BrowserModalComponent from '../components/browser-modal/browser-modal.jsx'; import { - closePreviewInfo + closePreviewInfo, + openImportInfo } from '../reducers/modals'; class PreviewModal extends React.Component { @@ -16,7 +17,8 @@ class PreviewModal extends React.Component { super(props); bindAll(this, [ 'handleTryIt', - 'handleCancel' + 'handleCancel', + 'handleViewProject' ]); this.state = { @@ -30,6 +32,9 @@ class PreviewModal extends React.Component { handleCancel () { window.location.replace('https://scratch.mit.edu'); } + handleViewProject () { + this.props.onViewProject(); + } supportedBrowser () { return !['IE', 'Opera', 'Opera Mini', 'Silk', 'Vivaldi'].includes(platform.name); } @@ -39,6 +44,7 @@ class PreviewModal extends React.Component { previewing={this.state.previewing} onCancel={this.handleCancel} onTryIt={this.handleTryIt} + onViewProject={this.handleViewProject} /> : <BrowserModalComponent onBack={this.handleCancel} @@ -48,7 +54,8 @@ class PreviewModal extends React.Component { } PreviewModal.propTypes = { - onTryIt: PropTypes.func + onTryIt: PropTypes.func, + onViewProject: PropTypes.func }; const mapStateToProps = () => ({}); @@ -56,6 +63,10 @@ const mapStateToProps = () => ({}); const mapDispatchToProps = dispatch => ({ onTryIt: () => { dispatch(closePreviewInfo()); + }, + onViewProject: () => { + dispatch(closePreviewInfo()); + dispatch(openImportInfo()); } }); diff --git a/src/reducers/modals.js b/src/reducers/modals.js index 6bb28c81b..481c44f29 100644 --- a/src/reducers/modals.js +++ b/src/reducers/modals.js @@ -6,6 +6,7 @@ const CLOSE_MODAL = 'scratch-gui/modals/CLOSE_MODAL'; const MODAL_BACKDROP_LIBRARY = 'backdropLibrary'; const MODAL_COSTUME_LIBRARY = 'costumeLibrary'; const MODAL_EXTENSION_LIBRARY = 'extensionLibrary'; +const MODAL_IMPORT_INFO = 'importInfo'; const MODAL_LOADING_PROJECT = 'loadingProject'; const MODAL_PREVIEW_INFO = 'previewInfo'; const MODAL_SOUND_LIBRARY = 'soundLibrary'; @@ -19,6 +20,7 @@ const initialState = { [MODAL_BACKDROP_LIBRARY]: false, [MODAL_COSTUME_LIBRARY]: false, [MODAL_EXTENSION_LIBRARY]: false, + [MODAL_IMPORT_INFO]: false, [MODAL_LOADING_PROJECT]: false, [MODAL_PREVIEW_INFO]: true, [MODAL_SOUND_LIBRARY]: false, @@ -66,6 +68,10 @@ const openExtensionLibrary = function () { analytics.pageview('/libraries/extensions'); return openModal(MODAL_EXTENSION_LIBRARY); }; +const openImportInfo = function () { + analytics.pageview('modals/import'); + return openModal(MODAL_IMPORT_INFO); +}; const openLoadingProject = function () { analytics.pageview('modals/loading'); return openModal(MODAL_LOADING_PROJECT); @@ -99,6 +105,9 @@ const closeCostumeLibrary = function () { const closeExtensionLibrary = function () { return closeModal(MODAL_EXTENSION_LIBRARY); }; +const closeImportInfo = function () { + return closeModal(MODAL_IMPORT_INFO); +}; const closeLoadingProject = function () { return closeModal(MODAL_LOADING_PROJECT); }; @@ -122,6 +131,7 @@ export { openBackdropLibrary, openCostumeLibrary, openExtensionLibrary, + openImportInfo, openLoadingProject, openPreviewInfo, openSoundLibrary, @@ -131,6 +141,7 @@ export { closeBackdropLibrary, closeCostumeLibrary, closeExtensionLibrary, + closeImportInfo, closeLoadingProject, closePreviewInfo, closeSpriteLibrary, diff --git a/test/integration/project-loading.test.js b/test/integration/project-loading.test.js index cb36f20e5..cb0477ec9 100644 --- a/test/integration/project-loading.test.js +++ b/test/integration/project-loading.test.js @@ -4,6 +4,7 @@ import SeleniumHelper from '../helpers/selenium-helper'; const { clickText, clickXpath, + findByXpath, getDriver, getLogs, loadUri @@ -30,6 +31,40 @@ describe('Loading scratch gui', () => { }); describe('Loading projects by ID', () => { + + test('Load 2.0 project using import modal', async () => { + await loadUri(uri); + await clickText('View 2.0 Project'); + const el = await findByXpath("//input[@placeholder='scratch.mit.edu/projects/123456789']"); + const projectId = '96708228'; + await el.sendKeys(`scratch.mit.edu/projects/${projectId}`); + await clickXpath('//button[@title="viewproject"]'); + await new Promise(resolve => setTimeout(resolve, 2000)); + await clickXpath('//img[@title="Go"]'); + await new Promise(resolve => setTimeout(resolve, 2000)); + await clickXpath('//img[@title="Stop"]'); + const logs = await getLogs(); + await expect(logs).toEqual([]); + }); + + test('Invalid url when loading project through modal lets you try again', async () => { + await loadUri(uri); + await clickText('View 2.0 Project'); + let el = await findByXpath("//input[@placeholder='scratch.mit.edu/projects/123456789']"); + await el.sendKeys('thisisnotaurl'); + await clickXpath('//button[@title="viewproject"]'); + el = await findByXpath("//input[@placeholder='scratch.mit.edu/projects/123456789']"); + await el.clear(); + await el.sendKeys('scratch.mit.edu/projects/96708228'); + await clickXpath('//button[@title="viewproject"]'); + await new Promise(resolve => setTimeout(resolve, 2000)); + await clickXpath('//img[@title="Go"]'); + await new Promise(resolve => setTimeout(resolve, 2000)); + await clickXpath('//img[@title="Stop"]'); + const logs = await getLogs(); + await expect(logs).toEqual([]); + }); + test('Load a project by ID directly through url', async () => { await driver.quit(); // Reset driver to test hitting # url directly driver = getDriver(); -- GitLab