From 3f37b02b7915745a0e4b7a9da21209bf25b9dd87 Mon Sep 17 00:00:00 2001 From: panda Date: Fri, 22 Apr 2022 02:53:19 +0800 Subject: [PATCH] ui: add configYamlFormat & fix bugs --- .../autocomplete/components/AutoComplete.tsx | 4 +- .../problemconfig/ProblemConfigEditor.tsx | 59 ++++++- .../problemconfig/ProblemConfigForm.tsx | 155 ++++++++---------- .../problemconfig/reducer/config.ts | 49 +++--- packages/ui-default/locales/zh.yaml | 14 ++ 5 files changed, 167 insertions(+), 114 deletions(-) diff --git a/packages/ui-default/components/autocomplete/components/AutoComplete.tsx b/packages/ui-default/components/autocomplete/components/AutoComplete.tsx index 5b79bc4cc..b6739046d 100644 --- a/packages/ui-default/components/autocomplete/components/AutoComplete.tsx +++ b/packages/ui-default/components/autocomplete/components/AutoComplete.tsx @@ -128,8 +128,10 @@ const AutoComplete = forwardRef(function Impl(props: AutoCompleteProps, re else onChange(selectedKeys.join(',')); }; + let first = !multi; useEffect(() => { - dispatchChange(); + if (first) first = false; + else dispatchChange(); const ids = []; for (const key of selectedKeys) if (!valueCache[key]) ids.push(key); if (!ids.length) return; diff --git a/packages/ui-default/components/problemconfig/ProblemConfigEditor.tsx b/packages/ui-default/components/problemconfig/ProblemConfigEditor.tsx index 0028f8c1b..e3debd546 100644 --- a/packages/ui-default/components/problemconfig/ProblemConfigEditor.tsx +++ b/packages/ui-default/components/problemconfig/ProblemConfigEditor.tsx @@ -4,6 +4,7 @@ import { connect } from 'react-redux'; import { load } from 'vj/components/monaco/loader'; import Editor from 'vj/components/editor'; import yaml from 'js-yaml'; +import type { ProblemConfigFile, TestCaseConfig } from 'hydrooj/src/interface'; const mapStateToProps = (state) => ({ config: state.config, @@ -22,10 +23,62 @@ interface Props { handleUpdateCode: Function; } +const configKey = [ + 'type', 'subType', 'target', 'score', 'time', + 'memory', 'filename', 'checker_type', 'checker', 'interactor', + 'user_extra_files', 'judge_extra_files', 'detail', 'outputs', 'redirect', + 'cases', 'subtasks', 'langs', +]; + +const subtasksKey = [ + 'time', 'memory', 'score', 'if', 'id', + 'type', 'cases', +]; + +const casesKey = ['time', 'memory', 'input', 'output']; + +function configYamlFormat(config: ProblemConfigFile) { + const formatConfig: ProblemConfigFile = {}; + configKey.forEach((key) => { + if (config[key] !== undefined) { + if (key === 'checker_type' && config.type !== 'default') return; + if (key === 'checker' + && (['default', 'strict'].includes(formatConfig.checker_type) || formatConfig.checker_type === undefined)) return; + if (key === 'interactor' && config.type !== 'interactive') return; + if (key === 'subtasks') { + formatConfig[key] = []; + config[key].forEach((subtask) => { + const formatSubtask: object = {}; + subtasksKey.forEach((subtaskKey) => { + if (subtask[subtaskKey] !== undefined) { + formatSubtask[subtaskKey] = subtask[subtaskKey]; + } + }); + formatConfig[key].push(formatSubtask); + }); + } else if (key === 'cases') { + formatConfig[key] = []; + config[key].forEach((caseItem) => { + const formatCase: TestCaseConfig = { + time: 1000, memory: 256, input: '', output: '', + }; + casesKey.forEach((caseKey) => { + if (caseItem[caseKey] !== undefined) formatCase[caseKey] = caseItem[caseKey]; + else delete formatCase[caseKey]; + }); + formatConfig[key].push(formatCase); + }); + } else formatConfig[key] = config[key]; + } + }); + return formatConfig; +} + export default connect(mapStateToProps, mapDispatchToProps)(class MonacoEditor extends React.PureComponent { disposable = []; containerElement: HTMLElement; private __preventUpdate = false; + private __preventFormat = false; editor: editor.IStandaloneCodeEditor; model: editor.ITextModel; @@ -40,7 +93,7 @@ export default connect(mapStateToProps, mapDispatchToProps)(class MonacoEditor e model: this.model, onChange: (value: string) => { this.__preventUpdate = true; - this.props.handleUpdateCode(value); + if (!this.__preventFormat) this.props.handleUpdateCode(value); this.__preventUpdate = false; }, }) as Editor; @@ -50,14 +103,16 @@ export default connect(mapStateToProps, mapDispatchToProps)(class MonacoEditor e componentDidUpdate(prevProps) { if (this.__preventUpdate) return; if (yaml.dump(prevProps.config) !== yaml.dump(this.props.config)) { + this.__preventFormat = true; this.model?.pushEditOperations( [], [{ range: this.model.getFullModelRange(), - text: yaml.dump(this.props.config), + text: yaml.dump(configYamlFormat(this.props.config)), }], undefined, ); + this.__preventFormat = false; } } diff --git a/packages/ui-default/components/problemconfig/ProblemConfigForm.tsx b/packages/ui-default/components/problemconfig/ProblemConfigForm.tsx index e2e1cd929..f70d4b410 100644 --- a/packages/ui-default/components/problemconfig/ProblemConfigForm.tsx +++ b/packages/ui-default/components/problemconfig/ProblemConfigForm.tsx @@ -1,4 +1,4 @@ -import React, { useRef, useState } from 'react'; +import React, { useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import i18n from 'vj/utils/i18n'; import { isEqual } from 'lodash'; @@ -13,8 +13,14 @@ const SelectValue = { task_type: ['min', 'max', 'sum'], }; -const eq = (a: SubtaskConfig[], b: SubtaskConfig[]) => isEqual(a, b); -const eq1 = (a: TestCaseConfig, b: TestCaseConfig) => isEqual(a, b); +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 @@ -120,7 +126,7 @@ function BasicInfo() { ? ( { dispatch({ type: 'CONFIG_FORM_UPDATE', key: 'checker_type', value: checkerType === 'strict' ? 'default' : 'strict' }); @@ -153,12 +159,12 @@ function ExtraFilesConfig() { const dispatch = useDispatch(); return ( - setshowTab(!showTab)} /> + setshowTab(!showTab)} />
(index === -1 - ? state.config.cases[subindex] : state.config.subtasks[index].cases[subindex]), eq1); + ? 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 => (ev) => { @@ -262,14 +268,7 @@ function CasesSubCasesTable({ index, subindex }) { } 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); - const Files = useSelector((state: RootState) => state.testdata); - const inputRef = useRef(null); - const outputRef = useRef(null); + const casesLength = useSelector((state: RootState) => (index === -1 ? state.config.cases?.length : state.config.subtasks[index].cases?.length)); const dispatch = useDispatch(); return ( @@ -279,73 +278,67 @@ function CasesTable({ index }) { index === -1 && ( <> ) } - {cases && cases.map((k, v) => )} + {casesLength && [...Array(casesLength).keys()].map((i) => )}
- {i18n('Time')}
- setTime(ev.currentTarget.value)} - className="textbox" - /> + {i18n('Time')}
- {i18n('Memory')}
- setMemory(ev.currentTarget.value)} - className="textbox" - /> + {i18n('Memory')}
- {i18n('Input')}
- + {i18n('Input')}
- {i18n('Output')}
- + {i18n('Output')}
-
{ - setTime(''); - setMemory(''); - inputRef?.current.setQuery(''); - outputRef?.current.setQuery(''); - dispatch({ - type: index === -1 ? 'CONFIG_CASES_UPDATE' : 'CONFIG_SUBTASK_UPDATE', - id: index, - key: 'cases-add', - value: index === -1 ? { - time, memory, input, output, - } : { input, output }, - }); - }} + 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: '' }, + })} >
); } -function SubtasksTable({ data, index }) { - const subtasks = useSelector((state: RootState) => state.config.subtasks, eq); +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 ( + + if + + `${item}`} + onChange={(val) => dispatch({ + type: 'CONFIG_SUBTASK_UPDATE', id: index, key: 'if', value: val.split(','), + })} + multi + /> + + + ); +} + +function SubtasksTable({ index }) { + const subtask = (key: string) => useSelector((state: RootState) => state.config.subtasks[index][key]); const dispatch = useDispatch(); const dispatcher = (key: string): React.ChangeEventHandler => (ev) => { dispatch({ @@ -353,7 +346,7 @@ function SubtasksTable({ data, index }) { }); }; return ( - + ); } function TaskConfig({ onAutoLoad }) { - const subtasks = useSelector((state: RootState) => state.config.subtasks, eq); - const cases = useSelector((state: RootState) => state.config.cases, eq); + const subtasksLength = useSelector((state: RootState) => state.config.subtasks?.length); + const casesLength = useSelector((state: RootState) => state.config.cases?.length); const dispatch = useDispatch(); return ( @@ -449,11 +430,11 @@ function TaskConfig({ onAutoLoad }) { - {cases && dispatch({ type: 'CONFIG_SUBTASKS_SWITCH', value: true })} />} + {casesLength && dispatch({ type: 'CONFIG_SUBTASKS_SWITCH', value: true })} />} { - subtasks || cases - ? subtasks &&
    {subtasks.map((k, v) => )}
- || cases && () + subtasksLength || casesLength + ? subtasksLength && [...Array(subtasksLength).keys()].map((i) => ) + || casesLength && () : (
onAutoLoad()}> {i18n('Auto Read Tasks')} @@ -480,7 +461,7 @@ function LangConfig() { }, [JSON.stringify(selectedKeys)]); return ( - setshowTab(!showTab)} /> + setshowTab(!showTab)} /> ({ + id: v, time: k.time, memory: k.memory, cases: [{ input: k.input, output: k.output }], + })); if (!next.subtasks.length) next.subtasks.push({ id: 0 }); delete next.cases; return next; } case 'CONFIG_CASES_UPDATE': { + if (['time', 'memory'].includes(action.casesKey) && action.value !== '') action.value = +action.value; if (action.key === 'cases-add') return { ...state, cases: [...state.cases, action.value] }; - if (action.key === 'cases-edit') { - const cases = cloneDeep(state.cases); - cases[action.casesId][action.casesKey] = action.value; - return { ...state, cases }; - } if (action.key === 'cases-delete') { + if (action.key === 'cases-delete') { return { ...state, cases: state.cases.filter((k, v) => v !== action.value) }; } - return state; + const cases = cloneDeep(state.cases); + cases[action.casesId][action.casesKey] = action.value; + if (action.value === '') delete cases[action.casesId][action.casesKey]; + return { ...state, cases }; } case 'CONFIG_SUBTASK_UPDATE': { + const subtasks = cloneDeep(state.subtasks); + const subsubtasks = cloneDeep(state.subtasks[action.id]); + if (action.value !== '' + && (['time', 'memory'].includes(action.casesKey) || ['time', 'memory', 'score', 'id'].includes(action.key)) + ) action.value = +action.value; + if (action.key === 'if' && action.value.join('') !== '') action.value = action.value.map((i) => +i); if (action.key.split('-')[0] === 'cases') { - const subtasks = cloneDeep(state.subtasks); - const subsubtasks = cloneDeep(state.subtasks[action.id]); if (action.key === 'cases-add') subsubtasks.cases.push(action.value); else if (action.key === 'cases-edit') subsubtasks.cases[action.casesId][action.casesKey] = action.value; else if (action.key === 'cases-delete') { - subsubtasks.cases = subsubtasks[action.id].cases.filter((k, v) => v !== action.value); + subsubtasks.cases = subsubtasks.cases.filter((k, v) => v !== action.value); } - subtasks[action.id] = subsubtasks; - return { ...state, subtasks }; + } else if (action.key === 'add') subtasks.push({ time: 1000, memory: 256, cases: [] }); + else if (action.key === 'delete') delete subtasks[action.key]; + else { + if (action.value === '' || (action.key === 'if' && action.value.join('') === '')) delete subsubtasks[action.key]; + else subsubtasks[action.key] = action.value; } - 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 next.subtasks[action.id][action.key] = action.value; - return next; + if (action.key !== 'delete') subtasks[action.id] = subsubtasks; + return { ...state, subtasks }; } default: return state; diff --git a/packages/ui-default/locales/zh.yaml b/packages/ui-default/locales/zh.yaml index 65ca01f98..73a82ef28 100644 --- a/packages/ui-default/locales/zh.yaml +++ b/packages/ui-default/locales/zh.yaml @@ -86,6 +86,7 @@ Attend Contest: 参加比赛 Attend contests: 参加比赛 Attended: 已参加 author: 作者 +Auto Read Tasks: 自动识别任务 Be Copied: 被复制 Begin at: 开始于 Begin Date: 开始日期 @@ -117,6 +118,8 @@ Change Email: 修改电子邮件 Change Password: 修改密码 Change Theme: 切换主题 Check In: 签到 +CheckerType: 比较器类型 +Checker: 比较器 Chinese: 中文 Choose the background image in your profile page.: 选择您资料页面的背景图片。 Claim homework: 认领作业 @@ -267,6 +270,7 @@ domain_user: 管理用户 domain: 域 Domain: 域 Don't have an account?: 还没有账户? +Don't ignore space and enter.: 不忽略行末回车与空格。 Done: 已结束 Download All: 下载全部 Download Dataset: 下载数据集 @@ -319,6 +323,7 @@ Export as CSV: 导出为 CSV Export as HTML: 导出为 HTML Extension (days): 最长延期 (日) Extension Score Penalty: 延期递交扣分规则 +Extra Files Config: 额外文件设置 Failed to join the domain. You are already a member.: 加入域失败,您已是该域的成员。 Feedback: 反馈 Field {0} or {1} validation failed.: 字段 {0} 或 {1} 验证失败。 @@ -401,6 +406,7 @@ Information: 信息 Initialize: 初始化 Inline codes: 行内代码 Inline formulas: 行内公式 +Input: 输入 Insert Code: 插入代码 Insert Image: 插入图像 Installed Modules: 已安装的模块 @@ -418,11 +424,14 @@ Join: 加入 Judge Config: 评测设置 Judge Status: 评测状态 judge_playground: 评测游乐场 +judge_extra_files: 评测额外文件 Judged At: 评测时间 Judged By: 评测机 Judging Queue: 评测队列 Keep current expiration: 保持当前过期设置 +langs: 语言 Language: 语言 +Langs Config: 语言设置 last active at: 最后活动于 last login at: 最后登录于 Last Submit At: 最后递交于 @@ -533,6 +542,7 @@ Or, with automatically filled invitation code: 或者,这是可以自动填写 Ordered List: 有序列表 Original Score: 原始分数 OS Info: 系统信息 +Output: 输出 Over {0} problems: 超过 {0} 道题目 Owner: 所有者 page.problem_detail.sidebar.show_category: 点击显示 @@ -585,6 +595,7 @@ Problem Solution: 题解列表 Problem Tags Visibility: 题目标签可见性 Problem Tags: 题目标签 problem_create: 创建题目 +problem_config: 评测设置 problem_edit: 编辑题目 problem_import: 导入题目 problem_main: 题库 @@ -758,6 +769,7 @@ Support: 支持 Switch account: 切换账户 Tags: 标签 tasks_list: 任务列表 +Task Settings: 任务设置 Technical Information: 技术信息 Temporary session: 临时会话 Terms of Service: 服务条款 @@ -832,12 +844,14 @@ Usage exceeded.: 用量超限。 Use admin specificed: 由管理员指定 Use algorithm calculated: 使用算法计算 Use average of above: 使用上面的平均值 +Use Subtasks Mode: 使用子任务模式 User {0} already exists.: 用户 {0} 已存在。 User {0} not found.: 用户 {0} 不存在。 User can join this domain by visiting the following URL: 用户可以访问此链接来加入此域 User ID: 用户 ID User will no longer be allowed to join the domain after expiration.: 过期后,用户将不能加入此域。 user_detail: 用户 +user_extra_files: 编译额外文件 user_login: 登录 user_logout: 登出 user_lostpass: 忘记密码