mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-20 01:38:19 +08:00
core&ui: add problem_config
Co-authored-by: undefined <i@undefined.moe>
This commit is contained in:
1 parent
0b71b089e0
commit
1f498484d2
20 files changed
+1128
-159
No files matched your search
@@ -81,7 +81,7 @@ export default class Editor extends DOMAttachedObject {
|
||||
? monaco.editor.getModel(monaco.Uri.parse(model))
|
||||
|| monaco.editor.createModel(value, language === 'auto' ? undefined : language, monaco.Uri.parse(model))
|
||||
: model;
|
||||
this.model.setValue(value);
|
||||
if (!this.options.model) this.model.setValue(value);
|
||||
const cfg: import('../monaco').default.editor.IStandaloneEditorConstructionOptions = {
|
||||
theme,
|
||||
lineNumbers: 'on',
|
||||
|
||||
@@ -42,6 +42,9 @@
|
||||
opacity: 0.85
|
||||
|
||||
a.menu__link, button.menu__link
|
||||
&.half
|
||||
padding-right: 0
|
||||
width: 50%
|
||||
&.disabled
|
||||
cursor: default
|
||||
opacity: 0.5
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
import React from 'react';
|
||||
import type { editor } from 'monaco-editor';
|
||||
import { connect } from 'react-redux';
|
||||
import { load } from 'vj/components/monaco/loader';
|
||||
import Editor from 'vj/components/editor';
|
||||
import yaml from 'js-yaml';
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
config: state.config,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
handleUpdateCode: (code) => {
|
||||
dispatch({
|
||||
type: 'CONFIG_CODE_UPDATE',
|
||||
payload: code,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
interface Props {
|
||||
config: object;
|
||||
handleUpdateCode: Function;
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(class MonacoEditor extends React.PureComponent<Props> {
|
||||
disposable = [];
|
||||
containerElement: HTMLElement;
|
||||
private __preventUpdate = false;
|
||||
|
||||
editor: editor.IStandaloneCodeEditor;
|
||||
model: editor.ITextModel;
|
||||
vjEditor: Editor;
|
||||
|
||||
async componentDidMount() {
|
||||
const { monaco } = await load(['yaml']);
|
||||
const uri = monaco.Uri.parse('hydro://problem/file/config.yaml');
|
||||
this.model = monaco.editor.createModel(yaml.dump(this.props.config), 'yaml', uri);
|
||||
this.vjEditor = Editor.getOrConstruct($(this.containerElement), {
|
||||
language: 'yaml',
|
||||
model: this.model,
|
||||
onChange: (value: string) => {
|
||||
this.__preventUpdate = true;
|
||||
this.props.handleUpdateCode(value);
|
||||
this.__preventUpdate = false;
|
||||
},
|
||||
}) as Editor;
|
||||
this.editor = this.vjEditor.editor;
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.__preventUpdate) return;
|
||||
if (yaml.dump(prevProps.config) !== yaml.dump(this.props.config)) {
|
||||
this.model?.pushEditOperations(
|
||||
[],
|
||||
[{
|
||||
range: this.model.getFullModelRange(),
|
||||
text: yaml.dump(this.props.config),
|
||||
}],
|
||||
undefined,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.vjEditor) this.vjEditor.destory();
|
||||
if (this.model) this.model.dispose();
|
||||
if (this.editor) this.editor.dispose();
|
||||
this.disposable.map((i) => i.dispose());
|
||||
}
|
||||
|
||||
assignRef = (component) => {
|
||||
this.containerElement = component;
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div
|
||||
ref={this.assignRef}
|
||||
style={{
|
||||
minHeight: '500px',
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
}}
|
||||
className="ConfigMonacoEditor"
|
||||
>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,455 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import i18n from 'vj/utils/i18n';
|
||||
import { isEqual } from 'lodash';
|
||||
import { Switch, Tab, Tabs } from '@blueprintjs/core';
|
||||
import type { TestCaseConfig } from 'hydrooj/src/interface';
|
||||
import type { RootState } from './reducer/index';
|
||||
import CustomSelectAutoComplete from '../autocomplete/components/CustomSelectAutoComplete';
|
||||
|
||||
const SelectValue = {
|
||||
type: ['default', 'interactive', 'submit_answer', 'objective'],
|
||||
checker_type: ['default', 'lemon', 'syzoj', 'hustoj', 'testlib', 'qduoj'],
|
||||
task_type: ['min', 'max', 'sum'],
|
||||
};
|
||||
|
||||
const eq = (a: TestCaseConfig[], b: TestCaseConfig[]) => isEqual(a, b);
|
||||
|
||||
function FormItem({
|
||||
columns, label, children, helpText = '', disableLabel = false, ...props
|
||||
}) {
|
||||
return (
|
||||
<div {...props} className={`${columns && `medium-${columns}`} columns form__item`}>
|
||||
<label htmlFor={`${label}-form`}>
|
||||
{!disableLabel && i18n(label)}
|
||||
{children}
|
||||
{helpText && (<p className="help-text">{i18n(helpText)}</p>)}
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type KeyType<K, T = string | number> = {
|
||||
[Q in keyof K]: K[Q] extends T ? Q : never;
|
||||
}[keyof K];
|
||||
|
||||
function ManagedInput({ placeholder, formKey }: { placeholder: string, formKey: KeyType<RootState['config']> }) {
|
||||
const value = useSelector((state: RootState) => state.config[formKey]);
|
||||
const dispatch = useDispatch();
|
||||
return (
|
||||
<input
|
||||
placeholder={i18n(placeholder)}
|
||||
value={value || ''}
|
||||
onChange={(ev) => {
|
||||
dispatch({ type: 'CONFIG_FORM_UPDATE', key: formKey, value: ev.currentTarget.value });
|
||||
}}
|
||||
className="textbox"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function ManagedSelect({ placeholder, formKey }: { placeholder: string, formKey: KeyType<RootState['config']> }) {
|
||||
const value = useSelector((state: RootState) => state.config[formKey]);
|
||||
const dispatch = useDispatch();
|
||||
return (
|
||||
<select
|
||||
placeholder={i18n(placeholder)}
|
||||
value={value || ''}
|
||||
onChange={(ev) => {
|
||||
dispatch({ type: 'CONFIG_FORM_UPDATE', key: formKey, value: ev.currentTarget.value });
|
||||
}}
|
||||
className="select"
|
||||
>
|
||||
{SelectValue[formKey].map((i) => (<option value={i} key={i}>{i}</option>))}
|
||||
</select>
|
||||
);
|
||||
}
|
||||
|
||||
function SingleFileSelect({ formKey }: { formKey: KeyType<RootState['config']> }) {
|
||||
const value = useSelector((state: RootState) => state.config[formKey]);
|
||||
const Files = useSelector((state: RootState) => state.testdata);
|
||||
const dispatch = useDispatch();
|
||||
return (
|
||||
<CustomSelectAutoComplete
|
||||
width="100%"
|
||||
data={Files}
|
||||
selectedKeys={[value]}
|
||||
onChange={(val) => dispatch({ type: 'CONFIG_FORM_UPDATE', key: formKey, value: val })}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function BasicInfo() {
|
||||
const Type = useSelector((state: RootState) => state.config.type);
|
||||
const checkerType = useSelector((state: RootState) => state.config.checker_type);
|
||||
const dispatch = useDispatch();
|
||||
return (
|
||||
<>
|
||||
<FormItem columns={6} label="Type">
|
||||
<ManagedSelect placeholder="type" formKey="type" />
|
||||
</FormItem>
|
||||
<FormItem columns={6} label="Filename" style={Type === 'interactive' ? { display: 'none' } : {}}>
|
||||
<ManagedInput placeholder="filename" formKey="filename" />
|
||||
</FormItem>
|
||||
<FormItem
|
||||
columns={12}
|
||||
label="CheckerType"
|
||||
disableLabel
|
||||
style={Type !== 'default' ? { display: 'none' } : {}}
|
||||
>
|
||||
<Tabs
|
||||
id="CheckerTypeTabs"
|
||||
selectedTabId={checkerType !== 'strict' ? checkerType : 'default'}
|
||||
onChange={(value) => {
|
||||
dispatch({ type: 'CONFIG_FORM_UPDATE', key: 'checker_type', value });
|
||||
}}
|
||||
renderActiveTabPanelOnly
|
||||
>
|
||||
<span>{i18n('CheckerType')}</span>
|
||||
<Tabs.Expander />
|
||||
{
|
||||
SelectValue.checker_type.map((i) => (
|
||||
<Tab
|
||||
id={i}
|
||||
title={i}
|
||||
key={i}
|
||||
panel={(
|
||||
<FormItem columns={12} label="Checker">
|
||||
{['default', 'strict'].includes(i)
|
||||
? (
|
||||
<Switch
|
||||
checked={checkerType === 'strict'}
|
||||
label="Don't ignore space and enter."
|
||||
onChange={
|
||||
() => {
|
||||
dispatch({ type: 'CONFIG_FORM_UPDATE', key: 'checker_type', value: checkerType === 'strict' ? 'default' : 'strict' });
|
||||
}
|
||||
}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<SingleFileSelect formKey="checker" />
|
||||
)}
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</Tabs>
|
||||
</FormItem>
|
||||
<FormItem columns={6} label="Interactor" style={Type !== 'interactive' ? { display: 'none' } : {}}>
|
||||
<SingleFileSelect formKey="interactor" />
|
||||
</FormItem>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function ExtraFilesConfig() {
|
||||
const [showTab, setshowTab] = useState(false);
|
||||
const Files = useSelector((state: RootState) => state.testdata);
|
||||
const userExtraFiles = useSelector((state: RootState) => state.config.user_extra_files);
|
||||
const judgeExtraFiles = useSelector((state: RootState) => state.config.judge_extra_files);
|
||||
const dispatch = useDispatch();
|
||||
return (
|
||||
<FormItem columns={12} label="ExtraFilesTabs" disableLabel>
|
||||
<Switch checked={showTab} label="Extra Files Config" onChange={() => setshowTab(!showTab)} />
|
||||
<div style={!showTab ? { display: 'none' } : {}}>
|
||||
<Tabs id="ExtraFilesTabs">
|
||||
<Tab
|
||||
id="user_extra_files"
|
||||
title="user_extra_files"
|
||||
panel={(
|
||||
<CustomSelectAutoComplete
|
||||
data={Files}
|
||||
selectedKeys={userExtraFiles || []}
|
||||
onChange={(val) => dispatch({ type: 'CONFIG_FORM_UPDATE', key: 'user_extra_files', value: val.split(',') })}
|
||||
multi
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Tab
|
||||
id="judge_extra_files"
|
||||
title="judge_extra_files"
|
||||
panel={(
|
||||
<CustomSelectAutoComplete
|
||||
data={Files}
|
||||
selectedKeys={judgeExtraFiles || []}
|
||||
onChange={(val) => dispatch({ type: 'CONFIG_FORM_UPDATE', key: 'judge_extra_files', value: val.split(',') })}
|
||||
multi
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Tabs>
|
||||
</div>
|
||||
</FormItem>
|
||||
);
|
||||
}
|
||||
|
||||
function CasesTable({ index }) {
|
||||
const [time, setTime] = useState('');
|
||||
const [memory, setMemory] = useState('');
|
||||
const [input, setInput] = useState('');
|
||||
const [output, setOutput] = useState('');
|
||||
const cases = useSelector((state: RootState) => (index === -1 ? state.config.cases : state.config.subtasks[index].cases), eq);
|
||||
console.log(cases);
|
||||
const Files = useSelector((state: RootState) => state.testdata);
|
||||
const dispatch = useDispatch();
|
||||
return (
|
||||
<table className="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
{
|
||||
index === -1 && (
|
||||
<>
|
||||
<th>
|
||||
{i18n('Time')}<br />
|
||||
<input
|
||||
value={time || ''}
|
||||
onChange={(ev) => setTime(ev.currentTarget.value)}
|
||||
className="textbox"
|
||||
/>
|
||||
</th>
|
||||
<th>
|
||||
{i18n('Memory')}<br />
|
||||
<input
|
||||
value={memory || ''}
|
||||
onChange={(ev) => setMemory(ev.currentTarget.value)}
|
||||
className="textbox"
|
||||
/>
|
||||
</th>
|
||||
</>
|
||||
)
|
||||
}
|
||||
<th>
|
||||
{i18n('Input')}<br />
|
||||
<CustomSelectAutoComplete
|
||||
width="100%"
|
||||
data={Files}
|
||||
selectedKeys={[input]}
|
||||
onChange={setInput}
|
||||
/>
|
||||
</th>
|
||||
<th>
|
||||
{i18n('Output')}<br />
|
||||
<CustomSelectAutoComplete
|
||||
width="100%"
|
||||
data={Files}
|
||||
selectedKeys={[output]}
|
||||
onChange={setOutput}
|
||||
/>
|
||||
</th>
|
||||
<th>
|
||||
<span className="icon icon-wrench"></span><br />
|
||||
<a
|
||||
onClick={() => {
|
||||
setTime('');
|
||||
setMemory('');
|
||||
setInput('');
|
||||
setOutput('');
|
||||
dispatch({
|
||||
type: index === -1 ? 'CONFIG_CASES_UPDATE' : 'CONFIG_SUBTASK_UPDATE',
|
||||
id: index,
|
||||
key: 'cases-add',
|
||||
value: index === -1 ? {
|
||||
time, memory, input, output,
|
||||
} : { input, output },
|
||||
});
|
||||
}}
|
||||
><span className="icon icon-add">{i18n('Add')}</span>
|
||||
</a>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{cases && cases.map((k, v) => (
|
||||
<tr key={JSON.stringify(k)}>
|
||||
{
|
||||
index === -1 && (
|
||||
<>
|
||||
<td>{k.time}</td>
|
||||
<td>{k.memory}</td>
|
||||
</>
|
||||
)
|
||||
}
|
||||
<td>{k.input}</td>
|
||||
<td>{k.output}</td>
|
||||
<td>
|
||||
<a
|
||||
onClick={() => dispatch({
|
||||
type: index === -1 ? 'CONFIG_CASES_UPDATE' : 'CONFIG_SUBTASK_UPDATE', id: index, key: 'cases-delete', value: v,
|
||||
})}
|
||||
><span className="icon icon-close"></span>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
|
||||
function SubtasksTable({ data, index }) {
|
||||
const subtasks = useSelector((state: RootState) => state.config.subtasks, eq);
|
||||
const dispatch = useDispatch();
|
||||
const dispatcher = (key: string): React.ChangeEventHandler<HTMLInputElement | HTMLSelectElement> => (ev) => {
|
||||
dispatch({
|
||||
type: 'CONFIG_SUBTASK_UPDATE', id: index, key, value: ev.currentTarget.value,
|
||||
});
|
||||
};
|
||||
return (
|
||||
<div key={index}>
|
||||
<span>Subtasks #{index} </span>
|
||||
<a onClick={() => { dispatch({ type: 'CONFIG_SUBTASK_UPDATE', id: index, key: 'add' }); }}><span className="icon icon-add"></span></a>
|
||||
<a
|
||||
style={index === 0 ? { display: 'none' } : {}}
|
||||
onClick={() => { dispatch({ type: 'CONFIG_SUBTASK_UPDATE', id: index, key: 'delete' }); }}
|
||||
><span className="icon icon-delete"></span>
|
||||
</a>
|
||||
<table className="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Score</th>
|
||||
<th>Type</th>
|
||||
<th>Time</th>
|
||||
<th>Memory</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<input
|
||||
value={data.id}
|
||||
onChange={dispatcher('id')}
|
||||
className="textbox"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
value={data.score || ''}
|
||||
onChange={dispatcher('score')}
|
||||
className="textbox"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<select
|
||||
value={data.type}
|
||||
onChange={dispatcher('type')}
|
||||
className="select"
|
||||
>
|
||||
<option aria-label="null" value="" style={{ display: 'none' }}></option>
|
||||
{SelectValue.task_type.map((i) => (<option value={i} key={i}>{i}</option>))}
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
value={data.time || ''}
|
||||
onChange={dispatcher('time')}
|
||||
className="textbox"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
value={data.memory || ''}
|
||||
onChange={dispatcher('memory')}
|
||||
className="textbox"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>if</td>
|
||||
<td colSpan={4}>
|
||||
<CustomSelectAutoComplete
|
||||
data={subtasks}
|
||||
selectedKeys={data.if || []}
|
||||
onChange={(val) => dispatch({
|
||||
type: 'CONFIG_SUBTASK_UPDATE', id: index, key: 'if', value: val.split(','),
|
||||
})}
|
||||
multi
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<CasesTable index={index} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TaskConfig() {
|
||||
const subtasks = useSelector((state: RootState) => state.config.subtasks, eq);
|
||||
const cases = useSelector((state: RootState) => state.config.cases, eq);
|
||||
const dispatch = useDispatch();
|
||||
return (
|
||||
<FormItem columns={12} label="Task Settings">
|
||||
<div className="row">
|
||||
<FormItem columns={4} label="Time">
|
||||
<ManagedInput placeholder="Time" formKey="time" />
|
||||
</FormItem>
|
||||
<FormItem columns={4} label="Memory">
|
||||
<ManagedInput placeholder="Memory" formKey="memory" />
|
||||
</FormItem>
|
||||
<FormItem columns={4} label="Score">
|
||||
<ManagedInput placeholder="Score" formKey="score" />
|
||||
</FormItem>
|
||||
<FormItem columns={12} label="Cases Settings" disableLabel>
|
||||
{cases && <Switch label="Use Subtasks" onChange={() => dispatch({ type: 'CONFIG_SUBTASKS_SWITCH', value: true })} />}
|
||||
{
|
||||
subtasks || cases
|
||||
? subtasks && <ul>{subtasks.map((k, v) => <SubtasksTable data={k} index={v} key={k.id} />)}</ul>
|
||||
|| cases && (<CasesTable index={-1} />)
|
||||
: (
|
||||
<a
|
||||
onClick={() => dispatch({ type: 'CONFIG_AUTOCASES_UPDATE' })}
|
||||
><span className="icon icon-settings"> {i18n('Auto Read Tasks')}</span>
|
||||
</a>
|
||||
)
|
||||
}
|
||||
</FormItem>
|
||||
</div>
|
||||
</FormItem>
|
||||
);
|
||||
}
|
||||
|
||||
function LangConfig() {
|
||||
const [showTab, setshowTab] = useState(false);
|
||||
const langs = useSelector((state: RootState) => state.config.langs) || [];
|
||||
const prefixes = new Set(Object.keys(LANGS).filter((i) => i.includes('.')).map((i) => i.split('.')[0]));
|
||||
const data = Object.keys(LANGS).filter((i) => !prefixes.has(i))
|
||||
.map((i) => ({ name: LANGS[i].display, _id: i }));
|
||||
const dispatch = useDispatch();
|
||||
const ref = React.useRef<any>();
|
||||
const selectedKeys = langs.filter((i) => !prefixes.has(i));
|
||||
React.useEffect(() => {
|
||||
ref.current.setSelectedKeys(selectedKeys);
|
||||
}, [JSON.stringify(selectedKeys)]);
|
||||
return (
|
||||
<FormItem columns={12} label="LangsTabs" disableLabel>
|
||||
<Switch checked={showTab} label="Langs Config" onChange={() => setshowTab(!showTab)} />
|
||||
<FormItem columns={12} label="langs" style={!showTab ? { display: 'none' } : {}}>
|
||||
<CustomSelectAutoComplete
|
||||
ref={ref}
|
||||
data={data}
|
||||
selectedKeys={selectedKeys}
|
||||
onChange={(val) => {
|
||||
const value = val.split(',');
|
||||
value.push(...Array.from(new Set(value.filter((i) => i.includes('.')).map((i) => i.split('.')[0]))));
|
||||
dispatch({ type: 'CONFIG_FORM_UPDATE', key: 'langs', value });
|
||||
}}
|
||||
multi
|
||||
/>
|
||||
</FormItem>
|
||||
</FormItem>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ProblemConfigForm() {
|
||||
return (
|
||||
<div className="row">
|
||||
<BasicInfo />
|
||||
<TaskConfig />
|
||||
<ExtraFilesConfig />
|
||||
<LangConfig />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import yaml from 'js-yaml';
|
||||
import type { ProblemConfigFile } from 'hydrooj/src/interface';
|
||||
|
||||
let autocases = { subtasks: [] };
|
||||
|
||||
export default function reducer(state = { type: 'default' } as ProblemConfigFile, action): ProblemConfigFile {
|
||||
switch (action.type) {
|
||||
case 'CONFIG_LOAD_FULFILLED': {
|
||||
// TODO set yaml schema
|
||||
autocases = action.payload.autocases;
|
||||
return { ...state, ...yaml.load(action.payload.config) as object };
|
||||
}
|
||||
case 'CONFIG_FORM_UPDATE': {
|
||||
const next = { ...state, [action.key]: action.value };
|
||||
if (!action.value || (typeof action.value === 'object' && !action.value.join(''))) delete next[action.key];
|
||||
if (action.key === 'type' && action.value !== 'default') {
|
||||
delete next.checker_type;
|
||||
delete next.checker;
|
||||
}
|
||||
if (action.key === 'type' && action.value !== 'interactive') delete next.interactor;
|
||||
if (action.key === 'checker_type' && ['default', 'strict'].includes(action.value)) delete next.checker;
|
||||
return next;
|
||||
}
|
||||
case 'CONFIG_CODE_UPDATE': {
|
||||
try {
|
||||
return yaml.load(action.payload);
|
||||
} catch {
|
||||
return state;
|
||||
}
|
||||
}
|
||||
case 'CONFIG_AUTOCASES_UPDATE': {
|
||||
const next = { ...state };
|
||||
if (autocases.subtasks.length === 1) {
|
||||
next.score = autocases.subtasks[0].score;
|
||||
next.cases = [];
|
||||
autocases.subtasks[0].cases.map((i) => next.cases.push({
|
||||
time: autocases.subtasks[0].time, memory: autocases.subtasks[0].memory, input: i.input, output: i.output,
|
||||
}));
|
||||
} else {
|
||||
next.subtasks = autocases.subtasks.map((subtask) => (
|
||||
{ ...subtask, ...{ cases: subtask.cases.map((i) => ({ input: i.input, output: i.output })) } }));
|
||||
}
|
||||
return next;
|
||||
}
|
||||
case 'CONFIG_SUBTASKS_SWITCH': {
|
||||
const next = { ...state };
|
||||
next.subtasks = next.cases;
|
||||
if (!next.subtasks.length) next.subtasks.push({ id: 0 });
|
||||
delete next.cases;
|
||||
return next;
|
||||
}
|
||||
case 'CONFIG_CASES_UPDATE': {
|
||||
const next = { ...state };
|
||||
if (action.key === 'cases-add') next.cases.push(action.value);
|
||||
else if (action.key === 'cases-delete') {
|
||||
next.cases = next.cases.filter((k, v) => v !== action.value);
|
||||
}
|
||||
return next;
|
||||
}
|
||||
case 'CONFIG_SUBTASK_UPDATE': {
|
||||
const next = { ...state };
|
||||
if (action.key === 'add') next.subtasks.splice(action.id, 0, { id: 0 });
|
||||
else if (action.key === 'delete') delete next.subtasks[action.key];
|
||||
else if (action.key === 'cases-add') next.subtasks[action.id].cases.push(action.value);
|
||||
else if (action.key === 'cases-delete') {
|
||||
next.subtasks[action.id].cases = next.subtasks[action.id].cases.filter((k, v) => v !== action.value);
|
||||
} else next.subtasks[action.id][action.key] = action.value;
|
||||
return next;
|
||||
}
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { combineReducers } from 'redux';
|
||||
import config from './config';
|
||||
import testdata from './testdata';
|
||||
|
||||
const reducer = combineReducers({
|
||||
config,
|
||||
testdata,
|
||||
});
|
||||
|
||||
export default reducer;
|
||||
export type RootState = ReturnType<typeof reducer>;
|
||||
@@ -0,0 +1,16 @@
|
||||
export default function reducer(state = [], action) {
|
||||
switch (action.type) {
|
||||
case 'CONFIG_LOAD_FULFILLED': {
|
||||
return state.concat(action.payload.testdata);
|
||||
}
|
||||
case 'CONFIG_DELETE_TESTDATA': {
|
||||
return state.filter((i) => i.name !== action.payload.name);
|
||||
}
|
||||
case 'CONFIG_ADD_TESTDATA': {
|
||||
state.push(action.payload);
|
||||
return state;
|
||||
}
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
@@ -268,6 +268,7 @@ domain: 域
|
||||
Domain: 域
|
||||
Don't have an account?: 还没有账户?
|
||||
Done: 已结束
|
||||
Download All: 下载全部
|
||||
Download Dataset: 下载数据集
|
||||
Download Selected: 下载选中
|
||||
Download: 下载
|
||||
@@ -414,6 +415,7 @@ Is rated: 计入排名
|
||||
It works!: 它成了!
|
||||
Italic: 斜体
|
||||
Join: 加入
|
||||
Judge Config: 评测设置
|
||||
Judge Status: 评测状态
|
||||
judge_playground: 评测游乐场
|
||||
Judged At: 评测时间
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
.page--problem_config
|
||||
.section--problem-sidebar-testdata
|
||||
.problem-sidebar-testdata__detail
|
||||
height: 270px
|
||||
overflow-y: auto
|
||||
|
||||
&.collapsed .collapsed--hidden,
|
||||
&.expanded .expanded--hidden
|
||||
display: none
|
||||
|
||||
&.collapsed .problem-sidebar-testdata__detail
|
||||
display: none
|
||||
|
||||
.col--size
|
||||
width: 80px
|
||||
.col--operation
|
||||
width: 40px
|
||||
|
||||
.textbox,.select
|
||||
margin: 0
|
||||
|
||||
.bp3-tab-panel
|
||||
margin-top: 5px
|
||||
@@ -0,0 +1,73 @@
|
||||
import { NamedPage } from 'vj/misc/Page';
|
||||
import { slideDown, slideUp } from 'vj/utils/slide';
|
||||
import request from 'vj/utils/request';
|
||||
import loadReactRedux from 'vj/utils/loadReactRedux';
|
||||
import i18n from 'vj/utils/i18n';
|
||||
import yaml from 'js-yaml';
|
||||
import Notification from 'vj/components/notification';
|
||||
|
||||
async function handleSection(ev: JQuery.ClickEvent<Document, undefined, any, any>, type: string) {
|
||||
const $section = $(ev.currentTarget).closest('.section--problem-sidebar-testdata');
|
||||
if ($section.is(`.${type}d, .animating`)) return;
|
||||
$section.addClass('animating');
|
||||
const $detail = $section.find('.problem-sidebar-testdata__detail');
|
||||
if (type === 'expand') {
|
||||
await slideDown($detail, 300, { opacity: 0 }, { opacity: 1 });
|
||||
} else {
|
||||
await slideUp($detail, 300, { opacity: 1 }, { opacity: 0 });
|
||||
}
|
||||
$section.addClass(type === 'expand' ? 'expanded' : 'collapsed');
|
||||
$section.removeClass(type === 'expand' ? 'collapsed' : 'expanded');
|
||||
$section.removeClass('animating');
|
||||
}
|
||||
|
||||
const page = new NamedPage('problem_config', () => {
|
||||
async function uploadConfig(config:object) {
|
||||
const configYaml = yaml.dump(config);
|
||||
Notification.info(i18n('Saving file...'));
|
||||
const data = new FormData();
|
||||
data.append('filename', 'config.yaml');
|
||||
data.append('file', new Blob([configYaml], { type: 'text/plain' }));
|
||||
data.append('type', 'testdata');
|
||||
data.append('operation', 'upload_file');
|
||||
await request.postFile('./files', data);
|
||||
Notification.success(i18n('File saved.'));
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
async function mountComponent() {
|
||||
const { default: ProblemConfigEditor } = await import('vj/components/problemconfig/ProblemConfigEditor');
|
||||
const { default: ProblemConfigForm } = await import('vj/components/problemconfig/ProblemConfigForm');
|
||||
const { default: ProblemConfigReducer } = await import('vj/components/problemconfig/reducer');
|
||||
|
||||
const {
|
||||
React, render, Provider, store,
|
||||
} = await loadReactRedux(ProblemConfigReducer);
|
||||
|
||||
store.dispatch({
|
||||
type: 'CONFIG_LOAD',
|
||||
payload: request.get(),
|
||||
});
|
||||
render(
|
||||
<Provider store={store}>
|
||||
<div className="row">
|
||||
<div className="medium-5 columns">
|
||||
<ProblemConfigEditor />
|
||||
</div>
|
||||
<div className="medium-7 columns">
|
||||
<ProblemConfigForm />
|
||||
</div>
|
||||
</div>
|
||||
<button className="rounded primary button" onClick={() => uploadConfig(store.getState().config)}>{i18n('Submit')}</button>
|
||||
</Provider>,
|
||||
$('#ProblemConfig').get(0),
|
||||
);
|
||||
}
|
||||
|
||||
mountComponent();
|
||||
|
||||
$(document).on('click', '[name="testdata__section__expand"]', (ev) => handleSection(ev, 'expand'));
|
||||
$(document).on('click', '[name="testdata__section__collapse"]', (ev) => handleSection(ev, 'collapse'));
|
||||
});
|
||||
|
||||
export default page;
|
||||
@@ -7,6 +7,7 @@ import { ConfirmDialog } from 'vj/components/dialog';
|
||||
import Dropdown from 'vj/components/dropdown/Dropdown';
|
||||
import Editor from 'vj/components/editor/index';
|
||||
import Notification from 'vj/components/notification';
|
||||
import { slideDown, slideUp } from 'vj/utils/slide';
|
||||
|
||||
const categories = {};
|
||||
const dirtyCategories = [];
|
||||
@@ -143,6 +144,21 @@ function buildCategoryFilter() {
|
||||
});
|
||||
}
|
||||
|
||||
async function handleSection(ev, sidebar, type) {
|
||||
const $section = $(ev.currentTarget).closest(`.section--problem-sidebar-${sidebar}`);
|
||||
if ($section.is(`.${type}d, .animating`)) return;
|
||||
$section.addClass('animating');
|
||||
const $detail = $section.find(`.section--problem-sidebar-${sidebar}__detail`);
|
||||
if (type === 'expand') {
|
||||
await slideDown($detail, 300, { opacity: 0 }, { opacity: 1 });
|
||||
} else {
|
||||
await slideUp($detail, 300, { opacity: 1 }, { opacity: 0 });
|
||||
}
|
||||
$section.addClass(type === 'expand' ? 'expanded' : 'collapsed');
|
||||
$section.removeClass(type === 'expand' ? 'collapsed' : 'expanded');
|
||||
$section.removeClass('animating');
|
||||
}
|
||||
|
||||
export default new NamedPage(['problem_create', 'problem_edit'], (pagename) => {
|
||||
let confirmed = false;
|
||||
$(document).on('click', '[name="operation"]', (ev) => {
|
||||
@@ -232,4 +248,8 @@ export default new NamedPage(['problem_create', 'problem_edit'], (pagename) => {
|
||||
ev.preventDefault();
|
||||
}
|
||||
});
|
||||
$(document).on('click', '[name="additional_file__section__expand"]', (ev) => handleSection(ev, 'additional_file', 'expand'));
|
||||
$(document).on('click', '[name="additional_file__section__collapse"]', (ev) => handleSection(ev, 'additional_file', 'collapse'));
|
||||
$(document).on('click', '[name="tags__section__expand"]', (ev) => handleSection(ev, 'tags', 'expand'));
|
||||
$(document).on('click', '[name="tags__section__collapse"]', (ev) => handleSection(ev, 'tags', 'collapse'));
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
.page--problem_edit
|
||||
.page--problem_edit,.page--problem_create
|
||||
.section__tab-header-item
|
||||
border-top: 3px solid transparent;
|
||||
transition: border-color 0.2s, background 0.2s, color 0.2s;
|
||||
@@ -10,4 +10,29 @@
|
||||
background: #f8f8f8;
|
||||
|
||||
&.tab--active
|
||||
border-color: #ed5f82;
|
||||
border-color: #ed5f82;
|
||||
|
||||
.section--problem-sidebar-tags
|
||||
&.collapsed .collapsed--hidden,
|
||||
&.expanded .expanded--hidden
|
||||
display: none
|
||||
|
||||
&.collapsed .problem-sidebar-tags__detail
|
||||
display: none
|
||||
|
||||
.section--problem-sidebar-additional_file
|
||||
.problem-sidebar-additional_file__detail
|
||||
height: 270px
|
||||
overflow-y: auto
|
||||
|
||||
&.collapsed .collapsed--hidden,
|
||||
&.expanded .expanded--hidden
|
||||
display: none
|
||||
|
||||
&.collapsed .problem-sidebar-additional_file__detail
|
||||
display: none
|
||||
|
||||
.col--size
|
||||
width: 80px
|
||||
.col--operation
|
||||
width: 40px
|
||||
@@ -0,0 +1,57 @@
|
||||
{% import "components/nothing.html" as nothing with context %}
|
||||
{% import "components/problem.html" as problem with context %}
|
||||
<div class="section side section--problem-sidebar-additional_file {{ 'expanded' if page_name == 'problem_edit' else 'collapsed'}}">
|
||||
<div class="section__header">
|
||||
<h1 class="section__title">{{ additional_file.length if additional_file.length else 0 }} {{_('Testdata')}}</h1>
|
||||
</div>
|
||||
<ol class="menu">
|
||||
<li class="menu__item flex-row">
|
||||
<a class="menu__link half" href="javascript:;" name="create_file">
|
||||
<span class="icon icon-add"></span> {{ _('Create') }}
|
||||
</a>
|
||||
<a class="menu__link half" href="javascript:;" name="upload_file">
|
||||
<span class="icon icon-upload"></span> {{ _('Upload') }}
|
||||
</a>
|
||||
</li>
|
||||
<li class="menu__seperator"></li>
|
||||
<li class="menu__item">
|
||||
<a class="menu__link expanded--hidden" href="javascript:;" name="additional_file__section__expand">
|
||||
<span class="icon icon-expand_more"></span> {{ _('expand') }}
|
||||
</a>
|
||||
<a class="menu__link collapsed--hidden" href="javascript:;" name="additional_file__section__collapse">
|
||||
<span class="icon icon-expand_less"></span> {{ _('collapse') }}
|
||||
</a>
|
||||
</li>
|
||||
</ol>
|
||||
<div class="section__body no-padding problem-sidebar-additional_file__detail" data-fragment-id="problem-sidebar-additional_file">
|
||||
{% if additional_file.length %}
|
||||
<table class="data-table">
|
||||
<colgroup>
|
||||
<col class="col--name">
|
||||
<col class="col--size">
|
||||
<col class="col--operation">
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="col--name">{{ _('Filename') }}</th>
|
||||
<th class="col--size">{{ _('Size') }}</th>
|
||||
<th class="col--operation"><span class="icon icon-wrench"></span></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{%- for file in additional_file -%}
|
||||
<tr data-filename="{{ file.name }}" data-size="{{ file.size }}">
|
||||
<td class="col--name" title="{{ file.name }}">
|
||||
<a href="{{ url('problem_file_download', {pid: pdoc.docId, filename: file.name}) }}">{{ file.name }}</a>
|
||||
</td>
|
||||
<td class="col--size">{{ size(file.size) }}</td>
|
||||
<td class="col--operation"><a href="javascript:;" name="{{ file.name }}"><span class="icon icon-delete"></span></a></td>
|
||||
</tr>
|
||||
{%- endfor -%}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
{{ nothing.render('No additional file at current.',compact = true) }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
@@ -83,6 +83,9 @@
|
||||
<li class="menu__item"><a class="menu__link{% if page_name == 'problem_edit' %} active{% endif %}" href="{{ url('problem_edit', pid=pdoc.pid|default(pdoc.docId)) }}">
|
||||
<span class="icon icon-edit"></span> {{ _('Edit') }}
|
||||
</a></li>
|
||||
<li class="menu__item"><a class="menu__link{% if page_name == 'problem_config' %} active{% endif %}" href="{{ url('problem_config', pid=pdoc.pid|default(pdoc.docId)) }}">
|
||||
<span class="icon icon-settings"></span> {{ _('Judge Config') }}
|
||||
</a></li>
|
||||
{% endif %}
|
||||
{% if page_name == 'problem_detail' %}
|
||||
<li class="menu__seperator nojs--hide"></li>
|
||||
@@ -100,14 +103,14 @@
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
{% if page_name != 'problem_detail' and page_name != 'homework_detail_problem' and page_name != 'contest_detail_problem' %}
|
||||
{% if page_name not in ['problem_config', 'problem_detail', 'homework_detail_problem', 'contest_detail_problem'] %}
|
||||
<div class="section side">
|
||||
<div class="section__header">
|
||||
<h1 class="section__title">{{ _('Information') }}</h1>
|
||||
</div>
|
||||
{% include "partials/problem-sidebar-information.html" %}
|
||||
</div>
|
||||
{% elif page_name != 'contest_detail_problem' %}
|
||||
{% elif page_name != 'contest_detail_problem' and page_name != 'problem_config' %}
|
||||
<div class="section side nojs--hide">
|
||||
<div class="section__header">
|
||||
<h1 class="section__title">{{ _('Statistics') }}</h1>
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
{% import "components/nothing.html" as nothing with context %}
|
||||
{% import "components/problem.html" as problem with context %}
|
||||
<div class="section side section--problem-sidebar-testdata collapsed">
|
||||
<div class="section__header">
|
||||
<h1 class="section__title">{{ testdata.length if testdata.length else 0 }} {{_('Testdata')}}</h1>
|
||||
</div>
|
||||
<ol class="menu">
|
||||
<li class="menu__item flex-row">
|
||||
<a class="menu__link half" href="javascript:;" name="create_file">
|
||||
<span class="icon icon-add"></span> {{ _('Create') }}
|
||||
</a>
|
||||
<a class="menu__link half" href="javascript:;" name="upload_file">
|
||||
<span class="icon icon-upload"></span> {{ _('Upload') }}
|
||||
</a>
|
||||
</li>
|
||||
<li class="menu__seperator"></li>
|
||||
<li class="menu__item flex-row">
|
||||
<a class="menu__link half" href="javascript:;" name="upload_file">
|
||||
<span class="icon icon-download"></span> {{ _('Download All') }}
|
||||
</a>
|
||||
<a class="menu__link half expanded--hidden" href="javascript:;" name="testdata__section__expand">
|
||||
<span class="icon icon-expand_more"></span> {{ _('expand') }}
|
||||
</a>
|
||||
<a class="menu__link half collapsed--hidden" href="javascript:;" name="testdata__section__collapse">
|
||||
<span class="icon icon-expand_less"></span> {{ _('collapse') }}
|
||||
</a>
|
||||
</li>
|
||||
</ol>
|
||||
<div class="section__body no-padding problem-sidebar-testdata__detail" data-fragment-id="problem-sidebar-testdata">
|
||||
{% if testdata.length %}
|
||||
<table class="data-table">
|
||||
<colgroup>
|
||||
<col class="col--name">
|
||||
<col class="col--size">
|
||||
<col class="col--operation">
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="col--name">{{ _('Filename') }}</th>
|
||||
<th class="col--size">{{ _('Size') }}</th>
|
||||
<th class="col--operation"><span class="icon icon-wrench"></span></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{%- for file in testdata -%}
|
||||
<tr data-filename="{{ file.name }}" data-size="{{ file.size }}">
|
||||
<td class="col--name" title="{{ file.name }}"><a href="{{ url('problem_file_download', {pid: pdoc.docId, filename: file.name, query:{type:'testdata'} }) }}">{{ file.name }}</a></td>
|
||||
<td class="col--size">{{ size(file.size) }}</td>
|
||||
<td class="col--operation"><a href="javascript:;" name="{{ file.name }}"><span class="icon icon-delete"></span></a></td>
|
||||
</tr>
|
||||
{%- endfor -%}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
{{ nothing.render('No testdata at current.',compact = true) }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,21 @@
|
||||
{% extends "layout/basic.html" %}
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="medium-9 columns">
|
||||
<div class="section">
|
||||
<div class="section__header">
|
||||
<h1 class="section__title">{{_('Judge Config')}}</h1>
|
||||
</div>
|
||||
<div id="ProblemConfig" class="section__body">
|
||||
{{ noscript_note.render() }}
|
||||
<div class="loader-container"><div class="loader"></div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="medium-3 columns">
|
||||
{% include "partials/problem_sidebar_testdata.html" %}
|
||||
{% set owner_udoc = udoc %}
|
||||
{% include "partials/problem_sidebar.html" %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -92,14 +92,23 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="medium-3 columns">
|
||||
<div class="section side visiable nojs--hide">
|
||||
<div class="section side visiable nojs--hide section--problem-sidebar-tags {{ 'collapsed' if page_name == 'problem_edit' else 'expanded'}}">
|
||||
<div class="section__header">
|
||||
<h1 class="section__title">{{ _('Categories') }} ({{ _('click to add') }})</h1>
|
||||
<div class="section__tools">
|
||||
<a class="expanded--hidden" href="javascript:;" name="tags__section__expand">
|
||||
<span class="icon icon-expand_more"></span>
|
||||
</a>
|
||||
<a class="collapsed--hidden" href="javascript:;" name="tags__section__collapse">
|
||||
<span class="icon icon-expand_less"></span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section__body">
|
||||
<div class="section__body problem-sidebar-tags__detail">
|
||||
{% include "partials/category.html" %}
|
||||
</div>
|
||||
</div>
|
||||
{% include "partials/problem_sidebar_additional_file.html" %}
|
||||
{% if page_name == 'problem_edit' %}
|
||||
{% set owner_udoc = udoc %}
|
||||
{% include "partials/problem_sidebar.html" %}
|
||||
|
||||
Reference in new issue
Block a user