You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
Hydro/packages/ui-default/components/problemconfig/TestCasesTable.tsx

163 lines
5.7 KiB
TypeScript

import { NumericInput, Tag } from '@blueprintjs/core';
import { parseMemoryMB, parseTimeMS } from '@hydrooj/utils/lib/common';
import type { SubtaskConfig } from 'hydrooj/src/interface';
import { isEqual } from 'lodash';
import React, { useEffect, useRef } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import i18n from 'vj/utils/i18n';
import FileSelectAutoComplete from '../autocomplete/components/FileSelectAutoComplete';
import type { RootState } from './reducer/index';
const eq = (a: SubtaskConfig, b: SubtaskConfig) => isEqual(a, b);
const eqArr = (a: any[], b: any[]) => isEqual(a, b);
export function TestCaseEntry({ index, subindex }) {
const testcase = useSelector((state: RootState) => state.config.subtasks[index].cases[subindex], eq);
const Files = useSelector((state: RootState) => state.testdata, eqArr);
const defaultTime = useSelector((state: RootState) => state.config.subtasks[index].time || state.config.time);
const defaultMemory = useSelector((state: RootState) => state.config.subtasks[index].memory || state.config.memory);
const dispatch = useDispatch();
const dispatcher = (casesKey: string, valueSuffix = '') => (ev: React.ChangeEvent<HTMLInputElement | HTMLSelectElement> | number) => {
let value = typeof ev !== 'object' ? ev : ev.currentTarget.value;
if (value === 0) value = '';
if (valueSuffix && value) value += valueSuffix;
dispatch({
type: 'CONFIG_SUBTASK_UPDATE',
id: index,
key: 'cases-edit',
casesId: subindex,
casesKey,
value,
});
if ((casesKey === 'input' && (value as string).endsWith('.in') && !testcase.output)
|| (casesKey === 'output' && (value as string).endsWith('.out') && !testcase.input)) {
const filename = (value as string).substring(0, (value as string).lastIndexOf('.'));
if (!testcase.output && (Files.map((f) => f.name).includes(`${filename}.out`) || Files.map((f) => f.name).includes(`${filename}.ans`))) {
dispatch({
type: 'CONFIG_SUBTASK_UPDATE',
id: index,
key: 'cases-edit',
casesId: subindex,
casesKey: 'output',
value: Files.map((f) => f.name).includes(`${filename}.out`) ? `${filename}.out` : `${filename}.ans`,
});
}
if (!testcase.input && Files.map((f) => f.name).includes(`${filename}.in`)) {
dispatch({
type: 'CONFIG_SUBTASK_UPDATE',
id: index,
key: 'cases-edit',
casesId: subindex,
casesKey: 'input',
value: `${filename}.in`,
});
}
}
};
const refs = {
input: useRef(),
output: useRef(),
};
for (const type of ['input', 'output']) {
useEffect(() => { // eslint-disable-line
refs[type].current?.setSelectedItems(testcase[type] ? [testcase[type]] : []);
}, [testcase[type]]);
}
if (!testcase || Object.keys(testcase).length === 0) {
return (
<tr><td colSpan={5}>{i18n('Failed to parse testcase.')}</td></tr>
);
}
return (
<tr>
<td>
<NumericInput
rightElement={<Tag minimal>ms</Tag>}
value={testcase.time ? parseTimeMS(testcase.time, false).toString() : ''}
placeholder={parseTimeMS(defaultTime || '1000ms', false).toString()}
onValueChange={dispatcher('time', 'ms')}
buttonPosition="none"
fill
/>
</td>
<td>
<NumericInput
rightElement={<Tag minimal>MB</Tag>}
value={testcase.memory ? parseMemoryMB(testcase.memory, false).toString() : ''}
placeholder={parseMemoryMB(defaultMemory || '256m', false).toString()}
onValueChange={dispatcher('memory', 'MB')}
buttonPosition="none"
fill
/>
</td>
{['input', 'output'].map((t) => (
<td key={t}>
<FileSelectAutoComplete
ref={refs[t]}
width="100%"
data={Files}
selectedKeys={[testcase[t]]}
onChange={dispatcher(t)}
/>
</td>
))}
<td className="col--operation">
<a
onClick={() => dispatch({
type: 'CONFIG_SUBTASK_UPDATE', id: index, key: 'cases-delete', value: subindex,
})}
><span className="icon icon-close"></span>
</a>
</td>
</tr>
);
}
export function CasesTable({ index }) {
const len = useSelector((state: RootState) => state.config.subtasks[index].cases?.length);
const dispatch = useDispatch();
return (
<table className="data-table">
<thead style={{ display: 'none' }}>
<tr>
<th>{i18n('Time')}</th>
<th>{i18n('Memory')}</th>
<th>{i18n('Input')}</th>
<th>{i18n('Output')}</th>
<th className="col--operation">
<a
onClick={() => dispatch({
type: 'CONFIG_SUBTASK_UPDATE',
id: index,
key: 'cases-add',
value: { input: '', output: '' },
})}
><span className="icon icon-add" />
</a>
</th>
</tr>
</thead>
<tbody>
<tr className="thead">
<td>{i18n('Time')}</td>
<td>{i18n('Memory')}</td>
<td>{i18n('Input')}</td>
<td>{i18n('Output')}</td>
<td className="col--operation">
<a
onClick={() => dispatch({
type: 'CONFIG_SUBTASK_UPDATE',
id: index,
key: 'cases-add',
value: { input: '', output: '' },
})}
><span className="icon icon-add" />
</a>
</td>
</tr>
{len > 0 && [...Array(len).keys()].map((i) => <TestCaseEntry index={index} subindex={i} key={i} />)}
</tbody>
</table>
);
}