mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-17 17:36:08 +08:00
ui: UI&UX enhance, bug fixes
Co-authored-by: panda <panda_dtdyy@outlook.com>
This commit is contained in:
1 parent
3f37b02b79
commit
310189bc4d
37 files changed
+1104
-948
No files matched your search
@@ -1,153 +1,36 @@
|
||||
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 { SubtaskConfig, TestCaseConfig } from 'hydrooj/src/interface';
|
||||
import {
|
||||
Card, Switch, Tab, Tabs, InputGroup, Tag,
|
||||
} from '@blueprintjs/core';
|
||||
import type { RootState } from './reducer/index';
|
||||
import CustomSelectAutoComplete from '../autocomplete/components/CustomSelectAutoComplete';
|
||||
import FileSelectAutoComplete from '../autocomplete/components/FileSelectAutoComplete';
|
||||
import { FormItem } from './BasicForm';
|
||||
import ProblemType from './ProblemType';
|
||||
import { TaskConfig } from './SubtaskTable';
|
||||
|
||||
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);
|
||||
const eqId = (a: SubtaskConfig[], b: SubtaskConfig[]) => {
|
||||
if (a.length !== b.length) return true;
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
if (a[i].id !== b[i].id) return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
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]);
|
||||
function FileIOConfig() {
|
||||
const filename = useSelector((state: RootState) => state.config.filename);
|
||||
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={i18n('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>
|
||||
</>
|
||||
<FormItem columns={12} label="FileIOConfig" disableLabel>
|
||||
<Card style={{ padding: 10 }}>
|
||||
<div className="row">
|
||||
<FormItem columns={6} label="FileIO">
|
||||
<InputGroup
|
||||
rightElement={<Tag minimal>.in/.out</Tag>}
|
||||
value={filename || ''}
|
||||
onChange={(ev) => {
|
||||
dispatch({ type: 'CONFIG_FORM_UPDATE', key: 'filename', value: ev.currentTarget.value });
|
||||
}}
|
||||
fill
|
||||
/>
|
||||
</FormItem>
|
||||
</div>
|
||||
</Card>
|
||||
</FormItem>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -166,7 +49,7 @@ function ExtraFilesConfig() {
|
||||
id="user_extra_files"
|
||||
title={i18n('user_extra_files')}
|
||||
panel={(
|
||||
<CustomSelectAutoComplete
|
||||
<FileSelectAutoComplete
|
||||
data={Files}
|
||||
selectedKeys={userExtraFiles || []}
|
||||
onChange={(val) => dispatch({ type: 'CONFIG_FORM_UPDATE', key: 'user_extra_files', value: val.split(',') })}
|
||||
@@ -178,7 +61,7 @@ function ExtraFilesConfig() {
|
||||
id="judge_extra_files"
|
||||
title={i18n('judge_extra_files')}
|
||||
panel={(
|
||||
<CustomSelectAutoComplete
|
||||
<FileSelectAutoComplete
|
||||
data={Files}
|
||||
selectedKeys={judgeExtraFiles || []}
|
||||
onChange={(val) => dispatch({ type: 'CONFIG_FORM_UPDATE', key: 'judge_extra_files', value: val.split(',') })}
|
||||
@@ -192,261 +75,6 @@ function ExtraFilesConfig() {
|
||||
);
|
||||
}
|
||||
|
||||
function CasesSubCasesTable({ index, subindex }) {
|
||||
const subcases = useSelector((state: RootState) => (index === -1
|
||||
? state.config.cases[subindex] : state.config.subtasks[index].cases[subindex]), eq);
|
||||
const Files = useSelector((state: RootState) => state.testdata);
|
||||
const dispatch = useDispatch();
|
||||
const dispatcher = (casesKey: string): React.ChangeEventHandler<HTMLInputElement | HTMLSelectElement> => (ev) => {
|
||||
dispatch({
|
||||
type: index === -1 ? 'CONFIG_CASES_UPDATE' : 'CONFIG_SUBTASK_UPDATE',
|
||||
id: index,
|
||||
key: 'cases-edit',
|
||||
casesId: subindex,
|
||||
casesKey,
|
||||
value: ev.currentTarget.value,
|
||||
});
|
||||
};
|
||||
const dispatcher1 = (casesKey: string): React.ChangeEventHandler<HTMLInputElement | HTMLSelectElement> => (val) => {
|
||||
dispatch({
|
||||
type: index === -1 ? 'CONFIG_CASES_UPDATE' : 'CONFIG_SUBTASK_UPDATE',
|
||||
id: index,
|
||||
key: 'cases-edit',
|
||||
casesId: subindex,
|
||||
casesKey,
|
||||
value: val,
|
||||
});
|
||||
};
|
||||
return (
|
||||
<tr>
|
||||
{
|
||||
index === -1 && (
|
||||
<>
|
||||
<td>
|
||||
<input
|
||||
value={subcases.time || ''}
|
||||
onChange={dispatcher('time')}
|
||||
className="textbox"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
value={subcases.memory || ''}
|
||||
onChange={dispatcher('memory')}
|
||||
className="textbox"
|
||||
/>
|
||||
</td>
|
||||
</>
|
||||
)
|
||||
}
|
||||
<td>
|
||||
<CustomSelectAutoComplete
|
||||
width="100%"
|
||||
data={Files}
|
||||
selectedKeys={[subcases.input]}
|
||||
onChange={dispatcher1('input')}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<CustomSelectAutoComplete
|
||||
width="100%"
|
||||
data={Files}
|
||||
selectedKeys={[subcases.output]}
|
||||
onChange={dispatcher1('output')}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<a
|
||||
onClick={() => dispatch({
|
||||
type: index === -1 ? 'CONFIG_CASES_UPDATE' : 'CONFIG_SUBTASK_UPDATE', id: index, key: 'cases-delete', value: subindex,
|
||||
})}
|
||||
><span className="icon icon-close"></span>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
function CasesTable({ index }) {
|
||||
const casesLength = useSelector((state: RootState) => (index === -1 ? state.config.cases?.length : state.config.subtasks[index].cases?.length));
|
||||
const dispatch = useDispatch();
|
||||
return (
|
||||
<table className="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
{
|
||||
index === -1 && (
|
||||
<>
|
||||
<th>
|
||||
{i18n('Time')}
|
||||
</th>
|
||||
<th>
|
||||
{i18n('Memory')}
|
||||
</th>
|
||||
</>
|
||||
)
|
||||
}
|
||||
<th>
|
||||
{i18n('Input')}
|
||||
</th>
|
||||
<th>
|
||||
{i18n('Output')}
|
||||
</th>
|
||||
<th className="col--operation">
|
||||
<a
|
||||
onClick={() => dispatch({
|
||||
type: index === -1 ? 'CONFIG_CASES_UPDATE' : 'CONFIG_SUBTASK_UPDATE',
|
||||
id: index,
|
||||
key: 'cases-add',
|
||||
value: index === -1 ? {
|
||||
time: 1000, memory: 256, input: '', output: '',
|
||||
} : { input: '', output: '' },
|
||||
})}
|
||||
><span className="icon icon-add" />
|
||||
</a>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{casesLength && [...Array(casesLength).keys()].map((i) => <CasesSubCasesTable index={index} subindex={i} key={i} />)}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
|
||||
function SubtasksIds({ index }) {
|
||||
const subtasks = useSelector((state: RootState) => state.config.subtasks, eqId);
|
||||
const subtaskIf = useSelector((state: RootState) => state.config.subtasks[index].if);
|
||||
const dispatch = useDispatch();
|
||||
const subtasksIds = subtasks.map((i) => i.id).filter((i) => i !== undefined);
|
||||
return (
|
||||
<tr>
|
||||
<td>if</td>
|
||||
<td colSpan={4}>
|
||||
<CustomSelectAutoComplete
|
||||
data={subtasksIds}
|
||||
selectedKeys={subtaskIf?.join(',').split(',') || []}
|
||||
itemKey={(item) => `${item}`}
|
||||
onChange={(val) => dispatch({
|
||||
type: 'CONFIG_SUBTASK_UPDATE', id: index, key: 'if', value: val.split(','),
|
||||
})}
|
||||
multi
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
function SubtasksTable({ index }) {
|
||||
const subtask = (key: string) => useSelector((state: RootState) => state.config.subtasks[index][key]);
|
||||
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 (
|
||||
<>
|
||||
<span>Subtasks #{index + 1} </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={subtask('id')?.toString() || ''}
|
||||
onChange={dispatcher('id')}
|
||||
className="textbox"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
value={subtask('score') || ''}
|
||||
onChange={dispatcher('score')}
|
||||
className="textbox"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<select
|
||||
value={subtask('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={subtask('time') || ''}
|
||||
onChange={dispatcher('time')}
|
||||
className="textbox"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
value={subtask('memory') || ''}
|
||||
onChange={dispatcher('memory')}
|
||||
className="textbox"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<SubtasksIds index={index} />
|
||||
</tbody>
|
||||
</table>
|
||||
<CasesTable index={index} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function TaskConfig({ onAutoLoad }) {
|
||||
const subtasksLength = useSelector((state: RootState) => state.config.subtasks?.length);
|
||||
const casesLength = useSelector((state: RootState) => state.config.cases?.length);
|
||||
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>
|
||||
{casesLength && <Switch label={i18n('Use Subtasks Mode')} onChange={() => dispatch({ type: 'CONFIG_SUBTASKS_SWITCH', value: true })} />}
|
||||
{
|
||||
subtasksLength || casesLength
|
||||
? subtasksLength && [...Array(subtasksLength).keys()].map((i) => <SubtasksTable index={i} key={i} />)
|
||||
|| casesLength && (<CasesTable index={-1} />)
|
||||
: (
|
||||
<a onClick={() => onAutoLoad()}>
|
||||
<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) || [];
|
||||
@@ -479,13 +107,19 @@ function LangConfig() {
|
||||
);
|
||||
}
|
||||
|
||||
export default function ProblemConfigForm({ onAutoLoad }) {
|
||||
export default function ProblemConfigForm() {
|
||||
const Type = useSelector((state: RootState) => state.config.type);
|
||||
return (
|
||||
<div className="row">
|
||||
<BasicInfo />
|
||||
<TaskConfig onAutoLoad={onAutoLoad} />
|
||||
<ExtraFilesConfig />
|
||||
<LangConfig />
|
||||
<div className="row problem-config-form">
|
||||
<ProblemType />
|
||||
{Type === 'default' && <FileIOConfig />}
|
||||
{!['submit_answer', 'objective'].includes(Type) && (
|
||||
<>
|
||||
<TaskConfig />
|
||||
<ExtraFilesConfig />
|
||||
<LangConfig />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user