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

View File

@ -435,7 +435,7 @@ class Editor extends Component {
domNode.setAttribute('aria-hidden', true);
domNode.style.background = 'yellow';
domNode.style.background = 'lightYellow';
domNode.style.left = this._editor.getLayoutInfo().contentLeft + 'px';
domNode.style.width = this._editor.getLayoutInfo().contentWidth + 'px';
domNode.style.top = this.getViewZoneTop();
@ -459,8 +459,6 @@ class Editor extends Component {
outputNode.style.zIndex = '10';
outputNode.setAttribute('aria-hidden', true);
outputNode.style.background = 'var(--secondary-background)';
outputNode.style.left = this._editor.getLayoutInfo().contentLeft + 'px';
outputNode.style.width = this._editor.getLayoutInfo().contentWidth + 'px';
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 { createSelector } from 'reselect';
import {
toggleVisibleEditor,
visibleEditorsSelector,
challengeFilesSelector
} from '../redux';
const propTypes = {
challengeFiles: PropTypes.object.isRequired,
toggleTab: PropTypes.func.isRequired,
toggleVisibleEditor: PropTypes.func.isRequired,
visibleEditors: PropTypes.shape({
indexjs: PropTypes.bool,
indexjsx: PropTypes.bool,
@ -12,52 +20,73 @@ const propTypes = {
})
};
const EditorTabs = ({ challengeFiles, toggleTab, visibleEditors }) => (
<div className='monaco-editor-tabs'>
{challengeFiles['indexjsx'] && (
<button
aria-selected={visibleEditors.indexjsx}
className='monaco-editor-tab'
onClick={() => toggleTab('indexjsx')}
role='tab'
>
script.jsx
</button>
)}
{challengeFiles['indexhtml'] && (
<button
aria-selected={visibleEditors.indexhtml}
className='monaco-editor-tab'
onClick={() => toggleTab('indexhtml')}
role='tab'
>
index.html
</button>
)}
{challengeFiles['indexcss'] && (
<button
aria-selected={visibleEditors.indexcss}
className='monaco-editor-tab'
onClick={() => toggleTab('indexcss')}
role='tab'
>
styles.css
</button>
)}
{challengeFiles['indexjs'] && (
<button
aria-selected={visibleEditors.indexjs}
className='monaco-editor-tab'
onClick={() => toggleTab('indexjs')}
role='tab'
>
script.js
</button>
)}
</div>
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'>
{challengeFiles['indexjsx'] && (
<button
aria-selected={visibleEditors.indexjsx}
className='monaco-editor-tab'
onClick={() => toggleVisibleEditor('indexjsx')}
role='tab'
>
script.jsx
</button>
)}
{challengeFiles['indexhtml'] && (
<button
aria-selected={visibleEditors.indexhtml}
className='monaco-editor-tab'
onClick={() => toggleVisibleEditor('indexhtml')}
role='tab'
>
index.html
</button>
)}
{challengeFiles['indexcss'] && (
<button
aria-selected={visibleEditors.indexcss}
className='monaco-editor-tab'
onClick={() => toggleVisibleEditor('indexcss')}
role='tab'
>
styles.css
</button>
)}
{challengeFiles['indexjs'] && (
<button
aria-selected={visibleEditors.indexjs}
className='monaco-editor-tab'
onClick={() => toggleVisibleEditor('indexjs')}
role='tab'
>
script.js
</button>
)}
</div>
);
}
}
EditorTabs.displayName = 'EditorTabs';
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 { currentTabSelector, moveToTab } from '../redux';
import { bindActionCreators } from 'redux';
import EditorTabs from './EditorTabs';
const mapStateToProps = createStructuredSelector({
currentTab: currentTabSelector
@ -66,6 +67,7 @@ class MobileLayout extends Component {
{instructions}
</TabPane>
<TabPane eventKey={2} title='Code' {...editorTabPaneProps}>
<EditorTabs />
{editor}
</TabPane>
<TabPane eventKey={3} title='Tests' {...editorTabPaneProps}>

View File

@ -3,7 +3,7 @@ import React, { Component, Suspense } from 'react';
import { connect } from 'react-redux';
import { ReflexContainer, ReflexElement, ReflexSplitter } from 'react-reflex';
import { createSelector } from 'reselect';
import { isEmpty } from 'lodash';
import { getTargetEditor } from '../utils/getTargetEditor';
import { isDonationModalOpenSelector, userSelector } from '../../../redux';
import {
canFocusEditorSelector,
@ -13,13 +13,12 @@ import {
saveEditorContent,
setAccessibilityMode,
setEditorFocusability,
visibleEditorsSelector,
updateFile
} from '../redux';
import './editor.css';
import { Loader } from '../../../components/helpers';
import EditorTabs from './EditorTabs';
import Editor from './Editor';
import { toSortedArray } from '../../../../../utils/sort-files';
const propTypes = {
canFocus: PropTypes.bool,
@ -45,16 +44,31 @@ const propTypes = {
setAccessibilityMode: PropTypes.func.isRequired,
setEditorFocusability: PropTypes.func,
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(
visibleEditorsSelector,
canFocusEditorSelector,
consoleOutputSelector,
inAccessibilityModeSelector,
isDonationModalOpenSelector,
userSelector,
(canFocus, output, accessibilityMode, open, { theme = 'default' }) => ({
(
visibleEditors,
canFocus,
output,
accessibilityMode,
open,
{ theme = 'default' }
) => ({
visibleEditors,
canFocus: open ? false : canFocus,
output,
inAccessibilityMode: accessibilityMode,
@ -70,15 +84,6 @@ const mapDispatchToProps = {
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 {
constructor(...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
this.focusOnEditor = this.focusOnEditor.bind(this);
}
@ -159,15 +157,6 @@ class MultifileEditor extends Component {
// this._editor.focus();
}
toggleTab = newFileKey => {
this.setState(state => ({
visibleEditors: {
...state.visibleEditors,
[newFileKey]: !state.visibleEditors[newFileKey]
}
}));
};
componentWillUnmount() {
// this.setState({ fileKey: null });
this.data = null;
@ -180,9 +169,9 @@ class MultifileEditor extends Component {
description,
editorRef,
theme,
resizeProps
resizeProps,
visibleEditors
} = this.props;
const { visibleEditors } = this.state;
const editorTheme = theme === 'night' ? 'vs-dark-custom' : 'vs-custom';
// TODO: the tabs mess up the rendering (scroll doesn't work properly and
// 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
// the in-editor description)
const targetEditor = getTargetEditor(challengeFiles);
return (
<ReflexContainer
orientation='horizontal'
{...reflexProps}
{...resizeProps}
>
<ReflexElement flex={0.1}>
<EditorTabs
challengeFiles={challengeFiles}
toggleTab={this.toggleTab.bind(this)}
visibleEditors={visibleEditors}
/>
</ReflexElement>
<ReflexElement flex={0.9} {...reflexProps} {...resizeProps}>
<ReflexElement flex={10} {...reflexProps} {...resizeProps}>
<ReflexContainer orientation='vertical'>
{visibleEditors.indexhtml && (
<ReflexElement {...reflexProps} {...resizeProps}>

View File

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

View File

@ -57,3 +57,54 @@
.monaco-menu .monaco-action-bar.vertical .action-label.separator {
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 {
display: flex;
padding: 0px 10px;
background-color: var(--primary-background);
border-bottom: 2px solid var(--primary-color);
#description {
background: var(--secondary-background);
}
.monaco-editor .margin-view-overlays .line-numbers,
.monaco-editor .margin-view-overlays .myLineDecoration + .line-numbers {
color: 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);
[widgetid='my.overlay.widget'] {
padding: 10px;
}
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);
}
.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;
.vs .monaco-scrollable-element > .scrollbar > .slider {
z-index: 11;
}

View File

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