Skip to content

Commit 0ff1bd7

Browse files
authored
Merge pull request #3798 from chrisgarrity/feature/refactor-all-tutorials
Move URL query string handling into an HOC
2 parents e6a277d + fdfd9d6 commit 0ff1bd7

File tree

6 files changed

+92
-63
lines changed

6 files changed

+92
-63
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';
@@ -50,7 +49,6 @@ class GUI extends React.Component {
5049
componentDidMount () {
5150
this.setReduxTitle(this.props.projectTitle);
5251
this.props.onStorageInit(storage);
53-
this.setActiveCards(detectTutorialId());
5452
}
5553
componentDidUpdate (prevProps) {
5654
if (this.props.projectId !== prevProps.projectId && this.props.projectId !== null) {
@@ -69,11 +67,6 @@ class GUI extends React.Component {
6967
this.props.onUpdateReduxProjectTitle(newTitle);
7068
}
7169
}
72-
setActiveCards (tutorialId) {
73-
if (tutorialId && tutorialId !== 'all') {
74-
this.props.onUpdateReduxDeck(tutorialId);
75-
}
76-
}
7770
render () {
7871
if (this.props.isError) {
7972
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,
@@ -127,7 +119,6 @@ GUI.propTypes = {
127119
onStorageInit: PropTypes.func,
128120
onUpdateProjectId: PropTypes.func,
129121
onUpdateProjectTitle: PropTypes.func,
130-
onUpdateReduxDeck: PropTypes.func,
131122
onUpdateReduxProjectTitle: PropTypes.func,
132123
previewInfoVisible: PropTypes.bool,
133124
projectHost: PropTypes.string,
@@ -178,7 +169,6 @@ const mapDispatchToProps = dispatch => ({
178169
onActivateSoundsTab: () => dispatch(activateTab(SOUNDS_TAB_INDEX)),
179170
onRequestCloseBackdropLibrary: () => dispatch(closeBackdropLibrary()),
180171
onRequestCloseCostumeLibrary: () => dispatch(closeCostumeLibrary()),
181-
onUpdateReduxDeck: tutorialId => dispatch(activateDeck(tutorialId)),
182172
onUpdateReduxProjectTitle: title => dispatch(setProjectTitle(title))
183173
});
184174

@@ -194,6 +184,7 @@ const WrappedGui = compose(
194184
LocalizationHOC,
195185
ErrorBoundaryHOC('Top Level App'),
196186
FontLoaderHOC,
187+
QueryParserHOC,
197188
ProjectFetcherHOC,
198189
ProjectSaverHOC,
199190
vmListenerHOC,

src/lib/app-state-hoc.jsx

Lines changed: 2 additions & 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

@@ -52,8 +51,7 @@ const AppStateHOC = function (WrappedComponent, localesOnly) {
5251
guiMiddleware,
5352
initFullScreen,
5453
initPlayer,
55-
initPreviewInfo,
56-
initTutorialLibrary
54+
initPreviewInfo
5755
} = guiRedux;
5856
const {ScratchPaintReducer} = require('scratch-paint');
5957

@@ -66,18 +64,7 @@ const AppStateHOC = function (WrappedComponent, localesOnly) {
6664
initializedGui = initPlayer(initializedGui);
6765
}
6866
} else {
69-
const tutorialId = detectTutorialId();
70-
if (tutorialId === null) {
71-
if (props.showPreviewInfo) {
72-
// Show preview info if requested and no tutorial ID found
73-
initializedGui = initPreviewInfo(initializedGui);
74-
}
75-
} else if (tutorialId === 'all') {
76-
// Specific tutorials are set in setActiveCards in the GUI container.
77-
// Handle ?tutorial=all here for beta, if we decide to keep this for the
78-
// project page, this functionality should move to GUI container also.
79-
initializedGui = initTutorialLibrary(initializedGui);
80-
}
67+
initializedGui = initPreviewInfo(initializedGui);
8168
}
8269
reducers = {
8370
locales: localesReducer,

src/lib/query-parser-hoc.jsx

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
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, closePreviewInfo} 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+
const tutorialId = detectTutorialId(queryParams);
21+
if (tutorialId) {
22+
if (tutorialId === 'all') {
23+
this.openTutorials();
24+
} else {
25+
this.setActiveCards(tutorialId);
26+
}
27+
}
28+
}
29+
setActiveCards (tutorialId) {
30+
this.props.onUpdateReduxDeck(tutorialId);
31+
}
32+
openTutorials () {
33+
this.props.onOpenTipsLibrary();
34+
}
35+
render () {
36+
const {
37+
onOpenTipsLibrary, // eslint-disable-line no-unused-vars
38+
onUpdateReduxDeck, // eslint-disable-line no-unused-vars
39+
...componentProps
40+
} = this.props;
41+
return (
42+
<WrappedComponent
43+
{...componentProps}
44+
/>
45+
);
46+
}
47+
}
48+
QueryParserComponent.propTypes = {
49+
onOpenTipsLibrary: PropTypes.func,
50+
onUpdateReduxDeck: PropTypes.func
51+
};
52+
const mapDispatchToProps = dispatch => ({
53+
onOpenTipsLibrary: () => {
54+
dispatch(openTipsLibrary());
55+
dispatch(closePreviewInfo());
56+
},
57+
onUpdateReduxDeck: tutorialId => {
58+
dispatch(activateDeck(tutorialId));
59+
dispatch(closePreviewInfo());
60+
}
61+
});
62+
return connect(
63+
null,
64+
mapDispatchToProps
65+
)(QueryParserComponent);
66+
};
67+
68+
export {
69+
QueryParserHOC as default
70+
};

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 & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -93,18 +93,6 @@ const initTutorialCard = function (currentState, deckId) {
9393
);
9494
};
9595

96-
const initTutorialLibrary = function (currentState) {
97-
return Object.assign(
98-
{},
99-
currentState,
100-
{
101-
modals: {
102-
tipsLibrary: true
103-
}
104-
}
105-
);
106-
};
107-
10896
const initPreviewInfo = function (currentState) {
10997
return Object.assign(
11098
{},
@@ -150,6 +138,5 @@ export {
150138
initFullScreen,
151139
initPlayer,
152140
initPreviewInfo,
153-
initTutorialCard,
154-
initTutorialLibrary
141+
initTutorialCard
155142
};

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)