Skip to content

Commit 310dba8

Browse files
authored
Merge pull request #1387 from ajgreenb/editable-project-instructions
Editable project instructions
2 parents 1d9a453 + 667fd47 commit 310dba8

19 files changed

Lines changed: 333 additions & 20 deletions

File tree

locales/en/translation.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
"export-gist": "Export Gist",
55
"export-repo": "Export Repo",
66
"share-to-classroom": "Share To Classroom",
7+
"add-instructions": "Add Instructions",
8+
"edit-instructions": "Edit Instructions",
79
"libraries": "Libraries",
810
"load-project": "Your Projects",
911
"new-project": "New Project",
@@ -60,7 +62,11 @@
6062
"css": "CSS",
6163
"javascript": "JS"
6264
},
63-
"output": "Output"
65+
"output": "Output",
66+
"instructions": {
67+
"cancel": "Cancel",
68+
"save": "Save"
69+
}
6470
}
6571
},
6672
"utility": {

src/actions/index.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
unhideComponent,
1616
toggleComponent,
1717
updateProjectSource,
18+
updateProjectInstructions,
1819
} from './projects';
1920

2021
import {
@@ -31,6 +32,8 @@ import {
3132
toggleEditorTextSize,
3233
toggleTopBarMenu,
3334
closeTopBarMenu,
35+
startEditingInstructions,
36+
cancelEditingInstructions,
3437
} from './ui';
3538

3639
import {
@@ -67,6 +70,7 @@ export {
6770
createSnapshot,
6871
changeCurrentProject,
6972
updateProjectSource,
73+
updateProjectInstructions,
7074
toggleLibrary,
7175
userAuthenticated,
7276
userLoggedOut,
@@ -92,6 +96,8 @@ export {
9296
toggleEditorTextSize,
9397
toggleTopBarMenu,
9498
closeTopBarMenu,
99+
startEditingInstructions,
100+
cancelEditingInstructions,
95101
logIn,
96102
logOut,
97103
evaluateConsoleEntry,

src/actions/projects.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ export const updateProjectSource = createAction(
1818
(_projectKey, _language, _newValue, timestamp = Date.now()) => ({timestamp}),
1919
);
2020

21+
export const updateProjectInstructions = createAction(
22+
'UPDATE_PROJECT_INSTRUCTIONS',
23+
(projectKey, newValue) => ({projectKey, newValue}),
24+
);
25+
2126
export const toggleLibrary = createAction(
2227
'TOGGLE_LIBRARY',
2328
(projectKey, libraryKey) => ({projectKey, libraryKey}),

src/actions/ui.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,3 +58,11 @@ export const toggleTopBarMenu = createAction(
5858
export const closeTopBarMenu = createAction(
5959
'CLOSE_TOP_BAR_MENU',
6060
);
61+
62+
export const startEditingInstructions = createAction(
63+
'START_EDITING_INSTRUCTIONS',
64+
);
65+
66+
export const cancelEditingInstructions = createAction(
67+
'CANCEL_EDITING_INSTRUCTIONS',
68+
);

src/components/Instructions.jsx

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,45 @@
11
import React from 'react';
22
import PropTypes from 'prop-types';
33
import {toReact as markdownToReact} from '../util/markdown';
4+
import InstructionsEditor from './InstructionsEditor';
45

5-
export default function Instructions({instructions, isOpen}) {
6-
if (!instructions || !isOpen) {
6+
export default function Instructions({
7+
instructions,
8+
isEditing,
9+
isOpen,
10+
projectKey,
11+
onCancelEditing,
12+
onSaveChanges,
13+
}) {
14+
if (!isEditing && !instructions || !isOpen) {
715
return null;
816
}
917

1018
return (
1119
<div
1220
className="layout__instructions"
1321
>
14-
<div className="instructions">
15-
{markdownToReact(instructions)}
16-
</div>
22+
{
23+
isEditing ?
24+
<InstructionsEditor
25+
instructions={instructions}
26+
projectKey={projectKey}
27+
onCancelEditing={onCancelEditing}
28+
onSaveChanges={onSaveChanges}
29+
/> :
30+
<div className="instructions">
31+
{markdownToReact(instructions)}
32+
</div>
33+
}
1734
</div>
1835
);
1936
}
2037

2138
Instructions.propTypes = {
2239
instructions: PropTypes.string.isRequired,
40+
isEditing: PropTypes.bool.isRequired,
2341
isOpen: PropTypes.bool.isRequired,
42+
projectKey: PropTypes.string.isRequired,
43+
onCancelEditing: PropTypes.func.isRequired,
44+
onSaveChanges: PropTypes.func.isRequired,
2445
};
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import React from 'react';
2+
import PropTypes from 'prop-types';
3+
import {t} from 'i18next';
4+
import bindAll from 'lodash/bindAll';
5+
6+
export default class InstructionsEditor extends React.Component {
7+
constructor() {
8+
super();
9+
bindAll(this, '_handleCancelEditing', '_handleSaveChanges', '_ref');
10+
}
11+
12+
_handleCancelEditing() {
13+
this.props.onCancelEditing();
14+
}
15+
16+
_handleSaveChanges() {
17+
const newValue = this._editor.value.trim();
18+
this.props.onSaveChanges(this.props.projectKey, newValue);
19+
}
20+
21+
_ref(editorElement) {
22+
this._editor = editorElement;
23+
}
24+
25+
render() {
26+
return (
27+
<div className="instructions-editor">
28+
<div className="instructions-editor__menu">
29+
<button
30+
className="instructions-editor__menu-button"
31+
onClick={this._handleSaveChanges}
32+
>
33+
{t('workspace.components.instructions.save')}
34+
</button>
35+
<button
36+
className="instructions-editor__menu-button"
37+
onClick={this._handleCancelEditing}
38+
>
39+
{t('workspace.components.instructions.cancel')}
40+
</button>
41+
</div>
42+
<div className="instructions-editor__input-container">
43+
<textarea
44+
className="instructions-editor__input"
45+
defaultValue={this.props.instructions}
46+
ref={this._ref}
47+
/>
48+
</div>
49+
<div className="instructions-editor__footer">
50+
<a
51+
className="instructions-editor__footer-link"
52+
href="https://guides.github.com/features/mastering-markdown/"
53+
rel="noopener noreferrer"
54+
target="_blank"
55+
>
56+
Styling with Markdown is supported
57+
</a>
58+
</div>
59+
</div>
60+
);
61+
}
62+
}
63+
64+
InstructionsEditor.propTypes = {
65+
instructions: PropTypes.string.isRequired,
66+
projectKey: PropTypes.string.isRequired,
67+
onCancelEditing: PropTypes.func.isRequired,
68+
onSaveChanges: PropTypes.func.isRequired,
69+
};

src/components/TopBar/HamburgerMenu.jsx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ const HamburgerMenu = createMenu({
1212
name: 'hamburger',
1313

1414
renderItems({
15+
hasInstructions,
16+
isEditingInstructions,
1517
isExperimental,
1618
isGistExportInProgress,
1719
isRepoExportInProgress,
@@ -20,6 +22,7 @@ const HamburgerMenu = createMenu({
2022
onExportGist,
2123
onExportRepo,
2224
onExportToClassroom,
25+
onStartEditingInstructions,
2326
}) {
2427
return tap([], (items) => {
2528
items.push(
@@ -31,6 +34,20 @@ const HamburgerMenu = createMenu({
3134
</MenuItem>,
3235
);
3336

37+
items.push(
38+
<MenuItem
39+
isDisabled={isEditingInstructions}
40+
key="addOrEditInstructions"
41+
onClick={onStartEditingInstructions}
42+
>
43+
{
44+
hasInstructions ?
45+
t('top-bar.edit-instructions') :
46+
t('top-bar.add-instructions')
47+
}
48+
</MenuItem>,
49+
);
50+
3451
if (isUserAuthenticated) {
3552
items.push(
3653
<MenuItem
@@ -96,7 +113,9 @@ const HamburgerMenu = createMenu({
96113

97114

98115
HamburgerMenu.propTypes = {
116+
hasInstructions: PropTypes.bool.isRequired,
99117
isClassroomExportInProgress: PropTypes.bool.isRequired,
118+
isEditingInstructions: PropTypes.bool.isRequired,
100119
isExperimental: PropTypes.bool.isRequired,
101120
isGistExportInProgress: PropTypes.bool.isRequired,
102121
isOpen: PropTypes.bool.isRequired,
@@ -105,6 +124,7 @@ HamburgerMenu.propTypes = {
105124
onExportGist: PropTypes.func.isRequired,
106125
onExportRepo: PropTypes.func.isRequired,
107126
onExportToClassroom: PropTypes.func.isRequired,
127+
onStartEditingInstructions: PropTypes.func.isRequired,
108128
};
109129

110130
export default HamburgerMenu;

src/components/TopBar/createMenu.jsx

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import classnames from 'classnames';
44
import {connect} from 'react-redux';
55
import constant from 'lodash/constant';
6+
import noop from 'lodash/noop';
67
import onClickOutside from 'react-onclickoutside';
78
import preventClickthrough from 'react-prevent-clickthrough';
89
import property from 'lodash/property';
@@ -11,13 +12,14 @@ import React from 'react';
1112
import {closeTopBarMenu, toggleTopBarMenu} from '../../actions';
1213
import {getOpenTopBarMenu} from '../../selectors';
1314

14-
export function MenuItem({children, isEnabled, onClick}) {
15+
export function MenuItem({children, isDisabled, isEnabled, onClick}) {
1516
return (
1617
<div
17-
className={classnames('top-bar__menu-item',
18-
{'top-bar__menu-item_active': isEnabled},
19-
)}
20-
onClick={onClick}
18+
className={classnames('top-bar__menu-item', {
19+
'top-bar__menu-item_active': isEnabled,
20+
'top-bar__menu-item_disabled': isDisabled,
21+
})}
22+
onClick={isDisabled ? noop : onClick}
2123
>
2224
{children}
2325
</div>
@@ -26,11 +28,13 @@ export function MenuItem({children, isEnabled, onClick}) {
2628

2729
MenuItem.propTypes = {
2830
children: PropTypes.node.isRequired,
31+
isDisabled: PropTypes.bool,
2932
isEnabled: PropTypes.bool,
3033
onClick: PropTypes.func.isRequired,
3134
};
3235

3336
MenuItem.defaultProps = {
37+
isDisabled: false,
3438
isEnabled: false,
3539
};
3640

src/components/TopBar/index.jsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ export default function TopBar({
2929
currentProjectKey,
3030
currentUser,
3131
enabledLibraries,
32+
hasInstructions,
33+
isEditingInstructions,
3234
isExperimental,
3335
isGistExportInProgress,
3436
isRepoExportInProgress,
@@ -49,6 +51,7 @@ export default function TopBar({
4951
onExportRepo,
5052
onExportToClassroom,
5153
onLogOut,
54+
onStartEditingInstructions,
5255
onStartLogIn,
5356
onToggleLibrary,
5457
onToggleTextSize,
@@ -92,7 +95,9 @@ export default function TopBar({
9295
onStartLogIn={onStartLogIn}
9396
/>
9497
<HamburgerMenu
98+
hasInstructions={hasInstructions}
9599
isClassroomExportInProgress={isClassroomExportInProgress}
100+
isEditingInstructions={isEditingInstructions}
96101
isExperimental={isExperimental}
97102
isGistExportInProgress={isGistExportInProgress}
98103
isOpen={openMenu === 'hamburger'}
@@ -102,6 +107,7 @@ export default function TopBar({
102107
onExportGist={onExportGist}
103108
onExportRepo={onExportRepo}
104109
onExportToClassroom={onExportToClassroom}
110+
onStartEditingInstructions={onStartEditingInstructions}
105111
/>
106112
</header>
107113
);
@@ -111,7 +117,9 @@ TopBar.propTypes = {
111117
currentProjectKey: PropTypes.string,
112118
currentUser: PropTypes.object.isRequired,
113119
enabledLibraries: PropTypes.arrayOf(PropTypes.string).isRequired,
120+
hasInstructions: PropTypes.bool.isRequired,
114121
isClassroomExportInProgress: PropTypes.bool.isRequired,
122+
isEditingInstructions: PropTypes.bool.isRequired,
115123
isExperimental: PropTypes.bool.isRequired,
116124
isGistExportInProgress: PropTypes.bool.isRequired,
117125
isRepoExportInProgress: PropTypes.bool.isRequired,
@@ -131,6 +139,7 @@ TopBar.propTypes = {
131139
onExportRepo: PropTypes.func.isRequired,
132140
onExportToClassroom: PropTypes.func.isRequired,
133141
onLogOut: PropTypes.func.isRequired,
142+
onStartEditingInstructions: PropTypes.func.isRequired,
134143
onStartLogIn: PropTypes.func.isRequired,
135144
onToggleLibrary: PropTypes.func.isRequired,
136145
onToggleTextSize: PropTypes.func.isRequired,

0 commit comments

Comments
 (0)