feat: add action row for challenges with ERMs on desktop (#39377)

This commit is contained in:
Ahmad Abdolsaheb
2020-08-12 13:11:17 +03:00
committed by Mrugesh Mohapatra
parent 69e3e138f6
commit a1a051bd3a
11 changed files with 267 additions and 177 deletions

View File

@ -0,0 +1,13 @@
import React from 'react';
import EditorTabs from './EditorTabs';
const ActionRow = () => (
<div className='action-row'>
<EditorTabs />
</div>
);
ActionRow.displayName = 'ActionRow';
export default ActionRow;

View File

@ -1,12 +1,14 @@
import React, { Component } from 'react'; import React, { Component, Fragment } from 'react';
import { ReflexContainer, ReflexSplitter, ReflexElement } from 'react-reflex'; import { ReflexContainer, ReflexSplitter, ReflexElement } from 'react-reflex';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { first } from 'lodash';
import EditorTabs from './EditorTabs';
import ActionRow from './ActionRow';
const propTypes = { const propTypes = {
challengeFile: PropTypes.shape({ challengeFiles: PropTypes.object,
key: PropTypes.string
}),
editor: PropTypes.element, editor: PropTypes.element,
hasEditableBoundries: PropTypes.bool,
hasPreview: PropTypes.bool, hasPreview: PropTypes.bool,
instructions: PropTypes.element, instructions: PropTypes.element,
preview: PropTypes.element, preview: PropTypes.element,
@ -24,17 +26,26 @@ const reflexProps = {
}; };
class DesktopLayout extends Component { class DesktopLayout extends Component {
getChallengeFile() {
const { challengeFiles } = this.props;
return first(Object.keys(challengeFiles).map(key => challengeFiles[key]));
}
render() { render() {
const { const {
resizeProps, resizeProps,
instructions, instructions,
challengeFile,
editor, editor,
testOutput, testOutput,
hasPreview, hasPreview,
preview preview,
hasEditableBoundries
} = this.props; } = this.props;
const challengeFile = this.getChallengeFile();
return ( return (
<Fragment>
{hasEditableBoundries && <ActionRow />}
<ReflexContainer className='desktop-layout' orientation='vertical'> <ReflexContainer className='desktop-layout' orientation='vertical'>
<ReflexElement flex={1} {...resizeProps}> <ReflexElement flex={1} {...resizeProps}>
{instructions} {instructions}
@ -44,7 +55,12 @@ class DesktopLayout extends Component {
{challengeFile && ( {challengeFile && (
<ReflexContainer key={challengeFile.key} orientation='horizontal'> <ReflexContainer key={challengeFile.key} orientation='horizontal'>
<ReflexElement flex={1} {...reflexProps} {...resizeProps}> <ReflexElement flex={1} {...reflexProps} {...resizeProps}>
{
<Fragment>
{!hasEditableBoundries && <EditorTabs />}
{editor} {editor}
</Fragment>
}
</ReflexElement> </ReflexElement>
<ReflexSplitter propagate={true} {...resizeProps} /> <ReflexSplitter propagate={true} {...resizeProps} />
<ReflexElement flex={0.25} {...reflexProps} {...resizeProps}> <ReflexElement flex={0.25} {...reflexProps} {...resizeProps}>
@ -60,6 +76,7 @@ class DesktopLayout extends Component {
</ReflexElement> </ReflexElement>
)} )}
</ReflexContainer> </ReflexContainer>
</Fragment>
); );
} }
} }

View File

@ -435,7 +435,7 @@ class Editor extends Component {
domNode.setAttribute('aria-hidden', true); domNode.setAttribute('aria-hidden', true);
domNode.style.background = 'yellow'; domNode.style.background = 'lightYellow';
domNode.style.left = this._editor.getLayoutInfo().contentLeft + 'px'; domNode.style.left = this._editor.getLayoutInfo().contentLeft + 'px';
domNode.style.width = this._editor.getLayoutInfo().contentWidth + 'px'; domNode.style.width = this._editor.getLayoutInfo().contentWidth + 'px';
domNode.style.top = this.getViewZoneTop(); domNode.style.top = this.getViewZoneTop();
@ -459,8 +459,6 @@ class Editor extends Component {
outputNode.style.zIndex = '10'; outputNode.style.zIndex = '10';
outputNode.setAttribute('aria-hidden', true); outputNode.setAttribute('aria-hidden', true);
outputNode.style.background = 'var(--secondary-background)';
outputNode.style.left = this._editor.getLayoutInfo().contentLeft + 'px'; outputNode.style.left = this._editor.getLayoutInfo().contentLeft + 'px';
outputNode.style.width = this._editor.getLayoutInfo().contentWidth + 'px'; outputNode.style.width = this._editor.getLayoutInfo().contentWidth + 'px';
outputNode.style.top = this.getOutputZoneTop(); outputNode.style.top = this.getOutputZoneTop();

View File

@ -1,9 +1,17 @@
import React from 'react'; import React, { Component } from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { createSelector } from 'reselect';
import {
toggleVisibleEditor,
visibleEditorsSelector,
challengeFilesSelector
} from '../redux';
const propTypes = { const propTypes = {
challengeFiles: PropTypes.object.isRequired, challengeFiles: PropTypes.object.isRequired,
toggleTab: PropTypes.func.isRequired, toggleVisibleEditor: PropTypes.func.isRequired,
visibleEditors: PropTypes.shape({ visibleEditors: PropTypes.shape({
indexjs: PropTypes.bool, indexjs: PropTypes.bool,
indexjsx: PropTypes.bool, indexjsx: PropTypes.bool,
@ -12,13 +20,29 @@ const propTypes = {
}) })
}; };
const EditorTabs = ({ challengeFiles, toggleTab, visibleEditors }) => ( const mapStateToProps = createSelector(
visibleEditorsSelector,
challengeFilesSelector,
(visibleEditors, challengeFiles) => ({
visibleEditors,
challengeFiles
})
);
const mapDispatchToProps = {
toggleVisibleEditor
};
class EditorTabs extends Component {
render() {
const { challengeFiles, toggleVisibleEditor, visibleEditors } = this.props;
return (
<div className='monaco-editor-tabs'> <div className='monaco-editor-tabs'>
{challengeFiles['indexjsx'] && ( {challengeFiles['indexjsx'] && (
<button <button
aria-selected={visibleEditors.indexjsx} aria-selected={visibleEditors.indexjsx}
className='monaco-editor-tab' className='monaco-editor-tab'
onClick={() => toggleTab('indexjsx')} onClick={() => toggleVisibleEditor('indexjsx')}
role='tab' role='tab'
> >
script.jsx script.jsx
@ -28,7 +52,7 @@ const EditorTabs = ({ challengeFiles, toggleTab, visibleEditors }) => (
<button <button
aria-selected={visibleEditors.indexhtml} aria-selected={visibleEditors.indexhtml}
className='monaco-editor-tab' className='monaco-editor-tab'
onClick={() => toggleTab('indexhtml')} onClick={() => toggleVisibleEditor('indexhtml')}
role='tab' role='tab'
> >
index.html index.html
@ -38,7 +62,7 @@ const EditorTabs = ({ challengeFiles, toggleTab, visibleEditors }) => (
<button <button
aria-selected={visibleEditors.indexcss} aria-selected={visibleEditors.indexcss}
className='monaco-editor-tab' className='monaco-editor-tab'
onClick={() => toggleTab('indexcss')} onClick={() => toggleVisibleEditor('indexcss')}
role='tab' role='tab'
> >
styles.css styles.css
@ -48,16 +72,21 @@ const EditorTabs = ({ challengeFiles, toggleTab, visibleEditors }) => (
<button <button
aria-selected={visibleEditors.indexjs} aria-selected={visibleEditors.indexjs}
className='monaco-editor-tab' className='monaco-editor-tab'
onClick={() => toggleTab('indexjs')} onClick={() => toggleVisibleEditor('indexjs')}
role='tab' role='tab'
> >
script.js script.js
</button> </button>
)} )}
</div> </div>
); );
}
}
EditorTabs.displayName = 'EditorTabs'; EditorTabs.displayName = 'EditorTabs';
EditorTabs.propTypes = propTypes; EditorTabs.propTypes = propTypes;
export default EditorTabs; export default connect(
mapStateToProps,
mapDispatchToProps
)(EditorTabs);

View File

@ -7,6 +7,7 @@ import ToolPanel from '../components/Tool-Panel';
import { createStructuredSelector } from 'reselect'; import { createStructuredSelector } from 'reselect';
import { currentTabSelector, moveToTab } from '../redux'; import { currentTabSelector, moveToTab } from '../redux';
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
import EditorTabs from './EditorTabs';
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
currentTab: currentTabSelector currentTab: currentTabSelector
@ -66,6 +67,7 @@ class MobileLayout extends Component {
{instructions} {instructions}
</TabPane> </TabPane>
<TabPane eventKey={2} title='Code' {...editorTabPaneProps}> <TabPane eventKey={2} title='Code' {...editorTabPaneProps}>
<EditorTabs />
{editor} {editor}
</TabPane> </TabPane>
<TabPane eventKey={3} title='Tests' {...editorTabPaneProps}> <TabPane eventKey={3} title='Tests' {...editorTabPaneProps}>

View File

@ -3,7 +3,7 @@ import React, { Component, Suspense } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { ReflexContainer, ReflexElement, ReflexSplitter } from 'react-reflex'; import { ReflexContainer, ReflexElement, ReflexSplitter } from 'react-reflex';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { isEmpty } from 'lodash'; import { getTargetEditor } from '../utils/getTargetEditor';
import { isDonationModalOpenSelector, userSelector } from '../../../redux'; import { isDonationModalOpenSelector, userSelector } from '../../../redux';
import { import {
canFocusEditorSelector, canFocusEditorSelector,
@ -13,13 +13,12 @@ import {
saveEditorContent, saveEditorContent,
setAccessibilityMode, setAccessibilityMode,
setEditorFocusability, setEditorFocusability,
visibleEditorsSelector,
updateFile updateFile
} from '../redux'; } from '../redux';
import './editor.css'; import './editor.css';
import { Loader } from '../../../components/helpers'; import { Loader } from '../../../components/helpers';
import EditorTabs from './EditorTabs';
import Editor from './Editor'; import Editor from './Editor';
import { toSortedArray } from '../../../../../utils/sort-files';
const propTypes = { const propTypes = {
canFocus: PropTypes.bool, canFocus: PropTypes.bool,
@ -45,16 +44,31 @@ const propTypes = {
setAccessibilityMode: PropTypes.func.isRequired, setAccessibilityMode: PropTypes.func.isRequired,
setEditorFocusability: PropTypes.func, setEditorFocusability: PropTypes.func,
theme: PropTypes.string, theme: PropTypes.string,
updateFile: PropTypes.func.isRequired updateFile: PropTypes.func.isRequired,
visibleEditors: PropTypes.shape({
indexjs: PropTypes.bool,
indexjsx: PropTypes.bool,
indexcss: PropTypes.bool,
indexhtml: PropTypes.bool
})
}; };
const mapStateToProps = createSelector( const mapStateToProps = createSelector(
visibleEditorsSelector,
canFocusEditorSelector, canFocusEditorSelector,
consoleOutputSelector, consoleOutputSelector,
inAccessibilityModeSelector, inAccessibilityModeSelector,
isDonationModalOpenSelector, isDonationModalOpenSelector,
userSelector, userSelector,
(canFocus, output, accessibilityMode, open, { theme = 'default' }) => ({ (
visibleEditors,
canFocus,
output,
accessibilityMode,
open,
{ theme = 'default' }
) => ({
visibleEditors,
canFocus: open ? false : canFocus, canFocus: open ? false : canFocus,
output, output,
inAccessibilityMode: accessibilityMode, inAccessibilityMode: accessibilityMode,
@ -70,15 +84,6 @@ const mapDispatchToProps = {
updateFile updateFile
}; };
function getTargetEditor(challengeFiles) {
let targetEditor = Object.values(challengeFiles).find(
({ editableRegionBoundaries }) => !isEmpty(editableRegionBoundaries)
)?.key;
// fallback for when there is no editable region.
return targetEditor || toSortedArray(challengeFiles)[0].key;
}
class MultifileEditor extends Component { class MultifileEditor extends Component {
constructor(...props) { constructor(...props) {
super(...props); super(...props);
@ -137,13 +142,6 @@ class MultifileEditor extends Component {
} }
}; };
const { challengeFiles } = this.props;
const targetEditor = getTargetEditor(challengeFiles);
this.state = {
visibleEditors: { [targetEditor]: true }
};
// TODO: we might want to store the current editor here // TODO: we might want to store the current editor here
this.focusOnEditor = this.focusOnEditor.bind(this); this.focusOnEditor = this.focusOnEditor.bind(this);
} }
@ -159,15 +157,6 @@ class MultifileEditor extends Component {
// this._editor.focus(); // this._editor.focus();
} }
toggleTab = newFileKey => {
this.setState(state => ({
visibleEditors: {
...state.visibleEditors,
[newFileKey]: !state.visibleEditors[newFileKey]
}
}));
};
componentWillUnmount() { componentWillUnmount() {
// this.setState({ fileKey: null }); // this.setState({ fileKey: null });
this.data = null; this.data = null;
@ -180,9 +169,9 @@ class MultifileEditor extends Component {
description, description,
editorRef, editorRef,
theme, theme,
resizeProps resizeProps,
visibleEditors
} = this.props; } = this.props;
const { visibleEditors } = this.state;
const editorTheme = theme === 'night' ? 'vs-dark-custom' : 'vs-custom'; const editorTheme = theme === 'night' ? 'vs-dark-custom' : 'vs-custom';
// TODO: the tabs mess up the rendering (scroll doesn't work properly and // TODO: the tabs mess up the rendering (scroll doesn't work properly and
// the in-editor description) // the in-editor description)
@ -207,21 +196,13 @@ class MultifileEditor extends Component {
// TODO: the tabs mess up the rendering (scroll doesn't work properly and // TODO: the tabs mess up the rendering (scroll doesn't work properly and
// the in-editor description) // the in-editor description)
const targetEditor = getTargetEditor(challengeFiles); const targetEditor = getTargetEditor(challengeFiles);
return ( return (
<ReflexContainer <ReflexContainer
orientation='horizontal' orientation='horizontal'
{...reflexProps} {...reflexProps}
{...resizeProps} {...resizeProps}
> >
<ReflexElement flex={0.1}> <ReflexElement flex={10} {...reflexProps} {...resizeProps}>
<EditorTabs
challengeFiles={challengeFiles}
toggleTab={this.toggleTab.bind(this)}
visibleEditors={visibleEditors}
/>
</ReflexElement>
<ReflexElement flex={0.9} {...reflexProps} {...resizeProps}>
<ReflexContainer orientation='vertical'> <ReflexContainer orientation='vertical'>
{visibleEditors.indexhtml && ( {visibleEditors.indexhtml && (
<ReflexElement {...reflexProps} {...resizeProps}> <ReflexElement {...reflexProps} {...resizeProps}>

View File

@ -5,7 +5,6 @@ import { createStructuredSelector } from 'reselect';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import Helmet from 'react-helmet'; import Helmet from 'react-helmet';
import { graphql } from 'gatsby'; import { graphql } from 'gatsby';
import { first } from 'lodash';
import Media from 'react-responsive'; import Media from 'react-responsive';
import LearnLayout from '../../../components/layouts/Learn'; import LearnLayout from '../../../components/layouts/Learn';
@ -184,11 +183,6 @@ class ShowClassic extends Component {
getVideoUrl = () => this.getChallenge().videoUrl; getVideoUrl = () => this.getChallenge().videoUrl;
getChallengeFile() {
const { files } = this.props;
return first(Object.keys(files).map(key => files[key]));
}
hasPreview() { hasPreview() {
const { challengeType } = this.getChallenge(); const { challengeType } = this.getChallenge();
return ( return (
@ -229,6 +223,7 @@ class ShowClassic extends Component {
containerRef={this.containerRef} containerRef={this.containerRef}
description={description} description={description}
editorRef={this.editorRef} editorRef={this.editorRef}
hasEditableBoundries={this.hasEditableBoundries()}
resizeProps={this.resizeProps} resizeProps={this.resizeProps}
/> />
) )
@ -255,6 +250,15 @@ class ShowClassic extends Component {
); );
} }
hasEditableBoundries() {
const { files } = this.props;
return Object.values(files).some(
file =>
file.editableRegionBoundaries &&
file.editableRegionBoundaries.length === 2
);
}
render() { render() {
const { const {
fields: { blockName }, fields: { blockName },
@ -265,8 +269,10 @@ class ShowClassic extends Component {
executeChallenge, executeChallenge,
pageContext: { pageContext: {
challengeMeta: { introPath, nextChallengePath, prevChallengePath } challengeMeta: { introPath, nextChallengePath, prevChallengePath }
} },
files
} = this.props; } = this.props;
return ( return (
<Hotkeys <Hotkeys
editorRef={this.editorRef} editorRef={this.editorRef}
@ -295,8 +301,9 @@ class ShowClassic extends Component {
</Media> </Media>
<Media minWidth={MAX_MOBILE_WIDTH + 1}> <Media minWidth={MAX_MOBILE_WIDTH + 1}>
<DesktopLayout <DesktopLayout
challengeFile={this.getChallengeFile()} challengeFiles={files}
editor={this.renderEditor()} editor={this.renderEditor()}
hasEditableBoundries={this.hasEditableBoundries()}
hasPreview={this.hasPreview()} hasPreview={this.hasPreview()}
instructions={this.renderInstructionsPanel({ instructions={this.renderInstructionsPanel({
showToolPanel: true showToolPanel: true

View File

@ -57,3 +57,54 @@
.monaco-menu .monaco-action-bar.vertical .action-label.separator { .monaco-menu .monaco-action-bar.vertical .action-label.separator {
display: none !important; display: none !important;
} }
.monaco-editor-tabs {
display: flex;
padding: 0px 10px;
background-color: var(--primary-background);
border-bottom: 2px solid var(--primary-color);
}
.monaco-editor-tab {
position: relative;
top: 2px;
padding: 4px 16px;
border: 2px solid var(--primary-color);
border-left: none;
background-color: var(--secondary-background);
}
button.monaco-editor-tab:hover {
color: var(--quaternary-color);
background-color: var(--quaternary-background);
}
.monaco-editor-tab:first-child {
border-left: 2px solid var(--primary-color);
}
.monaco-editor-tab-selected {
background-color: var(--primary-background);
border-bottom: 2px solid var(--primary-background);
}
.monaco-editor-tab[role='tab'][aria-selected='true'] {
border-color: var(--secondary-color);
background-color: var(--secondary-color);
color: var(--secondary-background);
}
.action-row {
padding: 10px;
border-bottom: 1px solid var(--quaternary-background);
}
.action-row .monaco-editor-tabs {
padding: 0;
border-bottom: none;
background-color: var(--secondary-background);
}
.action-row .monaco-editor-tabs .monaco-editor-tab {
top: 0;
}

View File

@ -1,53 +1,15 @@
.monaco-editor-tabs { #description {
display: flex; background: var(--secondary-background);
padding: 0px 10px; }
background-color: var(--primary-background); .monaco-editor .margin-view-overlays .line-numbers,
border-bottom: 2px solid var(--primary-color); .monaco-editor .margin-view-overlays .myLineDecoration + .line-numbers {
color: var(--primary-color);
} }
.monaco-editor-tab { [widgetid='my.overlay.widget'] {
position: relative; padding: 10px;
top: 2px;
padding: 4px 16px;
border: 2px solid var(--primary-color);
border-left: none;
background-color: var(--secondary-background);
} }
button.monaco-editor-tab:hover { .vs .monaco-scrollable-element > .scrollbar > .slider {
color: var(--quaternary-color); z-index: 11;
background-color: var(--quaternary-background);
}
.monaco-editor-tab:first-child {
border-left: 2px solid var(--primary-color);
}
.monaco-editor-tab-selected {
background-color: var(--primary-background);
border-bottom: 2px solid var(--primary-background);
}
.monaco-editor-tab[role='tab'][aria-selected='true'] {
border-color: var(--secondary-color);
background-color: var(--secondary-color);
color: var(--secondary-background);
}
.myInlineDecoration {
color: lightgray !important;
cursor: pointer;
text-decoration: underline;
font-weight: bold;
font-style: oblique;
}
.myLineDecoration {
background: lightblue;
width: 5px !important;
margin-left: 3px;
}
.do-not-edit {
background: grey;
} }

View File

@ -12,6 +12,7 @@ import codeStorageEpic from './code-storage-epic';
import { createExecuteChallengeSaga } from './execute-challenge-saga'; import { createExecuteChallengeSaga } from './execute-challenge-saga';
import { createCurrentChallengeSaga } from './current-challenge-saga'; import { createCurrentChallengeSaga } from './current-challenge-saga';
import { challengeTypes } from '../../../../utils/challengeTypes'; import { challengeTypes } from '../../../../utils/challengeTypes';
import { getTargetEditor } from '../utils/getTargetEditor';
import { completedChallengesSelector } from '../../../redux'; import { completedChallengesSelector } from '../../../redux';
import { isEmpty } from 'lodash'; import { isEmpty } from 'lodash';
@ -20,6 +21,7 @@ export const backendNS = 'backendChallenge';
const initialState = { const initialState = {
canFocusEditor: true, canFocusEditor: true,
visibleEditors: {},
challengeFiles: {}, challengeFiles: {},
challengeMeta: { challengeMeta: {
superBlock: '', superBlock: '',
@ -86,6 +88,7 @@ export const types = createTypes(
'moveToTab', 'moveToTab',
'setEditorFocusability', 'setEditorFocusability',
'toggleVisibleEditor',
'setAccessibilityMode', 'setAccessibilityMode',
'lastBlockChalSubmitted' 'lastBlockChalSubmitted'
@ -179,6 +182,7 @@ export const submitChallenge = createAction(types.submitChallenge);
export const moveToTab = createAction(types.moveToTab); export const moveToTab = createAction(types.moveToTab);
export const setEditorFocusability = createAction(types.setEditorFocusability); export const setEditorFocusability = createAction(types.setEditorFocusability);
export const toggleVisibleEditor = createAction(types.toggleVisibleEditor);
export const setAccessibilityMode = createAction(types.setAccessibilityMode); export const setAccessibilityMode = createAction(types.setAccessibilityMode);
export const lastBlockChalSubmitted = createAction( export const lastBlockChalSubmitted = createAction(
@ -258,6 +262,8 @@ export const challengeDataSelector = state => {
}; };
export const canFocusEditorSelector = state => state[ns].canFocusEditor; export const canFocusEditorSelector = state => state[ns].canFocusEditor;
export const visibleEditorsSelector = state => state[ns].visibleEditors;
export const inAccessibilityModeSelector = state => export const inAccessibilityModeSelector = state =>
state[ns].inAccessibilityMode; state[ns].inAccessibilityMode;
@ -265,7 +271,8 @@ export const reducer = handleActions(
{ {
[types.createFiles]: (state, { payload }) => ({ [types.createFiles]: (state, { payload }) => ({
...state, ...state,
challengeFiles: payload challengeFiles: payload,
visibleEditors: { [getTargetEditor(payload)]: true }
}), }),
[types.updateFile]: ( [types.updateFile]: (
state, state,
@ -399,6 +406,15 @@ export const reducer = handleActions(
...state, ...state,
canFocusEditor: payload canFocusEditor: payload
}), }),
[types.toggleVisibleEditor]: (state, { payload }) => {
return {
...state,
visibleEditors: {
...state.visibleEditors,
[payload]: !state.visibleEditors[payload]
}
};
},
[types.setAccessibilityMode]: (state, { payload }) => ({ [types.setAccessibilityMode]: (state, { payload }) => ({
...state, ...state,
inAccessibilityMode: payload inAccessibilityMode: payload

View File

@ -0,0 +1,14 @@
import { toSortedArray } from '../../../../../utils/sort-files';
import { isEmpty } from 'lodash';
export function getTargetEditor(challengeFiles) {
if (isEmpty(challengeFiles)) return null;
else {
let targetEditor = Object.values(challengeFiles).find(
({ editableRegionBoundaries }) => !isEmpty(editableRegionBoundaries)
)?.key;
// fallback for when there is no editable region.
return targetEditor || toSortedArray(challengeFiles)[0].key;
}
}