From 599778ec1b675d636d80246f52da9412af5324e4 Mon Sep 17 00:00:00 2001 From: Aaron Greenberg Date: Fri, 9 Mar 2018 13:09:47 +0000 Subject: [PATCH 01/23] Add hamburger menu button for editing Instructions --- locales/en/translation.json | 1 + src/components/TopBar/HamburgerMenu.jsx | 11 +++++++++++ 2 files changed, 12 insertions(+) diff --git a/locales/en/translation.json b/locales/en/translation.json index bb5f633d1a..cdd2b78980 100644 --- a/locales/en/translation.json +++ b/locales/en/translation.json @@ -4,6 +4,7 @@ "export-gist": "Export Gist", "export-repo": "Export Repo", "share-to-classroom": "Share To Classroom", + "add-or-edit-instructions": "Add/Edit Instructions", "libraries": "Libraries", "load-project": "Your Projects", "new-project": "New Project", diff --git a/src/components/TopBar/HamburgerMenu.jsx b/src/components/TopBar/HamburgerMenu.jsx index 11281eef34..296adfb385 100644 --- a/src/components/TopBar/HamburgerMenu.jsx +++ b/src/components/TopBar/HamburgerMenu.jsx @@ -31,6 +31,17 @@ const HamburgerMenu = createMenu({ , ); + if (isExperimental) { + items.push( + + {t('top-bar.add-or-edit-instructions')} + , + ); + } + if (isUserAuthenticated) { items.push( Date: Fri, 9 Mar 2018 15:37:05 +0000 Subject: [PATCH 02/23] Add state management logic for instructions --- src/actions/index.js | 4 ++++ src/actions/ui.js | 8 ++++++++ src/reducers/ui.js | 7 +++++++ src/selectors/index.js | 2 ++ src/selectors/isEditingInstructions.js | 3 +++ 5 files changed, 24 insertions(+) create mode 100644 src/selectors/isEditingInstructions.js diff --git a/src/actions/index.js b/src/actions/index.js index 61efc20fe1..f8ce9483fe 100644 --- a/src/actions/index.js +++ b/src/actions/index.js @@ -31,6 +31,8 @@ import { toggleEditorTextSize, toggleTopBarMenu, closeTopBarMenu, + startEditingInstructions, + stopEditingInstructions, } from './ui'; import { @@ -92,6 +94,8 @@ export { toggleEditorTextSize, toggleTopBarMenu, closeTopBarMenu, + startEditingInstructions, + stopEditingInstructions, logIn, logOut, evaluateConsoleEntry, diff --git a/src/actions/ui.js b/src/actions/ui.js index 9a39434445..4823bd2175 100644 --- a/src/actions/ui.js +++ b/src/actions/ui.js @@ -58,3 +58,11 @@ export const toggleTopBarMenu = createAction( export const closeTopBarMenu = createAction( 'CLOSE_TOP_BAR_MENU', ); + +export const startEditingInstructions = createAction( + 'START_EDITING_INSTRUCTIONS', +); + +export const stopEditingInstructions = createAction( + 'STOP_EDITING_INSTRUCTIONS', +); diff --git a/src/reducers/ui.js b/src/reducers/ui.js index 1fc92c4a6b..52d3bbffcc 100644 --- a/src/reducers/ui.js +++ b/src/reducers/ui.js @@ -11,6 +11,7 @@ export const DEFAULT_WORKSPACE = new Immutable.Map({ columnFlex: DEFAULT_COLUMN_FLEX, rowFlex: DEFAULT_ROW_FLEX, isDraggingColumnDivider: false, + isEditingInstructions: false, }); const defaultState = new Immutable.Map(). @@ -220,6 +221,12 @@ export default function ui(stateIn, action) { case 'PROJECT_COMPILED': return dismissNotification(state, 'project-compilation-failed'); + case 'START_EDITING_INSTRUCTIONS': + return state.setIn(['workspace', 'isEditingInstructions'], true); + + case 'STOP_EDITING_INSTRUCTIONS': + return state.setIn(['workspace', 'isEditingInstructions'], false); + default: return state; } diff --git a/src/selectors/index.js b/src/selectors/index.js index 3ee0136bff..68eb175b62 100644 --- a/src/selectors/index.js +++ b/src/selectors/index.js @@ -19,6 +19,7 @@ import getRequestedFocusedLine from './getRequestedFocusedLine'; import isCurrentlyValidating from './isCurrentlyValidating'; import isCurrentProjectSyntacticallyValid from './isCurrentProjectSyntacticallyValid'; +import isEditingInstructions from './isEditingInstructions'; import isExperimental from './isExperimental'; import isGistExportInProgress from './isGistExportInProgress'; import isRepoExportInProgress from './isRepoExportInProgress'; @@ -50,6 +51,7 @@ export { getRequestedFocusedLine, isCurrentlyValidating, isCurrentProjectSyntacticallyValid, + isEditingInstructions, isExperimental, isGistExportInProgress, isRepoExportInProgress, diff --git a/src/selectors/isEditingInstructions.js b/src/selectors/isEditingInstructions.js new file mode 100644 index 0000000000..c796881d30 --- /dev/null +++ b/src/selectors/isEditingInstructions.js @@ -0,0 +1,3 @@ +export default function isEditingInstructions(state) { + return state.getIn(['ui', 'workspace', 'isEditingInstructions']); +} From 027f35bd610f190f72fd869374f92d6c9f768e00 Mon Sep 17 00:00:00 2001 From: Aaron Greenberg Date: Fri, 9 Mar 2018 15:40:41 +0000 Subject: [PATCH 03/23] Wire hamburger menu to instructions editing state --- src/components/TopBar/HamburgerMenu.jsx | 6 +++++- src/components/TopBar/index.jsx | 6 ++++++ src/containers/TopBar.js | 7 +++++++ 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/components/TopBar/HamburgerMenu.jsx b/src/components/TopBar/HamburgerMenu.jsx index 296adfb385..2f0f8b6c35 100644 --- a/src/components/TopBar/HamburgerMenu.jsx +++ b/src/components/TopBar/HamburgerMenu.jsx @@ -12,6 +12,7 @@ const HamburgerMenu = createMenu({ name: 'hamburger', renderItems({ + isEditingInstructions, isExperimental, isGistExportInProgress, isRepoExportInProgress, @@ -20,6 +21,7 @@ const HamburgerMenu = createMenu({ onExportGist, onExportRepo, onExportToClassroom, + onStartEditingInstructions, }) { return tap([], (items) => { items.push( @@ -35,7 +37,7 @@ const HamburgerMenu = createMenu({ items.push( {t('top-bar.add-or-edit-instructions')} , @@ -108,6 +110,7 @@ const HamburgerMenu = createMenu({ HamburgerMenu.propTypes = { isClassroomExportInProgress: PropTypes.bool.isRequired, + isEditingInstructions: PropTypes.bool.isRequired, isExperimental: PropTypes.bool.isRequired, isGistExportInProgress: PropTypes.bool.isRequired, isOpen: PropTypes.bool.isRequired, @@ -116,6 +119,7 @@ HamburgerMenu.propTypes = { onExportGist: PropTypes.func.isRequired, onExportRepo: PropTypes.func.isRequired, onExportToClassroom: PropTypes.func.isRequired, + onStartEditingInstructions: PropTypes.func.isRequired, }; export default HamburgerMenu; diff --git a/src/components/TopBar/index.jsx b/src/components/TopBar/index.jsx index 694772ce96..eea2139514 100644 --- a/src/components/TopBar/index.jsx +++ b/src/components/TopBar/index.jsx @@ -29,6 +29,7 @@ export default function TopBar({ currentProjectKey, currentUser, enabledLibraries, + isEditingInstructions, isExperimental, isGistExportInProgress, isRepoExportInProgress, @@ -49,6 +50,7 @@ export default function TopBar({ onExportRepo, onExportToClassroom, onLogOut, + onStartEditingInstructions, onStartLogIn, onToggleLibrary, onToggleTextSize, @@ -93,6 +95,7 @@ export default function TopBar({ /> ); @@ -112,6 +116,7 @@ TopBar.propTypes = { currentUser: PropTypes.object.isRequired, enabledLibraries: PropTypes.arrayOf(PropTypes.string).isRequired, isClassroomExportInProgress: PropTypes.bool.isRequired, + isEditingInstructions: PropTypes.bool.isRequired, isExperimental: PropTypes.bool.isRequired, isGistExportInProgress: PropTypes.bool.isRequired, isRepoExportInProgress: PropTypes.bool.isRequired, @@ -131,6 +136,7 @@ TopBar.propTypes = { onExportRepo: PropTypes.func.isRequired, onExportToClassroom: PropTypes.func.isRequired, onLogOut: PropTypes.func.isRequired, + onStartEditingInstructions: PropTypes.func.isRequired, onStartLogIn: PropTypes.func.isRequired, onToggleLibrary: PropTypes.func.isRequired, onToggleTextSize: PropTypes.func.isRequired, diff --git a/src/containers/TopBar.js b/src/containers/TopBar.js index b95197a525..31f65bd997 100644 --- a/src/containers/TopBar.js +++ b/src/containers/TopBar.js @@ -7,6 +7,7 @@ import { getEnabledLibraries, getOpenTopBarMenu, getAllProjectKeys, + isEditingInstructions, isExperimental, isGistExportInProgress, isRepoExportInProgress, @@ -22,6 +23,7 @@ import { createProject, createSnapshot, exportProject, + startEditingInstructions, toggleEditorTextSize, toggleLibrary, toggleTopBarMenu, @@ -34,6 +36,7 @@ function mapStateToProps(state) { currentProjectKey: getCurrentProjectKey(state), currentUser: getCurrentUser(state), enabledLibraries: getEnabledLibraries(state), + isEditingInstructions: isEditingInstructions(state), isExperimental: isExperimental(state), isGistExportInProgress: isGistExportInProgress(state), isRepoExportInProgress: isRepoExportInProgress(state), @@ -90,6 +93,10 @@ function mapDispatchToProps(dispatch) { dispatch(logOut()); }, + onStartEditingInstructions() { + dispatch(startEditingInstructions()); + }, + onStartLogIn() { dispatch(logIn()); }, From 174a9934945b0eeeae0b0778dead35d2c0bb005c Mon Sep 17 00:00:00 2001 From: Aaron Greenberg Date: Fri, 9 Mar 2018 15:43:11 +0000 Subject: [PATCH 04/23] Create basic instructions editor --- src/components/Instructions.jsx | 17 ++++++++++++----- src/components/Workspace.jsx | 20 ++++++++++++++------ src/containers/Instructions.js | 2 ++ src/css/application.css | 8 ++++++++ 4 files changed, 36 insertions(+), 11 deletions(-) diff --git a/src/components/Instructions.jsx b/src/components/Instructions.jsx index 32068f8e30..36b635aa2b 100644 --- a/src/components/Instructions.jsx +++ b/src/components/Instructions.jsx @@ -2,8 +2,8 @@ import React from 'react'; import PropTypes from 'prop-types'; import {toReact as markdownToReact} from '../util/markdown'; -export default function Instructions({instructions, isOpen}) { - if (!instructions || !isOpen) { +export default function Instructions({instructions, isEditing, isOpen}) { + if (!isEditing && !instructions || !isOpen) { return null; } @@ -11,14 +11,21 @@ export default function Instructions({instructions, isOpen}) {
-
- {markdownToReact(instructions)} -
+ { + isEditing ? +
+            {instructions}
+          
: +
+ {markdownToReact(instructions)} +
+ }
); } Instructions.propTypes = { instructions: PropTypes.string.isRequired, + isEditing: PropTypes.bool.isRequired, isOpen: PropTypes.bool.isRequired, }; diff --git a/src/components/Workspace.jsx b/src/components/Workspace.jsx index a302dd984b..07c41723b2 100644 --- a/src/components/Workspace.jsx +++ b/src/components/Workspace.jsx @@ -29,7 +29,7 @@ import { } from '../actions'; import {isPristineProject} from '../util/projectUtils'; -import {getCurrentProject} from '../selectors'; +import {getCurrentProject, isEditingInstructions} from '../selectors'; import TopBar from '../containers/TopBar'; import Instructions from '../containers/Instructions'; @@ -45,6 +45,7 @@ function mapStateToProps(state) { isDraggingColumnDivider: state.getIn( ['ui', 'workspace', 'isDraggingColumnDivider'], ), + isEditingInstructions: isEditingInstructions(state), isUserTyping: state.getIn(['ui', 'editors', 'typing']), editorsFlex: state.getIn(['ui', 'workspace', 'columnFlex']).toJS(), rowsFlex: state.getIn(['ui', 'workspace', 'rowFlex']).toJS(), @@ -184,14 +185,20 @@ class Workspace extends React.Component { } _handleClickInstructionsBar() { - this.props.dispatch(toggleComponent( - get(this.props, ['currentProject', 'projectKey']), - 'instructions', - )); + if (!this.props.isEditingInstructions) { + this.props.dispatch(toggleComponent( + get(this.props, ['currentProject', 'projectKey']), + 'instructions', + )); + } } _renderInstructionsBar() { - if (!get(this.props, ['currentProject', 'instructions'])) { + const currentInstructions = get( + this.props, + ['currentProject', 'instructions'], + ); + if (!this.props.isEditingInstructions && !currentInstructions) { return null; } @@ -296,6 +303,7 @@ Workspace.propTypes = { editorsFlex: PropTypes.array.isRequired, errors: PropTypes.object.isRequired, isDraggingColumnDivider: PropTypes.bool.isRequired, + isEditingInstructions: PropTypes.bool.isRequired, isUserTyping: PropTypes.bool, rowsFlex: PropTypes.array.isRequired, ui: PropTypes.object.isRequired, diff --git a/src/containers/Instructions.js b/src/containers/Instructions.js index cd3758b6db..de6c972ccc 100644 --- a/src/containers/Instructions.js +++ b/src/containers/Instructions.js @@ -3,11 +3,13 @@ import Instructions from '../components/Instructions'; import { getCurrentProjectInstructions, getHiddenUIComponents, + isEditingInstructions, } from '../selectors'; function mapStateToProps(state) { return { instructions: getCurrentProjectInstructions(state), + isEditing: isEditingInstructions(state), isOpen: !getHiddenUIComponents(state).includes('instructions'), }; } diff --git a/src/css/application.css b/src/css/application.css index 9ee317486a..794fa7f301 100644 --- a/src/css/application.css +++ b/src/css/application.css @@ -342,6 +342,14 @@ body { } } +.instructions_editing { + margin: 0; + outline: 0; + word-wrap: break-word; + overflow-wrap: break-word; + white-space: normal; +} + /** @define project-preview */ .project-preview__label { From 3dfba1bec50f69d45bf35a3f661ece2a52be2630 Mon Sep 17 00:00:00 2001 From: Aaron Greenberg Date: Fri, 9 Mar 2018 17:41:44 +0000 Subject: [PATCH 05/23] Add a (stubbed) Cancel button This commit also extracts the editable pre element into a separate InstructionsEditor React component. --- src/components/Instructions.jsx | 17 ++++++++-------- src/components/InstructionsEditor.jsx | 19 ++++++++++++++++++ src/css/application.css | 29 +++++++++++++++++++++------ 3 files changed, 50 insertions(+), 15 deletions(-) create mode 100644 src/components/InstructionsEditor.jsx diff --git a/src/components/Instructions.jsx b/src/components/Instructions.jsx index 36b635aa2b..8f6ade41b7 100644 --- a/src/components/Instructions.jsx +++ b/src/components/Instructions.jsx @@ -1,6 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import {toReact as markdownToReact} from '../util/markdown'; +import InstructionsEditor from './InstructionsEditor'; export default function Instructions({instructions, isEditing, isOpen}) { if (!isEditing && !instructions || !isOpen) { @@ -11,15 +12,13 @@ export default function Instructions({instructions, isEditing, isOpen}) {
- { - isEditing ? -
-            {instructions}
-          
: -
- {markdownToReact(instructions)} -
- } +
+ { + isEditing ? + : + markdownToReact(instructions) + } +
); } diff --git a/src/components/InstructionsEditor.jsx b/src/components/InstructionsEditor.jsx new file mode 100644 index 0000000000..8ffd67bfab --- /dev/null +++ b/src/components/InstructionsEditor.jsx @@ -0,0 +1,19 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +export default function InstructionsEditor({instructions}) { + return ( +
+
+ +
+
+        {instructions}
+      
+
+ ); +} + +InstructionsEditor.propTypes = { + instructions: PropTypes.string.isRequired, +}; diff --git a/src/css/application.css b/src/css/application.css index 794fa7f301..07a722ebed 100644 --- a/src/css/application.css +++ b/src/css/application.css @@ -342,12 +342,29 @@ body { } } -.instructions_editing { - margin: 0; - outline: 0; - word-wrap: break-word; - overflow-wrap: break-word; - white-space: normal; +.instructions-editor { + height: 100%; + padding-top: calc(28px + 1rem); + + & pre { + height: 100%; + margin: 0; + outline: 0; + word-wrap: break-word; + overflow-wrap: break-word; + white-space: normal; + } +} + +.instructions-editor-menu { + position: absolute; + top: 0; + right: 0; + left: 0; + padding: .5rem; + margin-bottom: 100%; + text-align: right; + background-color: var(--color-light-gray); } /** @define project-preview */ From 2dd3edce524ebd488bb437d1cae7e93289e2c408 Mon Sep 17 00:00:00 2001 From: Aaron Greenberg Date: Fri, 9 Mar 2018 17:52:32 +0000 Subject: [PATCH 06/23] Wire cancel button to instructions state mgmt --- src/components/Instructions.jsx | 13 +++++++++++-- src/components/InstructionsEditor.jsx | 5 +++-- src/containers/Instructions.js | 10 ++++++++++ 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/src/components/Instructions.jsx b/src/components/Instructions.jsx index 8f6ade41b7..f01b4be176 100644 --- a/src/components/Instructions.jsx +++ b/src/components/Instructions.jsx @@ -3,7 +3,12 @@ import PropTypes from 'prop-types'; import {toReact as markdownToReact} from '../util/markdown'; import InstructionsEditor from './InstructionsEditor'; -export default function Instructions({instructions, isEditing, isOpen}) { +export default function Instructions({ + instructions, + isEditing, + isOpen, + onCancelEditing, +}) { if (!isEditing && !instructions || !isOpen) { return null; } @@ -15,7 +20,10 @@ export default function Instructions({instructions, isEditing, isOpen}) {
{ isEditing ? - : + : markdownToReact(instructions) }
@@ -27,4 +35,5 @@ Instructions.propTypes = { instructions: PropTypes.string.isRequired, isEditing: PropTypes.bool.isRequired, isOpen: PropTypes.bool.isRequired, + onCancelEditing: PropTypes.func.isRequired, }; diff --git a/src/components/InstructionsEditor.jsx b/src/components/InstructionsEditor.jsx index 8ffd67bfab..3bbb553703 100644 --- a/src/components/InstructionsEditor.jsx +++ b/src/components/InstructionsEditor.jsx @@ -1,11 +1,11 @@ import React from 'react'; import PropTypes from 'prop-types'; -export default function InstructionsEditor({instructions}) { +export default function InstructionsEditor({instructions, onCancelEditing}) { return (
- +
         {instructions}
@@ -16,4 +16,5 @@ export default function InstructionsEditor({instructions}) {
 
 InstructionsEditor.propTypes = {
   instructions: PropTypes.string.isRequired,
+  onCancelEditing: PropTypes.func.isRequired,
 };
diff --git a/src/containers/Instructions.js b/src/containers/Instructions.js
index de6c972ccc..a2c81c5bf9 100644
--- a/src/containers/Instructions.js
+++ b/src/containers/Instructions.js
@@ -5,6 +5,7 @@ import {
   getHiddenUIComponents,
   isEditingInstructions,
 } from '../selectors';
+import {stopEditingInstructions} from '../actions';
 
 function mapStateToProps(state) {
   return {
@@ -14,6 +15,15 @@ function mapStateToProps(state) {
   };
 }
 
+function mapDispatchToProps(dispatch) {
+  return {
+    onCancelEditing() {
+      dispatch(stopEditingInstructions());
+    },
+  };
+}
+
 export default connect(
   mapStateToProps,
+  mapDispatchToProps,
 )(Instructions);

From 54487ad46d0029f6f553c12a5f955d4b7cd9bf5d Mon Sep 17 00:00:00 2001
From: Aaron Greenberg 
Date: Fri, 9 Mar 2018 18:05:20 +0000
Subject: [PATCH 07/23] Use i18n functionality for button text

---
 locales/en/translation.json           | 5 ++++-
 src/components/InstructionsEditor.jsx | 5 ++++-
 2 files changed, 8 insertions(+), 2 deletions(-)

diff --git a/locales/en/translation.json b/locales/en/translation.json
index cdd2b78980..d6a7d3e731 100644
--- a/locales/en/translation.json
+++ b/locales/en/translation.json
@@ -61,7 +61,10 @@
         "css": "CSS",
         "javascript": "JS"
       },
-      "output": "Output"
+      "output": "Output",
+      "instructions": {
+        "cancel": "Cancel"
+      }
     }
   },
   "utility": {
diff --git a/src/components/InstructionsEditor.jsx b/src/components/InstructionsEditor.jsx
index 3bbb553703..e9bc62a67e 100644
--- a/src/components/InstructionsEditor.jsx
+++ b/src/components/InstructionsEditor.jsx
@@ -1,11 +1,14 @@
 import React from 'react';
 import PropTypes from 'prop-types';
+import {t} from 'i18next';
 
 export default function InstructionsEditor({instructions, onCancelEditing}) {
   return (
     
- +
         {instructions}

From f56e959ca37bc4d661b0eca3549998792818717c Mon Sep 17 00:00:00 2001
From: Aaron Greenberg 
Date: Sat, 10 Mar 2018 09:47:10 +0000
Subject: [PATCH 08/23] Add state mgmt logic for updating instructions

---
 src/actions/index.js     | 2 ++
 src/actions/projects.js  | 5 +++++
 src/reducers/projects.js | 6 ++++++
 src/reducers/ui.js       | 1 +
 4 files changed, 14 insertions(+)

diff --git a/src/actions/index.js b/src/actions/index.js
index f8ce9483fe..a988d2aa78 100644
--- a/src/actions/index.js
+++ b/src/actions/index.js
@@ -15,6 +15,7 @@ import {
   unhideComponent,
   toggleComponent,
   updateProjectSource,
+  updateProjectInstructions,
 } from './projects';
 
 import {
@@ -69,6 +70,7 @@ export {
   createSnapshot,
   changeCurrentProject,
   updateProjectSource,
+  updateProjectInstructions,
   toggleLibrary,
   userAuthenticated,
   userLoggedOut,
diff --git a/src/actions/projects.js b/src/actions/projects.js
index e9a662bbc0..0fc760e053 100644
--- a/src/actions/projects.js
+++ b/src/actions/projects.js
@@ -18,6 +18,11 @@ export const updateProjectSource = createAction(
   (_projectKey, _language, _newValue, timestamp = Date.now()) => ({timestamp}),
 );
 
+export const updateProjectInstructions = createAction(
+  'UPDATE_PROJECT_INSTRUCTIONS',
+  (projectKey, newValue) => ({projectKey, newValue}),
+);
+
 export const toggleLibrary = createAction(
   'TOGGLE_LIBRARY',
   (projectKey, libraryKey) => ({projectKey, libraryKey}),
diff --git a/src/reducers/projects.js b/src/reducers/projects.js
index abc2e2c3ef..8de0ac4c44 100644
--- a/src/reducers/projects.js
+++ b/src/reducers/projects.js
@@ -115,6 +115,12 @@ export default function reduceProjects(stateIn, action) {
         action.meta.timestamp,
       );
 
+    case 'UPDATE_PROJECT_INSTRUCTIONS':
+      return state.setIn(
+        [action.payload.projectKey, 'instructions'],
+        action.payload.newValue,
+      );
+
     case 'PROJECT_CREATED':
       return removePristineExcept(state, action.payload.projectKey).set(
         action.payload.projectKey,
diff --git a/src/reducers/ui.js b/src/reducers/ui.js
index 52d3bbffcc..863caf9e67 100644
--- a/src/reducers/ui.js
+++ b/src/reducers/ui.js
@@ -225,6 +225,7 @@ export default function ui(stateIn, action) {
       return state.setIn(['workspace', 'isEditingInstructions'], true);
 
     case 'STOP_EDITING_INSTRUCTIONS':
+    case 'UPDATE_PROJECT_INSTRUCTIONS':
       return state.setIn(['workspace', 'isEditingInstructions'], false);
 
     default:

From b07fc88c312301ae9d9d3c0a9251994eee1a80a0 Mon Sep 17 00:00:00 2001
From: Aaron Greenberg 
Date: Sat, 10 Mar 2018 09:50:00 +0000
Subject: [PATCH 09/23] Wire editor save button to state mgmt logic

---
 locales/en/translation.json           |  3 +-
 src/components/Instructions.jsx       |  6 ++++
 src/components/InstructionsEditor.jsx | 51 ++++++++++++++++++++-------
 src/containers/Instructions.js        |  7 +++-
 src/css/application.css               |  7 ++--
 5 files changed, 58 insertions(+), 16 deletions(-)

diff --git a/locales/en/translation.json b/locales/en/translation.json
index d6a7d3e731..3c51ce31a7 100644
--- a/locales/en/translation.json
+++ b/locales/en/translation.json
@@ -63,7 +63,8 @@
       },
       "output": "Output",
       "instructions": {
-        "cancel": "Cancel"
+        "cancel": "Cancel",
+        "save": "Save"
       }
     }
   },
diff --git a/src/components/Instructions.jsx b/src/components/Instructions.jsx
index f01b4be176..201a1e13a6 100644
--- a/src/components/Instructions.jsx
+++ b/src/components/Instructions.jsx
@@ -7,7 +7,9 @@ export default function Instructions({
   instructions,
   isEditing,
   isOpen,
+  projectKey,
   onCancelEditing,
+  onSaveChanges,
 }) {
   if (!isEditing && !instructions || !isOpen) {
     return null;
@@ -22,7 +24,9 @@ export default function Instructions({
           isEditing ?
              :
             markdownToReact(instructions)
         }
@@ -35,5 +39,7 @@ Instructions.propTypes = {
   instructions: PropTypes.string.isRequired,
   isEditing: PropTypes.bool.isRequired,
   isOpen: PropTypes.bool.isRequired,
+  projectKey: PropTypes.string.isRequired,
   onCancelEditing: PropTypes.func.isRequired,
+  onSaveChanges: PropTypes.func.isRequired,
 };
diff --git a/src/components/InstructionsEditor.jsx b/src/components/InstructionsEditor.jsx
index e9bc62a67e..136fb61145 100644
--- a/src/components/InstructionsEditor.jsx
+++ b/src/components/InstructionsEditor.jsx
@@ -1,23 +1,50 @@
 import React from 'react';
 import PropTypes from 'prop-types';
 import {t} from 'i18next';
+import bindAll from 'lodash/bindAll';
 
-export default function InstructionsEditor({instructions, onCancelEditing}) {
-  return (
-    
-
- +export default class InstructionsEditor extends React.Component { + constructor({instructions}) { + super(); + this.state = {instructions}; + bindAll(this, '_handleCancelEditing', '_handleSaveChanges', '_ref'); + } + + _handleCancelEditing() { + this.props.onCancelEditing(); + } + + _handleSaveChanges() { + const newValue = this.editor.innerText.trim(); + this.props.onSaveChanges(this.props.projectKey, newValue); + } + + _ref(preElement) { + this.editor = preElement; + } + + render() { + return ( +
+
+ + +
+
+          {this.state.instructions}
+        
-
-        {instructions}
-      
-
- ); + ); + } } InstructionsEditor.propTypes = { instructions: PropTypes.string.isRequired, + projectKey: PropTypes.string.isRequired, onCancelEditing: PropTypes.func.isRequired, + onSaveChanges: PropTypes.func.isRequired, }; diff --git a/src/containers/Instructions.js b/src/containers/Instructions.js index a2c81c5bf9..3f73fc66d7 100644 --- a/src/containers/Instructions.js +++ b/src/containers/Instructions.js @@ -2,16 +2,18 @@ import {connect} from 'react-redux'; import Instructions from '../components/Instructions'; import { getCurrentProjectInstructions, + getCurrentProjectKey, getHiddenUIComponents, isEditingInstructions, } from '../selectors'; -import {stopEditingInstructions} from '../actions'; +import {stopEditingInstructions, updateProjectInstructions} from '../actions'; function mapStateToProps(state) { return { instructions: getCurrentProjectInstructions(state), isEditing: isEditingInstructions(state), isOpen: !getHiddenUIComponents(state).includes('instructions'), + projectKey: getCurrentProjectKey(state), }; } @@ -20,6 +22,9 @@ function mapDispatchToProps(dispatch) { onCancelEditing() { dispatch(stopEditingInstructions()); }, + onSaveChanges(projectKey, newValue) { + dispatch(updateProjectInstructions(projectKey, newValue)); + }, }; } diff --git a/src/css/application.css b/src/css/application.css index 07a722ebed..764d90aa57 100644 --- a/src/css/application.css +++ b/src/css/application.css @@ -351,8 +351,7 @@ body { margin: 0; outline: 0; word-wrap: break-word; - overflow-wrap: break-word; - white-space: normal; + white-space: pre-wrap; } } @@ -365,6 +364,10 @@ body { margin-bottom: 100%; text-align: right; background-color: var(--color-light-gray); + + & button:not(:last-child) { + margin-right: .5em; + } } /** @define project-preview */ From bb8eb7f923fca2361405370c8a1f5e2f8dc2b070 Mon Sep 17 00:00:00 2001 From: Aaron Greenberg Date: Sun, 11 Mar 2018 07:10:25 +0000 Subject: [PATCH 10/23] Fix CSS BEM naming violations --- src/components/InstructionsEditor.jsx | 18 ++++++++++++++---- src/css/application.css | 26 ++++++++++++++------------ 2 files changed, 28 insertions(+), 16 deletions(-) diff --git a/src/components/InstructionsEditor.jsx b/src/components/InstructionsEditor.jsx index 136fb61145..4be111cb72 100644 --- a/src/components/InstructionsEditor.jsx +++ b/src/components/InstructionsEditor.jsx @@ -26,15 +26,25 @@ export default class InstructionsEditor extends React.Component { render() { return (
-
- -
-
+        
           {this.state.instructions}
         
diff --git a/src/css/application.css b/src/css/application.css index 764d90aa57..4be315c75b 100644 --- a/src/css/application.css +++ b/src/css/application.css @@ -342,32 +342,34 @@ body { } } +/** @define instructions-editor */ + .instructions-editor { height: 100%; padding-top: calc(28px + 1rem); +} - & pre { - height: 100%; - margin: 0; - outline: 0; - word-wrap: break-word; - white-space: pre-wrap; - } +.instructions-editor__input { + height: 100%; + margin: 0; + outline: 0; + word-wrap: break-word; + white-space: pre-wrap; } -.instructions-editor-menu { +.instructions-editor__menu { position: absolute; top: 0; right: 0; left: 0; - padding: .5rem; + padding: 0.5rem; margin-bottom: 100%; text-align: right; background-color: var(--color-light-gray); +} - & button:not(:last-child) { - margin-right: .5em; - } +.instructions-editor__menu-button:not(:last-child) { + margin-right: 0.5em; } /** @define project-preview */ From 1c958f73b0a23bb3f4664888d7ea0092303d5e6d Mon Sep 17 00:00:00 2001 From: Aaron Greenberg Date: Sun, 11 Mar 2018 08:49:27 +0100 Subject: [PATCH 11/23] Toggle instructions hamburger menu item on state --- locales/en/translation.json | 3 ++- src/components/TopBar/HamburgerMenu.jsx | 9 ++++++++- src/components/TopBar/createMenu.jsx | 11 +++++++---- src/components/TopBar/index.jsx | 3 +++ src/containers/TopBar.js | 2 ++ src/css/application.css | 6 ++++++ 6 files changed, 28 insertions(+), 6 deletions(-) diff --git a/locales/en/translation.json b/locales/en/translation.json index 3c51ce31a7..a0a8393b4b 100644 --- a/locales/en/translation.json +++ b/locales/en/translation.json @@ -4,7 +4,8 @@ "export-gist": "Export Gist", "export-repo": "Export Repo", "share-to-classroom": "Share To Classroom", - "add-or-edit-instructions": "Add/Edit Instructions", + "add-instructions": "Add Instructions", + "edit-instructions": "Edit Instructions", "libraries": "Libraries", "load-project": "Your Projects", "new-project": "New Project", diff --git a/src/components/TopBar/HamburgerMenu.jsx b/src/components/TopBar/HamburgerMenu.jsx index 2f0f8b6c35..75a32bca0e 100644 --- a/src/components/TopBar/HamburgerMenu.jsx +++ b/src/components/TopBar/HamburgerMenu.jsx @@ -12,6 +12,7 @@ const HamburgerMenu = createMenu({ name: 'hamburger', renderItems({ + hasInstructions, isEditingInstructions, isExperimental, isGistExportInProgress, @@ -36,10 +37,15 @@ const HamburgerMenu = createMenu({ if (isExperimental) { items.push( - {t('top-bar.add-or-edit-instructions')} + { + hasInstructions ? + t('top-bar.edit-instructions') : + t('top-bar.add-instructions') + } , ); } @@ -109,6 +115,7 @@ const HamburgerMenu = createMenu({ HamburgerMenu.propTypes = { + hasInstructions: PropTypes.bool.isRequired, isClassroomExportInProgress: PropTypes.bool.isRequired, isEditingInstructions: PropTypes.bool.isRequired, isExperimental: PropTypes.bool.isRequired, diff --git a/src/components/TopBar/createMenu.jsx b/src/components/TopBar/createMenu.jsx index 1e4a03423d..4bbf81ab07 100644 --- a/src/components/TopBar/createMenu.jsx +++ b/src/components/TopBar/createMenu.jsx @@ -11,12 +11,13 @@ import React from 'react'; import {closeTopBarMenu, toggleTopBarMenu} from '../../actions'; import {getOpenTopBarMenu} from '../../selectors'; -export function MenuItem({children, isEnabled, onClick}) { +export function MenuItem({children, isDisabled, isEnabled, onClick}) { return (
{children} @@ -26,11 +27,13 @@ export function MenuItem({children, isEnabled, onClick}) { MenuItem.propTypes = { children: PropTypes.node.isRequired, + isDisabled: PropTypes.bool, isEnabled: PropTypes.bool, onClick: PropTypes.func.isRequired, }; MenuItem.defaultProps = { + isDisabled: false, isEnabled: false, }; diff --git a/src/components/TopBar/index.jsx b/src/components/TopBar/index.jsx index eea2139514..db438e8321 100644 --- a/src/components/TopBar/index.jsx +++ b/src/components/TopBar/index.jsx @@ -29,6 +29,7 @@ export default function TopBar({ currentProjectKey, currentUser, enabledLibraries, + hasInstructions, isEditingInstructions, isExperimental, isGistExportInProgress, @@ -94,6 +95,7 @@ export default function TopBar({ onStartLogIn={onStartLogIn} /> Date: Sun, 11 Mar 2018 09:56:34 +0100 Subject: [PATCH 12/23] Add tests for new instructions reducer logic --- test/unit/reducers/projects.js | 12 ++++++++++++ test/unit/reducers/ui.js | 24 ++++++++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/test/unit/reducers/projects.js b/test/unit/reducers/projects.js index 7f7f60b198..e6b0dea901 100644 --- a/test/unit/reducers/projects.js +++ b/test/unit/reducers/projects.js @@ -21,6 +21,7 @@ import { unhideComponent, toggleComponent, updateProjectSource, + updateProjectInstructions, } from '../../../src/actions/projects'; import { snapshotImported, @@ -65,6 +66,17 @@ test('updateProjectSource', reducerTest( ), )); +tap(initProjects({1: false}), projects => + test('updateProjectInstructions', reducerTest( + reducer, + projects, + partial(updateProjectInstructions, '1', '# Instructions\n\nHello.'), + projects.update('1', projectIn => + projectIn.set('instructions', '# Instructions\n\nHello.'), + ), + )), +); + test('changeCurrentProject', (t) => { t.test('from modified to pristine', reducerTest( reducer, diff --git a/test/unit/reducers/ui.js b/test/unit/reducers/ui.js index 222dbc7e19..94f5108bc1 100644 --- a/test/unit/reducers/ui.js +++ b/test/unit/reducers/ui.js @@ -8,6 +8,7 @@ import { gistNotFound, gistImportError, updateProjectSource, + updateProjectInstructions, } from '../../../src/actions/projects'; import { dragColumnDivider, @@ -18,6 +19,8 @@ import { notificationTriggered, userDismissedNotification, toggleTopBarMenu, + startEditingInstructions, + stopEditingInstructions, } from '../../../src/actions/ui'; import { snapshotCreated, @@ -124,6 +127,27 @@ test('dragRowDivider', (t) => { )); }); +test('startEditingInstructions', reducerTest( + reducer, + initialState, + startEditingInstructions, + initialState.setIn(['workspace', 'isEditingInstructions'], true), +)); + +test('startEditingInstructions', reducerTest( + reducer, + initialState.setIn(['workspace', 'isEditingInstructions'], true), + stopEditingInstructions, + initialState, +)); + +test('updateProjectInstructions', reducerTest( + reducer, + initialState.setIn(['workspace', 'isEditingInstructions'], true), + updateProjectInstructions, + initialState, +)); + test('gistNotFound', reducerTest( reducer, initialState, From e451ea64b6f0cb8c2979095fc62729c26b3aa035 Mon Sep 17 00:00:00 2001 From: Aaron Greenberg Date: Sun, 11 Mar 2018 22:36:20 -0400 Subject: [PATCH 13/23] Use uncontrolled textarea for InstructionsEditor --- src/components/InstructionsEditor.jsx | 17 +++++++---------- src/css/application.css | 14 ++++++++++---- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/src/components/InstructionsEditor.jsx b/src/components/InstructionsEditor.jsx index 4be111cb72..e89eadc00d 100644 --- a/src/components/InstructionsEditor.jsx +++ b/src/components/InstructionsEditor.jsx @@ -4,9 +4,8 @@ import {t} from 'i18next'; import bindAll from 'lodash/bindAll'; export default class InstructionsEditor extends React.Component { - constructor({instructions}) { + constructor() { super(); - this.state = {instructions}; bindAll(this, '_handleCancelEditing', '_handleSaveChanges', '_ref'); } @@ -15,12 +14,12 @@ export default class InstructionsEditor extends React.Component { } _handleSaveChanges() { - const newValue = this.editor.innerText.trim(); + const newValue = this.editor.value.trim(); this.props.onSaveChanges(this.props.projectKey, newValue); } - _ref(preElement) { - this.editor = preElement; + _ref(editorElement) { + this.editor = editorElement; } render() { @@ -40,13 +39,11 @@ export default class InstructionsEditor extends React.Component { {t('workspace.components.instructions.cancel')}
-
-          {this.state.instructions}
-        
+ />
); } diff --git a/src/css/application.css b/src/css/application.css index ff852eb3ee..b328f94b69 100644 --- a/src/css/application.css +++ b/src/css/application.css @@ -83,6 +83,7 @@ --size-rounded-corners: 0.2em; --size-top-bar: 4em; --size-top-bar-menu-button: 2.8em; + --size-instructions-menu-padding: calc(28px + 1rem); --box-shadow: 0 0 0.1em 0 rgba(0, 0, 0, 0.5); --font-size-menu: 1.7vh; } @@ -351,16 +352,22 @@ body { /** @define instructions-editor */ .instructions-editor { - height: 100%; - padding-top: calc(28px + 1rem); + height: calc(100% - var(--size-instructions-menu-padding) - 1rem); + padding-top: var(--size-instructions-menu-padding); } .instructions-editor__input { - height: 100%; + width: 100%; + height: calc(100% - 1rem); margin: 0; + font-family: monospace; + font-size: 1rem; + background-color: transparent; + border: 0; outline: 0; word-wrap: break-word; white-space: pre-wrap; + resize: none; } .instructions-editor__menu { @@ -369,7 +376,6 @@ body { right: 0; left: 0; padding: 0.5rem; - margin-bottom: 100%; text-align: right; background-color: var(--color-light-gray); } From fd6dd05b05b564cd664d19e112d12287b645ac30 Mon Sep 17 00:00:00 2001 From: Aaron Greenberg Date: Fri, 16 Mar 2018 18:23:22 -0400 Subject: [PATCH 14/23] Add a Markdown blurb footer thingy --- src/components/Instructions.jsx | 24 +++++++------- src/components/InstructionsEditor.jsx | 14 ++++++--- src/css/application.css | 45 +++++++++++++++++---------- 3 files changed, 49 insertions(+), 34 deletions(-) diff --git a/src/components/Instructions.jsx b/src/components/Instructions.jsx index 201a1e13a6..050416f68c 100644 --- a/src/components/Instructions.jsx +++ b/src/components/Instructions.jsx @@ -19,18 +19,18 @@ export default function Instructions({
-
- { - isEditing ? - : - markdownToReact(instructions) - } -
+ { + isEditing ? + : +
+ {markdownToReact(instructions)} +
+ }
); } diff --git a/src/components/InstructionsEditor.jsx b/src/components/InstructionsEditor.jsx index e89eadc00d..e4c1c87202 100644 --- a/src/components/InstructionsEditor.jsx +++ b/src/components/InstructionsEditor.jsx @@ -39,11 +39,15 @@ export default class InstructionsEditor extends React.Component { {t('workspace.components.instructions.cancel')}
-