Skip to content

Commit 38b4ca8

Browse files
committed
Move URL query string handling into an HOC
This will work better if there are other query string options that need to be handled later. It also handles the distinction between ‘all’ and the other tutorials better.
1 parent b33d7c1 commit 38b4ca8

File tree

6 files changed

+92
-64
lines changed

6 files changed

+92
-64
lines changed

src/containers/gui.jsx

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,6 @@ import {
1313
getIsShowingProject
1414
} from '../reducers/project-state';
1515
import {setProjectTitle} from '../reducers/project-title';
16-
import {detectTutorialId} from '../lib/tutorial-from-url';
17-
import {activateDeck} from '../reducers/cards';
1816
import {
1917
activateTab,
2018
BLOCKS_TAB_INDEX,
@@ -31,6 +29,7 @@ import FontLoaderHOC from '../lib/font-loader-hoc.jsx';
3129
import LocalizationHOC from '../lib/localization-hoc.jsx';
3230
import ProjectFetcherHOC from '../lib/project-fetcher-hoc.jsx';
3331
import ProjectSaverHOC from '../lib/project-saver-hoc.jsx';
32+
import QueryParserHOC from '../lib/query-parser-hoc.jsx';
3433
import storage from '../lib/storage';
3534
import vmListenerHOC from '../lib/vm-listener-hoc.jsx';
3635
import vmManagerHOC from '../lib/vm-manager-hoc.jsx';
@@ -49,7 +48,6 @@ class GUI extends React.Component {
4948
componentDidMount () {
5049
this.setReduxTitle(this.props.projectTitle);
5150
this.props.onStorageInit(storage);
52-
this.setActiveCards(detectTutorialId());
5351
}
5452
componentDidUpdate (prevProps) {
5553
if (this.props.projectId !== prevProps.projectId && this.props.projectId !== null) {
@@ -68,11 +66,6 @@ class GUI extends React.Component {
6866
this.props.onUpdateReduxProjectTitle(newTitle);
6967
}
7068
}
71-
setActiveCards (tutorialId) {
72-
if (tutorialId && tutorialId !== 'all') {
73-
this.props.onUpdateReduxDeck(tutorialId);
74-
}
75-
}
7669
render () {
7770
if (this.props.isError) {
7871
throw new Error(
@@ -88,7 +81,6 @@ class GUI extends React.Component {
8881
isShowingProject,
8982
onStorageInit,
9083
onUpdateProjectId,
91-
onUpdateReduxDeck,
9284
onUpdateReduxProjectTitle,
9385
projectHost,
9486
projectId,
@@ -128,7 +120,6 @@ GUI.propTypes = {
128120
onStorageInit: PropTypes.func,
129121
onUpdateProjectId: PropTypes.func,
130122
onUpdateProjectTitle: PropTypes.func,
131-
onUpdateReduxDeck: PropTypes.func,
132123
onUpdateReduxProjectTitle: PropTypes.func,
133124
previewInfoVisible: PropTypes.bool,
134125
projectHost: PropTypes.string,
@@ -179,7 +170,6 @@ const mapDispatchToProps = dispatch => ({
179170
onActivateSoundsTab: () => dispatch(activateTab(SOUNDS_TAB_INDEX)),
180171
onRequestCloseBackdropLibrary: () => dispatch(closeBackdropLibrary()),
181172
onRequestCloseCostumeLibrary: () => dispatch(closeCostumeLibrary()),
182-
onUpdateReduxDeck: tutorialId => dispatch(activateDeck(tutorialId)),
183173
onUpdateReduxProjectTitle: title => dispatch(setProjectTitle(title))
184174
});
185175

@@ -195,6 +185,7 @@ const WrappedGui = compose(
195185
LocalizationHOC,
196186
ErrorBoundaryHOC('Top Level App'),
197187
FontLoaderHOC,
188+
QueryParserHOC,
198189
ProjectFetcherHOC,
199190
ProjectSaverHOC,
200191
vmListenerHOC,

src/lib/app-state-hoc.jsx

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import {setPlayer, setFullScreen} from '../reducers/mode.js';
1010

1111
import locales from 'scratch-l10n';
1212
import {detectLocale} from './detect-locale';
13-
import {detectTutorialId} from './tutorial-from-url';
1413

1514
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
1615

@@ -31,10 +30,6 @@ const AppStateHOC = function (WrappedComponent, localesOnly) {
3130
let reducers = {};
3231
let enhancer;
3332

34-
this.state = {
35-
tutorial: false
36-
};
37-
3833
let initializedLocales = localesInitialState;
3934
const locale = detectLocale(Object.keys(locales));
4035
if (locale !== 'en') {
@@ -55,8 +50,7 @@ const AppStateHOC = function (WrappedComponent, localesOnly) {
5550
guiInitialState,
5651
guiMiddleware,
5752
initFullScreen,
58-
initPlayer,
59-
initTutorialLibrary
53+
initPlayer
6054
} = guiRedux;
6155
const {ScratchPaintReducer} = require('scratch-paint');
6256

@@ -68,13 +62,6 @@ const AppStateHOC = function (WrappedComponent, localesOnly) {
6862
if (props.isPlayerOnly) {
6963
initializedGui = initPlayer(initializedGui);
7064
}
71-
} else {
72-
const tutorialId = detectTutorialId();
73-
// handle ?tutorial=all for beta
74-
// if we decide to keep this for www, functionality should move to
75-
// setActiveCards in the GUI container
76-
if (tutorialId === 'all') initializedGui = initTutorialLibrary(initializedGui);
77-
if (tutorialId) this.state.tutorial = true;
7865
}
7966
reducers = {
8067
locales: localesReducer,
@@ -109,7 +96,6 @@ const AppStateHOC = function (WrappedComponent, localesOnly) {
10996
isPlayerOnly, // eslint-disable-line no-unused-vars
11097
...componentProps
11198
} = this.props;
112-
if (this.state.tutorial) componentProps.hideIntro = true;
11399
return (
114100
<Provider store={this.store}>
115101
<ConnectedIntlProvider>

src/lib/query-parser-hoc.jsx

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import React from 'react';
2+
import PropTypes from 'prop-types';
3+
import queryString from 'query-string';
4+
import {connect} from 'react-redux';
5+
6+
import {detectTutorialId} from './tutorial-from-url';
7+
8+
import {activateDeck} from '../reducers/cards';
9+
import {openTipsLibrary} from '../reducers/modals';
10+
11+
/* Higher Order Component to get parameters from the URL query string and initialize redux state
12+
* @param {React.Component} WrappedComponent: component to render
13+
* @returns {React.Component} component with query parsing behavior
14+
*/
15+
const QueryParserHOC = function (WrappedComponent) {
16+
class QueryParserComponent extends React.Component {
17+
constructor (props) {
18+
super(props);
19+
const queryParams = queryString.parse(location.search);
20+
this.state = {
21+
tutorial: false
22+
};
23+
24+
const tutorialId = detectTutorialId(queryParams);
25+
if (tutorialId) {
26+
this.state.tutorial = true;
27+
if (tutorialId === 'all') {
28+
this.openTutorials();
29+
} else {
30+
this.setActiveCards(tutorialId);
31+
}
32+
}
33+
}
34+
setActiveCards (tutorialId) {
35+
this.props.onUpdateReduxDeck(tutorialId);
36+
}
37+
openTutorials () {
38+
this.props.onOpenTipsLibrary();
39+
}
40+
render () {
41+
const {
42+
hideIntro: hideIntroProp,
43+
...componentProps
44+
} = this.props;
45+
// override hideIntro if there is a tutorial
46+
componentProps.hideIntro = this.state.tutorial || hideIntroProp;
47+
return (
48+
<WrappedComponent
49+
{...componentProps}
50+
/>
51+
);
52+
}
53+
}
54+
QueryParserComponent.propTypes = {
55+
hideIntro: PropTypes.bool,
56+
onOpenTipsLibrary: PropTypes.func,
57+
onUpdateReduxDeck: PropTypes.func
58+
};
59+
const mapDispatchToProps = dispatch => ({
60+
onOpenTipsLibrary: () => dispatch(openTipsLibrary()),
61+
onUpdateReduxDeck: tutorialId => dispatch(activateDeck(tutorialId))
62+
});
63+
return connect(
64+
null,
65+
mapDispatchToProps
66+
)(QueryParserComponent);
67+
};
68+
69+
export {
70+
QueryParserHOC as default
71+
};

src/lib/tutorial-from-url.js

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55

66
import tutorials from './libraries/decks/index.jsx';
77
import analytics from './analytics';
8-
import queryString from 'query-string';
98

109
/**
1110
* Get the tutorial id from the given numerical id (representing the
@@ -31,11 +30,11 @@ const getDeckIdFromUrlId = urlId => {
3130
/**
3231
* Check if there's a tutorial id provided as a query parameter in the URL.
3332
* Return the corresponding tutorial id or null if not found.
33+
* @param {object} queryParams the results of parsing the query string
3434
* @return {string} The ID of the requested tutorial or null if no tutorial was
3535
* requested or found.
3636
*/
37-
const detectTutorialId = () => {
38-
const queryParams = queryString.parse(location.search);
37+
const detectTutorialId = queryParams => {
3938
const tutorialID = Array.isArray(queryParams.tutorial) ?
4039
queryParams.tutorial[0] :
4140
queryParams.tutorial;

src/reducers/gui.js

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -96,19 +96,6 @@ const initTutorialCard = function (currentState, deckId) {
9696
);
9797
};
9898

99-
const initTutorialLibrary = function (currentState) {
100-
return Object.assign(
101-
{},
102-
currentState,
103-
{
104-
modals: {
105-
previewInfo: false,
106-
tipsLibrary: true
107-
}
108-
}
109-
);
110-
};
111-
11299
const guiReducer = combineReducers({
113100
alerts: alertsReducer,
114101
assetDrag: assetDragReducer,
@@ -141,6 +128,5 @@ export {
141128
guiMiddleware,
142129
initFullScreen,
143130
initPlayer,
144-
initTutorialCard,
145-
initTutorialLibrary
131+
initTutorialCard
146132
};

test/unit/util/tutorial-from-url.test.js

Lines changed: 15 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -8,45 +8,40 @@ jest.mock('../../../src/lib/libraries/decks/index.jsx', () => ({
88
noUrlIdSandwich: {}
99
}));
1010

11+
import queryString from 'query-string';
1112
import {detectTutorialId} from '../../../src/lib/tutorial-from-url.js';
1213

13-
Object.defineProperty(
14-
window.location,
15-
'search',
16-
{value: '', writable: true}
17-
);
18-
1914
test('returns the tutorial ID if the urlId matches', () => {
20-
window.location.search = '?tutorial=one';
21-
expect(detectTutorialId()).toBe('foo');
15+
const queryParams = queryString.parse('?tutorial=one');
16+
expect(detectTutorialId(queryParams)).toBe('foo');
2217
});
2318

2419
test('returns null if no matching urlId', () => {
25-
window.location.search = '?tutorial=10';
26-
expect(detectTutorialId()).toBe(null);
20+
const queryParams = queryString.parse('?tutorial=10');
21+
expect(detectTutorialId(queryParams)).toBe(null);
2722
});
2823

2924
test('returns null if empty template', () => {
30-
window.location.search = '?tutorial=';
31-
expect(detectTutorialId()).toBe(null);
25+
const queryParams = queryString.parse('?tutorial=');
26+
expect(detectTutorialId(queryParams)).toBe(null);
3227
});
3328

3429
test('returns null if no query param', () => {
35-
window.location.search = '';
36-
expect(detectTutorialId()).toBe(null);
30+
const queryParams = queryString.parse('');
31+
expect(detectTutorialId(queryParams)).toBe(null);
3732
});
3833

3934
test('returns null if unrecognized template', () => {
40-
window.location.search = '?tutorial=asdf';
41-
expect(detectTutorialId()).toBe(null);
35+
const queryParams = queryString.parse('?tutorial=asdf');
36+
expect(detectTutorialId(queryParams)).toBe(null);
4237
});
4338

4439
test('takes the first of multiple', () => {
45-
window.location.search = '?tutorial=one&tutorial=two';
46-
expect(detectTutorialId()).toBe('foo');
40+
const queryParams = queryString.parse('?tutorial=one&tutorial=two');
41+
expect(detectTutorialId(queryParams)).toBe('foo');
4742
});
4843

4944
test('returns all for the tutorial library shortcut', () => {
50-
window.location.search = '?tutorial=all';
51-
expect(detectTutorialId()).toBe('all');
45+
const queryParams = queryString.parse('?tutorial=all');
46+
expect(detectTutorialId(queryParams)).toBe('all');
5247
});

0 commit comments

Comments
 (0)