Skip to content

Commit

Permalink
feat(client): implement code editor in the right sidebar (#516)
Browse files Browse the repository at this point in the history
  • Loading branch information
fushar authored Sep 28, 2023
1 parent d30a575 commit 3cf052d
Show file tree
Hide file tree
Showing 11 changed files with 189 additions and 5 deletions.
2 changes: 2 additions & 0 deletions judgels-client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"@blueprintjs/icons": "4.0.0-alpha.0",
"@blueprintjs/select": "4.0.0-alpha.0",
"@svgr/webpack": "5.5.0",
"ace-builds": "^1.26.0",
"babel-eslint": "^10.1.0",
"babel-jest": "^26.6.0",
"babel-loader": "8.1.0",
Expand Down Expand Up @@ -57,6 +58,7 @@
"pretty-bytes": "4.0.2",
"prompts": "2.4.0",
"query-string": "^5.1.0",
"react-ace": "^10.1.0",
"react-app-polyfill": "^2.0.0",
"react-async-script": "^0.9.1",
"react-dev-utils": "^11.0.3",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,13 @@ function ContentWithTopbar({ match, location, className, items }) {
const props = {
exact: item.id === '@',
path: resolveUrl(match.url, item.id),
component: item.component,
};
if (item.component) {
props.component = item.component;
}
if (item.render) {
props.render = item.render;
}
return <RouteC key={item.id} {...props} />;
});

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { Button, Callout, Intent, Tag } from '@blueprintjs/core';
import { BanCircle } from '@blueprintjs/icons';
import { Field, Form } from 'react-final-form';

import { ContentCard } from '../../../ContentCard/ContentCard';
import { MaxCodeLength50KB, Required } from '../../../forms/validations';
import FormAceEditor from '../../../forms/FormAceEditor/FormAceEditor';
import { FormSelect2 } from '../../../forms/FormSelect2/FormSelect2';
import { getAllowedGradingLanguages, gradingLanguageNamesMap } from '../../../../modules/api/gabriel/language.js';

import './ProblemSubmissionEditor.scss';

export function ProblemSubmissionEditor({
config: { sourceKeys, gradingEngine, gradingLanguageRestriction },
onSubmit,
reasonNotAllowedToSubmit,
preferredGradingLanguage,
}) {
const onSubmitEditor = data => {
const sourceFiles = {};
Object.keys(sourceKeys).forEach(key => {
sourceFiles[key] = new File([data.editor], 'solution.cpp', { type: 'text/plain' });
});

return onSubmit({
gradingLanguage: data.gradingLanguage,
sourceFiles,
});
};

const renderEditor = () => {
if (reasonNotAllowedToSubmit) {
return (
<Callout icon={<BanCircle />} className="secondary-info">
<span data-key="reason-not-allowed-to-submit">{this.props.reasonNotAllowedToSubmit}</span>
</Callout>
);
}

const gradingLanguages = getAllowedGradingLanguages(gradingEngine, gradingLanguageRestriction);

let defaultGradingLanguage = preferredGradingLanguage;
if (gradingLanguages.indexOf(defaultGradingLanguage) === -1) {
defaultGradingLanguage = gradingLanguages.length === 0 ? undefined : gradingLanguages[0];
}

const gradingLanguageField = {
name: 'gradingLanguage',
validate: Required,
optionValues: gradingLanguages,
optionNamesMap: gradingLanguageNamesMap,
small: true,
};

const editorField = {
name: 'editor',
validate: MaxCodeLength50KB,
autoFocus: true,
};

const initialValues = {
gradingLanguage: defaultGradingLanguage,
};

return (
<Form onSubmit={onSubmitEditor} initialValues={initialValues}>
{({ values, handleSubmit, submitting }) => (
<form onSubmit={handleSubmit}>
<div className="editor-heading">
<Field component={FormSelect2} {...gradingLanguageField} />
<p>
<Tag intent={Intent.WARNING}>BETA</Tag>
</p>
<p>
<small>Type or paste your code here</small>
</p>
</div>
<Field component={FormAceEditor} {...editorField} gradingLanguage={values.gradingLanguage} />
<Button type="submit" text="Submit" intent={Intent.PRIMARY} loading={submitting} />
</form>
)}
</Form>
);
};

return <ContentCard>{renderEditor()}</ContentCard>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
.editor-heading {
display: flex;
gap: 10px;

.bp4-form-group {
margin: 0;
}

.bp4-tag {
padding-top: 1px;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { FormGroup } from '@blueprintjs/core';
import AceEditor from 'react-ace';
import { connect } from 'react-redux';

import 'ace-builds/src-noconflict/mode-c_cpp';
import 'ace-builds/src-noconflict/mode-plain_text';
import 'ace-builds/src-noconflict/theme-tomorrow_night';
import 'ace-builds/src-noconflict/theme-tomorrow';
import 'ace-builds/src-noconflict/ext-language_tools';

import { getIntent } from '../meta';
import { FormInputValidation } from '../FormInputValidation/FormInputValidation';
import { selectIsDarkMode } from '../../../modules/webPrefs/webPrefsSelectors';
import { getGradingLanguageFamily } from '../../../modules/api/gabriel/language';

import './FormAceEditor.scss';

function FormAceEditor({ input, meta, autoFocus, isDarkMode, gradingLanguage }) {
return (
<FormGroup intent={getIntent(meta)}>
<AceEditor
mode={getGradingLanguageFamily(gradingLanguage) === 'C++' ? 'c_cpp' : 'plain_text'}
theme={isDarkMode ? 'tomorrow_night' : 'tomorrow'}
width="100%"
height="600px"
fontSize={14}
showPrintMargin={false}
editorProps={{ $blockScrolling: true }}
name={input.name}
onChange={input.onChange}
onFocus={input.onFocus}
value={input.value}
focus={autoFocus}
/>
<FormInputValidation meta={meta} />
</FormGroup>
);
}

const mapStateToProps = state => ({
isDarkMode: selectIsDarkMode(state),
});

export default connect(mapStateToProps)(FormAceEditor);
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#editor {
margin-bottom: 15px;
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import classNames from 'classnames';
import { getIntent, getIntentClassName } from '../meta';
import { FormInputValidation } from '../FormInputValidation/FormInputValidation';

export function FormSelect2({ input, className, label, meta, optionValues, optionNamesMap }) {
export function FormSelect2({ input, className, label, meta, optionValues, optionNamesMap, small }) {
const renderOption = (value, { handleClick, modifiers }) => {
return <MenuItem active={modifiers.active} key={value} onClick={handleClick} text={optionNamesMap[value]} />;
};
Expand All @@ -30,6 +30,7 @@ export function FormSelect2({ input, className, label, meta, optionValues, optio
alignText={Alignment.LEFT}
text={optionNamesMap[input.value]}
rightIcon={<CaretDown />}
small={small}
/>
</Select>
<FormInputValidation meta={meta} />
Expand Down
4 changes: 4 additions & 0 deletions judgels-client/src/components/forms/validations.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ export const EmailAddress = value =>
export const ConfirmPassword = (value, { password }) =>
value === password ? undefined : 'Confirmed password does not match';

export const MaxCodeLength50KB = value => {
return value && value.length <= 50 * 1024 ? undefined : 'Code length must be at most 50 KB';
};

export const MaxFileSize100KB = value => {
return value && value.size <= 100 * 1024 ? undefined : 'File size must be at most 100 KB';
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export default function ChapterProblemStatementRoutes({ worksheet }) {
id: '@',
title: 'Submit',
routeComponent: Route,
component: () => <ChapterProblemWorkspacePage worksheet={worksheet} />,
render: props => <ChapterProblemWorkspacePage {...props} worksheet={worksheet} />,
},
{
id: 'submissions',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { connect } from 'react-redux';

import { sendGAEvent } from '../../../../../../../../../../ga';
import { ProblemSubmissionCard } from '../../../../../../../../../../components/ProblemWorksheetCard/Programming/ProblemSubmissionCard/ProblemSubmissionCard';
import { ProblemSubmissionEditor } from '../../../../../../../../../../components/ProblemWorksheetCard/Programming/ProblemSubmissionEditor/ProblemSubmissionEditor';
import { getGradingLanguageFamily } from '../../../../../../../../../../modules/api/gabriel/language.js';
import { selectCourse } from '../../../../../../../modules/courseSelectors';
import { selectCourseChapter } from '../../../../../modules/courseChapterSelectors';
Expand Down Expand Up @@ -41,7 +41,7 @@ function ChapterProblemWorkspacePage({
};

return (
<ProblemSubmissionCard
<ProblemSubmissionEditor
config={submissionConfig}
onSubmit={createSubmission}
reasonNotAllowedToSubmit={reasonNotAllowedToSubmit}
Expand Down
26 changes: 26 additions & 0 deletions judgels-client/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3076,6 +3076,11 @@ accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.7:
mime-types "~2.1.24"
negotiator "0.6.2"

ace-builds@^1.26.0, ace-builds@^1.4.14:
version "1.26.0"
resolved "https://registry.yarnpkg.com/ace-builds/-/ace-builds-1.26.0.tgz#50642bc63f556e9cf66f9ccb5b9f69c833586df1"
integrity sha512-v8MyI5BpMypwd3/BOY0VSI/nqoAfjjscFg+y4iDgYyUGZjnHxJz1XSoRj52RHQYxfNLFJwx3HMVhUp9ecNEdCA==

acorn-globals@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-6.0.0.tgz#46cdd39f0f8ff08a876619b55f5ac8a6dc770b45"
Expand Down Expand Up @@ -5173,6 +5178,11 @@ [email protected]:
address "^1.0.1"
debug "^2.6.0"

diff-match-patch@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/diff-match-patch/-/diff-match-patch-1.0.5.tgz#abb584d5f10cd1196dfc55aa03701592ae3f7b37"
integrity sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==

diff-sequences@^26.6.2:
version "26.6.2"
resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-26.6.2.tgz#48ba99157de1923412eed41db6b6d4aa9ca7c0b1"
Expand Down Expand Up @@ -8497,6 +8507,11 @@ lodash.flattendeep@^4.4.0:
resolved "https://registry.yarnpkg.com/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz#fb030917f86a3134e5bc9bec0d69e0013ddfedb2"
integrity sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI=

lodash.get@^4.4.2:
version "4.4.2"
resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99"
integrity sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==

lodash.isequal@^4.5.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0"
Expand Down Expand Up @@ -10905,6 +10920,17 @@ rc@^1.2.7:
minimist "^1.2.0"
strip-json-comments "~2.0.1"

react-ace@^10.1.0:
version "10.1.0"
resolved "https://registry.yarnpkg.com/react-ace/-/react-ace-10.1.0.tgz#d348eac2b16475231779070b6cd16768deed565f"
integrity sha512-VkvUjZNhdYTuKOKQpMIZi7uzZZVgzCjM7cLYu6F64V0mejY8a2XTyPUIMszC6A4trbeMIHbK5fYFcT/wkP/8VA==
dependencies:
ace-builds "^1.4.14"
diff-match-patch "^1.0.5"
lodash.get "^4.4.2"
lodash.isequal "^4.5.0"
prop-types "^15.7.2"

react-app-polyfill@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/react-app-polyfill/-/react-app-polyfill-2.0.0.tgz#a0bea50f078b8a082970a9d853dc34b6dcc6a3cf"
Expand Down

0 comments on commit 3cf052d

Please sign in to comment.