-
Notifications
You must be signed in to change notification settings - Fork 141
Add a WYSIWYG-esque editor for instructions #1426
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 5 commits
3073cc9
c1ad7d8
2864084
1544c8e
c81a8cf
4bfae0f
e2588eb
04a6912
903ad12
d911f6c
229e3e1
19a5f33
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -65,6 +65,12 @@ export const startEditingInstructions = createAction( | |
| (_projectKey, timestamp = Date.now()) => ({timestamp}), | ||
| ); | ||
|
|
||
| export const continueEditingInstructions = createAction( | ||
| 'CONTINUE_EDITING_INSTRUCTIONS', | ||
| (projectKey, content) => ({projectKey, content}), | ||
| (_projectKey, timestamp = Date.now()) => ({timestamp}), | ||
|
||
| ); | ||
|
|
||
| export const cancelEditingInstructions = createAction( | ||
| 'CANCEL_EDITING_INSTRUCTIONS', | ||
| ); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,29 +3,25 @@ import PropTypes from 'prop-types'; | |
| import {t} from 'i18next'; | ||
| import bindAll from 'lodash/bindAll'; | ||
|
|
||
| import SimpleMDE from 'react-simplemde-editor'; | ||
|
|
||
| export default class InstructionsEditor extends React.Component { | ||
| constructor() { | ||
| super(); | ||
| bindAll(this, '_handleCancelEditing', '_handleSaveChanges', '_ref'); | ||
| } | ||
|
|
||
| componentDidMount() { | ||
| if (!this.props.instructions) { | ||
| this._editor.focus(); | ||
| } | ||
| bindAll(this, '_handleCancelEditing', '_handleContinueEditing', | ||
| '_handleSaveChanges'); | ||
|
||
| } | ||
|
|
||
| _handleCancelEditing() { | ||
| this.props.onCancelEditing(); | ||
| } | ||
|
|
||
| _handleSaveChanges() { | ||
| const newValue = this._editor.value.trim(); | ||
| this.props.onSaveChanges(this.props.projectKey, newValue); | ||
| this.props.onSaveChanges(this.props.projectKey, this.props.instructions); | ||
| } | ||
|
|
||
| _ref(editorElement) { | ||
| this._editor = editorElement; | ||
| _handleContinueEditing(newValue) { | ||
| this.props.onContinueEditing(this.props.projectKey, newValue); | ||
| } | ||
|
|
||
| render() { | ||
|
|
@@ -46,11 +42,13 @@ export default class InstructionsEditor extends React.Component { | |
| </button> | ||
| </div> | ||
| <div className="instructions-editor__input-container"> | ||
| <textarea | ||
| className="instructions-editor__input" | ||
| defaultValue={this.props.instructions} | ||
| placeholder="Type here..." | ||
| ref={this._ref} | ||
| <SimpleMDE | ||
| options={{ | ||
| autofocus: true, | ||
| spellChecker: false, | ||
| }} | ||
| value={this.props.instructions} | ||
| onChange={this._handleContinueEditing} | ||
| /> | ||
| </div> | ||
| <div className="instructions-editor__footer"> | ||
|
|
@@ -72,5 +70,6 @@ InstructionsEditor.propTypes = { | |
| instructions: PropTypes.string.isRequired, | ||
| projectKey: PropTypes.string.isRequired, | ||
| onCancelEditing: PropTypes.func.isRequired, | ||
| onContinueEditing: PropTypes.func.isRequired, | ||
| onSaveChanges: PropTypes.func.isRequired, | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -388,7 +388,7 @@ body { | |
| .instructions-editor__menu { | ||
| padding: 0.5rem; | ||
| text-align: right; | ||
| background-color: var(--color-light-gray); | ||
| background-color: var(--color-low-contrast-gray); | ||
| } | ||
|
|
||
| .instructions-editor__footer { | ||
|
|
@@ -865,3 +865,11 @@ body { | |
| .u__icon_disabled { | ||
| cursor: default; | ||
| } | ||
|
|
||
|
||
| /** | ||
| * simplemde v1.11.2 | ||
| * Copyright Next Step Webs, Inc. | ||
| * @link https://github.com/NextStepWebs/simplemde-markdown-editor | ||
| * @license MIT | ||
| */ | ||
| /* stylelint-disable-line */ .CodeMirror{color:#000}.CodeMirror-lines{padding:4px 0}.CodeMirror pre{padding:0 4px}.CodeMirror-gutter-filler,.CodeMirror-scrollbar-filler{background-color:#fff}.CodeMirror-gutters{border-right:1px solid #ddd;background-color:#f7f7f7;white-space:nowrap}.CodeMirror-linenumber{padding:0 3px 0 5px;min-width:20px;text-align:right;color:#999;white-space:nowrap}.CodeMirror-guttermarker{color:#000}.CodeMirror-guttermarker-subtle{color:#999}.CodeMirror-cursor{border-left:1px solid #000;border-right:none;width:0}.CodeMirror div.CodeMirror-secondarycursor{border-left:1px solid silver}.cm-fat-cursor .CodeMirror-cursor{width:auto;border:0!important;background:#7e7}.cm-fat-cursor div.CodeMirror-cursors{z-index:1}.cm-animate-fat-cursor{width:auto;border:0;-webkit-animation:blink 1.06s steps(1) infinite;-moz-animation:blink 1.06s steps(1) infinite;animation:blink 1.06s steps(1) infinite;background-color:#7e7}@-moz-keyframes blink{50%{background-color:transparent}}@-webkit-keyframes blink{50%{background-color:transparent}}@keyframes blink{50%{background-color:transparent}}.cm-tab{display:inline-block;text-decoration:inherit}.CodeMirror-ruler{border-left:1px solid #ccc;position:absolute}.cm-s-default .cm-header{color:#00f}.cm-s-default .cm-quote{color:#090}.cm-negative{color:#d44}.cm-positive{color:#292}.cm-header,.cm-strong{font-weight:700}.cm-em{font-style:italic}.cm-link{text-decoration:underline}.cm-strikethrough{text-decoration:line-through}.cm-s-default .cm-keyword{color:#708}.cm-s-default .cm-atom{color:#219}.cm-s-default .cm-number{color:#164}.cm-s-default .cm-def{color:#00f}.cm-s-default .cm-variable-2{color:#05a}.cm-s-default .cm-variable-3{color:#085}.cm-s-default .cm-comment{color:#a50}.cm-s-default .cm-string{color:#a11}.cm-s-default .cm-string-2{color:#f50}.cm-s-default .cm-meta,.cm-s-default .cm-qualifier{color:#555}.cm-s-default .cm-builtin{color:#30a}.cm-s-default .cm-bracket{color:#997}.cm-s-default .cm-tag{color:#170}.cm-s-default .cm-attribute{color:#00c}.cm-s-default .cm-hr{color:#999}.cm-s-default .cm-link{color:#00c}.cm-invalidchar,.cm-s-default .cm-error{color:red}.CodeMirror-composing{border-bottom:2px solid}div.CodeMirror span.CodeMirror-matchingbracket{color:#0f0}div.CodeMirror span.CodeMirror-nonmatchingbracket{color:#f22}.CodeMirror-matchingtag{background:rgba(255,150,0,.3)}.CodeMirror-activeline-background{background:#e8f2ff}.CodeMirror{position:relative;overflow:hidden;background:#fff}.CodeMirror-scroll{overflow:scroll!important;margin-bottom:-30px;margin-right:-30px;padding-bottom:30px;height:100%;outline:0;position:relative}.CodeMirror-sizer{position:relative;border-right:30px solid transparent}.CodeMirror-gutter-filler,.CodeMirror-hscrollbar,.CodeMirror-scrollbar-filler,.CodeMirror-vscrollbar{position:absolute;z-index:6;display:none}.CodeMirror-vscrollbar{right:0;top:0;overflow-x:hidden;overflow-y:scroll}.CodeMirror-hscrollbar{bottom:0;left:0;overflow-y:hidden;overflow-x:scroll}.CodeMirror-scrollbar-filler{right:0;bottom:0}.CodeMirror-gutter-filler{left:0;bottom:0}.CodeMirror-gutters{position:absolute;left:0;top:0;min-height:100%;z-index:3}.CodeMirror-gutter{white-space:normal;height:100%;display:inline-block;vertical-align:top;margin-bottom:-30px}.CodeMirror-gutter-wrapper{position:absolute;z-index:4;background:0 0!important;border:none!important;-webkit-user-select:none;-moz-user-select:none;user-select:none}.CodeMirror-gutter-background{position:absolute;top:0;bottom:0;z-index:4}.CodeMirror-gutter-elt{position:absolute;cursor:default;z-index:4}.CodeMirror-lines{cursor:text;min-height:1px}.CodeMirror pre{-moz-border-radius:0;-webkit-border-radius:0;border-radius:0;border-width:0;background:0 0;font-family:inherit;font-size:inherit;margin:0;white-space:pre;word-wrap:normal;line-height:inherit;color:inherit;z-index:2;position:relative;overflow:visible;-webkit-tap-highlight-color:transparent;-webkit-font-variant-ligatures:none;font-variant-ligatures:none}.CodeMirror-wrap pre{word-wrap:break-word;white-space:pre-wrap;word-break:normal}.CodeMirror-linebackground{position:absolute;left:0;right:0;top:0;bottom:0;z-index:0}.CodeMirror-linewidget{position:relative;z-index:2;overflow:auto}.CodeMirror-code{outline:0}.CodeMirror-gutter,.CodeMirror-gutters,.CodeMirror-linenumber,.CodeMirror-scroll,.CodeMirror-sizer{-moz-box-sizing:content-box;box-sizing:content-box}.CodeMirror-measure{position:absolute;width:100%;height:0;overflow:hidden;visibility:hidden}.CodeMirror-cursor{position:absolute}.CodeMirror-measure pre{position:static}div.CodeMirror-cursors{visibility:hidden;position:relative;z-index:3}.CodeMirror-focused div.CodeMirror-cursors,div.CodeMirror-dragcursors{visibility:visible}.CodeMirror-selected{background:#d9d9d9}.CodeMirror-focused .CodeMirror-selected,.CodeMirror-line::selection,.CodeMirror-line>span::selection,.CodeMirror-line>span>span::selection{background:#d7d4f0}.CodeMirror-crosshair{cursor:crosshair}.CodeMirror-line::-moz-selection,.CodeMirror-line>span::-moz-selection,.CodeMirror-line>span>span::-moz-selection{background:#d7d4f0}.cm-searching{background:#ffa;background:rgba(255,255,0,.4)}.cm-force-border{padding-right:.1px}@media print{.CodeMirror div.CodeMirror-cursors{visibility:hidden}}.cm-tab-wrap-hack:after{content:''}span.CodeMirror-selectedtext{background:0 0}.CodeMirror{height:auto;min-height:560px;border:1px solid #ddd;border-bottom-left-radius:4px;border-bottom-right-radius:4px;padding:10px;font:inherit;z-index:1}.CodeMirror-scroll{min-height:560px}.CodeMirror-fullscreen{background:#fff;position:fixed!important;top:50px;left:0;right:0;bottom:0;height:auto;z-index:9}.CodeMirror-sided{width:50%!important}.editor-toolbar{position:relative;opacity:.6;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;-o-user-select:none;user-select:none;padding:0 10px;border-top:1px solid #bbb;border-left:1px solid #bbb;border-right:1px solid #bbb;border-top-left-radius:4px;border-top-right-radius:4px}.editor-toolbar:after,.editor-toolbar:before{display:block;content:' ';height:1px}.editor-toolbar:before{margin-bottom:8px}.editor-toolbar:after{margin-top:8px}.editor-toolbar:hover,.editor-wrapper input.title:focus,.editor-wrapper input.title:hover{opacity:.8}.editor-toolbar.fullscreen{width:100%;height:50px;overflow-x:auto;overflow-y:hidden;white-space:nowrap;padding-top:10px;padding-bottom:10px;box-sizing:border-box;background:#fff;border:0;position:fixed;top:0;left:0;opacity:1;z-index:9}.editor-toolbar.fullscreen::before{width:20px;height:50px;background:-moz-linear-gradient(left,rgba(255,255,255,1) 0,rgba(255,255,255,0) 100%);background:-webkit-gradient(linear,left top,right top,color-stop(0,rgba(255,255,255,1)),color-stop(100%,rgba(255,255,255,0)));background:-webkit-linear-gradient(left,rgba(255,255,255,1) 0,rgba(255,255,255,0) 100%);background:-o-linear-gradient(left,rgba(255,255,255,1) 0,rgba(255,255,255,0) 100%);background:-ms-linear-gradient(left,rgba(255,255,255,1) 0,rgba(255,255,255,0) 100%);background:linear-gradient(to right,rgba(255,255,255,1) 0,rgba(255,255,255,0) 100%);position:fixed;top:0;left:0;margin:0;padding:0}.editor-toolbar.fullscreen::after{width:20px;height:50px;background:-moz-linear-gradient(left,rgba(255,255,255,0) 0,rgba(255,255,255,1) 100%);background:-webkit-gradient(linear,left top,right top,color-stop(0,rgba(255,255,255,0)),color-stop(100%,rgba(255,255,255,1)));background:-webkit-linear-gradient(left,rgba(255,255,255,0) 0,rgba(255,255,255,1) 100%);background:-o-linear-gradient(left,rgba(255,255,255,0) 0,rgba(255,255,255,1) 100%);background:-ms-linear-gradient(left,rgba(255,255,255,0) 0,rgba(255,255,255,1) 100%);background:linear-gradient(to right,rgba(255,255,255,0) 0,rgba(255,255,255,1) 100%);position:fixed;top:0;right:0;margin:0;padding:0}.editor-toolbar a{display:inline-block;text-align:center;text-decoration:none!important;color:#2c3e50!important;width:30px;height:30px;margin:0;border:1px solid transparent;border-radius:3px;cursor:pointer}.editor-toolbar a.active,.editor-toolbar a:hover{background:#fcfcfc;border-color:#95a5a6}.editor-toolbar a:before{line-height:30px}.editor-toolbar i.separator{display:inline-block;width:0;border-left:1px solid #d9d9d9;border-right:1px solid #fff;color:transparent;text-indent:-10px;margin:0 6px}.editor-toolbar a.fa-header-x:after{font-family:Arial,"Helvetica Neue",Helvetica,sans-serif;font-size:65%;vertical-align:text-bottom;position:relative;top:2px}.editor-toolbar a.fa-header-1:after{content:"1"}.editor-toolbar a.fa-header-2:after{content:"2"}.editor-toolbar a.fa-header-3:after{content:"3"}.editor-toolbar a.fa-header-bigger:after{content:"▲"}.editor-toolbar a.fa-header-smaller:after{content:"▼"}.editor-toolbar.disabled-for-preview a:not(.no-disable){pointer-events:none;background:#fff;border-color:transparent;text-shadow:inherit}@media only screen and (max-width:700px){.editor-toolbar a.no-mobile{display:none}}.editor-statusbar{padding:8px 10px;font-size:12px;color:#959694;text-align:right}.editor-statusbar span{display:inline-block;min-width:4em;margin-left:1em}.editor-preview,.editor-preview-side{padding:10px;background:#fafafa;overflow:auto;display:none;box-sizing:border-box}.editor-statusbar .lines:before{content:'lines: '}.editor-statusbar .words:before{content:'words: '}.editor-statusbar .characters:before{content:'characters: '}.editor-preview{position:absolute;width:100%;height:100%;top:0;left:0;z-index:7}.editor-preview-side{position:fixed;bottom:0;width:50%;top:50px;right:0;z-index:9;border:1px solid #ddd}.editor-preview-active,.editor-preview-active-side{display:block}.editor-preview-side>p,.editor-preview>p{margin-top:0}.editor-preview pre,.editor-preview-side pre{background:#eee;margin-bottom:10px}.editor-preview table td,.editor-preview table th,.editor-preview-side table td,.editor-preview-side table th{border:1px solid #ddd;padding:5px}.CodeMirror .CodeMirror-code .cm-tag{color:#63a35c}.CodeMirror .CodeMirror-code .cm-attribute{color:#795da3}.CodeMirror .CodeMirror-code .cm-string{color:#183691}.CodeMirror .CodeMirror-selected{background:#d9d9d9}.CodeMirror .CodeMirror-code .cm-header-1{font-size:200%;line-height:200%}.CodeMirror .CodeMirror-code .cm-header-2{font-size:160%;line-height:160%}.CodeMirror .CodeMirror-code .cm-header-3{font-size:125%;line-height:125%}.CodeMirror .CodeMirror-code .cm-header-4{font-size:110%;line-height:110%}.CodeMirror .CodeMirror-code .cm-comment{background:rgba(0,0,0,.05);border-radius:2px}.CodeMirror .CodeMirror-code .cm-link{color:#7f8c8d}.CodeMirror .CodeMirror-code .cm-url{color:#aab2b3}.CodeMirror .CodeMirror-code .cm-strikethrough{text-decoration:line-through}.CodeMirror .CodeMirror-placeholder{opacity:.5}.CodeMirror .cm-spell-error:not(.cm-url):not(.cm-comment):not(.cm-tag):not(.cm-word){background:rgba(255,0,0,.15)} | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,6 +11,7 @@ export const DEFAULT_WORKSPACE = new Immutable.Map({ | |
| columnFlex: DEFAULT_COLUMN_FLEX, | ||
| rowFlex: DEFAULT_ROW_FLEX, | ||
| isDraggingColumnDivider: false, | ||
| displayedInstructions: '', | ||
|
||
| isEditingInstructions: false, | ||
| }); | ||
|
|
||
|
|
@@ -224,7 +225,16 @@ export default function ui(stateIn, action) { | |
| case 'START_EDITING_INSTRUCTIONS': | ||
| return state.setIn(['workspace', 'isEditingInstructions'], true); | ||
|
|
||
| case 'CONTINUE_EDITING_INSTRUCTIONS': | ||
| return state.setIn( | ||
| ['workspace', 'displayedInstructions'], | ||
| action.payload.content, | ||
| ); | ||
|
|
||
| case 'CANCEL_EDITING_INSTRUCTIONS': | ||
| return state.setIn(['workspace', 'isEditingInstructions'], false). | ||
| setIn(['workspace', 'displayedInstructions'], ''); | ||
|
|
||
| case 'UPDATE_PROJECT_INSTRUCTIONS': | ||
| return state.setIn(['workspace', 'isEditingInstructions'], false); | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,9 +1,24 @@ | ||
| import {createSelector} from 'reselect'; | ||
| import getCurrentProjectKey from './getCurrentProjectKey'; | ||
| import getCurrentProjectInstructionsUnsaved | ||
| from './getCurrentProjectInstructionsUnsaved'; | ||
| import isEditingInstructions from './isEditingInstructions'; | ||
| import getProjects from './getProjects'; | ||
|
|
||
| export default createSelector( | ||
| [getCurrentProjectKey, getProjects], | ||
| (projectKey, projects) => | ||
| projectKey ? projects.getIn([projectKey, 'instructions']) : '', | ||
| [ | ||
| getCurrentProjectKey, getProjects, | ||
| isEditingInstructions, getCurrentProjectInstructionsUnsaved, | ||
|
||
| ], | ||
| (projectKey, projects, isEditing, instructionUnsaved) => { | ||
| if (!projectKey) { | ||
| return ''; | ||
| } | ||
|
|
||
| if (isEditing && instructionUnsaved) { | ||
| return instructionUnsaved; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So what’s the thinking behind changing the semantics of this selector? I’m a little wary of this as the more complex behavior may not be obvious/expected from the call site. If there are situations in which we do want a selector for “bleeding edge instructions” I would propose creating a new selector whose name describes its behavior more explicitly. But I’m down to be convinced otherwise! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I did add getCurrentProjectinstructionsUnsaved, and note that this selector invokes that one, but I did like that this was all pulled when the HOC is composed like this: function mapStateToProps(state) {
return {
instructions: getCurrentProjectInstructions(state),
isEditing: isEditingInstructions(state),
isOpen: !getHiddenUIComponents(state).includes('instructions'),
projectKey: getCurrentProjectKey(state),
};
}So this selector sort of becomes the router on which instructions to pull, without having to change the existing logic or add another wrapper on top of those two. I think if this needs to get any more complex in the future it would make sense to break it out, but for now I added some comments explaining. If you still want me to update it, happy to do it 👍 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I feel you on the appeal of having a “router” selector for sure. My concern is just making this selector that router—the name implies the more straightforward previous behavior. An example of where this would be an issue is if So, I would propose creating a new selector that composes That said, I am also not totally convinced we want to be routing the unsaved instructions through the WDYT? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. All good points. I would be fine with putting it in the render of the |
||
| } | ||
|
|
||
| return projectKey ? projects.getIn([projectKey, 'instructions']) : ''; | ||
| }, | ||
| ); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| export default function getCurrentProjectinstructionsUnsaved(state) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Small thing, but I think There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yep, I feel you, but I think clarity is again the first-order concern : ) |
||
| return state.getIn(['ui', 'workspace', 'displayedInstructions']); | ||
| } | ||

There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wasn’t sure what
continueEditingInstructionswas based on the name (had to look at where it was used)—maybe we could do something more explicit/verbose likeupdateInProgressInstructions?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is of course the camel case version of the action name, and for the action I was following the pattern of the actions
{verb}_EDITING_INSTRUCTIONS. Other verbs arestartandcancel. You would rather break that pattern? It just seems a bit less intuitive but I'm fine with it. So it would beUPDATE_IN_PROGRESS_INSTRUCTIONS? I figurecontinueEditingInstructionsfits in nicely withstartEditingInstructionsandcancelEditingInstructions. Another idea for something more verbose iscontinueEditingUnsavedInstructions.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Definitely understand the appeal of the symmetry, but ultimately I think clarity/expressiveness is the overriding concern here. Given that you went with
draftInstructionsfor the property name (which I like) I would proposeupdateDraftInstructionshere. WDYT?