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 ? (
) : null}
+ {importInfoVisible ? (
+
+ ) : null}
{isRendererSupported ? null : (
)}
@@ -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}) => (
+
+
+
+
+
+
+
+
+ {intl.formatMessage({...messages.title})}
+
+
+
+ {null}
+
+
+
+
+
+
+ {intl.formatMessage({...messages.formDescription})}
+
+
+
+
+
+ {props.hasValidationError ?
+
+
+
+
+ : null
+ }
+
+
+
+
+
+
+
+ )
+ }}
+ />
+
+
+
+);
+
+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}) => (
-
-
- )
- }}
+ id="gui.previewInfo.invitation"
/>
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+ )
+ }}
+ />
@@ -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 (
+
+ );
+ }
+}
+
+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}
/> :
({});
@@ -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();