mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-20 01:38:19 +08:00
ui-default: init
将ui-default合并至主存储库便于维护
This commit is contained in:
1 parent
6317c9057f
commit
5faa54c7ca
564 files changed
+23083
-14
No files matched your search
@@ -0,0 +1,17 @@
|
||||
.scratchpad__data-input
|
||||
border: 0
|
||||
border-left: 3px solid #E0E0E0
|
||||
background: #F8F8F8
|
||||
padding: rem(8px 4px)
|
||||
font-family: $code-font-family
|
||||
font-size: rem($font-size-secondary)
|
||||
outline: 0
|
||||
resize: none
|
||||
color: #666
|
||||
height: 100%
|
||||
width: 100%
|
||||
transition: background-color .2s linear, border-color .2s linear
|
||||
|
||||
&:focus
|
||||
background: #FFF
|
||||
border-color: lighten($primary-color, 70%)
|
||||
@@ -0,0 +1,34 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
|
||||
export default function DataInputComponent(props) {
|
||||
const {
|
||||
title,
|
||||
value,
|
||||
onChange,
|
||||
className,
|
||||
...rest
|
||||
} = props;
|
||||
const cn = classNames(className, 'flex-col flex-fill');
|
||||
return (
|
||||
<div {...rest} className={cn}>
|
||||
<textarea
|
||||
className="scratchpad__data-input"
|
||||
wrap="off"
|
||||
value={value}
|
||||
onChange={(ev) => {
|
||||
ev.stopPropagation();
|
||||
onChange(ev.target.value);
|
||||
}}
|
||||
placeholder={title}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
DataInputComponent.propTypes = {
|
||||
title: PropTypes.node,
|
||||
value: PropTypes.string,
|
||||
onChange: PropTypes.func,
|
||||
className: PropTypes.string,
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
.ScratchpadMonacoEditor
|
||||
flex: 1
|
||||
position: relative
|
||||
overflow: hidden
|
||||
|
||||
.cm-s-vjcm
|
||||
position: absolute
|
||||
left: 0
|
||||
top: 0
|
||||
width: 100%
|
||||
height: 100%
|
||||
font-family: $code-font-family
|
||||
font-size: rem($font-size-secondary)
|
||||
line-height: 1.2
|
||||
@@ -0,0 +1,8 @@
|
||||
@import './var.inc.styl'
|
||||
|
||||
.scratchpad__panel-title
|
||||
background: $primary-color
|
||||
line-height: rem($panel-header-height)
|
||||
padding: rem(0 5px)
|
||||
font-size: rem($font-size-small)
|
||||
color: #FFF
|
||||
@@ -0,0 +1,15 @@
|
||||
@import './var.inc.styl'
|
||||
|
||||
.scratchpad__panel-button
|
||||
background: none
|
||||
border: 0
|
||||
line-height: rem($panel-header-height)
|
||||
margin: 0
|
||||
color: #FFF
|
||||
padding: rem(0 5px)
|
||||
cursor: pointer
|
||||
font-size: rem($font-size-small)
|
||||
outline: 0
|
||||
|
||||
&:hover
|
||||
color: $tab-color
|
||||
@@ -0,0 +1,19 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
|
||||
export default function PanelButtonComponent(props) {
|
||||
const {
|
||||
className,
|
||||
children,
|
||||
...rest
|
||||
} = props;
|
||||
const cn = classNames(className, 'scratchpad__panel-button');
|
||||
return (
|
||||
<button {...rest} className={cn}>{children}</button>
|
||||
);
|
||||
}
|
||||
|
||||
PanelButtonComponent.propTypes = {
|
||||
className: PropTypes.string,
|
||||
children: PropTypes.node,
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import SplitPaneFillOverlay from 'vj/components/react-splitpane/SplitPaneFillOverlayComponent';
|
||||
|
||||
export default function PanelComponent(props) {
|
||||
const {
|
||||
title,
|
||||
className,
|
||||
children,
|
||||
...rest
|
||||
} = props;
|
||||
const cn = classNames(className, 'flex-col');
|
||||
return (
|
||||
<SplitPaneFillOverlay {...rest} className={cn}>
|
||||
<div className="scratchpad__panel-title">{title}</div>
|
||||
<div className="flex-col flex-fill">{children}</div>
|
||||
</SplitPaneFillOverlay>
|
||||
);
|
||||
}
|
||||
|
||||
PanelComponent.propTypes = {
|
||||
title: PropTypes.node,
|
||||
className: PropTypes.string,
|
||||
children: PropTypes.node,
|
||||
};
|
||||
@@ -0,0 +1,103 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
value: state.editor.code,
|
||||
language: window.MONACO_MODES[state.editor.lang],
|
||||
theme: 'vs-light',
|
||||
mainSize: state.ui.main.size,
|
||||
pretestSize: state.ui.pretest.size,
|
||||
recordSize: state.ui.records.size,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
handleUpdateCode: (code) => {
|
||||
dispatch({
|
||||
type: 'SCRATCHPAD_EDITOR_UPDATE_CODE',
|
||||
payload: code,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(class MonacoEditor extends React.PureComponent {
|
||||
componentDidMount() {
|
||||
const value = this.props.value || '';
|
||||
const { language, theme } = this.props;
|
||||
this.model = monaco.editor.createModel(value, language, monaco.Uri.parse('file://model'));
|
||||
if (this.containerElement) {
|
||||
this.editor = monaco.editor.create(
|
||||
this.containerElement,
|
||||
{
|
||||
theme,
|
||||
lineNumbers: true,
|
||||
glyphMargin: true,
|
||||
lightbulb: {
|
||||
enabled: true,
|
||||
},
|
||||
model: this.model,
|
||||
}
|
||||
);
|
||||
this._subscription = this.editor.onDidChangeModelContent((event) => {
|
||||
if (!this.__prevent_trigger_change_event) {
|
||||
this.props.handleUpdateCode(this.editor.getValue(), event);
|
||||
}
|
||||
});
|
||||
window.editor = this.editor;
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const {
|
||||
value, language, theme, mainSize, recordSize, pretestSize,
|
||||
} = this.props;
|
||||
const { editor, model } = this;
|
||||
if (this.props.value != null && this.props.value !== model.getValue()) {
|
||||
this.__prevent_trigger_change_event = true;
|
||||
editor.pushUndoStop();
|
||||
model.pushEditOperations(
|
||||
[],
|
||||
[
|
||||
{
|
||||
range: model.getFullModelRange(),
|
||||
text: value,
|
||||
},
|
||||
]
|
||||
);
|
||||
editor.pushUndoStop();
|
||||
this.__prevent_trigger_change_event = false;
|
||||
}
|
||||
if (prevProps.language !== language) {
|
||||
monaco.editor.setModelLanguage(model, language);
|
||||
editor.updateOptions({ mode: language });
|
||||
}
|
||||
if (prevProps.theme !== theme) monaco.editor.setTheme(theme);
|
||||
if (editor) {
|
||||
if (prevProps.mainSize !== mainSize
|
||||
|| prevProps.recordSize !== recordSize
|
||||
|| prevProps.pretestSize !== pretestSize) editor.layout();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.editor) this.editor.dispose();
|
||||
if (this.model) this.model.dispose();
|
||||
if (this._subscription) this._subscription.dispose();
|
||||
}
|
||||
|
||||
assignRef = (component) => {
|
||||
this.containerElement = component;
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div
|
||||
ref={this.assignRef}
|
||||
style={{
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
}}
|
||||
className="ScratchpadMonacoEditor"
|
||||
/>
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,53 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import i18n from 'vj/utils/i18n';
|
||||
import Icon from 'vj/components/react/IconComponent';
|
||||
import Panel from './PanelComponent';
|
||||
import DataInput from './DataInputComponent';
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
input: state.pretest.input,
|
||||
output: state.pretest.output,
|
||||
rid: state.pretest.rid,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
handleDataChange(type, value) {
|
||||
dispatch({
|
||||
type: 'SCRATCHPAD_PRETEST_DATA_CHANGE',
|
||||
payload: {
|
||||
type,
|
||||
value,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(class ScratchpadPretestContainer extends React.PureComponent {
|
||||
render() {
|
||||
return (
|
||||
<Panel
|
||||
title={(
|
||||
<span>
|
||||
<Icon name="edit" />
|
||||
{' '}
|
||||
{i18n('Pretest')}
|
||||
</span>
|
||||
)}
|
||||
>
|
||||
<div className="flex-row flex-fill">
|
||||
<DataInput
|
||||
title={i18n('Input')}
|
||||
value={this.props.input}
|
||||
onChange={(v) => this.props.handleDataChange('input', v)}
|
||||
/>
|
||||
<DataInput
|
||||
title={i18n('Output')}
|
||||
value={this.props.output}
|
||||
/>
|
||||
</div>
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,45 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import classNames from 'classnames';
|
||||
import i18n from 'vj/utils/i18n';
|
||||
import Icon from 'vj/components/react/IconComponent';
|
||||
import Panel from './PanelComponent';
|
||||
import ScratchpadRecordsRow from './ScratchpadRecordsRowContainer';
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
rows: state.records.rows,
|
||||
isLoading: state.ui.records.isLoading,
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps)(class ScratchpadRecordsContainer extends React.PureComponent {
|
||||
render() {
|
||||
const cn = classNames('data-table is--full-row scratchpad__records__table', {
|
||||
loading: this.props.isLoading,
|
||||
});
|
||||
return (
|
||||
<Panel
|
||||
title={(
|
||||
<span>
|
||||
<Icon name="flag" />
|
||||
{' '}
|
||||
{i18n('Records')}
|
||||
</span>
|
||||
)}
|
||||
>
|
||||
<table className={cn}>
|
||||
<colgroup>
|
||||
<col className="col--detail" />
|
||||
<col className="col--memory" />
|
||||
<col className="col--time" />
|
||||
<col className="col--at" />
|
||||
</colgroup>
|
||||
<tbody>
|
||||
{this.props.rows.map((rowId) => (
|
||||
<ScratchpadRecordsRow key={rowId} id={rowId} />
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,84 @@
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { connect } from 'react-redux';
|
||||
import TimeAgo from 'timeago-react';
|
||||
import moment from 'moment';
|
||||
import _ from 'lodash';
|
||||
|
||||
import substitute from 'vj/utils/substitute';
|
||||
import emulateAnchorClick from 'vj/utils/emulateAnchorClick';
|
||||
import { parse as parseMongoId } from 'vj/utils/mongoId';
|
||||
import i18n from 'vj/utils/i18n';
|
||||
import * as recordEnum from 'vj/constant/record';
|
||||
|
||||
const shouldShowDetail = (data) => recordEnum.STATUS_SCRATCHPAD_SHOW_DETAIL_FLAGS[data.status];
|
||||
|
||||
const getRecordDetail = (data) => {
|
||||
if (!shouldShowDetail(data)) {
|
||||
return (
|
||||
<span className={`record-status--text ${recordEnum.STATUS_CODES[data.status]}`}>
|
||||
{recordEnum.STATUS_TEXTS[data.status]}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
const stat = _.pick(
|
||||
_.groupBy(data.testCases || [], 'status'),
|
||||
_.keys(recordEnum.STATUS_SCRATCHPAD_SHORT_TEXTS),
|
||||
);
|
||||
return _.map(recordEnum.STATUS_SCRATCHPAD_SHORT_TEXTS, (text, status) => {
|
||||
const count = (stat[status] && stat[status].length) || 0;
|
||||
const cn = classNames('icol icol--stat', {
|
||||
'record-status--text': count > 0,
|
||||
[recordEnum.STATUS_CODES[data.status]]: count > 0,
|
||||
});
|
||||
return (
|
||||
<span key={text} className={cn}>
|
||||
{text}: {count}
|
||||
</span>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
items: state.records.items,
|
||||
});
|
||||
|
||||
const mergeProps = (stateProps, dispatchProps, ownProps) => ({
|
||||
...dispatchProps,
|
||||
data: stateProps.items[ownProps.id],
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, null, mergeProps)(class ScratchpadRecordsRowContainer extends React.PureComponent {
|
||||
handleRowClick(ev, id) {
|
||||
const url = substitute(
|
||||
decodeURIComponent(UiContext.getRecordDetailUrl),
|
||||
{ rid: id }
|
||||
);
|
||||
emulateAnchorClick(ev, url, true);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { data } = this.props;
|
||||
const submitAt = parseMongoId(data._id).timestamp * 1000;
|
||||
return data.hidden ? null : (
|
||||
<tr onClick={(ev) => this.handleRowClick(ev, data._id)}>
|
||||
<td className={`col--detail record-status--border ${recordEnum.STATUS_CODES[data.status]}`}>
|
||||
<span className={`icon record-status--icon ${recordEnum.STATUS_CODES[data.status]}`}></span>
|
||||
<span className="icol icol--pretest"></span>
|
||||
{getRecordDetail(data)}
|
||||
</td>
|
||||
<td className="col--memory">
|
||||
{shouldShowDetail(data) ? `${Math.ceil(data.memory / 1000)} MB` : '-'}
|
||||
</td>
|
||||
<td className="col--time">
|
||||
{shouldShowDetail(data) ? `${(data.time / 1000).toFixed(1)}s` : '-'}
|
||||
</td>
|
||||
<td className="col--at">
|
||||
<time data-tooltip={moment(submitAt).format('YYYY-MM-DD HH:mm:ss')}>
|
||||
<TimeAgo datetime={submitAt} locale={i18n('timeago_locale')} />
|
||||
</time>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,168 @@
|
||||
/* eslint-disable react/static-property-placement */
|
||||
import _ from 'lodash';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import i18n from 'vj/utils/i18n';
|
||||
import request from 'vj/utils/request';
|
||||
import Icon from 'vj/components/react/IconComponent';
|
||||
import Toolbar, {
|
||||
ToolbarItemComponent as ToolbarItem,
|
||||
ToolbarButtonComponent as ToolbarButton,
|
||||
ToolbarSplitComponent as ToolbarSplit,
|
||||
} from './ToolbarComponent';
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
pretestVisible: state.ui.pretest.visible,
|
||||
recordsVisible: state.ui.records.visible,
|
||||
isPosting: state.ui.isPosting,
|
||||
isRunning: state.pretest.isRunning,
|
||||
editorLang: state.editor.lang,
|
||||
editorCode: state.editor.code,
|
||||
pretestInput: state.pretest.input,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
togglePanel(uiElement) {
|
||||
dispatch({
|
||||
type: 'SCRATCHPAD_UI_TOGGLE_VISIBILITY',
|
||||
payload: { uiElement },
|
||||
});
|
||||
},
|
||||
setEditorLanguage(lang) {
|
||||
dispatch({
|
||||
type: 'SCRATCHPAD_EDITOR_SET_LANG',
|
||||
payload: lang,
|
||||
});
|
||||
},
|
||||
postPretest(props) {
|
||||
const req = request.post(UiContext.postPretestUrl, {
|
||||
lang: props.editorLang,
|
||||
code: props.editorCode,
|
||||
input: props.pretestInput,
|
||||
});
|
||||
dispatch({
|
||||
type: 'SCRATCHPAD_POST_PRETEST',
|
||||
payload: req,
|
||||
});
|
||||
},
|
||||
postSubmit(props) {
|
||||
const req = request.post(UiContext.postSubmitUrl, {
|
||||
lang: props.editorLang,
|
||||
code: props.editorCode,
|
||||
});
|
||||
dispatch({
|
||||
type: 'SCRATCHPAD_POST_SUBMIT',
|
||||
payload: req,
|
||||
});
|
||||
},
|
||||
loadSubmissions() {
|
||||
dispatch({
|
||||
type: 'SCRATCHPAD_RECORDS_LOAD_SUBMISSIONS',
|
||||
payload: request.get(UiContext.getSubmissionsUrl),
|
||||
});
|
||||
},
|
||||
handleClickRefresh() {
|
||||
this.loadSubmissions();
|
||||
},
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(class ScratchpadToolbarContainer extends React.PureComponent {
|
||||
static contextTypes = {
|
||||
store: PropTypes.object,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.props.loadSubmissions();
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Toolbar>
|
||||
<ToolbarButton
|
||||
disabled={this.props.isPosting || this.props.isRunning}
|
||||
className="scratchpad__toolbar__pretest"
|
||||
onClick={() => this.props.postPretest(this.props)}
|
||||
data-global-hotkey="f9"
|
||||
data-tooltip={`${i18n('Pretest Your Code')} (F9)`}
|
||||
>
|
||||
<Icon name="debug" />
|
||||
{' '}
|
||||
{i18n('Run Pretest')}
|
||||
{' '}
|
||||
(F9)
|
||||
</ToolbarButton>
|
||||
<ToolbarButton
|
||||
disabled={this.props.isPosting}
|
||||
className="scratchpad__toolbar__submit"
|
||||
onClick={() => this.props.postSubmit(this.props)}
|
||||
data-global-hotkey="f10"
|
||||
data-tooltip={`${i18n('Submit Your Code')} (F10)`}
|
||||
>
|
||||
<Icon name="play" />
|
||||
{' '}
|
||||
{i18n('Submit Solution')}
|
||||
{' '}
|
||||
(F10)
|
||||
</ToolbarButton>
|
||||
<ToolbarButton
|
||||
data-tooltip={i18n('Refresh Records')}
|
||||
className="scratchpad__toolbar__refresh"
|
||||
data-global-hotkey="alt+f"
|
||||
onClick={() => this.props.handleClickRefresh()}
|
||||
>
|
||||
<Icon name="refresh" />
|
||||
{' '}
|
||||
{i18n('Refresh')}
|
||||
{' '}
|
||||
(Alt+F)
|
||||
</ToolbarButton>
|
||||
<ToolbarButton
|
||||
data-global-hotkey="alt+q"
|
||||
data-tooltip={`${i18n('Quit Scratchpad')} (Alt+Q)`}
|
||||
name="problem-sidebar__quit-scratchpad"
|
||||
>
|
||||
<Icon name="close" />
|
||||
{' '}
|
||||
{i18n('Exit')}
|
||||
{' '}
|
||||
(Alt+Q)
|
||||
</ToolbarButton>
|
||||
<ToolbarItem>
|
||||
<select
|
||||
className="select"
|
||||
disabled={this.props.isPosting}
|
||||
value={this.props.editorLang}
|
||||
onChange={(ev) => this.props.setEditorLanguage(ev.target.value)}
|
||||
>
|
||||
{_.map(window.LANG_TEXTS, (val, key) => (
|
||||
<option value={key} key={key}>{val}</option>
|
||||
))}
|
||||
</select>
|
||||
</ToolbarItem>
|
||||
<ToolbarSplit />
|
||||
<ToolbarButton
|
||||
activated={this.props.pretestVisible}
|
||||
onClick={() => this.props.togglePanel('pretest')}
|
||||
data-global-hotkey="alt+p"
|
||||
data-tooltip={`${i18n('Toggle Pretest Panel')} (Alt+P)`}
|
||||
>
|
||||
<Icon name="edit" />
|
||||
{' '}
|
||||
{i18n('Pretest')}
|
||||
</ToolbarButton>
|
||||
<ToolbarButton
|
||||
activated={this.props.recordsVisible}
|
||||
onClick={() => this.props.togglePanel('records')}
|
||||
data-global-hotkey="alt+r"
|
||||
data-tooltip={`${i18n('Toggle Records Panel')} (Alt+R)`}
|
||||
>
|
||||
<Icon name="flag" />
|
||||
{' '}
|
||||
{i18n('Records')}
|
||||
</ToolbarButton>
|
||||
</Toolbar>
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,53 @@
|
||||
.scratchpad__toolbar
|
||||
background: $toolbar-bg
|
||||
padding: rem(8px)
|
||||
font-size: rem($font-size-small)
|
||||
font-weight: normal
|
||||
|
||||
.scratchpad__toolbar__item
|
||||
margin-right: rem(5px)
|
||||
|
||||
.select
|
||||
margin: 0
|
||||
height: auto
|
||||
line-height: 1
|
||||
|
||||
.scratchpad__toolbar__button
|
||||
padding: rem(5px)
|
||||
font-size: rem($font-size-small)
|
||||
font-weight: normal
|
||||
background: none
|
||||
border: 1px solid transparent
|
||||
color: $toolbar-fore
|
||||
outline: 0
|
||||
transition: .2s linear opacity
|
||||
|
||||
// Hack for Firefox bug #984869
|
||||
&>div
|
||||
display: flex
|
||||
flex-direction: row
|
||||
align-items: center
|
||||
|
||||
&.enabled
|
||||
cursor: pointer
|
||||
|
||||
.icon
|
||||
font-size: rem($font-size-icon)
|
||||
margin-right: 2px
|
||||
|
||||
&.enabled:hover
|
||||
color: $toolbar-fore-hover
|
||||
background: $toolbar-bg-hover
|
||||
|
||||
&.activated
|
||||
color: $toolbar-fore-selected !important
|
||||
background: $toolbar-bg-selected !important
|
||||
|
||||
&.disabled
|
||||
pointer-events: none
|
||||
opacity: $toolbar-disabled-opacity
|
||||
|
||||
.scratchpad__toolbar__split
|
||||
width: 1px
|
||||
height: 1rem
|
||||
background: #DDD
|
||||
@@ -0,0 +1,90 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
|
||||
export default function ToolbarComponent(props) {
|
||||
const {
|
||||
className,
|
||||
children,
|
||||
...rest
|
||||
} = props;
|
||||
const cn = classNames(className, 'scratchpad__toolbar flex-row flex-cross-center');
|
||||
return (
|
||||
<div {...rest} className={cn}>{children}</div>
|
||||
);
|
||||
}
|
||||
|
||||
ToolbarComponent.propTypes = {
|
||||
className: PropTypes.string,
|
||||
children: PropTypes.node,
|
||||
};
|
||||
|
||||
export function ToolbarButtonComponent(props) {
|
||||
const {
|
||||
activated,
|
||||
disabled,
|
||||
onClick,
|
||||
className,
|
||||
children,
|
||||
...rest
|
||||
} = props;
|
||||
const cn = classNames(className, 'scratchpad__toolbar__item scratchpad__toolbar__button', {
|
||||
activated,
|
||||
disabled,
|
||||
enabled: !disabled,
|
||||
});
|
||||
return (
|
||||
<button
|
||||
{...rest}
|
||||
tabIndex="-1"
|
||||
className={cn}
|
||||
onClick={() => !disabled && onClick && onClick()}
|
||||
>
|
||||
<div>{children}</div>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
ToolbarButtonComponent.propTypes = {
|
||||
activated: PropTypes.bool,
|
||||
disabled: PropTypes.bool,
|
||||
onClick: PropTypes.func,
|
||||
className: PropTypes.string,
|
||||
children: PropTypes.node,
|
||||
};
|
||||
|
||||
ToolbarButtonComponent.defaultProps = {
|
||||
activated: false,
|
||||
disabled: false,
|
||||
};
|
||||
|
||||
export function ToolbarSplitComponent(props) {
|
||||
const {
|
||||
className,
|
||||
...rest
|
||||
} = props;
|
||||
const cn = classNames(className, 'scratchpad__toolbar__item scratchpad__toolbar__split');
|
||||
return (
|
||||
<div {...rest} className={cn} />
|
||||
);
|
||||
}
|
||||
|
||||
ToolbarSplitComponent.propTypes = {
|
||||
className: PropTypes.string,
|
||||
};
|
||||
|
||||
export function ToolbarItemComponent(props) {
|
||||
const {
|
||||
className,
|
||||
children,
|
||||
...rest
|
||||
} = props;
|
||||
const cn = classNames(className, 'scratchpad__toolbar__item');
|
||||
return (
|
||||
<div {...rest} className={cn}>{children}</div>
|
||||
);
|
||||
}
|
||||
|
||||
ToolbarItemComponent.propTypes = {
|
||||
className: PropTypes.string,
|
||||
children: PropTypes.node,
|
||||
};
|
||||
@@ -0,0 +1,88 @@
|
||||
import React from 'react';
|
||||
import SplitPane from 'react-split-pane';
|
||||
import { connect } from 'react-redux';
|
||||
import _ from 'lodash';
|
||||
import SplitPaneFillOverlay from 'vj/components/react-splitpane/SplitPaneFillOverlayComponent';
|
||||
import Dom from 'vj/components/react/DomComponent';
|
||||
import ScratchpadToolbar from './ScratchpadToolbarContainer';
|
||||
import ScratchpadEditor from './ScratchpadEditorContainer';
|
||||
import ScratchpadPretest from './ScratchpadPretestContainer';
|
||||
import ScratchpadRecords from './ScratchpadRecordsContainer';
|
||||
|
||||
function buildNestedPane([a, ...panes]) {
|
||||
const elements = [
|
||||
a,
|
||||
...panes.filter((p) => p.props.visible),
|
||||
];
|
||||
if (elements.length === 1) {
|
||||
return a;
|
||||
}
|
||||
return elements
|
||||
.reduce((prev, curr) => (
|
||||
<SplitPane
|
||||
split="horizontal"
|
||||
primary="second"
|
||||
defaultSize={curr.props.size}
|
||||
key={curr.element.key}
|
||||
onChange={curr.onChange}
|
||||
>
|
||||
{prev}
|
||||
{curr.element}
|
||||
</SplitPane>
|
||||
));
|
||||
}
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
ui: state.ui,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
changeUiSize: _.debounce((uiElement, size) => {
|
||||
dispatch({
|
||||
type: 'SCRATCHPAD_UI_CHANGE_SIZE',
|
||||
payload: {
|
||||
uiElement,
|
||||
size,
|
||||
},
|
||||
});
|
||||
}, 500),
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(class ScratchpadContainer extends React.PureComponent {
|
||||
handleChangeSize(uiElement, size) {
|
||||
this.props.changeUiSize(uiElement, size);
|
||||
$('#scratchpad').trigger('vjScratchpadRelayout');
|
||||
this.props.ui.pretest.visible = !!this.props.ui.pretest.visible;
|
||||
this.forceUpdate();
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<SplitPane
|
||||
defaultSize={this.props.ui.main.size}
|
||||
minSize={250}
|
||||
split="vertical"
|
||||
primary="second"
|
||||
onChange={(size) => this.handleChangeSize('main', size)}
|
||||
>
|
||||
<Dom className="scratchpad__problem" childDom={$('.problem-content').get(0)} />
|
||||
{buildNestedPane([
|
||||
<SplitPaneFillOverlay key="editor" className="flex-col">
|
||||
<ScratchpadToolbar />
|
||||
<ScratchpadEditor />
|
||||
</SplitPaneFillOverlay>,
|
||||
{
|
||||
props: this.props.ui.pretest,
|
||||
onChange: (size) => this.handleChangeSize('pretest', size),
|
||||
element: <ScratchpadPretest key="pretest" />,
|
||||
},
|
||||
{
|
||||
props: this.props.ui.records,
|
||||
onChange: (size) => this.handleChangeSize('records', size),
|
||||
element: <ScratchpadRecords key="records" />,
|
||||
},
|
||||
])}
|
||||
</SplitPane>
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
export default function reducer(state = {
|
||||
lang: UiContext.codeLang,
|
||||
code: UiContext.codeTemplate,
|
||||
}, action) {
|
||||
switch (action.type) {
|
||||
case 'SCRATCHPAD_EDITOR_UPDATE_CODE': {
|
||||
return {
|
||||
...state,
|
||||
code: action.payload,
|
||||
};
|
||||
}
|
||||
case 'SCRATCHPAD_EDITOR_SET_LANG': {
|
||||
return {
|
||||
...state,
|
||||
lang: action.payload,
|
||||
};
|
||||
}
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { combineReducers } from 'redux';
|
||||
import ui from './ui';
|
||||
import editor from './editor';
|
||||
import pretest from './pretest';
|
||||
import records from './records';
|
||||
|
||||
const reducer = combineReducers({
|
||||
ui,
|
||||
editor,
|
||||
pretest,
|
||||
records,
|
||||
});
|
||||
|
||||
export default reducer;
|
||||
@@ -0,0 +1,29 @@
|
||||
export default function reducer(state = {
|
||||
input: '',
|
||||
output: '',
|
||||
rid: null,
|
||||
}, action) {
|
||||
if (action.type === 'SCRATCHPAD_PRETEST_DATA_CHANGE') {
|
||||
const { type, value } = action.payload;
|
||||
return {
|
||||
...state,
|
||||
[type]: value,
|
||||
};
|
||||
}
|
||||
if (action.type === 'SCRATCHPAD_RECORDS_PUSH') {
|
||||
const { rdoc } = action.payload;
|
||||
if (rdoc._id === state.rid && rdoc.testCases[0]) {
|
||||
return {
|
||||
...state,
|
||||
output: rdoc.testCases[0].message,
|
||||
};
|
||||
}
|
||||
}
|
||||
if (action.type === 'SCRATCHPAD_POST_PRETEST_FULFILLED') {
|
||||
return {
|
||||
...state,
|
||||
rid: action.payload.rid,
|
||||
};
|
||||
}
|
||||
return state;
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import _ from 'lodash';
|
||||
|
||||
export default function reducer(state = {
|
||||
rows: [],
|
||||
items: {},
|
||||
}, action) {
|
||||
switch (action.type) {
|
||||
case 'SCRATCHPAD_RECORDS_LOAD_SUBMISSIONS_FULFILLED': {
|
||||
const { rdocs } = action.payload;
|
||||
return {
|
||||
...state,
|
||||
rows: _.map(rdocs, '_id'),
|
||||
items: _.keyBy(rdocs, '_id'),
|
||||
};
|
||||
}
|
||||
case 'SCRATCHPAD_RECORDS_PUSH': {
|
||||
const { rdoc } = action.payload;
|
||||
const rows = [...state.rows];
|
||||
if (!rows.includes(rdoc._id)) {
|
||||
return {
|
||||
...state,
|
||||
rows: [rdoc._id, ...state.rows],
|
||||
items: {
|
||||
...state.items,
|
||||
[rdoc._id]: rdoc,
|
||||
},
|
||||
};
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
items: {
|
||||
...state.items,
|
||||
[rdoc._id]: rdoc,
|
||||
},
|
||||
};
|
||||
}
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
import Notification from 'vj/components/notification';
|
||||
|
||||
export default function reducer(state = {
|
||||
main: {
|
||||
size: '50%',
|
||||
},
|
||||
pretest: {
|
||||
visible: false,
|
||||
size: 200,
|
||||
},
|
||||
records: {
|
||||
visible: true,
|
||||
size: 200,
|
||||
isLoading: false,
|
||||
},
|
||||
isPosting: false,
|
||||
}, action) {
|
||||
switch (action.type) {
|
||||
case 'SCRATCHPAD_UI_CHANGE_SIZE': {
|
||||
const { uiElement, size } = action.payload;
|
||||
return {
|
||||
...state,
|
||||
[uiElement]: {
|
||||
...state[uiElement],
|
||||
size,
|
||||
},
|
||||
};
|
||||
}
|
||||
case 'SCRATCHPAD_UI_SET_VISIBILITY': {
|
||||
const { uiElement, visibility } = action.payload;
|
||||
return {
|
||||
...state,
|
||||
[uiElement]: {
|
||||
...state[uiElement],
|
||||
visible: visibility,
|
||||
},
|
||||
};
|
||||
}
|
||||
case 'SCRATCHPAD_UI_TOGGLE_VISIBILITY': {
|
||||
const { uiElement } = action.payload;
|
||||
return {
|
||||
...state,
|
||||
[uiElement]: {
|
||||
...state[uiElement],
|
||||
visible: !state[uiElement].visible,
|
||||
},
|
||||
};
|
||||
}
|
||||
case 'SCRATCHPAD_POST_PRETEST_PENDING':
|
||||
case 'SCRATCHPAD_POST_SUBMIT_PENDING': {
|
||||
return {
|
||||
...state,
|
||||
isPosting: true,
|
||||
};
|
||||
}
|
||||
case 'SCRATCHPAD_POST_PRETEST_FULFILLED':
|
||||
case 'SCRATCHPAD_POST_SUBMIT_FULFILLED': {
|
||||
return {
|
||||
...state,
|
||||
isPosting: false,
|
||||
};
|
||||
}
|
||||
case 'SCRATCHPAD_POST_PRETEST_REJECTED':
|
||||
case 'SCRATCHPAD_POST_SUBMIT_REJECTED': {
|
||||
Notification.error(action.payload.message);
|
||||
return {
|
||||
...state,
|
||||
isPosting: false,
|
||||
};
|
||||
}
|
||||
case 'SCRATCHPAD_RECORDS_LOAD_SUBMISSIONS_PENDING': {
|
||||
return {
|
||||
...state,
|
||||
records: {
|
||||
...state.records,
|
||||
isLoading: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
case 'SCRATCHPAD_RECORDS_LOAD_SUBMISSIONS_REJECTED': {
|
||||
Notification.error(action.payload.message);
|
||||
return {
|
||||
...state,
|
||||
records: {
|
||||
...state.records,
|
||||
isLoading: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
case 'SCRATCHPAD_RECORDS_LOAD_SUBMISSIONS_FULFILLED': {
|
||||
return {
|
||||
...state,
|
||||
records: {
|
||||
...state.records,
|
||||
isLoading: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
.scratchpad-container
|
||||
position: absolute
|
||||
background: #FFF
|
||||
|
||||
#scratchpad
|
||||
position: absolute
|
||||
left: 0
|
||||
top: $nav-item-height
|
||||
width: 100%
|
||||
bottom: 0
|
||||
|
||||
.scratchpad__problem
|
||||
position: absolute
|
||||
left: 0
|
||||
top: 0
|
||||
width: 100%
|
||||
height: 100%
|
||||
overflow: auto
|
||||
|
||||
.scratchpad__toolbar__pretest.enabled:hover .icon
|
||||
color: red
|
||||
|
||||
.scratchpad__toolbar__submit.enabled:hover .icon
|
||||
color: green
|
||||
|
||||
.scratchpad__records__table
|
||||
&.loading
|
||||
// make a flash by setting opacity, otherwise users
|
||||
// won't think that refreshing is completed!
|
||||
opacity: 0.6
|
||||
|
||||
.col--at
|
||||
width: rem(120px)
|
||||
|
||||
.col--time,
|
||||
.col--memory
|
||||
width: rem(70px)
|
||||
|
||||
.col--detail
|
||||
border-right: 1px solid $table-border-color
|
||||
|
||||
.icol
|
||||
display: inline-block
|
||||
|
||||
.icol--pretest
|
||||
width: rem(60px)
|
||||
|
||||
.flag
|
||||
font-size: rem(12px)
|
||||
color: #FFF
|
||||
padding: rem(2px 3px)
|
||||
|
||||
.icol--stat
|
||||
color: #AAA
|
||||
width: rem(60px)
|
||||
@@ -0,0 +1,2 @@
|
||||
$panel-header-height = 34px
|
||||
$tab-color = #fff170
|
||||
Reference in new issue
Block a user